1
This commit is contained in:
parent
ed90476942
commit
8d8b5c6f15
@ -1684,11 +1684,19 @@ def get_paper_trading_summary(days: int = 30) -> dict:
|
||||
cutoff = (datetime.now() - timedelta(days=days)).isoformat()
|
||||
conn = get_conn()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
open_rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM paper_trades
|
||||
WHERE opened_at >= %s
|
||||
WHERE status='open'
|
||||
ORDER BY opened_at DESC, id DESC
|
||||
"""
|
||||
).fetchall()
|
||||
closed_rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM paper_trades
|
||||
WHERE status='closed'
|
||||
AND COALESCE(closed_at, updated_at, opened_at) >= %s
|
||||
ORDER BY COALESCE(closed_at, updated_at, opened_at) DESC, id DESC
|
||||
""",
|
||||
(cutoff,),
|
||||
).fetchall()
|
||||
@ -1696,7 +1704,7 @@ def get_paper_trading_summary(days: int = 30) -> dict:
|
||||
finally:
|
||||
conn.close()
|
||||
cfg = paper_trading_config()
|
||||
items = [_decorate_trade(dict(r), cfg) for r in rows]
|
||||
items = [_decorate_trade(dict(r), cfg) for r in [*open_rows, *closed_rows]]
|
||||
open_items = [x for x in items if x.get("status") == "open"]
|
||||
closed_items = [x for x in items if x.get("status") == "closed"]
|
||||
wins = [x for x in closed_items if _safe_float(x.get("realized_pnl_pct")) > 0]
|
||||
|
||||
@ -140,8 +140,9 @@ def _opportunity_review(conn, since):
|
||||
|
||||
def _paper_review(conn, since, days):
|
||||
summary = get_paper_trading_summary(days=days)
|
||||
trade_attribution = (get_strategy_insights().get("trade_attribution") or {})
|
||||
watch_order_attribution = (get_strategy_insights().get("watch_order_attribution") or {})
|
||||
strategy_insights = get_strategy_insights(days=days)
|
||||
trade_attribution = (strategy_insights.get("trade_attribution") or {})
|
||||
watch_order_attribution = (strategy_insights.get("watch_order_attribution") or {})
|
||||
trades = [dict(r) for r in conn.execute(
|
||||
"""
|
||||
SELECT *
|
||||
|
||||
@ -261,13 +261,14 @@ def get_strategy_evaluation(days: int = 30) -> dict:
|
||||
|
||||
for row in conn.execute(
|
||||
"""
|
||||
SELECT strategy_code,
|
||||
SELECT COALESCE(NULLIF(po.strategy_code, ''), r.strategy_code) AS strategy_code,
|
||||
COUNT(*) AS order_count,
|
||||
SUM(CASE WHEN status='filled' THEN 1 ELSE 0 END) AS filled_count,
|
||||
SUM(CASE WHEN status IN ('canceled','rejected','expired') THEN 1 ELSE 0 END) AS canceled_count
|
||||
FROM paper_orders
|
||||
WHERE created_at >= %s
|
||||
GROUP BY strategy_code
|
||||
SUM(CASE WHEN po.status='filled' THEN 1 ELSE 0 END) AS filled_count,
|
||||
SUM(CASE WHEN po.status IN ('canceled','rejected','expired') THEN 1 ELSE 0 END) AS canceled_count
|
||||
FROM paper_orders po
|
||||
LEFT JOIN recommendation r ON r.id = po.recommendation_id
|
||||
WHERE po.created_at >= %s
|
||||
GROUP BY COALESCE(NULLIF(po.strategy_code, ''), r.strategy_code)
|
||||
""",
|
||||
(since,),
|
||||
).fetchall():
|
||||
@ -278,11 +279,15 @@ def get_strategy_evaluation(days: int = 30) -> dict:
|
||||
|
||||
for row in conn.execute(
|
||||
"""
|
||||
SELECT strategy_code, side, status, realized_pnl_pct, realized_pnl_usdt, pnl_pct
|
||||
FROM paper_trades
|
||||
WHERE opened_at >= %s
|
||||
SELECT COALESCE(NULLIF(pt.strategy_code, ''), r.strategy_code) AS strategy_code,
|
||||
pt.side,
|
||||
pt.status
|
||||
FROM paper_trades pt
|
||||
LEFT JOIN recommendation r ON r.id = pt.recommendation_id
|
||||
WHERE pt.opened_at >= %s
|
||||
OR (pt.status='closed' AND COALESCE(pt.closed_at, pt.updated_at, pt.opened_at) >= %s)
|
||||
""",
|
||||
(since,),
|
||||
(since, since),
|
||||
).fetchall():
|
||||
b = bucket(row.get("strategy_code"))
|
||||
status = row.get("status") or ""
|
||||
@ -294,18 +299,32 @@ def get_strategy_evaluation(days: int = 30) -> dict:
|
||||
b["long_trade_count"] += 1
|
||||
if status == "open":
|
||||
b["open_trade_count"] += 1
|
||||
if status == "closed":
|
||||
b["closed_trade_count"] += 1
|
||||
pnl_pct = _safe_float(row.get("realized_pnl_pct"))
|
||||
pnl_usdt = _safe_float(row.get("realized_pnl_usdt"))
|
||||
b["realized_pnl_usdt"] += pnl_usdt
|
||||
b["pnl_pct_values"].append(pnl_pct)
|
||||
if pnl_pct > 0:
|
||||
b["win_count"] += 1
|
||||
elif pnl_pct < 0:
|
||||
b["loss_count"] += 1
|
||||
b["best_pnl_pct"] = pnl_pct if b["best_pnl_pct"] is None else max(b["best_pnl_pct"], pnl_pct)
|
||||
b["worst_pnl_pct"] = pnl_pct if b["worst_pnl_pct"] is None else min(b["worst_pnl_pct"], pnl_pct)
|
||||
|
||||
for row in conn.execute(
|
||||
"""
|
||||
SELECT COALESCE(NULLIF(pt.strategy_code, ''), r.strategy_code) AS strategy_code,
|
||||
pt.side,
|
||||
pt.realized_pnl_pct,
|
||||
pt.realized_pnl_usdt
|
||||
FROM paper_trades pt
|
||||
LEFT JOIN recommendation r ON r.id = pt.recommendation_id
|
||||
WHERE pt.status='closed'
|
||||
AND COALESCE(pt.closed_at, pt.updated_at, pt.opened_at) >= %s
|
||||
""",
|
||||
(since,),
|
||||
).fetchall():
|
||||
b = bucket(row.get("strategy_code"))
|
||||
b["closed_trade_count"] += 1
|
||||
pnl_pct = _safe_float(row.get("realized_pnl_pct"))
|
||||
pnl_usdt = _safe_float(row.get("realized_pnl_usdt"))
|
||||
b["realized_pnl_usdt"] += pnl_usdt
|
||||
b["pnl_pct_values"].append(pnl_pct)
|
||||
if pnl_pct > 0:
|
||||
b["win_count"] += 1
|
||||
elif pnl_pct < 0:
|
||||
b["loss_count"] += 1
|
||||
b["best_pnl_pct"] = pnl_pct if b["best_pnl_pct"] is None else max(b["best_pnl_pct"], pnl_pct)
|
||||
b["worst_pnl_pct"] = pnl_pct if b["worst_pnl_pct"] is None else min(b["worst_pnl_pct"], pnl_pct)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@ -346,16 +365,30 @@ def get_strategy_evaluation(days: int = 30) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def get_strategy_insights():
|
||||
def get_strategy_insights(days: int | None = None):
|
||||
"""Strategy attribution based on opportunity and paper-trading conversion.
|
||||
|
||||
Recommendation rows are opportunities/signals, not an execution ledger.
|
||||
Therefore this read model does not use recommendation.pnl_pct as strategy
|
||||
PnL. Paper-trading PnL is exposed only as an execution-conversion metric.
|
||||
"""
|
||||
if days is not None:
|
||||
days = max(1, min(_safe_int(days, 30), 365))
|
||||
since = (datetime.now() - timedelta(days=days)).isoformat()
|
||||
rec_where = "WHERE r.rec_time >= %s"
|
||||
rec_params = (since,)
|
||||
trade_where = "WHERE pt.status='closed' AND COALESCE(pt.closed_at, pt.updated_at, pt.opened_at) >= %s"
|
||||
trade_params = (since,)
|
||||
else:
|
||||
days = None
|
||||
since = None
|
||||
rec_where = ""
|
||||
rec_params = ()
|
||||
trade_where = "WHERE pt.status='closed'"
|
||||
trade_params = ()
|
||||
conn = get_conn()
|
||||
rows = conn.execute(
|
||||
"""
|
||||
f"""
|
||||
SELECT
|
||||
r.*,
|
||||
pt.id AS paper_trade_id,
|
||||
@ -374,18 +407,46 @@ def get_strategy_insights():
|
||||
FROM recommendation r
|
||||
LEFT JOIN paper_trades pt ON pt.recommendation_id = r.id
|
||||
LEFT JOIN paper_orders po ON po.recommendation_id = r.id
|
||||
{rec_where}
|
||||
ORDER BY r.rec_time DESC, r.id DESC
|
||||
"""
|
||||
""",
|
||||
rec_params,
|
||||
).fetchall()
|
||||
trade_rows = conn.execute(
|
||||
f"""
|
||||
SELECT
|
||||
r.*,
|
||||
pt.id AS paper_trade_id,
|
||||
pt.status AS paper_status,
|
||||
pt.side AS paper_side,
|
||||
pt.source_status AS paper_source_status,
|
||||
pt.source_action AS paper_source_action,
|
||||
pt.strategy_code AS paper_strategy_code,
|
||||
pt.realized_pnl_pct AS paper_realized_pnl_pct,
|
||||
pt.realized_pnl_usdt AS paper_realized_pnl_usdt,
|
||||
pt.pnl_pct AS paper_pnl_pct,
|
||||
pt.exit_reason AS paper_exit_reason,
|
||||
po.id AS paper_order_id,
|
||||
po.status AS paper_order_status,
|
||||
po.strategy_code AS paper_order_strategy_code
|
||||
FROM paper_trades pt
|
||||
LEFT JOIN recommendation r ON r.id = pt.recommendation_id
|
||||
LEFT JOIN paper_orders po ON po.recommendation_id = pt.recommendation_id
|
||||
{trade_where}
|
||||
ORDER BY COALESCE(pt.closed_at, pt.updated_at, pt.opened_at) DESC, pt.id DESC
|
||||
""",
|
||||
trade_params,
|
||||
).fetchall()
|
||||
conn.close()
|
||||
items = [dict(r) for r in rows]
|
||||
trade_items = [dict(r) for r in trade_rows]
|
||||
|
||||
actionable_statuses = {"buy_now", "wait_pullback"}
|
||||
total = len(items)
|
||||
actionable = [x for x in items if (x.get("execution_status") or "") in actionable_statuses]
|
||||
buy_now = [x for x in items if (x.get("execution_status") or "") == "buy_now"]
|
||||
paper_items = [x for x in items if x.get("paper_trade_id")]
|
||||
closed_paper = [x for x in paper_items if x.get("paper_status") == "closed"]
|
||||
closed_paper = trade_items
|
||||
paper_wins = [x for x in closed_paper if float(x.get("paper_realized_pnl_pct") or 0) > 0]
|
||||
paper_realized_usdt = round(sum(float(x.get("paper_realized_pnl_usdt") or 0) for x in closed_paper), 4)
|
||||
overview = {
|
||||
@ -479,35 +540,45 @@ def get_strategy_insights():
|
||||
add_watch_bucket(watch_map, watch_bucket(item), item)
|
||||
if item.get("paper_order_id"):
|
||||
add_order_bucket(order_map, order_bucket(item), item)
|
||||
if item.get("paper_status") == "closed":
|
||||
for factor in labels:
|
||||
add_trade_bucket(trade_factor_map, str(factor).strip(), item)
|
||||
for group in factor_groups_from_breakdown(factor_breakdown):
|
||||
add_trade_bucket(trade_factor_group_map, group, item)
|
||||
add_trade_bucket(trade_entry_map, trade_entry_bucket(item), item)
|
||||
add_trade_bucket(trade_exit_map, item.get("paper_exit_reason") or "未记录退出原因", item)
|
||||
add_trade_bucket(trade_entry_map, f"方向:{item.get('paper_side') or item.get('side') or 'long'}", item)
|
||||
if item.get("paper_order_id"):
|
||||
add_trade_bucket(trade_entry_map, f"挂单路径:{item.get('paper_order_status') or 'filled'}", item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("机会分", score_components.get("opportunity_score")), item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("买点分", score_components.get("entry_score")), item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("风险分", score_components.get("risk_score")), item)
|
||||
if regime_name:
|
||||
add_trade_bucket(trade_regime_map, f"regime:{regime_name}", item)
|
||||
for bucket in env_buckets_from_market_context(mc):
|
||||
add_trade_bucket(trade_env_map, bucket, item)
|
||||
for code in codes:
|
||||
text = str(code or "").strip()
|
||||
if text.startswith(("sentiment_", "listing_", "ecosystem_")):
|
||||
add_trade_bucket(trade_evidence_map, "舆情:" + text, item)
|
||||
elif text.startswith(("dex_", "liquidity_", "exchange_", "whale_", "smart_money", "holder_", "onchain_")):
|
||||
add_trade_bucket(trade_evidence_map, "链上:" + text, item)
|
||||
if item.get("strategy_version"):
|
||||
add_trade_bucket(trade_version_map, str(item.get("strategy_version")).strip(), item)
|
||||
add_trade_bucket(trade_strategy_code_map, strategy_code, item)
|
||||
for item in trade_items:
|
||||
labels = safe_list_json(item.get("signal_labels_json")) or safe_list_json(item.get("signals"))
|
||||
codes = safe_list_json(item.get("signal_codes_json"))
|
||||
ep = safe_dict_json(item.get("entry_plan_json"))
|
||||
mc = safe_dict_json(item.get("market_context_json"))
|
||||
factor_breakdown = safe_dict_json(mc.get("factor_score_breakdown")) or safe_dict_json(ep.get("factor_score_breakdown"))
|
||||
score_components = safe_dict_json(mc.get("score_components")) or safe_dict_json(ep.get("score_components"))
|
||||
market_regime = safe_dict_json(mc.get("market_regime")) or safe_dict_json(ep.get("market_regime"))
|
||||
regime_name = market_regime.get("regime") or mc.get("market_regime")
|
||||
strategy_code = normalize_strategy_code(item.get("paper_strategy_code") or item.get("strategy_code") or item.get("paper_order_strategy_code"))
|
||||
for factor in labels:
|
||||
add_trade_bucket(trade_factor_map, str(factor).strip(), item)
|
||||
for group in factor_groups_from_breakdown(factor_breakdown):
|
||||
add_trade_bucket(trade_factor_group_map, group, item)
|
||||
add_trade_bucket(trade_entry_map, trade_entry_bucket(item), item)
|
||||
add_trade_bucket(trade_exit_map, item.get("paper_exit_reason") or "未记录退出原因", item)
|
||||
add_trade_bucket(trade_entry_map, f"方向:{item.get('paper_side') or item.get('side') or 'long'}", item)
|
||||
if item.get("paper_order_id"):
|
||||
add_trade_bucket(trade_entry_map, f"挂单路径:{item.get('paper_order_status') or 'filled'}", item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("机会分", score_components.get("opportunity_score")), item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("买点分", score_components.get("entry_score")), item)
|
||||
add_trade_bucket(trade_score_band_map, score_band("风险分", score_components.get("risk_score")), item)
|
||||
if regime_name:
|
||||
add_trade_bucket(trade_regime_map, f"regime:{regime_name}", item)
|
||||
for bucket in env_buckets_from_market_context(mc):
|
||||
add_trade_bucket(trade_env_map, bucket, item)
|
||||
for code in codes:
|
||||
text = str(code or "").strip()
|
||||
if text.startswith(("sentiment_", "listing_", "ecosystem_")):
|
||||
add_trade_bucket(trade_evidence_map, "舆情:" + text, item)
|
||||
elif text.startswith(("dex_", "liquidity_", "exchange_", "whale_", "smart_money", "holder_", "onchain_")):
|
||||
add_trade_bucket(trade_evidence_map, "链上:" + text, item)
|
||||
if item.get("strategy_version"):
|
||||
add_trade_bucket(trade_version_map, str(item.get("strategy_version")).strip(), item)
|
||||
add_trade_bucket(trade_strategy_code_map, strategy_code, item)
|
||||
|
||||
return {
|
||||
"overview": overview,
|
||||
"days": days,
|
||||
"metric_definition": {
|
||||
"opportunity_count": "进入 opportunity/recommendation 表的机会样本数,不代表交易。",
|
||||
"actionable_count": "确认层输出 buy_now 或 wait_pullback 的样本数。",
|
||||
|
||||
@ -57,9 +57,9 @@ function rows(items,label,value,sub){items=items||[];if(!items.length)return '<d
|
||||
function digestItems(items,label,sub){items=items||[];if(!items.length)return '<div class="digest-empty">暂无动作</div>';return items.slice(0,4).map(function(x){return '<div class="digest-item"><b>'+esc(label(x))+'</b><span>'+esc(sub?sub(x):'')+'</span></div>'}).join('')}
|
||||
function renderStrategyDigest(d){var it=d.iteration||{},dig=it.digest||{},latest=dig.latest||{},m=latest.metrics||{},decision=latest.decision||'hold';var badgeCls=decision==='release'?'ok':decision==='gray'?'warn':'warn';$('strategyDigest').innerHTML='<div class="digest-head"><div><div class="digest-title">策略迭代摘要</div><div class="digest-sub">'+esc(latest.title||'暂无复盘')+' · '+time(latest.time)+' · 版本 '+esc(latest.strategy_version||'--')+'</div></div><span class="badge '+badgeCls+'">'+esc(decision)+'</span></div><div class="kpis">'+[['因子生效调整',m.factor_weight_updates||0,'blue'],['候选 / 灰度',(it.summary&&it.summary.candidate_count||0)+' / '+(it.summary&&it.summary.gray_count||0),''],['本轮有效复盘',m.effective_review_count||0,''],['发布状态',latest.reason||'继续观察','']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('')+'</div><div class="digest-grid"><div class="digest-card"><h3>升权了什么</h3>'+digestItems(dig.upgraded,function(x){return x.signal||'--'},function(x){return '权重 '+x.old_weight+' → '+x.new_weight+' · 样本 '+(x.sample_size||0)+' · 命中 '+(x.hit_rate||0)+'%'})+'</div><div class="digest-card"><h3>降权 / 淘汰</h3>'+digestItems(dig.downgraded,function(x){return x.signal||'--'},function(x){return (x.action||'调整')+' · '+(x.old_weight!=null?'权重 '+x.old_weight+' → '+x.new_weight:x.detail||'')})+'</div><div class="digest-card"><h3>灰度观察</h3>'+digestItems(dig.gray,function(x){return x.signal||('候选 #'+x.id)},function(x){return '样本 '+(x.sample_size||0)+' · 置信 '+(x.confidence||0)+'% · '+(x.description||'')})+'</div><div class="digest-card"><h3>最近迭代</h3>'+digestItems(dig.timeline,function(x){return time(x.time)+' · '+(x.decision||'hold')},function(x){return x.reason||x.title||''})+'</div></div>'}
|
||||
function decisionCls(d){return ({promote:'promote',pause:'pause',gray:'gray',tune_entry:'tune_entry',review_entry_gate:'review_entry_gate'}[d]||'')}
|
||||
function renderStrategyBoard(d){var se=d.strategy_evaluation||{},s=se.summary||{},items=se.strategies||[];var cards=items.slice(0,6).map(function(x){var score=Math.max(0,Math.min(100,Number(x.evaluation_score||0)));return '<div class="strategy-card"><div class="strategy-top"><div><div class="strategy-name">'+esc(x.strategy_name||x.strategy_code)+'</div><div class="strategy-desc">'+esc(x.description||'暂无策略说明')+'</div></div><div class="score-ring" style="--s:'+score+'">'+score.toFixed(0)+'</div></div><div class="strategy-metrics"><div class="strategy-metric"><span>信号</span><b>'+esc(x.signal_count||0)+'</b></div><div class="strategy-metric"><span>机会</span><b>'+esc(x.opportunity_count||0)+'</b></div><div class="strategy-metric"><span>平仓</span><b>'+esc(x.closed_trade_count||0)+'</b></div><div class="strategy-metric"><span>胜率</span><b>'+num(x.win_rate_pct,1)+'%</b></div><div class="strategy-metric"><span>收益</span><b>'+usd(x.realized_pnl_usdt)+'</b></div><div class="strategy-metric"><span>均值</span><b>'+pct(x.avg_realized_pnl_pct)+'</b></div><div class="strategy-metric"><span>成交</span><b>'+num(x.order_fill_rate_pct,1)+'%</b></div><div class="strategy-metric"><span>转化</span><b>'+num(x.trade_conversion_pct,1)+'%</b></div></div><div class="strategy-actions"><span class="decision-pill '+decisionCls(x.decision)+'">'+esc(x.decision_label||x.decision)+'</span> '+esc((x.reasons||[])[0]||'继续观察')+'<br>'+esc((x.next_actions||[])[0]||'等待更多样本')+'</div></div>'}).join('')||'<div class="digest-empty">暂无策略评价数据</div>';$('strategyBoard').innerHTML='<div class="digest-head"><div><div class="digest-title">多策略优胜劣汰</div><div class="digest-sub">'+esc(se.definition||'按策略独立评价发现、执行、收益和风险。')+'</div></div><span class="decision-pill">策略 '+esc(s.strategy_count||0)+' · 已交易 '+esc(s.traded_strategy_count||0)+' · 待暂停 '+esc(s.pause_count||0)+'</span></div><div class="strategy-list">'+cards+'</div>'}
|
||||
function renderStrategyBoard(d){var se=d.strategy_evaluation||{},s=se.summary||{},items=se.strategies||[];var cards=items.slice(0,6).map(function(x){var score=Math.max(0,Math.min(100,Number(x.evaluation_score||0)));return '<div class="strategy-card"><div class="strategy-top"><div><div class="strategy-name">'+esc(x.strategy_name||x.strategy_code)+'</div><div class="strategy-desc">'+esc(x.description||'暂无策略说明')+'</div></div><div class="score-ring" style="--s:'+score+'">'+score.toFixed(0)+'</div></div><div class="strategy-metrics"><div class="strategy-metric"><span>信号</span><b>'+esc(x.signal_count||0)+'</b></div><div class="strategy-metric"><span>机会</span><b>'+esc(x.opportunity_count||0)+'</b></div><div class="strategy-metric"><span>窗口平仓</span><b>'+esc(x.closed_trade_count||0)+'</b></div><div class="strategy-metric"><span>胜率</span><b>'+num(x.win_rate_pct,1)+'%</b></div><div class="strategy-metric"><span>收益</span><b>'+usd(x.realized_pnl_usdt)+'</b></div><div class="strategy-metric"><span>均值</span><b>'+pct(x.avg_realized_pnl_pct)+'</b></div><div class="strategy-metric"><span>成交</span><b>'+num(x.order_fill_rate_pct,1)+'%</b></div><div class="strategy-metric"><span>转化</span><b>'+num(x.trade_conversion_pct,1)+'%</b></div></div><div class="strategy-actions"><span class="decision-pill '+decisionCls(x.decision)+'">'+esc(x.decision_label||x.decision)+'</span> '+esc((x.reasons||[])[0]||'继续观察')+'<br>'+esc((x.next_actions||[])[0]||'等待更多样本')+'</div></div>'}).join('')||'<div class="digest-empty">暂无策略评价数据</div>';$('strategyBoard').innerHTML='<div class="digest-head"><div><div class="digest-title">多策略优胜劣汰</div><div class="digest-sub">'+esc(se.definition||'按策略独立评价发现、执行、收益和风险。')+'</div></div><span class="decision-pill">策略 '+esc(s.strategy_count||0)+' · 已交易 '+esc(s.traded_strategy_count||0)+' · 待暂停 '+esc(s.pause_count||0)+'</span></div><div class="strategy-list">'+cards+'</div>'}
|
||||
function renderOpportunity(o){var s=o.summary||{};$('oppDef').textContent=o.definition||'';$('opportunityPanel').innerHTML=kpis([['机会样本',s.total_opportunities||0,'blue'],['可买/等回踩',(s.buy_now_count||0)+' / '+(s.wait_pullback_count||0),''],['策略执行',s.paper_executed_count||0,'green'],['漏选爆发',s.missed_explosion_count||0,'red'],['有效复盘',s.effective_review_count||0,''],['机会命中',num(s.opportunity_hit_rate,1)+'%','green'],['观察样本',s.observe_count||0,''],['失效样本',s.invalid_count||0,'red']])+'<div class="split"><div><div class="mini-title">状态分布</div>'+rows(o.status_distribution,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">复盘结果</div>'+rows(o.outcome_distribution,function(x){return x.name||'--'},function(x){return x.count})+'</div></div>'}
|
||||
function renderPaper(p){var s=p.summary||{},ta=p.trade_attribution||{},wo=p.watch_order_attribution||{},tf=ta.factor||[],te=ta.entry_path||[],tg=ta.factor_group||[],ts=ta.strategy_code||[],wr=wo.watch_pool||[],orows=wo.paper_orders||[];$('paperDef').textContent=p.definition||'';$('paperPanel').innerHTML=kpis([['当前余额','$'+num(s.current_balance_usdt,2),'blue'],['总收益',usd(s.total_pnl_usdt),Number(s.total_pnl_usdt||0)>=0?'green':'red'],['账户收益率',pct(s.account_total_return_pct),Number(s.account_total_return_pct||0)>=0?'green':'red'],['胜率',num(s.win_rate,1)+'%',''],['开仓/平仓',(s.open_count||0)+' / '+(s.closed_count||0),''],['已实现',usd(s.realized_pnl_usdt),Number(s.realized_pnl_usdt||0)>=0?'green':'red'],['未实现',usd(s.open_unrealized_pnl_usdt),Number(s.open_unrealized_pnl_usdt||0)>=0?'green':'red'],['累计杠杆',num(s.cumulative_leverage,2)+'x','']])+'<div class="split"><div><div class="mini-title">策略表现</div>'+rows(ts.slice(0,8),function(x){return x.strategy_name||x.strategy_code||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">退出原因</div>'+rows(p.exit_reasons,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">执行事件</div>'+rows(p.event_types,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">真实交易因子</div>'+rows(tf.slice(0,6),function(x){return x.factor||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">因子组表现</div>'+rows(tg.slice(0,6),function(x){return x.factor_group||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">入场路径表现</div>'+rows(te.slice(0,6),function(x){return x.entry_path||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">观察/挂单推进</div>'+rows(wr.concat(orows).slice(0,6),function(x){return x.watch_bucket||x.order_bucket||'--'},function(x){return (x.executed_pct!=null?num(x.executed_pct,1):num(x.fill_pct,1))+'%'},function(x){return '样本 '+(x.opportunity_count||x.order_count||0)+' · 执行/成交 '+(x.executed_count||x.filled_count||0)})+'</div></div>'}
|
||||
function renderPaper(p){var s=p.summary||{},ta=p.trade_attribution||{},wo=p.watch_order_attribution||{},tf=ta.factor||[],te=ta.entry_path||[],tg=ta.factor_group||[],ts=ta.strategy_code||[],wr=wo.watch_pool||[],orows=wo.paper_orders||[];$('paperDef').textContent=p.definition||'';$('paperPanel').innerHTML=kpis([['当前余额','$'+num(s.current_balance_usdt,2),'blue'],['总收益',usd(s.total_pnl_usdt),Number(s.total_pnl_usdt||0)>=0?'green':'red'],['账户收益率',pct(s.account_total_return_pct),Number(s.account_total_return_pct||0)>=0?'green':'red'],['胜率',num(s.win_rate,1)+'%',''],['当前持仓/窗口平仓',(s.open_count||0)+' / '+(s.closed_count||0),''],['窗口已实现',usd(s.realized_pnl_usdt),Number(s.realized_pnl_usdt||0)>=0?'green':'red'],['未实现',usd(s.open_unrealized_pnl_usdt),Number(s.open_unrealized_pnl_usdt||0)>=0?'green':'red'],['累计杠杆',num(s.cumulative_leverage,2)+'x','']])+'<div class="split"><div><div class="mini-title">策略表现</div>'+rows(ts.slice(0,8),function(x){return x.strategy_name||x.strategy_code||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '窗口平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">退出原因</div>'+rows(p.exit_reasons,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">执行事件</div>'+rows(p.event_types,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">真实交易因子</div>'+rows(tf.slice(0,6),function(x){return x.factor||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '窗口平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'% · 均值 '+pct(x.avg_realized_pnl_pct)})+'</div><div><div class="mini-title">因子组表现</div>'+rows(tg.slice(0,6),function(x){return x.factor_group||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '窗口平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">入场路径表现</div>'+rows(te.slice(0,6),function(x){return x.entry_path||'--'},function(x){return usd(x.realized_pnl_usdt)},function(x){return '窗口平仓 '+(x.closed_trade_count||0)+' · 胜率 '+num(x.win_rate_pct,1)+'%'})+'</div><div><div class="mini-title">观察/挂单推进</div>'+rows(wr.concat(orows).slice(0,6),function(x){return x.watch_bucket||x.order_bucket||'--'},function(x){return (x.executed_pct!=null?num(x.executed_pct,1):num(x.fill_pct,1))+'%'},function(x){return '样本 '+(x.opportunity_count||x.order_count||0)+' · 执行/成交 '+(x.executed_count||x.filled_count||0)})+'</div></div>'}
|
||||
function renderEvidence(e){var s=e.summary||{};$('evidenceDef').textContent=e.definition||'';$('evidencePanel').innerHTML=kpis([['新闻事件',s.news_count||0,'blue'],['有效舆情',s.actionable_news_count||0,''],['链上信号',s.onchain_signal_count||0,'blue'],['高置信链上',s.high_confidence_onchain_count||0,'green'],['原始链上',s.raw_onchain_count||0,''],['已映射原始',s.mapped_raw_onchain_count||0,'green'],['LLM 调用',s.llm_runs||0,''],['LLM 成功',s.llm_success_count||0,'green']])+'<div class="split"><div><div class="mini-title">链上信号</div>'+rows(e.onchain_signals,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">舆情决策</div>'+rows(e.news_decisions,function(x){return x.name||'未处理'},function(x){return x.count})+'</div></div>'}
|
||||
function renderIteration(i){var s=i.summary||{};$('iterationDef').textContent=i.definition||'';$('iterationPanel').innerHTML=kpis([['迭代记录',s.iteration_count||0,'blue'],['候选规则',s.candidate_count||0,''],['灰度规则',s.gray_count||0,'green'],['生效规则',s.active_count||0,'green']])+'<div class="row" style="margin-bottom:10px"><div class="row-main"><div class="row-title">最新发布结论:'+esc(s.latest_release_decision||'hold')+'</div><div class="row-sub">'+esc(s.latest_release_reason||'暂无发布说明')+'</div></div><div class="value">闸门</div></div><div class="split"><div><div class="mini-title">发布决策</div>'+rows(i.release_decisions,function(x){return x.name||'--'},function(x){return x.count})+'</div><div><div class="mini-title">候选状态</div>'+rows(i.candidate_status,function(x){return x.name||'--'},function(x){return x.count})+'</div></div>'}
|
||||
function renderRecent(d){var opp=(d.opportunity&&d.opportunity.missed_explosions)||[], trades=(d.paper_trading&&d.paper_trading.recent_trades)||[], news=(d.evidence&&d.evidence.recent_news)||[], chain=(d.evidence&&d.evidence.recent_onchain)||[];$('recentPanel').innerHTML='<div class="split"><div><div class="mini-title">最近策略交易</div>'+rows(trades.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.status||'--')},function(x){return x.status==='closed'?pct(x.realized_pnl_pct):pct(x.pnl_pct)},function(x){return time(x.opened_at)+' · '+(x.exit_reason||x.source_status||'')})+'</div><div><div class="mini-title">漏选爆发</div>'+rows(opp.slice(0,8),function(x){return x.symbol||'--'},function(x){return pct(x.gain_pct)},function(x){return time(x.detect_time)+' · '+(x.reason_missed||'')})+'</div><div><div class="mini-title">舆情事件</div>'+rows(news.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.title||'--')},function(x){return x.importance||'--'},function(x){return time(x.detected_at)+' · '+(x.source||'')+' · '+(x.decision||'未处理')})+'</div><div><div class="mini-title">链上信号</div>'+rows(chain.slice(0,8),function(x){return (x.symbol||'--')+' · '+(x.signal_label||x.signal_code||'--')},function(x){return x.severity||x.confidence||'--'},function(x){return time(x.detected_at)+' · '+(x.source||'')+' · '+(x.direction||'')})+'</div></div>'}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from fastapi.testclient import TestClient
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from app.db import auth_db
|
||||
from app.db.review_center import get_review_center_dashboard
|
||||
@ -86,6 +86,70 @@ def test_review_center_separates_opportunity_and_paper_pnl(pg_conn):
|
||||
assert "watch_order_attribution" in data["paper_trading"]
|
||||
|
||||
|
||||
def test_review_center_strategy_counts_use_same_closed_trade_window(pg_conn):
|
||||
now = datetime.now()
|
||||
recent_close = now - timedelta(days=2)
|
||||
old_close = now - timedelta(days=40)
|
||||
old_open_recent_close = now - timedelta(days=20)
|
||||
outside_open = now - timedelta(days=60)
|
||||
strategy_code = "volume_ignition_1h_v1"
|
||||
|
||||
pg_conn.execute(
|
||||
"""
|
||||
INSERT INTO recommendation (
|
||||
id, symbol, rec_time, rec_state, rec_score, entry_price,
|
||||
status, execution_status, action_status, display_bucket, entry_triggered,
|
||||
strategy_code, strategy_version, entry_plan_json
|
||||
) VALUES
|
||||
(101, 'WIN/USDT', %s, '爆发', 30, 10, 'active', 'buy_now', '可即刻买入', 'realtime', 1, %s, 'v-test', %s),
|
||||
(102, 'OLD/USDT', %s, '爆发', 30, 10, 'active', 'buy_now', '可即刻买入', 'realtime', 1, %s, 'v-test', %s)
|
||||
""",
|
||||
(
|
||||
old_open_recent_close.isoformat(timespec="seconds"),
|
||||
strategy_code,
|
||||
json.dumps({"strategy_code": strategy_code}, ensure_ascii=False),
|
||||
outside_open.isoformat(timespec="seconds"),
|
||||
strategy_code,
|
||||
json.dumps({"strategy_code": strategy_code}, ensure_ascii=False),
|
||||
),
|
||||
)
|
||||
pg_conn.execute(
|
||||
"""
|
||||
INSERT INTO paper_trades (
|
||||
recommendation_id, symbol, side, status, opened_at, closed_at,
|
||||
entry_price, exit_price, qty, notional_usdt, margin_usdt, leverage,
|
||||
current_price, pnl_pct, realized_pnl_pct, realized_pnl_usdt,
|
||||
source_status, source_action, strategy_code, created_at, updated_at
|
||||
) VALUES
|
||||
(101, 'WIN/USDT', 'long', 'closed', %s, %s, 10, 11, 500, 5000, 1000, 5, 11, 10, 10, 500, 'buy_now', '可即刻买入', %s, %s, %s),
|
||||
(102, 'OLD/USDT', 'long', 'closed', %s, %s, 10, 9, 500, 5000, 1000, 5, 9, -10, -10, -500, 'buy_now', '可即刻买入', %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
old_open_recent_close.isoformat(timespec="seconds"),
|
||||
recent_close.isoformat(timespec="seconds"),
|
||||
strategy_code,
|
||||
old_open_recent_close.isoformat(timespec="seconds"),
|
||||
recent_close.isoformat(timespec="seconds"),
|
||||
outside_open.isoformat(timespec="seconds"),
|
||||
old_close.isoformat(timespec="seconds"),
|
||||
strategy_code,
|
||||
outside_open.isoformat(timespec="seconds"),
|
||||
old_close.isoformat(timespec="seconds"),
|
||||
),
|
||||
)
|
||||
pg_conn.commit()
|
||||
|
||||
data = get_review_center_dashboard(days=30)
|
||||
board = next(x for x in data["strategy_evaluation"]["strategies"] if x["strategy_code"] == strategy_code)
|
||||
lower = next(x for x in data["paper_trading"]["trade_attribution"]["strategy_code"] if x["strategy_code"] == strategy_code)
|
||||
|
||||
assert board["closed_trade_count"] == 1
|
||||
assert lower["closed_trade_count"] == 1
|
||||
assert data["paper_trading"]["summary"]["closed_count"] == 1
|
||||
assert board["realized_pnl_usdt"] == 500
|
||||
assert lower["realized_pnl_usdt"] == 500
|
||||
|
||||
|
||||
def test_review_center_iteration_digest_summarizes_actions(pg_conn):
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
today = now[:10]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user