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")
|
@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 string(psycopg2写入),需要解析
|
|
||||||
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}
|
||||||
|
|
||||||
|
|||||||
@ -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
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(() => {
|
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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user