从 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。
196 lines
8.8 KiB
HTML
196 lines
8.8 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block content %}
|
||
|
||
<div class="callout {{ 'ok' if summary.oos_win_rate and summary.oos_win_rate >= 0.5 else 'warn' }}">
|
||
<strong>样本外结论:</strong>
|
||
{{ summary.windows }} 个滚动窗口中
|
||
<strong>{{ summary.oos_win_windows }}</strong> 个测试期为正
|
||
(胜率 {{ summary.oos_win_rate_s }})。
|
||
样本外收益均值 {{ summary.oos_total_return_mean_s }},
|
||
{% if summary.oos_excess_return_mean is not none %}
|
||
相对基准超额 <strong>{{ summary.oos_excess_return_mean_s }}</strong>。
|
||
{% endif %}
|
||
{% if summary.oos_stability is not none and summary.oos_stability < 1 %}
|
||
⚠ 稳定性指标 {{ summary.oos_stability_s }} 偏低(<1),说明各窗口差异较大,结论需谨慎。
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="kpi-grid">
|
||
<div class="kpi"><div class="label">窗口数</div><div class="value">{{ summary.windows }}</div>
|
||
<div class="note">{{ scheme }} · 训练 {{ train_years }} 年 / 测试 {{ test_years }} 年</div></div>
|
||
<div class="kpi"><div class="label">样本外胜率</div>
|
||
<div class="value {{ 'gain' if summary.oos_win_rate and summary.oos_win_rate >= 0.5 else 'loss' }}">{{ summary.oos_win_rate_s }}</div>
|
||
<div class="note">{{ summary.oos_win_windows }} / {{ summary.windows }} 个窗口为正</div></div>
|
||
<div class="kpi"><div class="label">样本外收益均值</div>
|
||
<div class="value {{ 'gain' if summary.oos_total_return_mean and summary.oos_total_return_mean > 0 else 'loss' }}">{{ summary.oos_total_return_mean_s }}</div>
|
||
<div class="note">中位数 {{ summary.oos_total_return_median_s }}</div></div>
|
||
<div class="kpi"><div class="label">最差窗口回撤</div>
|
||
<div class="value loss">{{ summary.oos_max_drawdown_worst_s }}</div>
|
||
<div class="note">样本外最大回撤的最差值</div></div>
|
||
<div class="kpi"><div class="label">超额收益均值</div>
|
||
<div class="value {{ 'gain' if summary.oos_excess_return_mean and summary.oos_excess_return_mean > 0 else 'loss' }}">{{ summary.oos_excess_return_mean_s }}</div>
|
||
<div class="note">相对 {{ benchmark_name }}</div></div>
|
||
<div class="kpi"><div class="label">稳定性</div><div class="value">{{ summary.oos_stability_s }}</div>
|
||
<div class="note">样本外收益均值 / 标准差,越高越可信</div></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>样本内 vs 样本外(plan.md §24/§25)</h2>
|
||
<div class="callout">
|
||
训练段用于<strong>校准阈值</strong>(把「历史 P75」转成绝对股息率),
|
||
测试段<strong>冻结</strong>该分布与参数 —— 引擎在代码层面不接受测试期自身数据参与分位计算,
|
||
因此这里不存在「用未来分布判断现在」的隐性未来函数。
|
||
</div>
|
||
<div id="chart-window" class="chart"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>逐窗口明细</h2>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th><th>训练区间</th><th>测试区间</th>
|
||
<th>训练收益</th><th>训练回撤</th>
|
||
<th>测试收益</th><th>测试回撤</th><th>测试Sharpe</th>
|
||
<th>测试成交</th><th>绝对买入阈值</th><th>绝对卖出阈值</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for w in windows %}
|
||
<tr>
|
||
<td class="num">{{ w.index }}</td>
|
||
<td class="num">{{ w.train_start }} ~ {{ w.train_end }}</td>
|
||
<td class="num">{{ w.test_start }} ~ {{ w.test_end }}</td>
|
||
<td class="num">{{ w.train_return }}</td>
|
||
<td class="num loss">{{ w.train_dd }}</td>
|
||
<td class="num {{ 'gain' if w.test_positive else 'loss' }}"><strong>{{ w.test_return }}</strong></td>
|
||
<td class="num loss">{{ w.test_dd }}</td>
|
||
<td class="num">{{ w.test_sharpe }}</td>
|
||
<td class="num">{{ w.test_trades }}</td>
|
||
<td class="num">{{ w.abs_entry }}</td>
|
||
<td class="num">{{ w.abs_exit }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td colspan="5">样本外汇总</td>
|
||
<td class="num">{{ summary.oos_total_return_mean_s }}</td>
|
||
<td class="num">{{ summary.oos_max_drawdown_worst_s }}</td>
|
||
<td class="num">{{ summary.oos_sharpe_mean_s }}</td>
|
||
<td colspan="3">均值 / 最差</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{% if frozen_rows %}
|
||
<div class="card">
|
||
<h2>校准出的绝对阈值(阈值稳定性)</h2>
|
||
<div class="callout">
|
||
若各窗口校准出的绝对股息率阈值<strong>差异很大</strong>,说明「历史 P75」这一相对口径
|
||
在不同时期对应的绝对水平不稳定,策略的市场环境依赖性较强。
|
||
</div>
|
||
<div id="chart-frozen" class="chart short"></div>
|
||
<div class="table-scroll">
|
||
<table class="data">
|
||
<thead><tr><th>#</th><th>校准窗口</th><th>买入阈值(绝对股息率)</th><th>卖出阈值</th><th>样本观测数</th><th>中位股息率</th></tr></thead>
|
||
<tbody>
|
||
{% for f in frozen_rows %}
|
||
<tr>
|
||
<td class="num">{{ f.index }}</td>
|
||
<td class="num">{{ f.window }}</td>
|
||
<td class="num">{{ f.entry }}</td>
|
||
<td class="num">{{ f.exit }}</td>
|
||
<td class="num">{{ f.obs }}</td>
|
||
<td class="num">{{ f.median }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="card">
|
||
<h2>可复现性</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">Walk-forward ID</td><td style="text-align:left" class="mono">{{ wf_id }}</td></tr>
|
||
<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">配置指纹</td><td style="text-align:left" class="mono">{{ config_hash }}</td></tr>
|
||
<tr><td style="text-align:left">数据版本</td><td style="text-align:left" class="mono">{{ data_version }}</td></tr>
|
||
<tr><td style="text-align:left">窗口方案</td><td style="text-align:left">{{ scheme }} · 训练 {{ train_years }} 年 · 测试 {{ test_years }} 年 · 步进 {{ step_months }} 月</td></tr>
|
||
<tr><td style="text-align:left">测试期冻结参数</td><td style="text-align:left">{{ freeze }}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
(function () {
|
||
if (typeof echarts === 'undefined') return;
|
||
var C = {{ chart_colors | json }};
|
||
|
||
var el = document.getElementById('chart-window');
|
||
if (el) {
|
||
var d = {{ window_chart | json }};
|
||
var c = echarts.init(el);
|
||
c.setOption({
|
||
color: C,
|
||
grid: { left: 66, right: 30, top: 44, bottom: 50 },
|
||
tooltip: { trigger: 'axis',
|
||
valueFormatter: function (v) { return v == null ? '—' : (v * 100).toFixed(2) + '%'; } },
|
||
legend: { top: 0, textStyle: { color: '#64748B' } },
|
||
xAxis: { type: 'category', data: d.labels, axisLabel: { color: '#64748B' } },
|
||
yAxis: { type: 'value', name: '收益', axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return (v * 100).toFixed(0) + '%'; } },
|
||
splitLine: { lineStyle: { color: '#E9EEF6' } } },
|
||
series: [
|
||
{ name: '样本内(训练)', type: 'bar', data: d.train,
|
||
itemStyle: { color: '#94A3B8' }, barMaxWidth: 26 },
|
||
{ name: '样本外(测试)', type: 'bar', data: d.test.map(function (v) {
|
||
return { value: v, itemStyle: { color: v >= 0 ? '#DC2626' : '#059669' } };
|
||
}), barMaxWidth: 26 },
|
||
{ name: '基准(测试期)', type: 'line', data: d.bench, showSymbol: true,
|
||
symbolSize: 6, lineStyle: { color: C[1], type: 'dashed', width: 1.4 } }
|
||
]
|
||
});
|
||
window.addEventListener('resize', function () { c.resize(); });
|
||
}
|
||
|
||
var fel = document.getElementById('chart-frozen');
|
||
if (fel) {
|
||
var f = {{ frozen_chart | json }};
|
||
var c2 = echarts.init(fel);
|
||
c2.setOption({
|
||
color: C,
|
||
grid: { left: 66, right: 30, top: 36, bottom: 44 },
|
||
tooltip: { trigger: 'axis',
|
||
valueFormatter: function (v) { return (v * 100).toFixed(2) + '%'; } },
|
||
legend: { top: 0, textStyle: { color: '#64748B' } },
|
||
xAxis: { type: 'category', data: f.labels, axisLabel: { color: '#64748B' } },
|
||
yAxis: { type: 'value', name: '绝对股息率', axisLabel: { color: '#64748B',
|
||
formatter: function (v) { return (v * 100).toFixed(0) + '%'; } },
|
||
splitLine: { lineStyle: { color: '#E9EEF6' } } },
|
||
series: [
|
||
{ name: '买入阈值', type: 'line', data: f.entry, showSymbol: true, symbolSize: 7,
|
||
lineStyle: { color: C[4], width: 2 } },
|
||
{ name: '卖出阈值', type: 'line', data: f.exit, showSymbol: true, symbolSize: 7,
|
||
lineStyle: { color: C[3], width: 2 } }
|
||
]
|
||
});
|
||
window.addEventListener('resize', function () { c2.resize(); });
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock %}
|