初始提交:高股息策略研究与回测系统

从 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。
This commit is contained in:
2026-10-03 13:54:56 +08:00
commit fce725e13c
127 changed files with 28001 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
{% extends "base.html" %}
{% block content %}
<div class="kpi-grid">
<div class="kpi">
<div class="label">当前股息率</div>
<div class="value {{ 'gain' if current_pct_num is not none and current_pct_num >= entry_p else '' }}">{{ current_yield }}</div>
<div class="note">TTM 每股分红 / 不复权收盘价</div>
</div>
<div class="kpi">
<div class="label">历史分位</div>
<div class="value {{ 'gain' if current_pct_num is not none and current_pct_num >= entry_p else '' }}">{{ current_pct }}</div>
<div class="note">当前值在自身历史分布中的位置</div>
</div>
<div class="kpi">
<div class="label">P75 / P25 参考</div>
<div class="value" style="font-size:18px">{{ p75 }} / {{ p25 }}</div>
<div class="note">策略买入 / 卖出阈值</div>
</div>
<div class="kpi">
<div class="label">样本观测</div>
<div class="value">{{ n_obs }}</div>
<div class="note">日频观测数({{ history_span }})</div>
</div>
</div>
{% if signal_note %}
<div class="callout {{ signal_class }}">
<strong>策略信号判定:</strong>{{ signal_note }}
</div>
{% endif %}
<div class="card">
<h2>K线 · 股息率 · PE · PB · 回撤(plan.md §33 四联图)</h2>
<div id="chart-main" class="chart" style="height:720px"></div>
</div>
<div class="chart-grid">
<div class="card">
<h2>股息率历史分布(plan.md §9)</h2>
<div id="chart-hist" class="chart"></div>
</div>
<div class="card">
<h2>股息率波动特征(plan.md §11)</h2>
<div id="chart-vol" class="chart"></div>
</div>
</div>
<div class="card">
<h2>历史分布统计(plan.md §9 要求的分位)</h2>
<div class="table-scroll">
<table class="data">
<thead>
<tr>
<th style="text-align:left">指标</th><th>窗口</th><th>样本</th><th>Min</th><th>P10</th><th>P25</th>
<th>P50</th><th>P75</th><th>P90</th><th>Max</th><th>Mean</th><th>Std</th>
<th>当前值</th><th>当前分位</th><th>状态</th>
</tr>
</thead>
<tbody>
{% for r in dist_rows %}
<tr>
<td style="text-align:left">{{ r.label }}</td>
<td class="num">{{ r.window }}</td>
<td class="num">{{ r.n_obs }}</td>
<td class="num">{{ r.min }}</td>
<td class="num">{{ r.p10 }}</td>
<td class="num">{{ r.p25 }}</td>
<td class="num">{{ r.p50 }}</td>
<td class="num">{{ r.p75 }}</td>
<td class="num">{{ r.p90 }}</td>
<td class="num">{{ r.max }}</td>
<td class="num">{{ r.mean }}</td>
<td class="num">{{ r.std }}</td>
<td class="num"><strong>{{ r.current }}</strong></td>
<td class="num {{ 'gain' if r.pct_high else '' }}">{{ r.pct }}</td>
<td><span class="badge {{ 'OK' if r.status == 'OK' else 'WARN' }}">{{ r.status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="chart-grid">
<div class="card">
<h2>分红质量(plan.md §14)</h2>
<div class="table-scroll">
<table class="data">
<thead><tr><th style="text-align:left">指标</th><th>取值</th><th style="text-align:left">说明</th></tr></thead>
<tbody>
{% for r in dividend_rows %}
<tr><td style="text-align:left">{{ r.label }}</td><td class="num">{{ r.value }}</td>
<td style="text-align:left" class="small muted">{{ r.note }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card">
<h2>安全边际分项得分(plan.md §15)</h2>
{% if score_rows %}
<div id="chart-score" class="chart short"></div>
<div class="table-scroll">
<table class="data">
<thead><tr><th style="text-align:left">分项</th><th>得分</th></tr></thead>
<tbody>
{% for s in score_rows %}
<tr><td style="text-align:left">{{ s.label }}</td><td class="num">{{ s.score }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="callout warn">分项数据不足,未计算得分(不使用残缺数据造分)</div>
{% endif %}
</div>
</div>
{% if missing %}
<div class="callout warn">
<strong>数据缺失提示:</strong>{{ missing }}
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
(function () {
if (typeof echarts === 'undefined') return;
var C = {{ chart_colors | json }};
var S = {{ series | json }};
var cur = {{ current_line | json }};
/* ---- 主图:K线 + 股息率 + PE + PB + 回撤,共享 X 轴 ---- */
var main = document.getElementById('chart-main');
if (main && S.dates && S.dates.length) {
var ch = echarts.init(main);
var zoom = [{ type: 'inside' }, { type: 'slider', height: 18, bottom: 8 }];
function grid(i, top, h) {
return { left: 62, right: 62, top: top, height: h };
}
ch.setOption({
color: C,
tooltip: { trigger: 'axis', axisPointer: { type: 'cross' } },
axisPointer: { link: [{ xAxisIndex: 'all' }] },
legend: { top: 0, textStyle: { color: '#64748B' } },
grid: [grid(0, 34, 210), grid(1, 282, 110), grid(2, 424, 90), grid(3, 546, 90)],
xAxis: [
{ type: 'category', data: S.dates, gridIndex: 0, axisLabel: { show: false },
axisTick: { show: false } },
{ type: 'category', data: S.dates, gridIndex: 1, axisLabel: { show: false },
axisTick: { show: false } },
{ type: 'category', data: S.dates, gridIndex: 2, axisLabel: { show: false },
axisTick: { show: false } },
{ type: 'category', data: S.dates, gridIndex: 3, axisLabel: { color: '#64748B',
formatter: function (v) { return String(v).slice(0, 7); } } }
],
yAxis: [
{ scale: true, gridIndex: 0, name: '价格', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
{ scale: true, gridIndex: 1, name: '股息率%', axisLabel: { color: '#64748B',
formatter: function (v) { return (v * 100).toFixed(1); } },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
{ scale: true, gridIndex: 2, name: 'PE', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
{ scale: true, gridIndex: 3, name: '回撤%', axisLabel: { color: '#64748B',
formatter: function (v) { return (v * 100).toFixed(0); } },
splitLine: { lineStyle: { color: '#E9EEF6' } } }
],
dataZoom: zoom,
series: [
{ name: '收盘价', type: 'line', xAxisIndex: 0, yAxisIndex: 0, data: S.close,
showSymbol: false, lineStyle: { width: 1.4, color: C[0] } },
{ name: '股息率', type: 'line', xAxisIndex: 1, yAxisIndex: 1, data: S.dv_yield,
showSymbol: false, lineStyle: { width: 1.4, color: C[1] },
areaStyle: { opacity: 0.12 },
markLine: cur.p75 == null ? undefined : {
silent: true, symbol: 'none',
data: [
{ yAxis: cur.p75, lineStyle: { color: C[4], type: 'dashed' },
label: { formatter: '买入 P75', color: C[4], position: 'insideEndTop' } },
{ yAxis: cur.p25, lineStyle: { color: C[3], type: 'dashed' },
label: { formatter: '卖出 P25', color: C[3], position: 'insideEndBottom' } }
]
} },
{ name: 'PE(TTM)', type: 'line', xAxisIndex: 2, yAxisIndex: 2, data: S.pe_ttm,
showSymbol: false, lineStyle: { width: 1.2, color: C[2] } },
{ name: '回撤', type: 'line', xAxisIndex: 3, yAxisIndex: 3, data: S.drawdown,
showSymbol: false, lineStyle: { width: 1.2, color: C[4] },
areaStyle: { opacity: 0.1 } }
]
});
window.addEventListener('resize', function () { ch.resize(); });
}
/* ---- 直方图 ---- */
var hist = document.getElementById('chart-hist');
if (hist) {
var h = {{ histogram | json }};
var c2 = echarts.init(hist);
c2.setOption({
color: C,
grid: { left: 56, right: 24, top: 28, bottom: 40 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: h.labels, name: '股息率',
axisLabel: { color: '#64748B' } },
yAxis: { type: 'value', name: '天数', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
series: [{ type: 'bar', data: h.counts, itemStyle: { color: C[0] }, barMaxWidth: 26,
markLine: cur.p75 == null ? undefined : { silent: true, symbol: 'none',
data: [{ xAxis: h.current_bucket,
lineStyle: { color: C[4], type: 'dashed' },
label: { formatter: '当前', color: C[4] } }] } }]
});
window.addEventListener('resize', function () { c2.resize(); });
}
/* ---- 波动率 ---- */
var vol = document.getElementById('chart-vol');
if (vol) {
var v = {{ vol_chart | json }};
var c3 = echarts.init(vol);
c3.setOption({
color: C,
grid: { left: 60, right: 24, top: 28, bottom: 40 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: v.labels, axisLabel: { color: '#64748B' } },
yAxis: { type: 'value', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
series: [{ type: 'bar', data: v.values, itemStyle: { color: C[1] }, barMaxWidth: 40,
label: { show: true, position: 'top', color: '#0F172A', fontSize: 10 } }]
});
window.addEventListener('resize', function () { c3.resize(); });
}
/* ---- 得分雷达 ---- */
var sc = document.getElementById('chart-score');
if (sc) {
var d = {{ score_chart | json }};
var c4 = echarts.init(sc);
c4.setOption({
color: C,
tooltip: {},
radar: {
indicator: d.map(function (x) { return { name: x.name, max: 100 }; }),
axisName: { color: '#64748B', fontSize: 11 },
splitLine: { lineStyle: { color: '#E9EEF6' } },
splitArea: { areaStyle: { color: ['#FFFFFF', '#F8FAFC'] } }
},
series: [{ type: 'radar', data: [{ value: d.map(function (x) { return x.value; }),
name: '安全边际', areaStyle: { opacity: 0.2 },
lineStyle: { color: C[0] }, itemStyle: { color: C[0] } }] }]
});
window.addEventListener('resize', function () { c4.resize(); });
}
})();
</script>
{% endblock %}