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)
|
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]}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user