From 7ebdb986437ada8f5918b59061500ab42065c356 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 2 Mar 2026 02:52:17 +0000 Subject: [PATCH] feat: store and serve indicators per strategy - signal_indicators table: added strategy column - Each strategy gets its own row per cycle - API /api/signals/latest?strategy=v51_baseline|v52_8signals - API /api/signals/signal-history?strategy=... - V5.1 page reads v51_baseline data, V5.2 reads v52_8signals - Now V5.1 and V5.2 show truly independent scores --- backend/main.py | 14 ++++++------ backend/signal_engine.py | 16 ++++++++------ docs/AB_TEST_CHECKLIST.md | 36 +++++++++++++++++++++++++++++++ frontend/app/signals-v52/page.tsx | 4 ++-- frontend/app/signals/page.tsx | 4 ++-- 5 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 docs/AB_TEST_CHECKLIST.md diff --git a/backend/main.py b/backend/main.py index 35dffe2..901b271 100644 --- a/backend/main.py +++ b/backend/main.py @@ -422,18 +422,17 @@ async def get_signal_indicators( @app.get("/api/signals/latest") -async def get_signal_latest(user: dict = Depends(get_current_user)): +async def get_signal_latest(user: dict = Depends(get_current_user), strategy: str = "v52_8signals"): result = {} for sym in SYMBOLS: row = await async_fetchrow( "SELECT ts, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, " "vwap_30m, price, p95_qty, p99_qty, score, signal, factors " - "FROM signal_indicators WHERE symbol = $1 ORDER BY ts DESC LIMIT 1", - sym + "FROM signal_indicators WHERE symbol = $1 AND strategy = $2 ORDER BY ts DESC LIMIT 1", + sym, strategy ) if row: data = dict(row) - # factors可能是JSON string(psycopg2写入),需要解析 if isinstance(data.get("factors"), str): try: data["factors"] = json.loads(data["factors"]) @@ -568,15 +567,16 @@ async def get_market_indicators(user: dict = Depends(get_current_user)): async def get_signal_history( symbol: str = "BTC", limit: int = 50, + strategy: str = "v52_8signals", 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 + "WHERE symbol = $1 AND strategy = $2 AND signal IS NOT NULL " + "ORDER BY ts DESC LIMIT $3", + sym_full, strategy, limit ) return {"symbol": symbol, "count": len(rows), "data": rows} diff --git a/backend/signal_engine.py b/backend/signal_engine.py index 6831ce3..55009a1 100644 --- a/backend/signal_engine.py +++ b/backend/signal_engine.py @@ -684,16 +684,16 @@ def fetch_new_trades(symbol: str, last_id: int) -> list: for r in cur.fetchall()] -def save_indicator(ts: int, symbol: str, result: dict): +def save_indicator(ts: int, symbol: str, result: dict, strategy: str = "v52_8signals"): with get_sync_conn() as conn: with conn.cursor() as cur: import json as _json3 factors_json = _json3.dumps(result.get("factors")) if result.get("factors") else None cur.execute( "INSERT INTO signal_indicators " - "(ts,symbol,cvd_fast,cvd_mid,cvd_day,cvd_fast_slope,atr_5m,atr_percentile,vwap_30m,price,p95_qty,p99_qty,score,signal,factors) " - "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", - (ts, symbol, result["cvd_fast"], result["cvd_mid"], result["cvd_day"], result["cvd_fast_slope"], + "(ts,symbol,strategy,cvd_fast,cvd_mid,cvd_day,cvd_fast_slope,atr_5m,atr_percentile,vwap_30m,price,p95_qty,p99_qty,score,signal,factors) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + (ts, symbol, strategy, result["cvd_fast"], result["cvd_mid"], result["cvd_day"], result["cvd_fast_slope"], result["atr"], result["atr_pct"], result["vwap"], result["price"], result["p95"], result["p99"], result["score"], result.get("signal"), factors_json) ) @@ -990,14 +990,18 @@ def main(): strategy_result = state.evaluate_signal(now_ms, strategy_cfg=strategy_cfg, snapshot=snapshot) strategy_results.append((strategy_cfg, strategy_result)) + # 每个策略独立存储indicator + for strategy_cfg, strategy_result in strategy_results: + sname = strategy_cfg.get("name", "v51_baseline") + save_indicator(now_ms, sym, strategy_result, strategy=sname) + + # 1m表仍用primary(图表用) primary_result = strategy_results[0][1] for strategy_cfg, strategy_result in strategy_results: if strategy_cfg.get("name") == primary_strategy_name: primary_result = strategy_result break - save_indicator(now_ms, sym, primary_result) - bar_1m = (now_ms // 60000) * 60000 if last_1m_save.get(sym) != bar_1m: save_indicator_1m(now_ms, sym, primary_result) diff --git a/docs/AB_TEST_CHECKLIST.md b/docs/AB_TEST_CHECKLIST.md new file mode 100644 index 0000000..ce1065b --- /dev/null +++ b/docs/AB_TEST_CHECKLIST.md @@ -0,0 +1,36 @@ +# AB测试观测清单(2026-03-02 ~ 03-16) + +## 冻结期规则 +- 不改权重、不改阈值、不改评分逻辑 +- 如需改动必须打新版本号并分段统计 +- 单写入源(小周生产环境) + +## 两周后评审项目 + +### 1. 确认层重复计分审计 +- **问题**:方向层和确认层都用CVD_fast/CVD_mid,同源重复 +- **审计方法**:统计确认层=15 vs 确认层=0时的胜率差异 +- **如果差异不显著**:V5.3降权或重构为"CVD斜率加速+趋势强度" + +### 2. 拥挤层 vs FR相关性 +- **审计**:`corr(FR_score, crowd_score)` +- **如果>0.7**:说明重复表达,降一层权重 + +### 3. OI持续性审计 +- **字段**:`oi_persist_n`(连续同向窗口数)— 目前未记录,需V5.3加 +- **审计**:高分单里`oi_persist_n=1`的胜率是否显著差于`>=2` +- **如果差异明显**:升为正式门槛 + +### 4. 清算触发率审计(按币种) +- 各币种清算信号触发率 +- 触发后净R分布 +- 避免某币种几乎不触发/过度触发 + +### 5. config_hash落库(V5.3) +- 每笔强制落库:`strategy`, `strategy_version`, `config_hash`, `engine_instance` +- 报表按config_hash分组 + +## 数据目标 +- V5.1:500+笔(当前282) +- V5.2:200+笔(当前12) +- 每策略每币种50+笔 diff --git a/frontend/app/signals-v52/page.tsx b/frontend/app/signals-v52/page.tsx index 09dbda0..d9cb012 100644 --- a/frontend/app/signals-v52/page.tsx +++ b/frontend/app/signals-v52/page.tsx @@ -173,7 +173,7 @@ function SignalHistory({ symbol }: { symbol: Symbol }) { useEffect(() => { const fetchData = async () => { try { - const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20`); + const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20&strategy=v52_8signals`); if (!res.ok) return; const json = await res.json(); setData(json.data || []); @@ -225,7 +225,7 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) { useEffect(() => { const fetch = async () => { try { - const res = await authFetch("/api/signals/latest"); + const res = await authFetch("/api/signals/latest?strategy=v52_8signals"); if (!res.ok) return; const json = await res.json(); setData(json[symbol] || null); diff --git a/frontend/app/signals/page.tsx b/frontend/app/signals/page.tsx index 8e45496..c2e78e0 100644 --- a/frontend/app/signals/page.tsx +++ b/frontend/app/signals/page.tsx @@ -175,7 +175,7 @@ function SignalHistory({ symbol }: { symbol: Symbol }) { useEffect(() => { const fetchData = async () => { try { - const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20`); + const res = await authFetch(`/api/signals/signal-history?symbol=${symbol}&limit=20&strategy=v51_baseline`); if (!res.ok) return; const json = await res.json(); setData(json.data || []); @@ -227,7 +227,7 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) { useEffect(() => { const fetch = async () => { try { - const res = await authFetch("/api/signals/latest"); + const res = await authFetch("/api/signals/latest?strategy=v51_baseline"); if (!res.ok) return; const json = await res.json(); setData(json[symbol] || null);