feat: add strategy-plaza backend API endpoints
This commit is contained in:
parent
8cab470231
commit
602d9ae034
204
backend/main.py
204
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]}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user