feat: add strategy-plaza backend API endpoints

This commit is contained in:
root 2026-03-07 06:21:15 +00:00
parent 8cab470231
commit 602d9ae034

View File

@ -1975,3 +1975,207 @@ async def live_config_update(request: Request, user: dict = Depends(get_current_
) )
updated.append(key) updated.append(key)
return {"updated": updated} 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]}