从 Point-in-Time 股票筛选到统一 Web 前端的完整链路: 筛选 → 画像 → 策略 → 回测 → Walk-forward → 绩效分析 → 报告/前端。 架构 - 数据层与策略层分离;策略代码不写 SQL,只经 data/repo.py 取数 - 所有业务阈值集中在 config/*.yml,代码零硬编码(字段写错直接报错) - 报告只做「run_id → SQL → 渲染」,不做任何计算,数字可追溯 - 前后端分离:output/ 静态站点 + hdiv web 提供的 REST API 数据安全 - 只增不删:SQL 钩子拦截 DELETE/DROP/TRUNCATE,并有源码扫描测试守护 - qlib 原有表只读,本项目数据写入 hd_ 前缀表 - 回补使用 INSERT IGNORE,保证既有行零改动 - .env 存密钥且已 gitignore;output/、logs/、.venv/ 不入库 交付物 - 30 张 hd_* 表、7 个 YAML 配置、283 项自动化测试 - 统一 Web 前端(hash 路由 SPA)+ nginx 部署配置与 launchd 托管脚本 如实声明的限制 - 策略缺少稳定的样本外超额收益(Walk-forward 7 窗口均值 -0.95%, 基准 +2.29%);其价值体现在回撤控制,而非超额收益 - 涨跌停/停牌约束仅覆盖 2019 年起;index_weight 尚未填充 - AI Agent 层(plan.md 第四版 P8)未实现 详见 docs/user-guide.md 与 docs/implementation-status.md。
329 lines
14 KiB
HTML
329 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block content %}
|
||
|
||
<div class="kpi-grid">
|
||
<div class="kpi">
|
||
<div class="label">总收益</div>
|
||
<div class="value {{ 'gain' if m.total_return and m.total_return > 0 else ('loss' if m.total_return and m.total_return < 0 else '') }}">{{ m.total_return_s }}</div>
|
||
<div class="note">{{ start_date }} ~ {{ end_date }}</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="label">年化 CAGR</div>
|
||
<div class="value {{ 'gain' if m.cagr and m.cagr > 0 else ('loss' if m.cagr and m.cagr < 0 else '') }}">{{ m.cagr_s }}</div>
|
||
<div class="note">区间 {{ m.years_s }} 年</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="label">最大回撤</div>
|
||
<div class="value loss">{{ m.max_drawdown_s }}</div>
|
||
<div class="note">{{ dd.peak_date }} → {{ dd.trough_date }}{% if dd.recovery_date %} → 修复 {{ dd.recovery_date }}{% endif %}</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="label">Sharpe</div>
|
||
<div class="value {{ 'gain' if m.sharpe and m.sharpe > 0 else 'loss' }}">{{ m.sharpe_s }}</div>
|
||
<div class="note">无风险利率 {{ rf }}</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="label">成交笔数</div>
|
||
<div class="value">{{ m.trade_count }}</div>
|
||
<div class="note">平仓 {{ m.closing_trade_count }} 笔 · 胜率 {{ m.win_rate_s }}</div>
|
||
</div>
|
||
<div class="kpi">
|
||
<div class="label">累计现金分红</div>
|
||
<div class="value">{{ dividend_total }}</div>
|
||
<div class="note">已扣红利税 {{ dividend_tax }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if superseded %}
|
||
<div class="callout fail">
|
||
<strong>⚠ 这是一份历史运行报告,已被更新的运行取代。</strong>
|
||
同策略同模式下还有 {{ superseded.newer_count }} 次更新的回测,最新为
|
||
<span class="mono">{{ superseded.latest_run }}</span>({{ superseded.latest_at }})。
|
||
旧运行可能产生于成本模型或筛选条件修复之前,<strong>请以最新运行为准</strong>。
|
||
本报告保留是为了满足「回测结果必须可复现」的要求,不代表当前结论。
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if unbalanced %}
|
||
<div class="callout fail">
|
||
<strong>资金对账未通过!</strong>残差 {{ rc.residual }}(容差 1.0 元)。
|
||
这通常意味着成本或分红入账存在遗漏,请先修复再解读绩效指标。
|
||
</div>
|
||
{% else %}
|
||
<div class="callout ok">
|
||
<strong>资金对账通过</strong> — 恒等式「期初 = 期末现金 + 买入 − 卖出 + 费用 − 分红」
|
||
残差仅 {{ rc.residual }} 元(浮点误差量级),说明成本与分红入账完整无遗漏。
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<h2>净值曲线与基准对比</h2>
|
||
<div id="chart-equity" class="chart"></div>
|
||
</div>
|
||
|
||
<div class="chart-grid">
|
||
<div class="card">
|
||
<h2>回撤曲线</h2>
|
||
<div id="chart-dd" class="chart short"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>年度收益</h2>
|
||
<div id="chart-year" class="chart short"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>绩效指标(plan.md §28 四类)</h2>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead><tr><th style="text-align:left">类别</th><th style="text-align:left">指标</th><th>数值</th><th style="text-align:left">说明</th></tr></thead>
|
||
<tbody>
|
||
{% for r in metric_rows %}
|
||
<tr>
|
||
<td style="text-align:left"><span class="badge info">{{ r.category }}</span></td>
|
||
<td style="text-align:left">{{ r.label }}</td>
|
||
<td class="num"><strong>{{ r.value }}</strong></td>
|
||
<td style="text-align:left" class="small muted">{{ r.note }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>基准比较(plan.md §28.4)</h2>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead><tr><th style="text-align:left">基准</th><th>总收益</th><th>CAGR</th><th>最大回撤</th><th>年化波动</th><th>超额收益</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td style="text-align:left"><strong>策略</strong></td>
|
||
<td class="num">{{ m.total_return_s }}</td>
|
||
<td class="num">{{ m.cagr_s }}</td>
|
||
<td class="num">{{ m.max_drawdown_s }}</td>
|
||
<td class="num">{{ m.volatility_s }}</td>
|
||
<td class="num">—</td>
|
||
</tr>
|
||
{% for b in benchmarks %}
|
||
<tr>
|
||
<td style="text-align:left">{{ b.name }} <span class="mono small muted">{{ b.code }}</span></td>
|
||
<td class="num">{{ b.total_return }}</td>
|
||
<td class="num">{{ b.cagr }}</td>
|
||
<td class="num">{{ b.max_drawdown }}</td>
|
||
<td class="num">{{ b.volatility }}</td>
|
||
<td class="num {{ 'gain' if b.excess_positive else 'loss' }}">{{ b.excess_return }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{% if sector_rows %}
|
||
<div class="card">
|
||
<h2>行业暴露(plan.md §20 集中度)</h2>
|
||
<div id="chart-sector" class="chart short"></div>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead><tr><th style="text-align:left">行业</th><th>最大权重</th><th>平均权重</th><th>股票数</th></tr></thead>
|
||
<tbody>
|
||
{% for s in sector_rows %}
|
||
<tr><td style="text-align:left">{{ s.industry }}</td><td class="num">{{ s.max_weight }}</td>
|
||
<td class="num">{{ s.avg_weight }}</td><td class="num">{{ s.symbols }}</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<h2>成交流水(含 reason —— plan.md §32「为什么买」)</h2>
|
||
<div class="callout">
|
||
每笔交易都记录了触发时的完整理由:股息率取值、历史分位、所用参照窗口、
|
||
阈值与规则文本,使 AI 或人工都能事后复核决策依据。
|
||
</div>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead>
|
||
<tr><th>#</th><th>代码</th><th>信号日</th><th>成交日</th><th>方向</th>
|
||
<th>价格</th><th>数量</th><th>金额</th><th>费用</th><th>已实现盈亏</th>
|
||
<th>持有天数</th><th style="text-align:left">触发理由</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for t in trade_rows %}
|
||
<tr>
|
||
<td class="num">{{ loop.index }}</td>
|
||
<td class="mono">{{ t.symbol }}</td>
|
||
<td class="num">{{ t.signal_date }}</td>
|
||
<td class="num">{{ t.execution_date }}</td>
|
||
<td><span class="badge {{ 'BUY' if t.side == 'BUY' else 'SELL' }}">{{ t.side }}</span></td>
|
||
<td class="num">{{ t.price }}</td>
|
||
<td class="num">{{ t.quantity }}</td>
|
||
<td class="num">{{ t.amount }}</td>
|
||
<td class="num">{{ t.fees }}</td>
|
||
<td class="num {{ 'gain' if t.pnl_positive else ('loss' if t.pnl_negative else '') }}">{{ t.pnl }}</td>
|
||
<td class="num">{{ t.holding_days }}</td>
|
||
<td style="text-align:left" class="small">{{ t.reason }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{% if signal_skips %}
|
||
<div class="card">
|
||
<h2>未成交信号与原因</h2>
|
||
<div class="callout warn">
|
||
这些信号「想交易但没成交」。区分它们与已成交记录,才能判断策略表现
|
||
是否被涨跌停/停牌等约束实质影响。
|
||
</div>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead><tr><th>代码</th><th>信号日</th><th>类型</th><th>分位</th><th style="text-align:left">未成交原因</th></tr></thead>
|
||
<tbody>
|
||
{% for s in signal_skips %}
|
||
<tr><td class="mono">{{ s.symbol }}</td><td class="num">{{ s.signal_date }}</td>
|
||
<td><span class="badge info">{{ s.kind }}</span></td><td class="num">{{ s.pct }}</td>
|
||
<td style="text-align:left" class="small">{{ s.reason }}</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if unimplemented %}
|
||
<div class="callout warn">
|
||
<strong>未建模部分(如实声明,不假装已实现):</strong>
|
||
<ul style="margin:4px 0 0 18px">
|
||
{% for u in unimplemented %}<li>{{ u }}</li>{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<h2>回测配置与可复现性(plan.md §47 原则 6)</h2>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead><tr><th style="text-align:left">项</th><th style="text-align:left">值</th></tr></thead>
|
||
<tbody>
|
||
<tr><td style="text-align:left">策略</td><td style="text-align:left" class="mono">{{ strategy_id }} v{{ strategy_version }}</td></tr>
|
||
<tr><td style="text-align:left">策略指纹 config_hash</td><td style="text-align:left" class="mono">{{ config_hash }}</td></tr>
|
||
<tr><td style="text-align:left">数据版本 data_version</td><td style="text-align:left" class="mono">{{ data_version }}</td></tr>
|
||
<tr><td style="text-align:left">代码版本 code_version</td><td style="text-align:left" class="mono">{{ code_version }}</td></tr>
|
||
<tr><td style="text-align:left">随机种子 seed</td><td style="text-align:left" class="mono">{{ seed }}</td></tr>
|
||
<tr><td style="text-align:left">信号评估频率</td><td style="text-align:left">{{ signal_freq }}</td></tr>
|
||
<tr><td style="text-align:left">分位参照口径</td><td style="text-align:left">{{ reference_mode }}</td></tr>
|
||
<tr><td style="text-align:left">成交价</td><td style="text-align:left">{{ fill_price }}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<details>
|
||
<summary>查看生效的策略配置</summary>
|
||
<pre class="mono small" style="white-space:pre-wrap;max-height:420px;overflow:auto">{{ strategy_json }}</pre>
|
||
</details>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(function () {
|
||
if (typeof echarts === 'undefined') return;
|
||
var C = {{ chart_colors | json }};
|
||
|
||
var eq = document.getElementById('chart-equity');
|
||
if (eq) {
|
||
var d = {{ equity_chart | json }};
|
||
var c = echarts.init(eq);
|
||
c.setOption({
|
||
color: C,
|
||
grid: { left: 66, right: 30, top: 40, bottom: 60 },
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { top: 0, textStyle: { color: '#64748B' } },
|
||
xAxis: { type: 'category', data: d.dates, axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return String(v).slice(0, 7); } } },
|
||
yAxis: { type: 'value', scale: true, name: '净值', axisLabel: { color: '#64748B' },
|
||
splitLine: { lineStyle: { color: '#E9EEF6' } } },
|
||
dataZoom: [{ type: 'inside' }, { type: 'slider', height: 18, bottom: 12 }],
|
||
series: [
|
||
{ name: '策略净值', type: 'line', data: d.nav, showSymbol: false,
|
||
lineStyle: { width: 2, color: C[0] } },
|
||
{ name: '基准净值', type: 'line', data: d.bench, showSymbol: false,
|
||
lineStyle: { width: 1.4, color: C[1], type: 'dashed' } }
|
||
]
|
||
});
|
||
window.addEventListener('resize', function () { c.resize(); });
|
||
}
|
||
|
||
var dd = document.getElementById('chart-dd');
|
||
if (dd) {
|
||
var d2 = {{ equity_chart | json }};
|
||
var c2 = echarts.init(dd);
|
||
c2.setOption({
|
||
color: C,
|
||
grid: { left: 66, right: 24, top: 24, bottom: 44 },
|
||
tooltip: { trigger: 'axis',
|
||
valueFormatter: function (v) { return (v * 100).toFixed(2) + '%'; } },
|
||
xAxis: { type: 'category', data: d2.dates, axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return String(v).slice(0, 7); } } },
|
||
yAxis: { type: 'value', name: '回撤', axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return (v * 100).toFixed(0) + '%'; } },
|
||
splitLine: { lineStyle: { color: '#E9EEF6' } } },
|
||
series: [{ name: '回撤', type: 'line', data: d2.dd, showSymbol: false,
|
||
areaStyle: { opacity: 0.18 }, lineStyle: { width: 1.2, color: C[4] } }]
|
||
});
|
||
window.addEventListener('resize', function () { c2.resize(); });
|
||
}
|
||
|
||
var yr = document.getElementById('chart-year');
|
||
if (yr) {
|
||
var y = {{ year_chart | json }};
|
||
var c3 = echarts.init(yr);
|
||
c3.setOption({
|
||
color: C,
|
||
grid: { left: 62, right: 24, top: 24, bottom: 44 },
|
||
tooltip: { trigger: 'axis',
|
||
valueFormatter: function (v) { return (v * 100).toFixed(2) + '%'; } },
|
||
xAxis: { type: 'category', data: y.years, axisLabel: { color: '#64748B' } },
|
||
yAxis: { type: 'value', axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return (v * 100).toFixed(0) + '%'; } },
|
||
splitLine: { lineStyle: { color: '#E9EEF6' } } },
|
||
series: [{ type: 'bar', data: y.values.map(function (v) {
|
||
return { value: v, itemStyle: { color: v >= 0 ? '#DC2626' : '#059669' } };
|
||
}), barMaxWidth: 46,
|
||
label: { show: true, position: 'top', fontSize: 10, color: '#0F172A',
|
||
formatter: function (p) { return (p.value * 100).toFixed(1) + '%'; } } }]
|
||
});
|
||
window.addEventListener('resize', function () { c3.resize(); });
|
||
}
|
||
|
||
var sec = document.getElementById('chart-sector');
|
||
if (sec) {
|
||
var s = {{ sector_chart | json }};
|
||
var c4 = echarts.init(sec);
|
||
c4.setOption({
|
||
color: C,
|
||
grid: { left: 110, right: 40, top: 20, bottom: 30 },
|
||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' },
|
||
valueFormatter: function (v) { return (v * 100).toFixed(2) + '%'; } },
|
||
xAxis: { type: 'value', axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return (v * 100).toFixed(0) + '%'; } },
|
||
splitLine: { lineStyle: { color: '#E9EEF6' } } },
|
||
yAxis: { type: 'category', data: s.names, axisLabel: { color: '#64748B' } },
|
||
series: [{ type: 'bar', data: s.values, itemStyle: { color: C[0] },
|
||
barMaxWidth: 22,
|
||
markLine: s.limit == null ? undefined : { silent: true, symbol: 'none',
|
||
data: [{ xAxis: s.limit, lineStyle: { color: C[4], type: 'dashed' },
|
||
label: { formatter: '行业上限', color: C[4] } }] } }]
|
||
});
|
||
window.addEventListener('resize', function () { c4.resize(); });
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|