feat: V5.1 frontend - 5-layer scoring display + market indicators panel
This commit is contained in:
parent
5c38a2f9bf
commit
340d8eb3a1
Binary file not shown.
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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位置 = 核心3条件。加分:ATR扩张(+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-15分→2%仓 / 20-40分→5%仓 / 45-60分→8%仓。冷却10分钟,时间止损30分钟。</p>
|
<p><span className="text-blue-600 font-medium">开仓档位:</span><60不开仓,60-74轻仓,75-84标准仓位,≥85允许加仓;信号冷却10分钟。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user