diff --git a/backend/main.py b/backend/main.py index 70932a6..c7c2c7a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1975,3 +1975,207 @@ async def live_config_update(request: Request, user: dict = Depends(get_current_ ) updated.append(key) return {"updated": updated} + + +# ───────────────────────────────────────────── +# 策略广场 API +# ───────────────────────────────────────────── + +import json as _json +import os as _os +import statistics as _statistics + +_STRATEGY_META = { + "v53": { + "display_name": "V5.3 标准版", + "cvd_windows": "30m / 4h", + "description": "标准版:30分钟+4小时CVD双轨,适配1小时信号周期", + "initial_balance": 10000, + }, + "v53_fast": { + "display_name": "V5.3 Fast版", + "cvd_windows": "5m / 30m", + "description": "快速版:5分钟+30分钟CVD双轨,捕捉短期动量", + "initial_balance": 10000, + }, + "v53_middle": { + "display_name": "V5.3 Middle版", + "cvd_windows": "15m / 1h", + "description": "中速版:15分钟+1小时CVD双轨,平衡噪音与时效", + "initial_balance": 10000, + }, +} + + +async def _get_strategy_status(strategy_id: str) -> str: + """根据 paper_config 和最新心跳判断策略状态""" + config_path = _os.path.join(_os.path.dirname(__file__), "paper_config.json") + try: + with open(config_path) as f: + config = _json.load(f) + enabled = strategy_id in config.get("enabled_strategies", []) + except Exception: + enabled = False + + if not enabled: + return "paused" + + # 检查最近5分钟内是否有心跳 + cutoff = int((__import__("time").time() - 300) * 1000) + row = await async_fetch( + "SELECT ts FROM signal_indicators WHERE strategy=$1 AND ts > $2 ORDER BY ts DESC LIMIT 1", + strategy_id, cutoff + ) + if row: + return "running" + return "error" + + +@app.get("/api/strategy-plaza") +async def strategy_plaza(user: dict = Depends(get_current_user)): + """策略广场总览:返回所有策略卡片数据""" + now_ms = int(__import__("time").time() * 1000) + cutoff_24h = now_ms - 86400000 + + results = [] + for sid, meta in _STRATEGY_META.items(): + # 累计统计 + rows = await async_fetch( + "SELECT pnl_r, entry_ts, exit_ts FROM paper_trades " + "WHERE strategy=$1 AND exit_ts IS NOT NULL", + sid + ) + # 活跃持仓 + open_rows = await async_fetch( + "SELECT COUNT(*) as cnt FROM paper_trades WHERE strategy=$1 AND exit_ts IS NULL", + sid + ) + open_positions = int(open_rows[0]["cnt"]) if open_rows else 0 + + # 24h 统计 + rows_24h = [r for r in rows if (r["exit_ts"] or 0) >= cutoff_24h] + + pnl_rs = [float(r["pnl_r"]) for r in rows] + wins = [p for p in pnl_rs if p > 0] + losses = [p for p in pnl_rs if p <= 0] + net_r = round(sum(pnl_rs), 3) + net_usdt = round(net_r * 200, 0) + + pnl_rs_24h = [float(r["pnl_r"]) for r in rows_24h] + pnl_r_24h = round(sum(pnl_rs_24h), 3) + pnl_usdt_24h = round(pnl_r_24h * 200, 0) + + std_r = round(_statistics.stdev(pnl_rs), 3) if len(pnl_rs) > 1 else 0.0 + + started_at = min(r["entry_ts"] for r in rows) if rows else now_ms + last_trade_at = max(r["exit_ts"] for r in rows if r["exit_ts"]) if rows else None + + status = await _get_strategy_status(sid) + + results.append({ + "id": sid, + "display_name": meta["display_name"], + "status": status, + "started_at": started_at, + "initial_balance": meta["initial_balance"], + "current_balance": meta["initial_balance"] + int(net_usdt), + "net_usdt": int(net_usdt), + "net_r": net_r, + "trade_count": len(pnl_rs), + "win_rate": round(len(wins) / len(pnl_rs) * 100, 1) if pnl_rs else 0.0, + "avg_win_r": round(sum(wins) / len(wins), 3) if wins else 0.0, + "avg_loss_r": round(sum(losses) / len(losses), 3) if losses else 0.0, + "open_positions": open_positions, + "pnl_usdt_24h": int(pnl_usdt_24h), + "pnl_r_24h": pnl_r_24h, + "std_r": std_r, + "last_trade_at": last_trade_at, + }) + + return {"strategies": results} + + +@app.get("/api/strategy-plaza/{strategy_id}/summary") +async def strategy_plaza_summary(strategy_id: str, user: dict = Depends(get_current_user)): + """策略详情 summary:卡片数据 + 详情字段""" + if strategy_id not in _STRATEGY_META: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Strategy not found") + + # 先拿广场数据 + plaza_data = await strategy_plaza(user) + card = next((s for s in plaza_data["strategies"] if s["id"] == strategy_id), None) + if not card: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Strategy not found") + + meta = _STRATEGY_META[strategy_id] + + # 读策略 JSON 获取权重和阈值 + strategy_file = _os.path.join(_os.path.dirname(__file__), "strategies", f"{strategy_id}.json") + weights = {} + thresholds = {} + symbols = [] + try: + with open(strategy_file) as f: + cfg = _json.load(f) + weights = { + "direction": cfg.get("direction_weight", 55), + "crowding": cfg.get("crowding_weight", 25), + "environment": cfg.get("environment_weight", 15), + "auxiliary": cfg.get("auxiliary_weight", 5), + } + thresholds = { + "signal_threshold": cfg.get("threshold", 75), + "flip_threshold": cfg.get("flip_threshold", 85), + } + symbols = list(cfg.get("symbol_gates", {}).keys()) + except Exception: + pass + + return { + **card, + "cvd_windows": meta["cvd_windows"], + "description": meta["description"], + "symbols": symbols, + "weights": weights, + "thresholds": thresholds, + } + + +@app.get("/api/strategy-plaza/{strategy_id}/signals") +async def strategy_plaza_signals( + strategy_id: str, + limit: int = 50, + user: dict = Depends(get_current_user) +): + """策略信号列表(复用现有逻辑,加 strategy 过滤)""" + if strategy_id not in _STRATEGY_META: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Strategy not found") + rows = await async_fetch( + "SELECT ts, symbol, score, signal, price, factors, cvd_5m, cvd_15m, cvd_30m, cvd_1h, cvd_4h, atr_value " + "FROM signal_indicators WHERE strategy=$1 ORDER BY ts DESC LIMIT $2", + strategy_id, limit + ) + return {"signals": [dict(r) for r in rows]} + + +@app.get("/api/strategy-plaza/{strategy_id}/trades") +async def strategy_plaza_trades( + strategy_id: str, + limit: int = 50, + user: dict = Depends(get_current_user) +): + """策略交易记录(复用现有逻辑,加 strategy 过滤)""" + if strategy_id not in _STRATEGY_META: + from fastapi import HTTPException + raise HTTPException(status_code=404, detail="Strategy not found") + rows = await async_fetch( + "SELECT id, symbol, direction, score, tier, entry_price, exit_price, " + "tp1_price, tp2_price, sl_price, tp1_hit, pnl_r, risk_distance, " + "entry_ts, exit_ts, status, strategy " + "FROM paper_trades WHERE strategy=$1 ORDER BY entry_ts DESC LIMIT $2", + strategy_id, limit + ) + return {"trades": [dict(r) for r in rows]}