从 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。
195 lines
8.3 KiB
HTML
195 lines
8.3 KiB
HTML
{% 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 %}
|