Files
simon fce725e13c 初始提交:高股息策略研究与回测系统
从 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。
2026-10-03 13:54:56 +08:00

196 lines
8.8 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}