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:
parent
7dee6bffbd
commit
7ebdb98643
@ -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}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
36
docs/AB_TEST_CHECKLIST.md
Normal file
36
docs/AB_TEST_CHECKLIST.md
Normal 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.1:500+笔(当前282)
|
||||
- V5.2:200+笔(当前12)
|
||||
- 每策略每币种50+笔
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user