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
This commit is contained in:
root 2026-03-02 02:52:17 +00:00
parent 7dee6bffbd
commit 7ebdb98643
5 changed files with 57 additions and 17 deletions

View File

@ -422,18 +422,17 @@ async def get_signal_indicators(
@app.get("/api/signals/latest") @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 = {} result = {}
for sym in SYMBOLS: for sym in SYMBOLS:
row = await async_fetchrow( row = await async_fetchrow(
"SELECT ts, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, " "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 " "vwap_30m, price, p95_qty, p99_qty, score, signal, factors "
"FROM signal_indicators WHERE symbol = $1 ORDER BY ts DESC LIMIT 1", "FROM signal_indicators WHERE symbol = $1 AND strategy = $2 ORDER BY ts DESC LIMIT 1",
sym sym, strategy
) )
if row: if row:
data = dict(row) data = dict(row)
# factors可能是JSON stringpsycopg2写入需要解析
if isinstance(data.get("factors"), str): if isinstance(data.get("factors"), str):
try: try:
data["factors"] = json.loads(data["factors"]) 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( async def get_signal_history(
symbol: str = "BTC", symbol: str = "BTC",
limit: int = 50, limit: int = 50,
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 FROM signal_indicators "
"WHERE symbol = $1 AND signal IS NOT NULL " "WHERE symbol = $1 AND strategy = $2 AND signal IS NOT NULL "
"ORDER BY ts DESC LIMIT $2", "ORDER BY ts DESC LIMIT $3",
sym_full, limit sym_full, strategy, limit
) )
return {"symbol": symbol, "count": len(rows), "data": rows} return {"symbol": symbol, "count": len(rows), "data": rows}

View File

@ -684,16 +684,16 @@ def fetch_new_trades(symbol: str, last_id: int) -> list:
for r in cur.fetchall()] 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 get_sync_conn() as conn:
with conn.cursor() as cur: with conn.cursor() as cur:
import json as _json3 import json as _json3
factors_json = _json3.dumps(result.get("factors")) if result.get("factors") else None factors_json = _json3.dumps(result.get("factors")) if result.get("factors") else None
cur.execute( cur.execute(
"INSERT INTO signal_indicators " "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) " "(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)", "VALUES (%s,%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, result["cvd_fast"], result["cvd_mid"], result["cvd_day"], result["cvd_fast_slope"],
result["atr"], result["atr_pct"], result["vwap"], result["price"], result["atr"], result["atr_pct"], result["vwap"], result["price"],
result["p95"], result["p99"], result["score"], result.get("signal"), factors_json) 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_result = state.evaluate_signal(now_ms, strategy_cfg=strategy_cfg, snapshot=snapshot)
strategy_results.append((strategy_cfg, strategy_result)) 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] primary_result = strategy_results[0][1]
for strategy_cfg, strategy_result in strategy_results: for strategy_cfg, strategy_result in strategy_results:
if strategy_cfg.get("name") == primary_strategy_name: if strategy_cfg.get("name") == primary_strategy_name:
primary_result = strategy_result primary_result = strategy_result
break break
save_indicator(now_ms, sym, primary_result)
bar_1m = (now_ms // 60000) * 60000 bar_1m = (now_ms // 60000) * 60000
if last_1m_save.get(sym) != bar_1m: if last_1m_save.get(sym) != bar_1m:
save_indicator_1m(now_ms, sym, primary_result) save_indicator_1m(now_ms, sym, primary_result)

36
docs/AB_TEST_CHECKLIST.md Normal file
View File

@ -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.1500+笔当前282
- V5.2200+笔当前12
- 每策略每币种50+笔

View File

@ -173,7 +173,7 @@ function SignalHistory({ symbol }: { symbol: Symbol }) {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { 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; if (!res.ok) return;
const json = await res.json(); const json = await res.json();
setData(json.data || []); setData(json.data || []);
@ -225,7 +225,7 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) {
useEffect(() => { useEffect(() => {
const fetch = async () => { const fetch = async () => {
try { try {
const res = await authFetch("/api/signals/latest"); const res = await authFetch("/api/signals/latest?strategy=v52_8signals");
if (!res.ok) return; if (!res.ok) return;
const json = await res.json(); const json = await res.json();
setData(json[symbol] || null); setData(json[symbol] || null);

View File

@ -175,7 +175,7 @@ function SignalHistory({ symbol }: { symbol: Symbol }) {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { 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; if (!res.ok) return;
const json = await res.json(); const json = await res.json();
setData(json.data || []); setData(json.data || []);
@ -227,7 +227,7 @@ function IndicatorCards({ symbol }: { symbol: Symbol }) {
useEffect(() => { useEffect(() => {
const fetch = async () => { const fetch = async () => {
try { try {
const res = await authFetch("/api/signals/latest"); const res = await authFetch("/api/signals/latest?strategy=v51_baseline");
if (!res.ok) return; if (!res.ok) return;
const json = await res.json(); const json = await res.json();
setData(json[symbol] || null); setData(json[symbol] || null);