fix: paper latest signals use signal-history with strategy filter + layer scores

- V5.1 paper: signal-history?strategy=v51_baseline
- V5.2 paper: signal-history?strategy=v52_8signals
- API now returns factors in signal-history
- Each signal shows layer score badges below
This commit is contained in:
root 2026-03-02 03:08:19 +00:00
parent 5b704a0a0e
commit 984019d9ab
3 changed files with 56 additions and 47 deletions

View File

@ -570,14 +570,21 @@ async def get_signal_history(
strategy: str = "v52_8signals", strategy: str = "v52_8signals",
user: dict = Depends(get_current_user), user: dict = Depends(get_current_user),
): ):
"""返回最近的信号历史(只返回有信号的记录)""" """返回最近的信号历史(只返回有信号的记录),含各层分数"""
sym_full = symbol.upper() + "USDT" sym_full = symbol.upper() + "USDT"
rows = await async_fetch( rows = await async_fetch(
"SELECT ts, score, signal FROM signal_indicators " "SELECT ts, score, signal, factors FROM signal_indicators "
"WHERE symbol = $1 AND strategy = $2 AND signal IS NOT NULL " "WHERE symbol = $1 AND strategy = $2 AND signal IS NOT NULL "
"ORDER BY ts DESC LIMIT $3", "ORDER BY ts DESC LIMIT $3",
sym_full, strategy, limit sym_full, strategy, limit
) )
# factors可能是JSON string
for r in rows:
if isinstance(r.get("factors"), str):
try:
r["factors"] = json.loads(r["factors"])
except Exception:
pass
return {"symbol": symbol, "count": len(rows), "data": rows} return {"symbol": symbol, "count": len(rows), "data": rows}

View File

@ -166,17 +166,19 @@ function LatestSignals() {
const [signals, setSignals] = useState<Record<string, any>>({}); const [signals, setSignals] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
const f = async () => { const f = async () => {
for (const sym of COINS) {
try { try {
const r = await authFetch("/api/signals/latest?strategy=v52_8signals"); const r = await authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT","")}&limit=1&strategy=v52_8signals`);
if (r.ok) { if (r.ok) {
const j = await r.json(); const j = await r.json();
setSignals(j); if (j.data && j.data.length > 0) {
setSignals(prev => ({ ...prev, [sym]: j.data[0] }));
}
} }
} catch {} } catch {}
}
}; };
f(); f(); const iv = setInterval(f, 15000); return () => clearInterval(iv);
const iv = setInterval(f, 15000);
return () => clearInterval(iv);
}, []); }, []);
return ( return (
@ -185,11 +187,11 @@ function LatestSignals() {
<h3 className="font-semibold text-slate-800 text-xs"></h3> <h3 className="font-semibold text-slate-800 text-xs"></h3>
</div> </div>
<div className="divide-y divide-slate-50"> <div className="divide-y divide-slate-50">
{COINS.map((sym) => { {COINS.map(sym => {
const s = signals[sym];
const coin = sym.replace("USDT", ""); const coin = sym.replace("USDT", "");
const s = signals[coin];
const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null; const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null;
const f = s?.factors; const fc = s?.factors;
return ( return (
<div key={sym} className="px-3 py-2"> <div key={sym} className="px-3 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -201,25 +203,23 @@ function LatestSignals() {
{s.signal === "LONG" ? "🟢" : "🔴"} {s.signal} {s.signal === "LONG" ? "🟢" : "🔴"} {s.signal}
</span> </span>
<span className="font-mono text-xs font-bold text-slate-800">{s.score}</span> <span className="font-mono text-xs font-bold text-slate-800">{s.score}</span>
<span className="text-[9px] px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 font-medium">{s.score >= 85 ? "加仓" : "标准"}</span>
</> </>
) : ( ) : (
<> <span className="text-[10px] text-slate-400"></span>
<span className="text-[10px] text-slate-400"> </span>
<span className="font-mono text-[10px] text-slate-500">{s?.score ?? 0}</span>
</>
)} )}
</div> </div>
{ago !== null && <span className="text-[10px] text-slate-400">{ago < 60 ? `${ago}m前` : `${Math.round(ago / 60)}h前`}</span>} {ago !== null && <span className="text-[10px] text-slate-400">{ago < 60 ? `${ago}m前` : `${Math.round(ago/60)}h前`}</span>}
</div> </div>
{f && ( {fc && (
<div className="flex gap-1 mt-1 flex-wrap"> <div className="flex gap-1 mt-1 flex-wrap">
<span className="text-[9px] px-1 py-0.5 rounded bg-blue-50 text-blue-700">{f.direction?.score ?? 0}/{f.direction?.max ?? 40}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-blue-50 text-blue-700">{fc.direction?.score ?? 0}/{fc.direction?.max ?? 40}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-violet-50 text-violet-700">{f.crowding?.score ?? 0}/{f.crowding?.max ?? 18}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-violet-50 text-violet-700">{fc.crowding?.score ?? 0}/{fc.crowding?.max ?? 18}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-cyan-50 text-cyan-700">FR{f.funding_rate?.score ?? 0}/{f.funding_rate?.max ?? 5}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-cyan-50 text-cyan-700">FR{fc.funding_rate?.score ?? 0}/{fc.funding_rate?.max ?? 5}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-emerald-50 text-emerald-700">{f.environment?.score ?? 0}/{f.environment?.max ?? 12}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-emerald-50 text-emerald-700">{fc.environment?.score ?? 0}/{fc.environment?.max ?? 12}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-amber-50 text-amber-700">{f.confirmation?.score ?? 0}/{f.confirmation?.max ?? 15}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-amber-50 text-amber-700">{fc.confirmation?.score ?? 0}/{fc.confirmation?.max ?? 15}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-orange-50 text-orange-700">{f.liquidation?.score ?? 0}/{f.liquidation?.max ?? 5}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-orange-50 text-orange-700">{fc.liquidation?.score ?? 0}/{fc.liquidation?.max ?? 5}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-slate-100 text-slate-600">{f.auxiliary?.score ?? 0}/{f.auxiliary?.max ?? 5}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-slate-100 text-slate-600">{fc.auxiliary?.score ?? 0}/{fc.auxiliary?.max ?? 5}</span>
</div> </div>
)} )}
</div> </div>

View File

@ -118,13 +118,17 @@ function LatestSignals() {
const [signals, setSignals] = useState<Record<string, any>>({}); const [signals, setSignals] = useState<Record<string, any>>({});
useEffect(() => { useEffect(() => {
const f = async () => { const f = async () => {
for (const sym of COINS) {
try { try {
const r = await authFetch("/api/signals/latest?strategy=v51_baseline"); const r = await authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT","")}&limit=1&strategy=v51_baseline`);
if (r.ok) { if (r.ok) {
const j = await r.json(); const j = await r.json();
setSignals(j); if (j.data && j.data.length > 0) {
setSignals(prev => ({ ...prev, [sym]: j.data[0] }));
}
} }
} catch {} } catch {}
}
}; };
f(); const iv = setInterval(f, 15000); return () => clearInterval(iv); f(); const iv = setInterval(f, 15000); return () => clearInterval(iv);
}, []); }, []);
@ -136,10 +140,10 @@ function LatestSignals() {
</div> </div>
<div className="divide-y divide-slate-50"> <div className="divide-y divide-slate-50">
{COINS.map(sym => { {COINS.map(sym => {
const s = signals[sym];
const coin = sym.replace("USDT", ""); const coin = sym.replace("USDT", "");
const s = signals[coin];
const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null; const ago = s?.ts ? Math.round((Date.now() - s.ts) / 60000) : null;
const f = s?.factors; const fc = s?.factors;
return ( return (
<div key={sym} className="px-3 py-2"> <div key={sym} className="px-3 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -151,23 +155,21 @@ function LatestSignals() {
{s.signal === "LONG" ? "🟢" : "🔴"} {s.signal} {s.signal === "LONG" ? "🟢" : "🔴"} {s.signal}
</span> </span>
<span className="font-mono text-xs font-bold text-slate-800">{s.score}</span> <span className="font-mono text-xs font-bold text-slate-800">{s.score}</span>
<span className="text-[9px] px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-700 font-medium">{s.score >= 85 ? "加仓" : "标准"}</span>
</> </>
) : ( ) : (
<> <span className="text-[10px] text-slate-400"></span>
<span className="text-[10px] text-slate-400"> </span>
<span className="font-mono text-[10px] text-slate-500">{s?.score ?? 0}</span>
</>
)} )}
</div> </div>
{ago !== null && <span className="text-[10px] text-slate-400">{ago < 60 ? `${ago}m前` : `${Math.round(ago/60)}h前`}</span>} {ago !== null && <span className="text-[10px] text-slate-400">{ago < 60 ? `${ago}m前` : `${Math.round(ago/60)}h前`}</span>}
</div> </div>
{f && ( {fc && (
<div className="flex gap-1 mt-1 flex-wrap"> <div className="flex gap-1 mt-1 flex-wrap">
<span className="text-[9px] px-1 py-0.5 rounded bg-blue-50 text-blue-700">{f.direction?.score ?? 0}/{f.direction?.max ?? 45}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-blue-50 text-blue-700">{fc.direction?.score ?? 0}/{fc.direction?.max ?? 45}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-violet-50 text-violet-700">{f.crowding?.score ?? 0}/{f.crowding?.max ?? 20}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-violet-50 text-violet-700">{fc.crowding?.score ?? 0}/{fc.crowding?.max ?? 20}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-emerald-50 text-emerald-700">{f.environment?.score ?? 0}/{f.environment?.max ?? 15}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-emerald-50 text-emerald-700">{fc.environment?.score ?? 0}/{fc.environment?.max ?? 15}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-amber-50 text-amber-700">{f.confirmation?.score ?? 0}/{f.confirmation?.max ?? 15}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-amber-50 text-amber-700">{fc.confirmation?.score ?? 0}/{fc.confirmation?.max ?? 15}</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-slate-100 text-slate-600">{f.auxiliary?.score ?? 0}/{f.auxiliary?.max ?? 5}</span> <span className="text-[9px] px-1 py-0.5 rounded bg-slate-100 text-slate-600">{fc.auxiliary?.score ?? 0}/{fc.auxiliary?.max ?? 5}</span>
</div> </div>
)} )}
</div> </div>