From e63344b6328fadef4d1308bc7df32df96d7c53f5 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Mon, 18 May 2026 16:31:45 +0800 Subject: [PATCH] update --- app/db/altcoin_db.py | 151 +++++---- app/db/review_center.py | 304 ++++++++++++++++++ app/web/routes_pages.py | 11 + app/web/routes_review_center.py | 13 + app/web/web_server.py | 2 + static/base.html | 5 +- static/iteration.html | 16 +- static/review_center.html | 64 ++++ static/strategy.html | 38 ++- .../test_personalization_strategy_stage2_3.py | 56 +++- tests/test_review_center.py | 82 +++++ 11 files changed, 647 insertions(+), 95 deletions(-) create mode 100644 app/db/review_center.py create mode 100644 app/web/routes_review_center.py create mode 100644 static/review_center.html create mode 100644 tests/test_review_center.py diff --git a/app/db/altcoin_db.py b/app/db/altcoin_db.py index f789653..0a384c4 100644 --- a/app/db/altcoin_db.py +++ b/app/db/altcoin_db.py @@ -2359,63 +2359,80 @@ def _safe_dict_json(value): def get_strategy_insights(): - """阶段3:策略可信度看板数据 — 总体表现、因子归因、市场环境归因。 + """Strategy attribution based on opportunity and paper-trading conversion. - 只统计已出结果的样本,避免策略页把仍在实时看板里的 active 浮亏/回撤 - 与历史推荐页的已完成样本混在一起,造成口径不一致。 + 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. """ conn = get_conn() - rows = conn.execute("SELECT * FROM recommendation ORDER BY rec_time DESC").fetchall() + rows = conn.execute( + """ + SELECT + r.*, + pt.id AS paper_trade_id, + pt.status AS paper_status, + 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 + FROM recommendation r + LEFT JOIN paper_trades pt ON pt.recommendation_id = r.id + ORDER BY r.rec_time DESC, r.id DESC + """ + ).fetchall() conn.close() - raw_items = [dict(r) for r in rows] - - def outcome(item): - status = item.get("status") or "" - if status in ("hit_tp1", "hit_tp2"): - return "success" - if status == "stopped_out": - return "failed" - if (item.get("max_pnl_pct") or 0) >= 5: - return "success" - if (item.get("pnl_pct") or 0) <= -3 or (item.get("max_drawdown_pct") or 0) <= -5: - return "failed" - return "pending" - - items = [x for x in raw_items if outcome(x) in ("success", "failed")] + items = [dict(r) for r in rows] + actionable_statuses = {"buy_now", "wait_pullback"} total = len(items) - success = sum(1 for x in items if outcome(x) == "success") - failed = sum(1 for x in items if outcome(x) == "failed") - resolved = success + failed - pnl_values = [float(x.get("pnl_pct") or 0) for x in items] - gains = [p for p in pnl_values if p > 0] - losses = [p for p in pnl_values if p < 0] + 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"] + 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 = { - "total_signals": total, - "resolved_count": resolved, - "success_count": success, - "failed_count": failed, - "pending_count": total - resolved, - "win_rate_pct": round(success / resolved * 100, 1) if resolved else 0, - "avg_pnl_pct": round(sum(pnl_values) / len(pnl_values), 2) if pnl_values else 0, - "avg_gain_pct": round(sum(gains) / len(gains), 2) if gains else 0, - "avg_loss_pct": round(sum(losses) / len(losses), 2) if losses else 0, - "max_gain_pct": round(max([float(x.get("max_pnl_pct") or x.get("pnl_pct") or 0) for x in items] or [0]), 2), - "max_drawdown_pct": round(min([float(x.get("max_drawdown_pct") or 0) for x in items] or [0]), 2), + "total_opportunities": total, + "actionable_count": len(actionable), + "buy_now_count": len(buy_now), + "paper_trade_count": len(paper_items), + "closed_paper_trade_count": len(closed_paper), + "paper_win_count": len(paper_wins), + "paper_win_rate_pct": round(len(paper_wins) / len(closed_paper) * 100, 1) if closed_paper else 0, + "paper_realized_pnl_usdt": paper_realized_usdt, + "actionable_conversion_pct": round(len(actionable) / total * 100, 1) if total else 0, + "paper_conversion_pct": round(len(paper_items) / len(buy_now) * 100, 1) if buy_now else 0, + "definition": "策略归因只看机会转化和模拟交易转化;收益只来自 paper_trades,不读取 recommendation.pnl_pct。", } def add_bucket(bucket_map, key, item): if not key: return - b = bucket_map.setdefault(key, {"total_count": 0, "success_count": 0, "failed_count": 0, "pending_count": 0, "pnl_values": [], "max_gains": [], "drawdowns": []}) - b["total_count"] += 1 - oc = outcome(item) - if oc == "success": b["success_count"] += 1 - elif oc == "failed": b["failed_count"] += 1 - else: b["pending_count"] += 1 - b["pnl_values"].append(float(item.get("pnl_pct") or 0)) - b["max_gains"].append(float(item.get("max_pnl_pct") or item.get("pnl_pct") or 0)) - b["drawdowns"].append(float(item.get("max_drawdown_pct") or 0)) + b = bucket_map.setdefault(key, { + "opportunity_count": 0, + "actionable_count": 0, + "buy_now_count": 0, + "paper_trade_count": 0, + "closed_paper_trade_count": 0, + "paper_win_count": 0, + "paper_realized_pnl_usdt": 0.0, + }) + execution_status = item.get("execution_status") or "" + paper_status = item.get("paper_status") or "" + b["opportunity_count"] += 1 + if execution_status in actionable_statuses: + b["actionable_count"] += 1 + if execution_status == "buy_now": + b["buy_now_count"] += 1 + if item.get("paper_trade_id"): + b["paper_trade_count"] += 1 + if paper_status == "closed": + b["closed_paper_trade_count"] += 1 + pnl_pct = float(item.get("paper_realized_pnl_pct") or 0) + if pnl_pct > 0: + b["paper_win_count"] += 1 + b["paper_realized_pnl_usdt"] += float(item.get("paper_realized_pnl_usdt") or 0) def env_buckets_from_market_context(mc): """把当前实际存在的 market_context_json 数值字段转成可归因桶。 @@ -2472,18 +2489,24 @@ def get_strategy_insights(): factor_map = {} env_map = {} version_map = {} + evidence_map = {} for item in items: - for factor in _safe_list_json(item.get("signals")): + 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")) + for factor in labels: add_bucket(factor_map, str(factor).strip(), item) + for code in codes: + text = str(code or "").strip() + if text.startswith(("sentiment_", "listing_", "ecosystem_")): + add_bucket(evidence_map, "舆情:" + text, item) + elif text.startswith(("dex_", "liquidity_", "exchange_", "whale_", "smart_money", "holder_")): + add_bucket(evidence_map, "链上:" + text, item) mc = _safe_dict_json(item.get("market_context_json")) - added_env = False for key in ("btc_trend", "market_regime", "altcoin_regime", "sentiment"): if mc.get(key): add_bucket(env_map, f"{key}:{mc.get(key)}", item) - added_env = True for bucket in env_buckets_from_market_context(mc): add_bucket(env_map, bucket, item) - added_env = True if item.get("strategy_version"): add_bucket(version_map, str(item.get("strategy_version")).strip(), item) @@ -2506,27 +2529,35 @@ def get_strategy_insights(): def serialize(name_key, bucket_map, sort_by_version=False): rows = [] for key, b in bucket_map.items(): - resolved_count = b["success_count"] + b["failed_count"] rows.append({ name_key: key, - "total_count": b["total_count"], - "success_count": b["success_count"], - "failed_count": b["failed_count"], - "pending_count": b["pending_count"], - "win_rate_pct": round(b["success_count"] / resolved_count * 100, 1) if resolved_count else 0, - "avg_pnl_pct": round(sum(b["pnl_values"]) / len(b["pnl_values"]), 2) if b["pnl_values"] else 0, - "max_gain_pct": round(max(b["max_gains"] or [0]), 2), - "max_drawdown_pct": round(min(b["drawdowns"] or [0]), 2), + "opportunity_count": b["opportunity_count"], + "actionable_count": b["actionable_count"], + "buy_now_count": b["buy_now_count"], + "paper_trade_count": b["paper_trade_count"], + "closed_paper_trade_count": b["closed_paper_trade_count"], + "paper_win_count": b["paper_win_count"], + "actionable_conversion_pct": round(b["actionable_count"] / b["opportunity_count"] * 100, 1) if b["opportunity_count"] else 0, + "paper_conversion_pct": round(b["paper_trade_count"] / b["buy_now_count"] * 100, 1) if b["buy_now_count"] else 0, + "paper_win_rate_pct": round(b["paper_win_count"] / b["closed_paper_trade_count"] * 100, 1) if b["closed_paper_trade_count"] else 0, + "paper_realized_pnl_usdt": round(b["paper_realized_pnl_usdt"], 4), }) if sort_by_version: - rows.sort(key=lambda x: (version_sort_key(x[name_key]), x["total_count"], x["win_rate_pct"]), reverse=True) + rows.sort(key=lambda x: (version_sort_key(x[name_key]), x["opportunity_count"], x["actionable_conversion_pct"]), reverse=True) else: - rows.sort(key=lambda x: (-x["total_count"], -x["win_rate_pct"], x[name_key])) + rows.sort(key=lambda x: (-x["opportunity_count"], -x["actionable_conversion_pct"], x[name_key])) return rows return { "overview": overview, + "metric_definition": { + "opportunity_count": "进入 opportunity/recommendation 表的机会样本数,不代表交易。", + "actionable_count": "确认层输出 buy_now 或 wait_pullback 的样本数。", + "paper_trade_count": "已经被模拟交易账本执行的样本数。", + "paper_realized_pnl_usdt": "仅来自 paper_trades 的已平仓模拟收益。", + }, "factor_attribution": serialize("factor", factor_map)[:30], "market_environment": serialize("environment", env_map)[:20], + "evidence_attribution": serialize("evidence", evidence_map)[:20], "version_performance": serialize("strategy_version", version_map, sort_by_version=True)[:20], } diff --git a/app/db/review_center.py b/app/db/review_center.py new file mode 100644 index 0000000..f226e65 --- /dev/null +++ b/app/db/review_center.py @@ -0,0 +1,304 @@ +"""Review center read models. + +This module keeps the new review/iteration semantics explicit: +- opportunity review describes whether the system found useful opportunities; +- paper trading review is the only place where PnL is treated as execution PnL; +- evidence attribution describes whether onchain/sentiment/LLM evidence helped. +""" + +from __future__ import annotations + +import json +from datetime import datetime, timedelta + +from app.db.paper_trading import get_paper_trading_summary +from app.db.schema import get_conn + + +def _safe_int(value, default=0): + try: + return int(value or 0) + except Exception: + return default + + +def _safe_float(value, default=0.0): + try: + return float(value or 0) + except Exception: + return default + + +def _loads(value, default=None): + try: + if isinstance(value, (dict, list)): + return value + if isinstance(value, str) and value.strip(): + return json.loads(value) + except Exception: + pass + return default if default is not None else {} + + +def _since(days): + return (datetime.now() - timedelta(days=max(1, min(_safe_int(days, 30), 365)))).isoformat() + + +def _bucket_count(rows, key, fallback="unknown"): + counts = {} + for row in rows: + value = (row.get(key) or fallback) if isinstance(row, dict) else fallback + counts[value] = counts.get(value, 0) + 1 + return [{"name": k, "count": v} for k, v in sorted(counts.items(), key=lambda x: (-x[1], x[0]))] + + +def _opportunity_review(conn, since): + rec_rows = [dict(r) for r in conn.execute( + """ + SELECT id, symbol, rec_time, status, display_bucket, execution_status, action_status, + entry_triggered, strategy_version, signal_codes_json, signal_labels_json, + market_context_json, derivatives_context_json, sector_context_json + FROM recommendation + WHERE rec_time >= %s + ORDER BY rec_time DESC, id DESC + """, + (since,), + ).fetchall()] + review_rows = [dict(r) for r in conn.execute( + """ + SELECT * + FROM review_log + WHERE review_time >= %s + ORDER BY review_time DESC, id DESC + """, + (since,), + ).fetchall()] + missed_rows = [dict(r) for r in conn.execute( + """ + SELECT * + FROM missed_explosions + WHERE detect_time >= %s + ORDER BY gain_pct DESC, detect_time DESC + LIMIT 20 + """, + (since,), + ).fetchall()] + + total = len(rec_rows) + executed_ids = { + int(r["recommendation_id"]) + for r in conn.execute("SELECT recommendation_id FROM paper_trades WHERE opened_at >= %s", (since,)).fetchall() + if r.get("recommendation_id") + } + buy_now = [r for r in rec_rows if r.get("execution_status") == "buy_now"] + wait_pullback = [r for r in rec_rows if r.get("execution_status") == "wait_pullback"] + observe = [r for r in rec_rows if r.get("execution_status") == "observe" or r.get("display_bucket") == "watch_pool"] + invalid = [r for r in rec_rows if r.get("execution_status") == "invalid" or r.get("status") in ("expired", "invalid", "archived")] + executed = [r for r in rec_rows if int(r.get("id") or 0) in executed_ids] + + outcomes = _bucket_count(review_rows, "outcome", "未复盘") + reviewed_effective = [r for r in review_rows if r.get("outcome") in ("爆发", "失败", "横盘")] + hit_count = sum(1 for r in reviewed_effective if r.get("outcome") == "爆发") + + return { + "definition": "机会复盘只评价机会发现、确认和漏选,不代表交易收益。", + "summary": { + "total_opportunities": total, + "buy_now_count": len(buy_now), + "wait_pullback_count": len(wait_pullback), + "observe_count": len(observe), + "invalid_count": len(invalid), + "paper_executed_count": len(executed), + "reviewed_count": len(review_rows), + "effective_review_count": len(reviewed_effective), + "opportunity_hit_rate": round(hit_count / len(reviewed_effective) * 100, 2) if reviewed_effective else 0, + "missed_explosion_count": len(missed_rows), + }, + "status_distribution": _bucket_count(rec_rows, "execution_status", "unknown"), + "outcome_distribution": outcomes, + "missed_explosions": missed_rows[:10], + "recent_reviews": review_rows[:12], + } + + +def _paper_review(conn, since, days): + summary = get_paper_trading_summary(days=days) + trades = [dict(r) for r in conn.execute( + """ + SELECT * + FROM paper_trades + WHERE opened_at >= %s + ORDER BY opened_at DESC, id DESC + LIMIT 20 + """, + (since,), + ).fetchall()] + events = [dict(r) for r in conn.execute( + """ + SELECT * + FROM paper_trade_events + WHERE event_time >= %s + ORDER BY event_time DESC, id DESC + LIMIT 30 + """, + (since,), + ).fetchall()] + exit_reasons = _bucket_count([t for t in trades if t.get("status") == "closed"], "exit_reason", "unknown") + event_types = _bucket_count(events, "event_type", "unknown") + return { + "definition": "模拟交易复盘是唯一收益口径,基于 paper_trades 的开仓、平仓、移动止盈事件。", + "summary": summary, + "exit_reasons": exit_reasons, + "event_types": event_types, + "recent_trades": trades, + "recent_events": events, + } + + +def _evidence_review(conn, since): + news_rows = [dict(r) for r in conn.execute( + """ + SELECT source, symbol, importance, event_type, decision, tech_score, processed, detected_at, title + FROM event_news + WHERE detected_at >= %s + ORDER BY detected_at DESC, id DESC + LIMIT 80 + """, + (since,), + ).fetchall()] + onchain_rows = [dict(r) for r in conn.execute( + """ + SELECT source, chain, symbol, signal_code, signal_label, direction, value_usd, + confidence, severity, detected_at + FROM onchain_events + WHERE detected_at >= %s + ORDER BY detected_at DESC, id DESC + LIMIT 80 + """, + (since,), + ).fetchall()] + raw_onchain_rows = [dict(r) for r in conn.execute( + """ + SELECT source, chain, event_type, symbol_guess, mapped_symbol, mapping_status, + importance, detected_at, title + FROM onchain_raw_events + WHERE detected_at >= %s + ORDER BY importance DESC, detected_at DESC, id DESC + LIMIT 80 + """, + (since,), + ).fetchall()] + llm_rows = [dict(r) for r in conn.execute( + """ + SELECT target_type, insight_type, status, model, prompt_version, updated_at + FROM llm_insights + WHERE updated_at >= %s + ORDER BY updated_at DESC, id DESC + LIMIT 80 + """, + (since,), + ).fetchall()] + + mapped_raw = [r for r in raw_onchain_rows if r.get("mapping_status") == "mapped" or r.get("mapped_symbol")] + high_onchain = [r for r in onchain_rows if _safe_int(r.get("confidence")) >= 70 or str(r.get("severity") or "").upper() in ("A", "S")] + actionable_news = [r for r in news_rows if r.get("decision") in ("recommend", "observe", "risk") or str(r.get("importance") or "").upper() in ("A", "S")] + llm_success = [r for r in llm_rows if r.get("status") == "success"] + + return { + "definition": "多源归因只判断证据贡献:舆情、链上、LLM 是否帮助发现/解释机会,不直接生成交易收益。", + "summary": { + "news_count": len(news_rows), + "actionable_news_count": len(actionable_news), + "onchain_signal_count": len(onchain_rows), + "high_confidence_onchain_count": len(high_onchain), + "raw_onchain_count": len(raw_onchain_rows), + "mapped_raw_onchain_count": len(mapped_raw), + "llm_runs": len(llm_rows), + "llm_success_count": len(llm_success), + }, + "news_sources": _bucket_count(news_rows, "source", "unknown"), + "news_decisions": _bucket_count(news_rows, "decision", "unprocessed"), + "onchain_sources": _bucket_count(onchain_rows, "source", "unknown"), + "onchain_signals": _bucket_count(onchain_rows, "signal_code", "unknown")[:12], + "raw_mapping": _bucket_count(raw_onchain_rows, "mapping_status", "unknown"), + "llm_status": _bucket_count(llm_rows, "status", "unknown"), + "recent_news": news_rows[:8], + "recent_onchain": onchain_rows[:8], + "recent_llm": llm_rows[:8], + } + + +def _iteration_review(conn, since): + logs = [dict(r) for r in conn.execute( + """ + SELECT * + FROM strategy_iteration_log + WHERE created_at >= %s + ORDER BY created_at DESC, id DESC + LIMIT 12 + """, + (since,), + ).fetchall()] + candidates = [dict(r) for r in conn.execute( + """ + SELECT * + FROM strategy_rule_candidate + ORDER BY created_at DESC, id DESC + LIMIT 30 + """ + ).fetchall()] + for item in logs: + for field, fallback in ( + ("metrics_json", {}), + ("findings_json", []), + ("problems_json", []), + ("actions_json", []), + ("candidate_rules_json", []), + ): + item[field.replace("_json", "")] = _loads(item.get(field), fallback) + return { + "definition": "策略迭代只产生候选假设和发布闸门结论,不直接等于收益提升。", + "summary": { + "iteration_count": len(logs), + "candidate_count": len(candidates), + "gray_count": sum(1 for c in candidates if c.get("status") == "gray"), + "active_count": sum(1 for c in candidates if c.get("status") == "active"), + "latest_release_decision": (logs[0].get("release_decision") if logs else "") or "hold", + "latest_release_reason": (logs[0].get("release_reason") if logs else "") or "", + }, + "release_decisions": _bucket_count(logs, "release_decision", "unknown"), + "candidate_status": _bucket_count(candidates, "status", "candidate"), + "recent_logs": logs, + "recent_candidates": candidates[:12], + } + + +def get_review_center_dashboard(days=30): + days = max(1, min(_safe_int(days, 30), 365)) + since = _since(days) + conn = get_conn() + try: + opportunity = _opportunity_review(conn, since) + paper = _paper_review(conn, since, days) + evidence = _evidence_review(conn, since) + iteration = _iteration_review(conn, since) + finally: + conn.close() + + return { + "days": days, + "generated_at": datetime.now().isoformat(timespec="seconds"), + "principles": [ + "机会归档不计算交易收益,只记录发现、确认、失效和漏选。", + "真实收益口径只来自模拟交易或未来真实交易账本。", + "链上、舆情、LLM 属于证据层,只做发现和解释,不直接改变推荐状态。", + "策略迭代只发布经过样本约束和灰度闸门验证的规则。", + ], + "opportunity": opportunity, + "paper_trading": paper, + "evidence": evidence, + "iteration": iteration, + } + + +__all__ = ["get_review_center_dashboard"] diff --git a/app/web/routes_pages.py b/app/web/routes_pages.py index 4cff7c3..caadcd0 100644 --- a/app/web/routes_pages.py +++ b/app/web/routes_pages.py @@ -118,6 +118,17 @@ def build_router(templates, repo_root: Path, stock_report_template: str): return HTMLResponse(content=f"
{exc.detail}
返回看板", status_code=exc.status_code) return render_page("paper_trading.html", request, active_nav="paper_trading") + @router.get("/review-center", response_class=HTMLResponse) + async def review_center_page(request: Request): + user, redirect = require_page_user(request) + if redirect: + return redirect + try: + require_admin(request.cookies.get("altcoin_session", "")) + except HTTPException as exc: + return HTMLResponse(content=f"{exc.detail}
返回看板", status_code=exc.status_code) + return render_page("review_center.html", request, active_nav="review_center") + @router.get("/strategy", response_class=HTMLResponse) async def strategy_page(request: Request): user, redirect = require_page_user(request) diff --git a/app/web/routes_review_center.py b/app/web/routes_review_center.py new file mode 100644 index 0000000..77e57b7 --- /dev/null +++ b/app/web/routes_review_center.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, Cookie + +from app.db.review_center import get_review_center_dashboard +from app.web.shared import require_admin + + +router = APIRouter() + + +@router.get("/api/review-center/dashboard") +async def api_review_center_dashboard(days: int = 30, altcoin_session: str = Cookie(default="")): + require_admin(altcoin_session) + return get_review_center_dashboard(days=days) diff --git a/app/web/web_server.py b/app/web/web_server.py index 179fca7..f4c074a 100644 --- a/app/web/web_server.py +++ b/app/web/web_server.py @@ -23,6 +23,7 @@ from app.web.routes_onchain import router as onchain_router from app.web.routes_paper_trading import router as paper_trading_router from app.web.routes_pages import build_router as build_pages_router from app.web.routes_recommendations import router as recommendations_router +from app.web.routes_review_center import router as review_center_router from app.web.routes_strategy import router as strategy_router from app.web.shared import current_request from app.web.shared import require_active_subscription as _require_active_subscription @@ -47,6 +48,7 @@ templates = Jinja2Templates(directory=str(REPO_ROOT / "static")) app.include_router(auth_router) app.include_router(chat_router) app.include_router(recommendations_router) +app.include_router(review_center_router) app.include_router(strategy_router) app.include_router(onchain_router) app.include_router(paper_trading_router) diff --git a/static/base.html b/static/base.html index cbc513d..a374b85 100644 --- a/static/base.html +++ b/static/base.html @@ -183,10 +183,11 @@ a { color: inherit; text-decoration: none; } 邀请 模拟交易 + 复盘中心 链路日志 AI 记录 - 策略 - 迭代 + 策略归因 + 策略迭代 问答日志 配置中心 调度中心 diff --git a/static/iteration.html b/static/iteration.html index 7a6b7b2..642972b 100644 --- a/static/iteration.html +++ b/static/iteration.html @@ -114,14 +114,14 @@ h2 { font-size:26px; font-weight:900; margin:0 0 8px; color:var(--ink); } - +| 来源 | 当前阶段 | 预演结论 | 规律 | 样本 | 成功/失败 | 可信度 | 平均表现 | 为什么还没发布 |
|---|---|---|---|---|---|---|---|---|
| '+esc(sourceLabel(c))+' | '+badge(c.status||'candidate')+' | '+badge(d.dry_run_status||c.status||'candidate')+' | '+esc(c.rule_description||c.signal_name||'--')+' | '+esc(d.sample_size!=null?d.sample_size:(c.sample_size||0))+' | '+esc(d.success_count!=null?d.success_count:(c.success_count||0))+' / '+esc(d.fail_count!=null?d.fail_count:(c.fail_count||0))+' | '+esc(d.confidence_score!=null?d.confidence_score:(c.confidence_score||0))+' | '+esc(d.avg_pnl!=null?d.avg_pnl:(c.avg_pnl||0))+' | '+esc(d.gate_reason||'等待样本验证')+' |
| 预演结论 | 规律 | 样本 | 成功/失败 | 可信度 | 平均表现 | 原因 |
|---|---|---|---|---|---|---|
| '+badge(x.dry_run_status||'candidate')+' | '+esc(x.rule_description||x.signal_name||'--')+' | '+esc(x.sample_size||0)+' | '+esc(x.success_count||0)+' / '+esc(x.fail_count||0)+' | '+esc(x.confidence_score||0)+' | '+esc(x.avg_pnl||0)+' | '+esc(x.gate_reason||'--')+' |
| 版本 | 推荐数 | 成功 | 失败 | 待观察 | 成功率 | 均值收益 |
|---|---|---|---|---|---|---|
| '+esc(v.strategy_version)+' | '+esc(v.recommendation_count)+' | '+esc(v.success_count)+' | '+esc(v.failed_count)+' | '+esc(v.pending_count)+' | '+esc(v.success_rate_pct)+' | '+esc(v.avg_pnl_pct)+' |
| 来源 | 当前阶段 | 预演结论 | 规律 | 样本 | 成功/失败 | 可信度 | 机会表现 | 为什么还没发布 |
|---|---|---|---|---|---|---|---|---|
| '+esc(sourceLabel(c))+' | '+badge(c.status||'candidate')+' | '+badge(d.dry_run_status||c.status||'candidate')+' | '+esc(c.rule_description||c.signal_name||'--')+' | '+esc(d.sample_size!=null?d.sample_size:(c.sample_size||0))+' | '+esc(d.success_count!=null?d.success_count:(c.success_count||0))+' / '+esc(d.fail_count!=null?d.fail_count:(c.fail_count||0))+' | '+esc(d.confidence_score!=null?d.confidence_score:(c.confidence_score||0))+' | '+esc(d.avg_pnl!=null?d.avg_pnl:(c.avg_pnl||0))+' | '+esc(d.gate_reason||'等待样本验证')+' |
| 预演结论 | 规律 | 样本 | 成功/失败 | 可信度 | 机会表现 | 原因 |
|---|---|---|---|---|---|---|
| '+badge(x.dry_run_status||'candidate')+' | '+esc(x.rule_description||x.signal_name||'--')+' | '+esc(x.sample_size||0)+' | '+esc(x.success_count||0)+' / '+esc(x.fail_count||0)+' | '+esc(x.confidence_score||0)+' | '+esc(x.avg_pnl||0)+' | '+esc(x.gate_reason||'--')+' |
| 版本 | 机会数 | 成功 | 失败 | 待观察 | 机会成功率 | 机会均值 |
|---|---|---|---|---|---|---|
| '+esc(v.strategy_version)+' | '+esc(v.recommendation_count)+' | '+esc(v.success_count)+' | '+esc(v.failed_count)+' | '+esc(v.pending_count)+' | '+esc(v.success_rate_pct)+' | '+esc(v.avg_pnl_pct)+' |
把机会发现、模拟交易收益、多源证据和策略迭代拆开看。收益只看模拟交易;机会归档只看发现和确认质量。
+系统可信度、版本表现、因子归因与市场环境归因。
看清哪些因子、环境和证据源更容易把机会推进到可执行,以及是否真的进入模拟交易。这里不把机会价格波动当作交易收益。
+