Files
ggx/templates/reports/profile.html
T
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

262 lines
10 KiB
HTML
Raw 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="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 %}