This commit is contained in:
aaron 2026-05-21 09:58:52 +08:00
parent cc4a0c7eb7
commit 4fa4dcb965
18 changed files with 58 additions and 58 deletions

View File

@ -197,7 +197,7 @@ def _decision_archive_where(archive_filter):
)
"""
if archive_filter == "executed":
# 已执行口径以 paper trading 账本为准。正在持仓中的模拟交易,
# 已执行口径以策略交易账本为准。正在持仓中的策略交易,
# 其 recommendation 仍可能是 active/watch_pool不能被归档条件挡掉。
return executed_where
if archive_filter == "invalid":

View File

@ -162,7 +162,7 @@ def _paper_review(conn, since, days):
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 的开仓、平仓、移动止盈事件。",
"definition": "策略交易复盘是唯一收益口径,基于交易账本的开仓、平仓、移动止盈事件。",
"summary": summary,
"exit_reasons": exit_reasons,
"event_types": event_types,
@ -306,7 +306,7 @@ def get_review_center_dashboard(days=30):
"generated_at": datetime.now().isoformat(timespec="seconds"),
"principles": [
"机会归档不计算交易收益,只记录发现、确认、失效和漏选。",
"真实收益口径只来自模拟交易或未来真实交易账本。",
"真实收益口径只来自策略交易或未来真实交易账本。",
"链上、舆情、LLM 属于证据层,只做发现和解释,不直接改变推荐状态。",
"策略迭代只发布经过样本约束和灰度闸门验证的规则。",
],

View File

@ -45,7 +45,7 @@ DEFAULT_JOBS = [
"every_seconds": 180,
"initial_delay": 30,
"lock_group": "paper_trading_write",
"description": "模拟交易账本同步",
"description": "策略交易账本同步",
"sort_order": 25,
},
{
@ -386,7 +386,7 @@ def _display_job_name(job_name):
"sentiment": "舆情",
"onchain": "链上",
"llm-sentiment": "AI舆情",
"paper-trader": "模拟交易",
"paper-trader": "策略交易",
"review": "复盘",
}.get(job_name, job_name)

View File

@ -75,7 +75,7 @@ def get_strategy_insights():
"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。",
"definition": "策略归因只看机会转化和策略交易转化;收益只来自交易账本,不读取 recommendation.pnl_pct。",
}
def add_bucket(bucket_map, key, item):
@ -135,8 +135,8 @@ def get_strategy_insights():
"metric_definition": {
"opportunity_count": "进入 opportunity/recommendation 表的机会样本数,不代表交易。",
"actionable_count": "确认层输出 buy_now 或 wait_pullback 的样本数。",
"paper_trade_count": "已经被模拟交易账本执行的样本数。",
"paper_realized_pnl_usdt": "仅来自 paper_trades 的已平仓模拟收益。",
"paper_trade_count": "已经被策略交易账本执行的样本数。",
"paper_realized_pnl_usdt": "仅来自交易账本的已平仓策略收益。",
},
"factor_attribution": serialize_buckets("factor", factor_map)[:30],
"market_environment": serialize_buckets("environment", env_map)[:20],

View File

@ -1299,7 +1299,7 @@ def confirm_burst(symbol, cand):
def _watch_candidate_plan(symbol, result, cand_detail):
"""把强势但未形成交易买点的样本写成机会观察,不触发模拟交易。"""
"""把强势但未形成交易买点的样本写成机会观察,不触发策略交易。"""
market_context = result.get("market_context") or {}
signals = list(result.get("signals") or [])
price = float(result.get("price") or 0)

View File

@ -123,7 +123,7 @@ def extract_symbol(message: str, session=None, preferences=None) -> str:
def detect_intent(message: str, symbol: str = "") -> str:
text = str(message or "").lower()
if any(k in text for k in ("模拟交易", "纸面交易", "paper trading", "paper-trading", "paper")):
if any(k in text for k in ("策略交易", "模拟交易", "纸面交易", "paper trading", "paper-trading", "paper")):
return "restricted"
if not _is_crypto_question(text, symbol):
return "unsupported"
@ -479,8 +479,8 @@ def build_context(intent: str, message: str, symbol: str, preferences=None) -> d
def _fallback_answer(intent: str, message: str, context: dict) -> dict:
if intent == "restricted":
return {
"summary": "内部模拟交易数据不可在智能问答中直接访问。",
"answer": "我不能读取或解释内部模拟交易数据。你可以继续问公开行情、单币技术面、推荐解释、链上异动、舆情影响或复盘结果(不含纸面交易明细)。",
"summary": "内部策略交易数据不可在智能问答中直接访问。",
"answer": "我不能读取或解释内部策略交易数据。你可以继续问公开行情、单币技术面、推荐解释、链上异动、舆情影响或复盘结果(不含交易账本明细)。",
"evidence": [],
"related_records": [],
"followups": ["分析 BTC/USDT 的技术面", "解释最新推荐为什么不是可买"],
@ -614,7 +614,7 @@ def _call_chat_llm(message: str, context: dict, history=None) -> dict:
"context": context,
"recent_history": (history or [])[-8:],
"rules": [
"只回答加密货币、AlphaX 当前数据、技术面、链上、舆情、复盘相关问题,不要访问内部模拟交易数据。",
"只回答加密货币、AlphaX 当前数据、技术面、链上、舆情、复盘相关问题,不要访问内部策略交易数据。",
"不要给真实下单指令,不要修改推荐状态,不要承诺收益。",
"回答使用中文,采用两段式:先结论,再证据。",
"根据 intent 选择 answer_styletechnical/decision/market/news/onchain/review/notice/help/default。",

View File

@ -52,7 +52,7 @@ def main(limit: int = 100):
except Exception as exc:
finished_at = datetime.now()
log_cron_run(
job_name="模拟交易",
job_name="策略交易",
script_name="paper_trader.py",
run_status="error",
result_status="exception",
@ -65,7 +65,7 @@ def main(limit: int = 100):
raise
finished_at = datetime.now()
log_cron_run(
job_name="模拟交易",
job_name="策略交易",
script_name="paper_trader.py",
run_status="success",
result_status=output.get("status", "completed"),

View File

@ -1,7 +1,7 @@
"""
山寨币爆发监控系统 推荐信号跟踪 + paper trading 执行账本
山寨币爆发监控系统 推荐信号跟踪 + 策略交易执行账本
趋势反转检测1H连续阴动K量价背离空头加速替代MACD/RSI
推荐层只管理信号状态模拟成交TP/SL移动止盈由 paper_trading 独立负责
推荐层只管理信号状态策略交易成交TP/SL移动止盈由 paper_trading 独立负责
"""
import sys, os, shutil
@ -140,14 +140,14 @@ def analyze_tracking_signals(symbol, rec, current_price):
# ---- 止盈信号检测 ----
pnl_pct = ((current_price / entry_price) - 1) * 100 if entry_price > 0 else 0
# TP/SL 是模拟交易生命周期,不再写成推荐信号动作。
# TP/SL 是策略交易生命周期,不再写成推荐信号动作。
if tp1 > 0 and current_price >= tp1:
sell_signals.append(f"模拟交易目标价已到达(${tp1:.4f}),执行结果以 paper trading 为准")
sell_signals.append(f"策略交易目标价已到达(${tp1:.4f}),执行结果以交易账本为准")
rules = load_rules()
if tp1 == 0 and pnl_pct >= 15:
sell_signals.append(f"无TP保护但浮盈已达+{pnl_pct:.1f}%,仅作为信号风险提醒,是否平仓由 paper trading/人工处理")
sell_signals.append(f"无TP保护但浮盈已达+{pnl_pct:.1f}%,仅作为信号风险提醒,是否平仓由策略交易/人工处理")
# ---- 止损接近警告 ----
if stop_loss > 0:
@ -155,7 +155,7 @@ def analyze_tracking_signals(symbol, rec, current_price):
if loss_pct < 3: # 当前价离止损不到3%
sell_signals.append(f"⚠️ 接近止损!当前${current_price:.4f}离止损${stop_loss:.4f}{loss_pct:.1f}%")
if current_price <= stop_loss:
sell_signals.append(f"🔴 模拟交易止损价已触达!${current_price:.4f}≤${stop_loss:.4f},执行结果以 paper trading 为准")
sell_signals.append(f"🔴 策略交易止损价已触达!${current_price:.4f}≤${stop_loss:.4f},执行结果以交易账本为准")
# ---- 趋势反转信号PA行为检测替代MACD ----
if h1_df is not None and len(h1_df) >= 30 and atr_1h > 0:

View File

@ -904,12 +904,12 @@ function historyOutcome(r) {
if (paper && paper.status === 'closed') {
var exitReason = String(paper.exit_reason || '').toLowerCase();
if (exitReason === 'stop_loss' || exitReason === 'sl' || exitReason === 'stopped_out') {
return { resolved: true, type: 'executed_failed', label: '模拟交易止损', detail: '已进入模拟交易并触发止损' };
return { resolved: true, type: 'executed_failed', label: '策略交易止损', detail: '已进入策略交易并触发止损' };
}
return { resolved: true, type: 'executed_success', label: '模拟交易兑现', detail: '已进入模拟交易并完成退出' };
return { resolved: true, type: 'executed_success', label: '策略交易兑现', detail: '已进入策略交易并完成退出' };
}
if (paper && paper.status === 'open') {
return { resolved: true, type: 'executed_open', label: '模拟交易持有', detail: '已进入模拟交易,仍在持仓中' };
return { resolved: true, type: 'executed_open', label: '策略交易持有', detail: '已进入策略交易,仍在持仓中' };
}
if (status === 'hit_tp1' || status === 'hit_tp2' || execution === 'completed') {
return { resolved: true, type: 'executed_success', label: '执行后兑现', detail: '已进入模拟/持仓口径验证' };
@ -949,7 +949,7 @@ async function loadHistoryRecommendations(reset) {
$('histCount').textContent = totalCount ? ' ' + totalCount : '';
$('historyStats').innerHTML =
'<div class="hstat"><div class="num" style="color:var(--blue)">'+totalCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--blue)"><use href="#svg-target"/></svg> 归档信号</div><div class="sub">机会/观察历史</div></div>'+
'<div class="hstat"><div class="num" style="color:var(--green)">'+executedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--green)"><use href="#svg-trendup"/></svg> 进入执行</div><div class="sub">收益见模拟交易</div></div>'+
'<div class="hstat"><div class="num" style="color:var(--green)">'+executedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--green)"><use href="#svg-trendup"/></svg> 进入执行</div><div class="sub">收益见策略交易</div></div>'+
'<div class="hstat"><div class="num" style="color:var(--yellow-dark)">'+notExecutedCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--yellow-dark)"><use href="#svg-star"/></svg> 未执行归档</div><div class="sub">观察/等回踩失效</div></div>'+
'<div class="hstat"><div class="num" style="color:var(--red)">'+invalidCount+'</div><div class="lbl"><svg width="14" height="14" color="var(--red)"><use href="#svg-shield"/></svg> 信号失效</div><div class="sub">含过期/风控失效</div></div>';
var items = Array.isArray(page.items) ? page.items : [];
@ -994,14 +994,14 @@ async function loadHistoryRecommendations(reset) {
var sigs = Array.isArray(r.signals)?r.signals:[];
var sigHtml = sigs.slice(0,4).map(function(s){ return '<span class=\"sig info\">'+cleanDisplayText(s).replace(/^(\\d+H|\\d+m|日线|周线)\\s*/,'').slice(0,12)+'</span>'; }).join('');
var duration = daysBetween(r.rec_time, r.last_track_time||r.hit_tp1_time||r.stopped_out_time);
var execText = hasPaper ? (paper.status === 'closed' ? '模拟交易已完成' : '模拟交易持有中') : (Number(r.entry_triggered || 0) ? '已触发执行' : '未执行');
var execText = hasPaper ? (paper.status === 'closed' ? '策略交易已完成' : '策略交易持有中') : (Number(r.entry_triggered || 0) ? '已触发执行' : '未执行');
var signalStateText = hasPaper ? '已执行归档' : (historyArchiveFilter === 'invalid' ? '失效归档' : '未执行归档');
var outcomeText = outcome.label;
var outcomeDetail = outcome.detail;
return '<div class=\"card\">'+
'<div class=\"card-bar\"><div class=\"coin-left\"><div class=\"coin-icon\">'+base.slice(0,2).toUpperCase()+'</div><div><span class=\"coin-symbol\">'+base+'</span></div></div><span class=\"hist-result-badge '+resultCls+'\">'+outcomeText+'</span></div>'+
'<div class=\"h-pnl-row\">'+(hasPaper ? '<span class=\"price h-entry-price\">$'+fmtN(entryP)+'</span><span class=\"h-arrow neutral\">&rarr;</span><span class=\"price h-exit-price '+(paper.status === 'closed' ? resultCls : 'muted')+'\">'+(paper.status === 'closed' ? '$'+fmtN(exitP) : '持有中')+'</span>' : '<span class=\"price h-entry-price muted\">未执行</span><span class=\"h-arrow neutral\">&rarr;</span><span class=\"price h-exit-price muted\">失效/归档</span>')+'<span class=\"hist-score-pill '+scoreCls+'\">机会分 '+score+'</span><span class=\"h-duration\">'+duration+'</span></div>'+
'<div class=\"hist-metric-row\"><div class=\"hist-metric\"><span class=\"hm-label\">交易阶段</span><span class=\"hm-val '+(hasPaper ? 'win' : 'blue')+'\">'+(hasPaper ? (paper.status === 'closed' ? '已完成模拟交易' : '模拟交易持有中') : '未执行归档')+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">结果说明</span><span class=\"hm-val blue\">'+outcomeDetail+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">执行状态</span><span class=\"hm-val '+(Number(r.entry_triggered||0)?'win':'blue')+'\">'+execText+'</span></div></div>'+
'<div class=\"hist-metric-row\"><div class=\"hist-metric\"><span class=\"hm-label\">交易阶段</span><span class=\"hm-val '+(hasPaper ? 'win' : 'blue')+'\">'+(hasPaper ? (paper.status === 'closed' ? '已完成策略交易' : '策略交易持有中') : '未执行归档')+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">结果说明</span><span class=\"hm-val blue\">'+outcomeDetail+'</span></div><div class=\"hist-metric\"><span class=\"hm-label\">执行状态</span><span class=\"hm-val '+(Number(r.entry_triggered||0)?'win':'blue')+'\">'+execText+'</span></div></div>'+
'<div class=\"kline-wrap\" id=\"wrap_'+hid+'\"><div class=\"kline-int-bar\"><button class=\"kline-int-btn\" data-int=\"15m\" onclick=\"switchKlineInterval(this);event.stopPropagation()\">15m</button><button class=\"kline-int-btn active\" data-int=\"1h\" onclick=\"switchKlineInterval(this);event.stopPropagation()\">1H</button><button class=\"kline-int-btn\" data-int=\"4h\" onclick=\"switchKlineInterval(this);event.stopPropagation()\">4H</button><button class=\"kline-int-btn\" data-int=\"1d\" onclick=\"switchKlineInterval(this);event.stopPropagation()\">1D</button></div><div class=\"kline-container loading\" id=\"'+hid+'\" data-symbol=\"'+(r.symbol||'')+'\" data-entry-price=\"'+hEntryPrice+'\" data-stop-loss=\"'+hSl+'\" data-tp1=\"'+hTp+'\" data-rec-time=\"'+hEntryTime+'\" data-tp1-time=\"'+hTpTime+'\" data-sl-time=\"'+hSlTime+'\" data-ref-price=\"'+(r.current_price||hEntryPrice||hTp||hSl||0)+'\" data-status=\"'+(r.status||'')+'\" ><div class=\"chart-loading\"><svg class=\"spin\" width=\"16\" height=\"16\" color=\"#8e91a0\"><use href=\"#svg-spinner\"/></svg></div></div></div>'+
(sigHtml?'<div class=\"signals-row\">'+sigHtml+'</div>':'')+
'<div class=\"card-footer hist-footer\"><span>'+fmtTime(r.rec_time)+'</span><span class=\"card-ver\">'+(r.strategy_version||'')+'</span></div></div>';

View File

@ -183,7 +183,7 @@ a { color: inherit; text-decoration: none; }
<a class="sidebar-link {% if active_nav == 'subscription' %}active{% endif %}" href="/subscription"><svg class="link-icon"><use href="#svg-subscribe"/></svg>订阅</a>
<a class="sidebar-link {% if active_nav == 'referral' %}active{% endif %}" href="/referral"><svg class="link-icon"><use href="#svg-referral"/></svg>邀请</a>
<div class="sidebar-section-label admin-link" style="display:none">管理员菜单</div>
<a class="sidebar-link admin-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading" style="display:none"><svg class="link-icon"><use href="#svg-paper"/></svg>模拟交易</a>
<a class="sidebar-link admin-link {% if active_nav == 'paper_trading' %}active{% endif %}" href="/paper-trading" style="display:none"><svg class="link-icon"><use href="#svg-paper"/></svg>策略交易</a>
<a class="sidebar-link admin-link {% if active_nav == 'review_center' %}active{% endif %}" href="/review-center" style="display:none"><svg class="link-icon"><use href="#svg-iterate"/></svg>复盘中心</a>
<a class="sidebar-link admin-link {% if active_nav == 'pipeline' %}active{% endif %}" href="/pipeline" style="display:none"><svg class="link-icon"><use href="#svg-pipeline"/></svg>链路日志</a>
<a class="sidebar-link admin-link {% if active_nav == 'llm_insights' %}active{% endif %}" href="/llm-insights" style="display:none"><svg class="link-icon"><use href="#svg-ai"/></svg>AI 记录</a>

View File

@ -23,7 +23,7 @@
<button class="btn" onclick="loadConfigs()">刷新</button>
</div>
</div>
<div class="hint">建议新闻源、LLM、链上、调度、模拟交易属于系统配置;复盘 meta、learned_rules、策略覆盖属于策略运行时配置。</div>
<div class="hint">建议新闻源、LLM、链上、调度、策略交易属于系统配置;复盘 meta、learned_rules、策略覆盖属于策略运行时配置。</div>
<div class="layout">
<section class="panel">
<div class="panel-head"><div class="panel-title">配置列表</div><div class="panel-note" id="countNote">--</div></div>

View File

@ -34,7 +34,7 @@
<div class="panel-title">导出内容</div>
<div class="list">
<div class="item"><b>推荐链路</b><span>recommendation / screening_log / cron_run_log</span></div>
<div class="item"><b>交易账本</b><span>paper_orders / paper_trades / events</span></div>
<div class="item"><b>策略交易</b><span>挂单 / 持仓 / 交易事件</span></div>
<div class="item"><b>复盘迭代</b><span>review / missed / strategy rules</span></div>
<div class="item"><b>证据源</b><span>舆情 / 链上 / AI 记录</span></div>
<div class="item"><b>运行配置</b><span>策略与系统配置快照</span></div>

View File

@ -118,7 +118,7 @@ h2 { font-size:26px; font-weight:900; margin:0 0 8px; color:var(--ink); }
</div>
<div class="panel active" id="panel-timeline"><div class="timeline" id="timeline"><div class="loading">加载中…</div></div></div>
<div class="panel" id="panel-candidates"><div class="card"><div class="card-title">发现的规律 <span class="badge hold">未达标不发布</span></div><div class="user-tabs-note">这些是系统复盘后发现的可能规律。只有样本、机会表现和稳定性都达标,才会进入线上策略;交易收益以模拟交易页为准。</div><div id="candidates"></div></div></div>
<div class="panel" id="panel-candidates"><div class="card"><div class="card-title">发现的规律 <span class="badge hold">未达标不发布</span></div><div class="user-tabs-note">这些是系统复盘后发现的可能规律。只有样本、机会表现和稳定性都达标,才会进入线上策略;交易收益以策略交易页为准。</div><div id="candidates"></div></div></div>
<div class="panel" id="panel-dryrun"><div class="card"><div class="card-title">发布预演 <span class="badge hold">只读评估,不改线上策略</span></div><div id="dryrun"></div></div></div>
<div class="panel" id="panel-failures"><div class="board"><div class="card"><div class="card-title">主要失败原因</div><div id="failureSummary"></div></div><div class="card"><div class="card-title">失败样本</div><div id="failures"></div></div></div></div>
<div class="panel" id="panel-versions"><div class="card"><div class="card-title">版本机会表现</div><div id="versions"></div></div></div>

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@
<div class="page-head">
<div>
<h1>复盘中心</h1>
<p>把机会发现、模拟交易收益、多源证据和策略迭代拆开看。收益只看模拟交易;机会归档只看发现和确认质量。</p>
<p>把机会发现、策略交易收益、多源证据和策略迭代拆开看。收益只看策略交易;机会归档只看发现和确认质量。</p>
</div>
<div class="actions">
<select class="select" id="daysSel" onchange="loadAll()">
@ -29,7 +29,7 @@
<div class="panel-body" id="opportunityPanel"><div class="loading">加载中...</div></div>
</section>
<section class="panel">
<div class="panel-head"><div><div class="panel-title">模拟交易复盘</div><div class="panel-desc" id="paperDef">--</div></div><span class="badge ok">唯一收益口径</span></div>
<div class="panel-head"><div><div class="panel-title">策略交易复盘</div><div class="panel-desc" id="paperDef">--</div></div><span class="badge ok">唯一收益口径</span></div>
<div class="panel-body" id="paperPanel"><div class="loading">加载中...</div></div>
</section>
<section class="panel">
@ -52,11 +52,11 @@
var API='';function $(id){return document.getElementById(id)}function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}function num(v,d){return Number(v||0).toFixed(d==null?0:d)}function pct(v){return (Number(v||0)>=0?'+':'')+Number(v||0).toFixed(2)+'%'}function usd(v){v=Number(v||0);return (v>=0?'+':'-')+'$'+Math.abs(v).toFixed(2)}function time(t){if(!t)return '--';var d=new Date(t);if(isNaN(d.getTime()))return t;return (d.getMonth()+1)+'/'+d.getDate()+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)}
function kpis(items){return '<div class="kpis">'+items.map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('')+'</div>'}
function rows(items,label,value,sub){items=items||[];if(!items.length)return '<div class="empty">暂无数据</div>';return '<div class="list">'+items.map(function(x){return '<div class="row"><div class="row-main"><div class="row-title">'+esc(label(x))+'</div><div class="row-sub">'+esc(sub?sub(x):'')+'</div></div><div class="value">'+esc(value?value(x):'')+'</div></div>'}).join('')+'</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 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||{};$('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(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>'}
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>'}
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>'}
function render(d){$('principles').innerHTML=(d.principles||[]).map(function(x){return '<div class="principle">'+esc(x)+'</div>'}).join('');renderOpportunity(d.opportunity||{});renderPaper(d.paper_trading||{});renderEvidence(d.evidence||{});renderIteration(d.iteration||{});renderRecent(d)}
async function loadAll(){try{var days=$('daysSel').value;var d=await (await fetch(API+'/api/review-center/dashboard?days='+days+'&_ts='+Date.now(),{cache:'no-store'})).json();render(d)}catch(e){['principles','opportunityPanel','paperPanel','evidencePanel','iterationPanel','recentPanel'].forEach(function(id){$(id).innerHTML='<div class="empty">加载失败</div>'})}}
loadAll();

View File

@ -10,16 +10,16 @@
<div class="page-head">
<div>
<h1>策略归因</h1>
<p>看清哪些因子、环境和证据源更容易把机会推进到可执行,以及是否真的进入模拟交易。这里不把机会价格波动当作交易收益。</p>
<p>看清哪些因子、环境和证据源更容易把机会推进到可执行,以及是否真的进入策略交易。这里不把机会价格波动当作交易收益。</p>
</div>
<span class="pill">收益只来自模拟交易</span>
<span class="pill">收益只来自策略交易</span>
</div>
<div class="kpis" id="metrics"><div class="loading">加载中...</div></div>
<div class="note" id="definition">策略归因只看机会转化和模拟交易转化;收益只来自 paper_trades</div>
<div class="note" id="definition">策略归因只看机会转化和策略交易转化;收益只来自交易账本</div>
<div class="flow">
<div class="flow-step"><b>机会样本</b><span>系统发现并入库的机会,不等于交易。</span></div>
<div class="flow-step"><b>可执行转化</b><span>确认层输出现在可买或等回踩。</span></div>
<div class="flow-step"><b>模拟执行</b><span>只有 buy_now 被 paper trading 开仓才进入收益账本。</span></div>
<div class="flow-step"><b>策略执行</b><span>只有 buy_now 被策略交易开仓才进入收益账本。</span></div>
<div class="flow-step"><b>证据归因</b><span>链上、舆情、技术因子只做贡献分析。</span></div>
</div>
<div class="grid">
@ -34,7 +34,7 @@
<script>
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}
function pct(x){return Number(x||0).toFixed(1)+'%'}function usd(x){x=Number(x||0);return (x>=0?'+':'-')+'$'+Math.abs(x).toFixed(2)}
function table(rows,key){if(!rows||!rows.length)return'<div class="empty">暂无数据</div>';return '<div class="table-wrap"><table class="table"><thead><tr><th>归因项</th><th>机会</th><th>可执行</th><th>现买</th><th>模拟执行</th><th>执行转化</th><th>模拟胜率</th><th>模拟已实现</th></tr></thead><tbody>'+rows.map(function(r){return '<tr><td><span class="tag">'+esc(r[key]||'--')+'</span></td><td class="num">'+esc(r.opportunity_count||0)+'</td><td>'+esc(r.actionable_count||0)+'</td><td>'+esc(r.buy_now_count||0)+'</td><td>'+esc(r.paper_trade_count||0)+'</td><td class="num">'+pct(r.actionable_conversion_pct)+'</td><td class="num">'+pct(r.paper_win_rate_pct)+'</td><td class="'+(Number(r.paper_realized_pnl_usdt||0)>=0?'green':'red')+'">'+usd(r.paper_realized_pnl_usdt)+'</td></tr>'}).join('')+'</tbody></table></div>'}
async function load(){try{var d=await (await fetch('/api/strategy/insights?_ts='+Date.now(),{cache:'no-store'})).json();var o=d.overview||{};definition.textContent=o.definition||'策略归因只看机会转化和模拟交易转化。';metrics.innerHTML=[['机会样本',o.total_opportunities||0,'blue'],['可执行机会',o.actionable_count||0,''],['现在可买',o.buy_now_count||0,'green'],['模拟执行',o.paper_trade_count||0,'green'],['模拟胜率',pct(o.paper_win_rate_pct),''],['已实现收益',usd(o.paper_realized_pnl_usdt),Number(o.paper_realized_pnl_usdt||0)>=0?'green':'red']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('');factorPerf.innerHTML=table(d.factor_attribution,'factor');envPerf.innerHTML=table(d.market_environment,'environment');evidencePerf.innerHTML=table(d.evidence_attribution,'evidence');versionPerf.innerHTML=table(d.version_performance,'strategy_version')}catch(e){metrics.innerHTML='<div class="empty">加载失败</div>'}}load();
function table(rows,key){if(!rows||!rows.length)return'<div class="empty">暂无数据</div>';return '<div class="table-wrap"><table class="table"><thead><tr><th>归因项</th><th>机会</th><th>可执行</th><th>现买</th><th>策略执行</th><th>执行转化</th><th>策略胜率</th><th>策略已实现</th></tr></thead><tbody>'+rows.map(function(r){return '<tr><td><span class="tag">'+esc(r[key]||'--')+'</span></td><td class="num">'+esc(r.opportunity_count||0)+'</td><td>'+esc(r.actionable_count||0)+'</td><td>'+esc(r.buy_now_count||0)+'</td><td>'+esc(r.paper_trade_count||0)+'</td><td class="num">'+pct(r.actionable_conversion_pct)+'</td><td class="num">'+pct(r.paper_win_rate_pct)+'</td><td class="'+(Number(r.paper_realized_pnl_usdt||0)>=0?'green':'red')+'">'+usd(r.paper_realized_pnl_usdt)+'</td></tr>'}).join('')+'</tbody></table></div>'}
async function load(){try{var d=await (await fetch('/api/strategy/insights?_ts='+Date.now(),{cache:'no-store'})).json();var o=d.overview||{};definition.textContent=o.definition||'策略归因只看机会转化和策略交易转化。';metrics.innerHTML=[['机会样本',o.total_opportunities||0,'blue'],['可执行机会',o.actionable_count||0,''],['现在可买',o.buy_now_count||0,'green'],['策略执行',o.paper_trade_count||0,'green'],['策略胜率',pct(o.paper_win_rate_pct),''],['已实现收益',usd(o.paper_realized_pnl_usdt),Number(o.paper_realized_pnl_usdt||0)>=0?'green':'red']].map(function(x){return '<div class="kpi"><span>'+esc(x[0])+'</span><b class="'+(x[2]||'')+'">'+esc(x[1])+'</b></div>'}).join('');factorPerf.innerHTML=table(d.factor_attribution,'factor');envPerf.innerHTML=table(d.market_environment,'environment');evidencePerf.innerHTML=table(d.evidence_attribution,'evidence');versionPerf.innerHTML=table(d.version_performance,'strategy_version')}catch(e){metrics.innerHTML='<div class="empty">加载失败</div>'}}load();
</script>
{% endblock %}

View File

@ -94,12 +94,12 @@ def test_chat_rejects_paper_trading_questions(monkeypatch):
client = TestClient(web_server.app)
client.cookies.set("altcoin_session", token)
resp = client.post("/api/chat/send", json={"message": "帮我看一下模拟交易开仓和收益"})
resp = client.post("/api/chat/send", json={"message": "帮我看一下策略交易开仓和收益"})
assert resp.status_code == 200
data = resp.json()
assert data["intent"] == "restricted"
assert "内部模拟交易数据不可在智能问答中直接访问" in data["answer"]["summary"]
assert "内部策略交易数据不可在智能问答中直接访问" in data["answer"]["summary"]
assert data["answer"]["evidence"] == []

View File

@ -48,7 +48,7 @@ def test_paper_trading_admin_can_access_page_and_api():
summary = client.get("/api/paper-trading/summary")
assert page.status_code == 200
assert "模拟交易" in page.text
assert "策略交易" in page.text
assert summary.status_code == 200
assert "account_equity_usdt" in summary.json()