From 9e8e50b4e76a49db745379ee18c594e2aa784e15 Mon Sep 17 00:00:00 2001 From: fanziqi <403508380@qq.com> Date: Sat, 14 Mar 2026 00:42:02 +0800 Subject: [PATCH] feat: add all signals modal for strategy signals --- .../strategy-plaza/[id]/SignalsGeneric.tsx | 209 +++++++++++++++++- 1 file changed, 208 insertions(+), 1 deletion(-) diff --git a/frontend/app/strategy-plaza/[id]/SignalsGeneric.tsx b/frontend/app/strategy-plaza/[id]/SignalsGeneric.tsx index a0d555e..2e8aa73 100644 --- a/frontend/app/strategy-plaza/[id]/SignalsGeneric.tsx +++ b/frontend/app/strategy-plaza/[id]/SignalsGeneric.tsx @@ -56,6 +56,15 @@ interface SignalRecord { signal: string; } +interface AllSignalRow { + ts: number; + score: number; + signal: string | null; + price?: number; + // factors 结构与 LatestIndicator.factors 基本一致,兼容 string/json + factors?: LatestIndicator["factors"] | string | null; +} + interface Gates { obi_threshold: number; whale_usd_threshold: number; @@ -326,6 +335,186 @@ function SignalHistory({ coin, strategyName }: { coin: string; strategyName: str ); } +function AllSignalsModal({ + open, + onClose, + symbol, + strategyName, +}: { + open: boolean; + onClose: () => void; + symbol: string; + strategyName: string; +}) { + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!open) return; + const fetchAll = async () => { + setLoading(true); + setError(null); + try { + const res = await authFetch( + `/api/signals/history?symbol=${symbol}&limit=200&strategy=${strategyName}` + ); + if (!res.ok) { + setError(`加载失败 (${res.status})`); + setRows([]); + return; + } + const json = await res.json(); + setRows(json.items || []); + } catch (e) { + console.error(e); + setError("加载失败,请稍后重试"); + setRows([]); + } finally { + setLoading(false); + } + }; + fetchAll(); + }, [open, symbol, strategyName]); + + const parseFactors = (r: AllSignalRow): LatestIndicator["factors"] | null => { + const f = r.factors; + if (!f) return null; + if (typeof f === "string") { + try { + return JSON.parse(f); + } catch { + return null; + } + } + return f; + }; + + if (!open) return null; + + return ( +
+
+
+
+

+ 所有历史信号(含未开仓) +

+

+ 最近 200 条 · {symbol} · {strategyName} +

+
+ +
+ +
+ {loading ? ( +
+ 加载中... +
+ ) : error ? ( +
+ {error} +
+ ) : rows.length === 0 ? ( +
+ 暂无历史信号 +
+ ) : ( + + + + + + + + + + + + {rows.map((r, idx) => { + const f = parseFactors(r); + const dirScore = f?.direction?.score ?? 0; + const envScore = f?.environment?.score ?? 0; + const auxScore = f?.auxiliary?.score ?? 0; + const momScore = f?.crowding?.score ?? 0; + const gateBlock = + (f?.gate_block as string | undefined) || + (f?.block_reason as string | undefined) || + ""; + const gatePassed = + typeof f?.gate_passed === "boolean" + ? f?.gate_passed + : !gateBlock; + return ( + + + + + + + + ); + })} + +
时间信号总分 + 四层评分 + 门控
+ {bjtFull(r.ts)} + + + + {r.signal === "LONG" + ? "多" + : r.signal === "SHORT" + ? "空" + : "无"} + + + + {r.score} + +
+ 方:{dirScore} + 环:{envScore} + 辅:{auxScore} + 动:{momScore} +
+
+ {gatePassed ? ( + + 通过 + + ) : ( + + 拒绝 + {gateBlock ? ` · ${gateBlock}` : ""} + + )} +
+ )} +
+
+
+ ); +} + function CVDChart({ sym, minutes, strategyName, cvdFastWindow, cvdSlowWindow }: { sym: string; minutes: number; strategyName: string; cvdFastWindow: string; cvdSlowWindow: string; }) { @@ -398,6 +587,7 @@ export default function SignalsGeneric({ strategyId, symbol, cvdFastWindow, cvdS const [minutes, setMinutes] = useState(240); const coin = symbol.replace("USDT", ""); const strategyName = `custom_${strategyId.slice(0, 8)}`; + const [showAllSignals, setShowAllSignals] = useState(false); return (
@@ -408,7 +598,17 @@ export default function SignalsGeneric({ strategyId, symbol, cvdFastWindow, cvdS CVD {cvdFastWindow}/{cvdSlowWindow} · 权重 {weights.direction}/{weights.env}/{weights.aux}/{weights.momentum} · {coin}

- {coin} +
+ + + {coin} + +
+ + setShowAllSignals(false)} + symbol={symbol} + strategyName={strategyName} + /> ); }