feat: signal history list + always compute scoring even without signal

This commit is contained in:
root 2026-02-28 06:09:32 +00:00
parent ca25938adc
commit 424cb993f8
3 changed files with 93 additions and 6 deletions

View File

@ -461,6 +461,23 @@ async def get_market_indicators(user: dict = Depends(get_current_user)):
return result return result
@app.get("/api/signals/history")
async def get_signal_history(
symbol: str = "BTC",
limit: int = 50,
user: dict = Depends(get_current_user),
):
"""返回最近的信号历史(只返回有信号的记录)"""
sym_full = symbol.upper() + "USDT"
rows = await async_fetch(
"SELECT ts, score, signal FROM signal_indicators "
"WHERE symbol = $1 AND signal IS NOT NULL "
"ORDER BY ts DESC LIMIT $2",
sym_full, limit
)
return {"symbol": symbol, "count": len(rows), "data": rows}
@app.get("/api/signals/trades") @app.get("/api/signals/trades")
async def get_signal_trades( async def get_signal_trades(
status: str = "all", status: str = "all",

View File

@ -241,15 +241,18 @@ class SymbolState:
if self.warmup or price == 0 or atr == 0: if self.warmup or price == 0 or atr == 0:
return result return result
if now_ms - self.last_signal_ts < COOLDOWN_MS:
return result # 判断倾向方向(用于评分展示,即使冷却或方向不一致也计算)
no_direction = False
in_cooldown = (now_ms - self.last_signal_ts < COOLDOWN_MS)
if cvd_fast > 0 and cvd_mid > 0: if cvd_fast > 0 and cvd_mid > 0:
direction = "LONG" direction = "LONG"
elif cvd_fast < 0 and cvd_mid < 0: elif cvd_fast < 0 and cvd_mid < 0:
direction = "SHORT" direction = "SHORT"
else: else:
return result direction = "LONG" if cvd_fast > 0 else "SHORT"
no_direction = True
# V5.1 五层评分体系总分100方向层可因加速额外+5 # V5.1 五层评分体系总分100方向层可因加速额外+5
# 1) 方向层45分 + 5加速分 # 1) 方向层45分 + 5加速分
@ -345,13 +348,13 @@ class SymbolState:
"auxiliary": {"score": aux_score, "coinbase_premium": coinbase_premium}, "auxiliary": {"score": aux_score, "coinbase_premium": coinbase_premium},
} }
if total_score >= 85: if total_score >= 85 and not no_direction and not in_cooldown:
result["signal"] = direction result["signal"] = direction
result["tier"] = "heavy" result["tier"] = "heavy"
elif total_score >= 75: elif total_score >= 75 and not no_direction and not in_cooldown:
result["signal"] = direction result["signal"] = direction
result["tier"] = "standard" result["tier"] = "standard"
elif total_score >= 60: elif total_score >= 60 and not no_direction and not in_cooldown:
result["signal"] = direction result["signal"] = direction
result["tier"] = "light" result["tier"] = "light"
else: else:

View File

@ -162,6 +162,70 @@ function MarketIndicatorsCards({ symbol }: { symbol: Symbol }) {
); );
} }
// ─── 信号历史 ────────────────────────────────────────────────────
interface SignalRecord {
ts: number;
score: number;
signal: string;
}
function bjtFull(ms: number) {
const d = new Date(ms + 8 * 3600 * 1000);
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")} ${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
}
function SignalHistory({ symbol }: { symbol: Symbol }) {
const [data, setData] = useState<SignalRecord[]>([]);
useEffect(() => {
const fetchData = async () => {
try {
const res = await authFetch(`/api/signals/history?symbol=${symbol}&limit=20`);
if (!res.ok) return;
const json = await res.json();
setData(json.data || []);
} catch {}
};
fetchData();
const iv = setInterval(fetchData, 15000);
return () => clearInterval(iv);
}, [symbol]);
if (data.length === 0) return null;
return (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100">
<h3 className="font-semibold text-slate-800 text-sm"></h3>
<p className="text-xs text-slate-400 mt-0.5">20</p>
</div>
<div className="divide-y divide-slate-100">
{data.map((s, i) => (
<div key={i} className="px-4 py-2 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`text-sm font-bold ${s.signal === "LONG" ? "text-emerald-600" : "text-red-500"}`}>
{s.signal === "LONG" ? "🟢 LONG" : "🔴 SHORT"}
</span>
<span className="text-xs text-slate-400">{bjtFull(s.ts)}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-slate-700">{s.score}/100</span>
<span className={`text-xs px-1.5 py-0.5 rounded ${
s.score >= 85 ? "bg-red-100 text-red-700" :
s.score >= 75 ? "bg-blue-100 text-blue-700" :
"bg-slate-100 text-slate-600"
}`}>
{s.score >= 85 ? "加仓" : s.score >= 75 ? "标准" : "轻仓"}
</span>
</div>
</div>
))}
</div>
</div>
);
}
// ─── 实时指标卡片 ──────────────────────────────────────────────── // ─── 实时指标卡片 ────────────────────────────────────────────────
function IndicatorCards({ symbol }: { symbol: Symbol }) { function IndicatorCards({ symbol }: { symbol: Symbol }) {
@ -410,6 +474,9 @@ export default function SignalsPage() {
<MarketIndicatorsCards symbol={symbol} /> <MarketIndicatorsCards symbol={symbol} />
</div> </div>
{/* 信号历史 */}
<SignalHistory symbol={symbol} />
{/* CVD三轨图 */} {/* CVD三轨图 */}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden"> <div className="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between flex-wrap gap-2"> <div className="px-4 py-3 border-b border-slate-100 flex items-center justify-between flex-wrap gap-2">