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

从 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
+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="zh-CN" data-theme="{{ theme }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<style>{{ css | safe }}</style>
{{ echarts_tag | safe }}
</head>
<body>
<div class="wrap" style="--max-width: {{ max_width }}px">
<header class="page-head">
<h1>{{ title }}</h1>
{% if subtitle %}<div class="sub">{{ subtitle }}</div>{% endif %}
<div class="sub mono">生成时间 {{ generated_at }}{% if data_asof %} · 数据截止 {{ data_asof }}{% endif %}</div>
</header>
{% block content %}{% endblock %}
{% if show_provenance %}
<footer class="page-foot">
<strong>数据溯源</strong> — 本页所有数值均由下列 SQL 直接查询数据库得到,模板层不做任何计算。
<dl>
{% if source_run_ids %}<dt>来源 run</dt><dd class="mono">{{ source_run_ids }}</dd>{% endif %}
<dt>生成时间</dt><dd class="mono">{{ generated_at }}</dd>
{% if config_hash %}<dt>配置指纹</dt><dd class="mono">{{ config_hash }}</dd>{% endif %}
{% if data_version %}<dt>数据版本</dt><dd class="mono">{{ data_version }}</dd>{% endif %}
</dl>
<details>
<summary>查看数据来源 SQL({{ provenance_count }} 条)</summary>
<pre class="mono small" style="white-space:pre-wrap">{{ provenance_pretty }}</pre>
</details>
</footer>
{% endif %}
</div>
{% block scripts %}{% endblock %}
</body>
</html>
+200
View File
@@ -0,0 +1,200 @@
{% extends "base.html" %}
{% block content %}
<div class="callout {{ 'ok' if overall == 'OK' else ('warn' if overall == 'WARN' else 'fail') }}">
<strong>总体结论:{{ overall }}</strong> —
共 {{ counts.OK + counts.WARN + counts.FAIL }} 项检查,
通过 <strong>{{ counts.OK }}</strong>,
警告 <strong>{{ counts.WARN }}</strong>,
失败 <strong>{{ counts.FAIL }}</strong>。
{% if fail_codes %}未通过项:<span class="mono">{{ fail_codes }}</span>{% endif %}
</div>
<div class="kpi-grid">
<div class="kpi">
<div class="label">检查项</div>
<div class="value">{{ counts.OK + counts.WARN + counts.FAIL }}</div>
<div class="note">覆盖缺口 G1–G6 与一致性</div>
</div>
<div class="kpi">
<div class="label">通过率</div>
<div class="value">{{ pass_rate }}</div>
<div class="note">{{ counts.OK }} / {{ counts.OK + counts.WARN + counts.FAIL }}</div>
</div>
<div class="kpi">
<div class="label">阻塞项 (FAIL)</div>
<div class="value {{ 'loss' if counts.FAIL == 0 else 'gain' }}">{{ counts.FAIL }}</div>
<div class="note">为 0 才可进入策略回测</div>
</div>
<div class="kpi">
<div class="label">审计编号</div>
<div class="value small mono" style="font-size:14px">{{ audit_id }}</div>
<div class="note">可在 hd_data_audit 表回溯</div>
</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 style="text-align:left">说明</th>
</tr>
</thead>
<tbody>
{% for r in gap_results %}
<tr>
<td class="mono">{{ r.code }}</td>
<td style="text-align:left">{{ r.name }}</td>
<td><span class="badge {{ r.severity }}">{{ r.severity }}</span></td>
<td class="mono small">{{ r.table or '—' }}</td>
<td style="text-align:left" class="small">{{ r.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if coverage_rows %}
<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 style="text-align:left">目标</th><th>状态</th>
</tr>
</thead>
<tbody>
{% for c in coverage_rows %}
<tr>
<td class="mono">{{ c.table }}</td>
<td class="num">{{ c.rows }}</td>
<td class="num">{{ c.min or '—' }}</td>
<td class="num">{{ c.max or '—' }}</td>
<td class="num">{{ c.min_per_day if c.min_per_day is not none else '—' }}</td>
<td style="text-align:left" class="small muted">{{ c.target }}</td>
<td><span class="badge {{ c.severity }}">{{ c.severity }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card">
<h2>分红数据明细(G1 · 高股息策略的数据基座)</h2>
{% if dividend_summary %}
<div class="kpi-grid">
<div class="kpi"><div class="label">覆盖股票</div><div class="value">{{ dividend_summary.symbols }}</div><div class="note">只</div></div>
<div class="kpi"><div class="label">可用分红记录</div><div class="value">{{ dividend_summary.valid }}</div><div class="note">div_proc=实施 且有除权/派息日</div></div>
<div class="kpi"><div class="label">报告期最早</div><div class="value" style="font-size:16px">{{ dividend_summary.min }}</div><div class="note">end_date</div></div>
<div class="kpi"><div class="label">报告期最晚</div><div class="value" style="font-size:16px">{{ dividend_summary.max }}</div><div class="note">end_date</div></div>
</div>
{% endif %}
{% if dividend_by_proc %}
<h3>按 div_proc 分布</h3>
<div class="table-scroll">
<table class="data">
<thead><tr><th>div_proc</th><th>记录数</th><th>占比</th></tr></thead>
<tbody>
{% for r in dividend_by_proc %}
<tr>
<td class="mono">{{ r.div_proc }}</td>
<td class="num">{{ r.n }}</td>
<td class="num">{{ r.share }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="callout">
口径说明(决策 D6):连续性判定只认 <code>div_proc = '实施'</code> 且存在
<code>ex_date</code> 或 <code>pay_date</code> 的记录,且以<strong>税前</strong>现金分红
<code>cash_div_tax &gt; 0</code> 计一次分红。其余状态(预案/股东大会通过/股东提议)
保留在库中供研究,但不参与筛选判定。
</div>
{% endif %}
{% if dividend_by_year %}
<h3>按除权年度分布(近 15 年)</h3>
<div id="chart-div-year" class="chart short"></div>
{% endif %}
</div>
<div class="card">
<h2>全部检查结果</h2>
<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 r in all_results %}
<tr>
<td class="mono">{{ r.code }}</td>
<td style="text-align:left">{{ r.name }}</td>
<td><span class="badge {{ r.severity }}">{{ r.severity }}</span></td>
<td class="mono small">{{ r.table or '—' }}</td>
<td style="text-align:left" class="small">{{ r.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<details>
<summary>查看各项检查的原始指标(metric_json)</summary>
<div class="table-scroll">
<table class="data">
<thead><tr><th>检查码</th><th style="text-align:left">指标</th></tr></thead>
<tbody>
{% for r in all_results %}
{% if r.metrics %}
<tr>
<td class="mono">{{ r.code }}</td>
<td style="text-align:left"><pre class="mono small" style="margin:0;white-space:pre-wrap">{{ r.metrics_pretty }}</pre></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</details>
</div>
{% endblock %}
{% block scripts %}
{% if dividend_by_year %}
<script>
(function () {
var el = document.getElementById('chart-div-year');
if (!el || typeof echarts === 'undefined') return;
var chart = echarts.init(el);
var data = {{ dividend_by_year | json }};
chart.setOption({
color: {{ chart_colors | json }},
grid: { left: 60, right: 24, top: 32, bottom: 40 },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.map(function (d) { return d.year; }),
axisLabel: { color: '#64748B' } },
yAxis: { type: 'value', name: '分红实施次数', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
series: [{
type: 'bar', data: data.map(function (d) { return d.n; }),
itemStyle: { color: '#1E40AF' }, barMaxWidth: 28
}]
});
window.addEventListener('resize', function () { chart.resize(); });
})();
</script>
{% endif %}
{% endblock %}
+328
View File
@@ -0,0 +1,328 @@
{% 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 %}
+83
View File
@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block content %}
<div class="kpi-grid">
<div class="kpi"><div class="label">报告总数</div><div class="value">{{ total }}</div>
<div class="note">{{ groups|length }} 个类型</div></div>
<div class="kpi"><div class="label">最新报告</div><div class="value">{{ latest_count }}</div>
<div class="note">每个类型的首份</div></div>
<div class="kpi"><div class="label">离线资源</div>
<div class="value" style="font-size:15px" class="mono">{{ asset_hint }}</div>
<div class="note">图表库必须与本页同级目录一起部署</div></div>
</div>
{% if hidden %}
<div class="callout">
另有 <strong>{{ hidden }}</strong> 份 Walk-forward 子运行报告(各窗口的训练/测试段)
未在此列出 —— 它们是可下钻的原始证据而非独立交付物,文件仍在
<code class="mono">output/</code> 下,可在 <code class="mono">hd_report</code> 表中按 run_id 找到。
</div>
{% endif %}
{% if missing %}
<div class="callout fail">
<strong>有 {{ missing }} 份登记的报告文件在磁盘上不存在</strong> ——
索引中的链接会失效。请重新生成对应报告,或整体重新部署 <code class="mono">output/</code> 目录。
</div>
{% endif %}
<div class="callout warn">
<strong>部署须知:图表依赖本地资源。</strong>
报告通过相对路径引用 <code class="mono">assets/echarts.min.js</code>。
若你把报告发布到 <code class="mono">/ggx/</code> 这样的路径下,
<strong>必须同时部署 <code class="mono">output/assets/</code> 目录</strong>,
使其可通过 <code class="mono">/ggx/assets/echarts.min.js</code> 访问;
否则页面能打开但<strong>所有图表都不会显示</strong>。
如无法部署该目录,可把 <code class="mono">config/report.yml</code> 的
<code class="mono">asset_mode</code> 改为 <code class="mono">inline</code>
重新生成,让每份 HTML 自带图表库(体积由约 10MB 增至约 79MB)。
</div>
{% for g in groups %}
<div class="card">
<h2>{{ g.label }} <span class="badge info">{{ g.reports|length }}</span></h2>
<div class="report-list">
{% for it in g.reports %}
<a class="report-item" href="{{ it.path }}" {% if not it.exists %}style="opacity:.5"{% endif %}>
<div class="t">
{% if it.is_latest %}<span class="badge OK">最新</span> {% endif %}
{{ it.title }}
</div>
<div class="m mono">{{ it.path }}</div>
<div class="m">{{ it.size }} · {{ it.generated_at }}
{% if not it.exists %} · <span style="color:#DC2626">文件缺失</span>{% endif %}
</div>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
<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">数据源</td><td style="text-align:left">本机 MariaDB <code class="mono">127.0.0.1:3306/qlib</code>(与 qlib 平台共用)+ Tushare Pro</td></tr>
<tr><td style="text-align:left">配置</td><td style="text-align:left">全部阈值来自 <code class="mono">config/*.yml</code>,代码零硬编码</td></tr>
<tr><td style="text-align:left">结果</td><td style="text-align:left">优先写入数据库 <code class="mono">hd_*</code> 表;HTML 仅作呈现</td></tr>
<tr><td style="text-align:left">数据安全</td><td style="text-align:left">只增不删:所有 SQL 经安全钩子拦截 <code class="mono">DELETE/DROP/TRUNCATE</code></td></tr>
<tr><td style="text-align:left">离线</td><td style="text-align:left">不依赖任何 CDN;图表库本地提供</td></tr>
<tr><td style="text-align:left">报告时效</td><td style="text-align:left">同一类型的多份报告是<strong>历史运行</strong>,请以标注「最新」的为准</td></tr>
<tr><td style="text-align:left">使用文档</td><td style="text-align:left">
<code class="mono">docs/user-guide.md</code> 使用手册(安装/配置/命令/报告解读/故障排查)<br>
<code class="mono">docs/implementation-status.md</code> 实施状态(含<strong>实测结论与已知限制</strong>)
</td></tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
+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 %}
+194
View File
@@ -0,0 +1,194 @@
{% extends "base.html" %}
{% block content %}
{% if analysis %}
<div class="callout {{ 'ok' if analysis.robust else 'fail' }}">
<strong>敏感性结论:{{ analysis.verdict }}</strong><br>
CAGR 跨度 <strong>{{ analysis.cagr_range_s }}</strong>
({{ analysis.cagr_min_s }} ~ {{ analysis.cagr_max_s }}),
相邻档最大跳变 <strong>{{ analysis.max_jump_s }}</strong>,
平滑度 <strong>{{ analysis.smoothness_s }}</strong>
{% if analysis.spikes %},检出 <strong>{{ analysis.spikes|length }}</strong> 处尖峰{% endif %}。
</div>
{% else %}
<div class="callout warn">扫描点数不足,无法评估参数敏感性。</div>
{% endif %}
<div class="kpi-grid">
<div class="kpi"><div class="label">参数组合数</div><div class="value">{{ point_count }}</div>
<div class="note">扫描网格 {{ sweep }}</div></div>
<div class="kpi"><div class="label">CAGR 区间</div>
<div class="value" style="font-size:17px">{{ analysis.cagr_min_s }} ~ {{ analysis.cagr_max_s }}</div>
<div class="note">跨度 {{ analysis.cagr_range_s }}</div></div>
<div class="kpi"><div class="label">平滑度</div>
<div class="value {{ 'gain' if analysis.robust else 'loss' }}">{{ analysis.smoothness_s }}</div>
<div class="note">越接近 1 越平滑(≥0.6 且无尖峰视为稳健)</div></div>
<div class="kpi"><div class="label">尖峰数</div>
<div class="value {{ 'loss' if analysis.spikes else 'gain' }}">{{ analysis.spikes|length if analysis.spikes else 0 }}</div>
<div class="note">某点显著高于左右邻居即为尖峰</div></div>
</div>
<div class="callout">
<strong>如何判读(plan.md §27)</strong>:若买入分位 P75→13%、P80→13.5%、P85→13.2%,
说明策略对参数<strong>不敏感</strong>,结论相对可信;
若 P79→18%、P80→20%、P81→8%,说明存在<strong>尖峰</strong>,很可能过拟合。
平滑度指标把这一判断从主观观察变成了可量化的数字。
</div>
{% if point_count > 1 %}
<div class="card">
<h2>绩效随参数的变化</h2>
<div id="chart-sweep" class="chart"></div>
</div>
{% endif %}
<div class="card">
<h2>扫描明细</h2>
<div class="table-scroll">
<table class="data">
<thead>
<tr>
{% for k in sweep_keys %}<th>{{ k }}</th>{% endfor %}
<th>CAGR</th><th>总收益</th><th>最大回撤</th>
<th>Sharpe</th><th>Calmar</th><th>Sortino</th><th>成交</th><th>年换手</th>
</tr>
</thead>
<tbody>
{% for p in points %}
<tr class="{{ 'spike-row' if p.is_spike else '' }}">
{% for v in p.key_values %}<td class="mono">{{ v }}</td>{% endfor %}
<td class="num {{ 'gain' if p.cagr_positive else 'loss' }}"><strong>{{ p.cagr }}</strong></td>
<td class="num">{{ p.total_return }}</td>
<td class="num loss">{{ p.max_drawdown }}</td>
<td class="num">{{ p.sharpe }}</td>
<td class="num">{{ p.calmar }}</td>
<td class="num">{{ p.sortino }}</td>
<td class="num">{{ p.trade_count }}</td>
<td class="num">{{ p.turnover }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if analysis.spikes %}
<div class="callout fail">
<strong>检出的尖峰:</strong>
<ul style="margin:4px 0 0 18px">
{% for s in analysis.spikes %}
<li>第 {{ s.index }} 点 CAGR {{ s.cagr_s }},高于左右邻居均值 {{ s.excess_s }}
(邻居均值 {{ s.neighbour_mean_s }})</li>
{% endfor %}
</ul>
这意味着收益对参数的微小变化异常敏感,是过拟合的典型信号。
</div>
{% endif %}
</div>
<div class="card">
<h2>性能与参数的关系</h2>
<div class="chart-grid">
<div><h3>年化收益 CAGR</h3><div id="chart-cagr" class="chart short"></div></div>
<div><h3>最大回撤</h3><div id="chart-dd" class="chart short"></div></div>
<div><h3>Sharpe</h3><div id="chart-sharpe" class="chart short"></div></div>
<div><h3>成交笔数(换手)</h3><div id="chart-trades" class="chart short"></div></div>
</div>
</div>
<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">敏感性 ID</td><td style="text-align:left" class="mono">{{ sens_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">{{ sweep }}</td></tr>
<tr><td style="text-align:left">区间</td><td style="text-align:left">{{ period }}</td></tr>
<tr><td style="text-align:left">数据版本</td><td style="text-align:left" class="mono">{{ data_version }}</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<strong>注意</strong>:敏感性结论依赖扫描区间长度与股票池规模。
若股票池仅数十只、区间仅数年,即使平滑度低也可能只是样本噪声 ——
应结合 Walk-forward 的样本外结果共同判断。
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
if (typeof echarts === 'undefined') return;
var C = {{ chart_colors | json }};
var P = {{ param_values | json }};
function lineChart(id, values, name, formatter, color, markSpikes) {
var el = document.getElementById(id);
if (!el) return;
var c = echarts.init(el);
c.setOption({
color: [color || C[0]],
grid: { left: 62, right: 20, top: 28, bottom: 40 },
tooltip: { trigger: 'axis', valueFormatter: formatter },
xAxis: { type: 'category', data: P, name: '{{ sweep_keys[0] if sweep_keys else "参数" }}',
axisLabel: { color: '#64748B' } },
yAxis: { type: 'value', axisLabel: { color: '#64748B', formatter: formatter },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
series: [{
name: name, type: 'line', data: values, showSymbol: true, symbolSize: 7,
lineStyle: { width: 2 },
markPoint: markSpikes ? {
symbol: 'pin', symbolSize: 40,
data: {{ spike_points | json }}.map(function (i) {
return { coord: [i, values[i]], value: '尖峰', itemStyle: { color: '#DC2626' } };
})
} : undefined
}]
});
window.addEventListener('resize', function () { c.resize(); });
}
var pct = function (v) { return v == null ? '—' : (v * 100).toFixed(2) + '%'; };
var num = function (v) { return v == null ? '—' : Number(v).toFixed(2); };
lineChart('chart-cagr', {{ charts.cagr | json }}, 'CAGR', pct, C[0], true);
lineChart('chart-dd', {{ charts.max_drawdown | json }}, '最大回撤', pct, C[4], false);
lineChart('chart-sharpe', {{ charts.sharpe | json }}, 'Sharpe', num, C[2], false);
lineChart('chart-trades', {{ charts.trade_count | json }}, '成交笔数', num, C[1], false);
var el = document.getElementById('chart-sweep');
if (el) {
var c = echarts.init(el);
c.setOption({
color: C,
grid: { left: 66, right: 66, top: 44, bottom: 46 },
tooltip: { trigger: 'axis' },
legend: { top: 0, textStyle: { color: '#64748B' } },
xAxis: { type: 'category', data: P, axisLabel: { color: '#64748B' } },
yAxis: [
{ type: 'value', name: '收益/回撤', axisLabel: { color: '#64748B',
formatter: function (v) { return (v * 100).toFixed(0) + '%'; } },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
{ type: 'value', name: 'Sharpe', axisLabel: { color: '#64748B' },
splitLine: { show: false } }
],
series: [
{ name: 'CAGR', type: 'line', data: {{ charts.cagr | json }},
showSymbol: true, symbolSize: 7, lineStyle: { width: 2.4, color: C[0] } },
{ name: '最大回撤', type: 'line', data: {{ charts.max_drawdown | json }},
showSymbol: true, symbolSize: 6, lineStyle: { width: 1.6, color: C[4] } },
{ name: 'Sharpe', type: 'line', yAxisIndex: 1, data: {{ charts.sharpe | json }},
showSymbol: true, symbolSize: 6, lineStyle: { width: 1.6, type: 'dashed', color: C[2] } }
]
});
window.addEventListener('resize', function () { c.resize(); });
}
})();
</script>
<style>
tr.spike-row td { background: #FEF2F2; font-weight: 600; }
</style>
{% endblock %}
+210
View File
@@ -0,0 +1,210 @@
{% extends "base.html" %}
{% block content %}
<div class="kpi-grid">
<div class="kpi"><div class="label">候选股票</div><div class="value">{{ candidate_count }}</div><div class="note">截至 asof 已上市且未退市</div></div>
<div class="kpi"><div class="label">入选股票</div><div class="value">{{ member_count }}</div><div class="note">通过全部滤网</div></div>
<div class="kpi"><div class="label">通过率</div><div class="value">{{ pass_rate }}</div><div class="note">{{ member_count }} / {{ candidate_count }}</div></div>
<div class="kpi"><div class="label">求值时点</div><div class="value" style="font-size:18px">{{ asof_date }}</div><div class="note">Point-in-Time</div></div>
</div>
{% if warnings %}
<div class="callout warn">
<strong>注意</strong>
<ul style="margin:4px 0 0 18px">
{% for w in warnings %}<li>{{ w }}</li>{% endfor %}
</ul>
</div>
{% endif %}
<div class="card">
<h2>滤网淘汰过程</h2>
<div id="chart-funnel" 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><th style="text-align:left">说明</th></tr>
</thead>
<tbody>
{% for s in stages %}
<tr>
<td class="num">{{ loop.index }}</td>
<td style="text-align:left" class="mono">{{ s.name }}</td>
<td class="num">{{ s.incoming }}</td>
<td class="num">{{ s.removed }}</td>
<td class="num">{{ s.survived }}</td>
<td class="num">{{ s.rate }}</td>
<td style="text-align:left" class="small muted">{{ s.desc }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card">
<h2>入选股票(按股息率降序)</h2>
<div class="table-scroll">
<table class="data">
<thead>
<tr>
<th>代码</th><th style="text-align:left">名称</th><th style="text-align:left">行业</th>
<th>股息率</th><th>PE(TTM)</th><th>PB</th><th>总市值</th>
<th>连续分红</th><th>ROE(年均)</th><th>负债率</th><th>支付率</th>
</tr>
</thead>
<tbody>
{% for m in members %}
<tr>
<td class="mono">{{ m.symbol }}</td>
<td style="text-align:left">{{ m.name }}</td>
<td style="text-align:left" class="small">{{ m.industry }}</td>
<td class="num">{{ m.dividend_yield }}</td>
<td class="num">{{ m.pe_ttm }}</td>
<td class="num">{{ m.pb }}</td>
<td class="num">{{ m.total_mv }}</td>
<td class="num">{{ m.continuity }}</td>
<td class="num">{{ m.roe }}</td>
<td class="num">{{ m.debt_ratio }}</td>
<td class="num">{{ m.payout }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if industry_rows %}
<div class="chart-grid">
<div class="card">
<h2>入选股票行业分布</h2>
<div id="chart-industry" class="chart"></div>
</div>
<div class="card">
<h2>股息率分布(入选 vs 候选)</h2>
<div id="chart-yield" class="chart"></div>
</div>
</div>
<div class="card">
<h2>行业集中度明细</h2>
<div class="callout">
单行业权重上限由策略的 <code>position.sector_max_position</code> 控制;
此处仅呈现筛选结果的行业分布,用于事前判断集中度风险。
</div>
<div class="table-scroll">
<table class="data">
<thead><tr><th style="text-align:left">行业</th><th>入选数</th><th>占入选比例</th><th>候选数</th><th>入选率</th></tr></thead>
<tbody>
{% for i in industry_rows %}
<tr>
<td style="text-align:left">{{ i.industry }}</td>
<td class="num">{{ i.selected }}</td>
<td class="num">{{ i.share }}</td>
<td class="num">{{ i.candidates }}</td>
<td class="num">{{ i.hit_rate }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<div class="card">
<h2>被淘汰股票与原因(可回答「为什么没选上」)</h2>
<div class="table-scroll">
<table class="data">
<thead>
<tr><th>代码</th><th style="text-align:left">名称</th><th style="text-align:left">行业</th>
<th style="text-align:left">首个未通过滤网</th><th style="text-align:left">原因</th></tr>
</thead>
<tbody>
{% for r in rejected %}
<tr>
<td class="mono">{{ r.symbol }}</td>
<td style="text-align:left">{{ r.name }}</td>
<td style="text-align:left" class="small">{{ r.industry }}</td>
<td style="text-align:left"><span class="badge info">{{ r.fail_stage }}</span></td>
<td style="text-align:left" class="small">{{ r.fail_reason }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card">
<h2>生效的筛选配置</h2>
<pre class="mono small" style="white-space:pre-wrap;max-height:520px;overflow:auto">{{ config_json }}</pre>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
if (typeof echarts === 'undefined') return;
var C = {{ chart_colors | json }};
var funnel = document.getElementById('chart-funnel');
if (funnel) {
var f = {{ funnel | json }};
var ch = echarts.init(funnel);
ch.setOption({
color: C,
grid: { left: 90, right: 40, top: 20, bottom: 30 },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
xAxis: { type: 'value', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
yAxis: { type: 'category', data: f.map(function (d) { return d.name; }),
axisLabel: { color: '#64748B' } },
series: [{
type: 'bar', data: f.map(function (d) { return d.value; }),
itemStyle: { color: '#1E40AF' }, barMaxWidth: 30,
label: { show: true, position: 'right', color: '#0F172A', fontFamily: 'monospace' }
}]
});
window.addEventListener('resize', function () { ch.resize(); });
}
var ind = document.getElementById('chart-industry');
if (ind) {
var d = {{ industry_chart | json }};
var c2 = echarts.init(ind);
c2.setOption({
color: C,
tooltip: { trigger: 'item' },
series: [{
type: 'pie', radius: ['38%', '68%'], center: ['50%', '52%'],
data: d, label: { color: '#0F172A', fontSize: 11 },
itemStyle: { borderColor: '#fff', borderWidth: 1 }
}]
});
window.addEventListener('resize', function () { c2.resize(); });
}
var yl = document.getElementById('chart-yield');
if (yl) {
var y = {{ yield_hist | json }};
var c3 = echarts.init(yl);
c3.setOption({
color: C,
grid: { left: 60, right: 24, top: 36, bottom: 40 },
tooltip: { trigger: 'axis' },
legend: { data: y.series.map(function (s) { return s.name; }), top: 0,
textStyle: { color: '#64748B' } },
xAxis: { type: 'category', data: y.buckets, name: '股息率',
axisLabel: { color: '#64748B' } },
yAxis: { type: 'value', name: '股票数', axisLabel: { color: '#64748B' },
splitLine: { lineStyle: { color: '#E9EEF6' } } },
series: y.series.map(function (s, i) {
return { name: s.name, type: 'bar', data: s.data,
itemStyle: { color: C[i % C.length] }, barMaxWidth: 22 };
})
});
window.addEventListener('resize', function () { c3.resize(); });
}
})();
</script>
{% endblock %}
+195
View File
@@ -0,0 +1,195 @@
{% 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 %}