feat: V5.1 frontend - 5-layer scoring display + market indicators panel

This commit is contained in:
root 2026-02-28 05:33:40 +00:00
parent 5c38a2f9bf
commit 340d8eb3a1
3 changed files with 133 additions and 14 deletions

View File

@ -426,13 +426,29 @@ async def get_signal_latest(user: dict = Depends(get_current_user)):
result = {} result = {}
for sym in ["BTCUSDT", "ETHUSDT"]: for sym in ["BTCUSDT", "ETHUSDT"]:
row = await async_fetchrow( row = await async_fetchrow(
"SELECT ts, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, " "SELECT to_jsonb(si) AS data FROM signal_indicators si WHERE symbol = $1 ORDER BY ts DESC LIMIT 1",
"vwap_30m, price, p95_qty, p99_qty, score, signal "
"FROM signal_indicators WHERE symbol = $1 ORDER BY ts DESC LIMIT 1",
sym sym
) )
if row: if row:
result[sym.replace("USDT", "")] = row result[sym.replace("USDT", "")] = row["data"]
return result
@app.get("/api/signals/market-indicators")
async def get_market_indicators(user: dict = Depends(get_current_user)):
"""返回最新的market_indicators数据V5.1新增4个数据源"""
result = {}
for sym in ["BTCUSDT", "ETHUSDT"]:
indicators = {}
for ind_type in ["long_short_ratio", "top_trader_position", "open_interest_hist", "coinbase_premium"]:
row = await async_fetchrow(
"SELECT value, timestamp_ms FROM market_indicators WHERE symbol = $1 AND indicator_type = $2 ORDER BY timestamp_ms DESC LIMIT 1",
sym,
ind_type,
)
if row:
indicators[ind_type] = {"value": row["value"], "ts": row["timestamp_ms"]}
result[sym.replace("USDT", "")] = indicators
return result return result

View File

@ -37,6 +37,26 @@ interface LatestIndicator {
p99_qty: number; p99_qty: number;
score: number; score: number;
signal: string | null; signal: string | null;
tier?: "light" | "standard" | "heavy" | null;
factors?: {
direction?: { score?: number };
crowding?: { score?: number };
environment?: { score?: number };
confirmation?: { score?: number };
auxiliary?: { score?: number };
} | null;
}
interface MarketIndicatorValue {
value: number;
ts: number;
}
interface MarketIndicatorSet {
long_short_ratio?: MarketIndicatorValue;
top_trader_position?: MarketIndicatorValue;
open_interest_hist?: MarketIndicatorValue;
coinbase_premium?: MarketIndicatorValue;
} }
const WINDOWS = [ const WINDOWS = [
@ -57,6 +77,75 @@ function fmt(v: number, decimals = 1): string {
return v.toFixed(decimals); return v.toFixed(decimals);
} }
function pct(v: number, digits = 1): string {
return `${(v * 100).toFixed(digits)}%`;
}
function LayerScore({ label, score, max, colorClass }: { label: string; score: number; max: number; colorClass: string }) {
const ratio = Math.max(0, Math.min((score / max) * 100, 100));
return (
<div className="space-y-1">
<div className="flex items-center justify-between text-xs text-slate-600">
<span>{label}</span>
<span className="font-mono">{score}/{max}</span>
</div>
<div className="h-1.5 rounded-full bg-slate-100 overflow-hidden">
<div className={`h-full ${colorClass}`} style={{ width: `${ratio}%` }} />
</div>
</div>
);
}
function MarketIndicatorsCards({ symbol }: { symbol: Symbol }) {
const [data, setData] = useState<MarketIndicatorSet | null>(null);
useEffect(() => {
const fetch = async () => {
try {
const res = await authFetch("/api/signals/market-indicators");
if (!res.ok) return;
const json = await res.json();
setData(json[symbol] || null);
} catch {}
};
fetch();
const iv = setInterval(fetch, 5000);
return () => clearInterval(iv);
}, [symbol]);
if (!data) return <div className="text-center text-slate-400 text-sm py-3">...</div>;
const ls = Number(data.long_short_ratio?.value ?? 1);
const top = Number(data.top_trader_position?.value ?? 0.5);
const oi = Number(data.open_interest_hist?.value ?? 0);
const premium = Number(data.coinbase_premium?.value ?? 0);
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2">
<div className="bg-white rounded-xl border border-slate-200 p-3">
<p className="text-xs text-slate-400"> (L/S)</p>
<p className="text-sm text-slate-800 mt-1">Long: {(ls / (1 + ls) * 100).toFixed(1)}%</p>
<p className="text-sm text-slate-600">Short: {(100 - (ls / (1 + ls) * 100)).toFixed(1)}%</p>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-3">
<p className="text-xs text-slate-400"></p>
<p className="text-sm text-slate-800 mt-1">: {(top * 100).toFixed(1)}%</p>
<p className="text-sm text-slate-600">: {top >= 0.55 ? "多头占优" : top <= 0.45 ? "空头占优" : "中性"}</p>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-3">
<p className="text-xs text-slate-400">OI变化</p>
<p className="text-sm text-slate-800 mt-1">{pct(oi, 2)}</p>
<p className="text-sm text-slate-600">: {oi >= 0.03 ? "高" : oi > 0 ? "中" : "低"}</p>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-3">
<p className="text-xs text-slate-400">Coinbase Premium</p>
<p className={`text-sm mt-1 ${premium >= 0 ? "text-emerald-600" : "text-red-500"}`}>{premium >= 0 ? "+" : ""}{pct(premium, 2)}</p>
<p className="text-sm text-slate-600">: {premium > 0.0005 ? "偏多" : premium < -0.0005 ? "偏空" : "中性"}</p>
</div>
</div>
);
}
// ─── 实时指标卡片 ──────────────────────────────────────────────── // ─── 实时指标卡片 ────────────────────────────────────────────────
function IndicatorCards({ symbol }: { symbol: Symbol }) { function IndicatorCards({ symbol }: { symbol: Symbol }) {
@ -149,8 +238,8 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) {
</div> </div>
</div> </div>
{/* 信号状态 */} {/* 信号状态V5.1 */}
<div className={`rounded-lg border p-3 ${ <div className={`rounded-xl border p-3 ${
data.signal === "LONG" ? "border-emerald-300 bg-emerald-50" : data.signal === "LONG" ? "border-emerald-300 bg-emerald-50" :
data.signal === "SHORT" ? "border-red-300 bg-red-50" : data.signal === "SHORT" ? "border-red-300 bg-red-50" :
"border-slate-200 bg-slate-50" "border-slate-200 bg-slate-50"
@ -167,12 +256,20 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) {
</p> </p>
</div> </div>
<div className="text-right"> <div className="text-right">
<p className="text-xs text-slate-500"></p> <p className="text-xs text-slate-500"></p>
<p className="font-mono font-bold text-slate-800">{data.score}/60</p> <p className="font-mono font-bold text-slate-800">{data.score}/100</p>
<p className="text-xs text-slate-500 mt-0.5">: {data.tier === "heavy" ? "加仓" : data.tier === "standard" ? "标准" : data.tier === "light" ? "轻仓" : "不开仓"}</p>
</div> </div>
</div> </div>
<div className="mt-3 space-y-2">
<LayerScore label="方向层" score={Number(data.factors?.direction?.score ?? 0)} max={45} colorClass="bg-blue-600" />
<LayerScore label="拥挤层" score={Number(data.factors?.crowding?.score ?? 0)} max={20} colorClass="bg-violet-600" />
<LayerScore label="环境层" score={Number(data.factors?.environment?.score ?? 0)} max={15} colorClass="bg-emerald-600" />
<LayerScore label="确认层" score={Number(data.factors?.confirmation?.score ?? 0)} max={15} colorClass="bg-amber-500" />
<LayerScore label="辅助层" score={Number(data.factors?.auxiliary?.score ?? 0)} max={5} colorClass="bg-slate-500" />
</div>
{data.signal && ( {data.signal && (
<div className="mt-2 grid grid-cols-3 gap-2 text-xs"> <div className="mt-2 grid grid-cols-3 gap-2 text-xs text-slate-600">
<span>{core1} CVD_fast方向</span> <span>{core1} CVD_fast方向</span>
<span>{core2} CVD_mid方向</span> <span>{core2} CVD_mid方向</span>
<span>{core3} VWAP位置</span> <span>{core3} VWAP位置</span>
@ -275,8 +372,8 @@ export default function SignalsPage() {
{/* 标题 */} {/* 标题 */}
<div className="flex items-center justify-between flex-wrap gap-2"> <div className="flex items-center justify-between flex-wrap gap-2">
<div> <div>
<h1 className="text-xl font-bold text-slate-900">V5 </h1> <h1 className="text-xl font-bold text-slate-900">V5.1 </h1>
<p className="text-slate-500 text-xs mt-0.5">CVD三轨 + ATR + VWAP + /</p> <p className="text-slate-500 text-xs mt-0.5">100 + + </p>
</div> </div>
<div className="flex gap-1.5"> <div className="flex gap-1.5">
{(["BTC", "ETH"] as Symbol[]).map(s => ( {(["BTC", "ETH"] as Symbol[]).map(s => (
@ -291,6 +388,12 @@ export default function SignalsPage() {
{/* 实时指标卡片 */} {/* 实时指标卡片 */}
<IndicatorCards symbol={symbol} /> <IndicatorCards symbol={symbol} />
{/* Market Indicators */}
<div className="rounded-xl border border-slate-200 bg-white p-3">
<h3 className="font-semibold text-slate-800 text-sm mb-2">Market Indicators</h3>
<MarketIndicatorsCards symbol={symbol} />
</div>
{/* 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">
@ -313,9 +416,9 @@ export default function SignalsPage() {
</div> </div>
{/* 说明 */} {/* 说明 */}
<div className="rounded-lg border border-blue-100 bg-blue-50 px-4 py-3 text-xs text-slate-600 space-y-1"> <div className="rounded-xl border border-blue-100 bg-blue-50 px-4 py-3 text-xs text-slate-700 space-y-1">
<p><span className="text-blue-600 font-medium"></span>CVD_fast方向 + CVD_mid方向 + VWAP位置 = 3ATR扩张(+25) + (+20) + (+15)</p> <p><span className="text-blue-600 font-medium">V5.1</span>45 + 20 + 15 + 15 + 5+5</p>
<p><span className="text-blue-600 font-medium"></span>0-152% / 20-405% / 45-608%1030</p> <p><span className="text-blue-600 font-medium"></span>&lt;6060-7475-848510</p>
</div> </div>
</div> </div>
); );