- name
- fund-quant-backtest
- description
- A 股指数/基金估值定投与择时策略的可运行回测脚手架。
- version
- 0.1.0
- author
- Hermes Agent (curator-managed)
- license
- MIT
- metadata
- {"hermes":{"tags":["finance","backtest","a-stock","dca","valuation"],"related_skills":["retail-trading-manual-writer","a-stock-prediction","finance-core-analysis"]}}
# Fund Quant Backtest
## When to Use
触发词 (任一即用): 回测、定投算法、估值定投、智能定投、年化对比、分位算法、复现一遍、对比普通定投。
典型场景:
- 主人在某个指数 / 基金上想试一种估值定投策略, 问"这个比普通定投好吗"
- 主人改了档位参数 (比如"30-50% 不买"), 需要在新参数下出回测结论
- 主人质疑支付宝 / 天天基金的"智能定投" 黑盒, 需要自有数据复现
- 主人问"X 指数 PE 现在几分位", 想做当下决策但顺便跑过去 10 年验证
**不要用本 skill**:
- 投教文章 / 长文产出 → `retail-trading-manual-writer`
- 短期 T+1 涨跌预测 → `a-stock-prediction` / `us-stock-prediction`
- 个股财报深度分析 → `financial-report-analysis`
- 行业体系讲透 → `industry-deep-explainer`
## Class Contract
本 skill 提供 **可运行的 Python 回测脚手架**,用于在 A 股指数/基金上验证估值定投 / 择时类策略。核心交付物是:
1. 一份脚本 `/root/new/tmp/<task>/<date>/backtest_*.py`, 跑出结论 + CSV
2. 一份策略汇总报告(可在终端打印,也可写到 .md)
3. 可视化图表(可选, 但推荐: 让主人一眼看懂)
输入: 指数或基金代码 + 回测区间 + 策略档位(分位阈值 + 倍率) + 基础定投金额。
输出: 各策略的总投入 / 总份额 / 终值 / 年化 / 买入月数 / 最大回撤 + 关键时间点的决策明细。
**触发词**: 回测、定投算法、估值定投、智能定投、年化对比、分位算法、复现一遍。
**明确边界**: 本 skill 不负责以下任务, 请跳转其他 skill:
- 投教类长文产出 → `retail-trading-manual-writer`
- 短期 T+1 走势预测 → `a-stock-prediction` / `us-stock-prediction`
- 个股财报深度分析 → `financial-report-analysis`
- 估值因子与宏观经济体系讲透 → `industry-deep-explainer`
## Default Workflow
1. **先抓指数, 不抓基金净值。**
- A 股宽基指数的历史收盘价 + 滚动 PE 在 akshare 中证接口一次拉齐:
```python
ak.stock_zh_index_hist_csindex(symbol="H00300", start_date="...", end_date="...")
```
输出列含 `收盘` 和 `滚动市盈率`, **无需二次拼接 PE 数据**。
- 其他指数的中证代码:
- 沪深300 → `H00300`
- 中证500 → `H00905`
- 中证红利 → `H00922`
- 上证50 → `H00016`
- 创业板指 → `H03900`
- 科创50 → `H02021`
- 申万行业指数 / 港股指数 PE 通常需要别的源(乌龟量化 / 万得), 不要硬拉 akshare 这条接口。
2. **不要用 PE 静态版(乐咕乐股)** —— 接口挂了 csrf, 直接放弃。
3. **不要拉基金净值代替指数。** 净值有跟踪误差 + 管理费 + 分红再投近似问题, 用指数回测结论更干净。**最后再**用真实净值做交叉验证(可选)。
4. **滚动分位的标准做法**:
```python
WIN = 1252 # 5 年 ~ 1252 个交易日
df['pe_pct'] = df['pe_ttm'].rolling(WIN, min_periods=252).apply(
lambda x: np.sum(x[:-1] <= x[-1]) / len(x[:-1]),
raw=True
)
```
- 窗口选择: 5 年跨 1-2 个完整牛熊, 7-8 年更平滑, 10 年反应滞后。**与主人确认**。
- 头 252 交易日分位用全历史填充, 但只用于显示, 不用于前 252 天决策。
5. **每月决策 = 每月第一个交易日**:
```python
df['ym'] = df['日期'].dt.to_period('M')
month_first = df.groupby('ym').first().reset_index()
```
6. **回测循环核心(伪代码)**:
```python
for s in ['s1_naive', 's2_zhifubao', 's3_v1', 's4_v2']:
month_first[f'{s}_amt'] = month_first[s] * BASE_AMT
month_first[f'{s}_share'] = month_first[f'{s}_amt'] / month_first['close']
total_invest = month_first[f'{s}_amt'].sum()
total_shares = month_first[f'{s}_share'].sum()
final_value = total_shares * df['close'].iloc[-1]
cagr = (final_value / total_invest) ** (1/years) - 1 if total_invest > 0 else 0
```
7. **必须输出的指标**(表格):
- 总投入、总份额、终值、终值收益额
- 年化收益(CAGR)
- 买入月数 / 总月数(验证策略是否触发)
- 最大回撤(逐月市值)
8. **必须输出与一次性买入对比** —— 提醒主人"起点敏感性问题"。
9. **回测起点很重要**:
- 不要从牛市顶点(2015-06, 2021-02)起, 那会反过来证明定投更好, 结论不普适。
- 不要从最低点起, 那是上帝视角, 不能复现。
- 推荐从整段牛熊都覆盖的窗口起, 例如 2014-01 ~ 2024-12。
## Standard Strategy Ladder
默认提供的策略档位(可改):
| 策略名 | 触发条件 | 扣款 |
|---|---|---|
| `s1_naive` | 不论分位 | 1.0x |
| `s2_zhifubao` | PE 分位 < 30% | 1.0x, 否则 0 |
| `s3_v1_middle_buy` | 三指标任一触发低估 | 1.5x, 30-70% 区间 0.5x, > 70% 0 |
| `s4_v2_final` | < 30% → 1.5x, 30-50% → 0.5x, > 50% → 0 | (主人最终采纳版, 单 PE 信号) |
| `s5_v4_three_indicators` | PE 分位 + 股债比 + 60月均线趋势 | 1.5x / 0.5x / 0 / 极低估 3x + 趋势折扣 (主人首选, 三指标版) |
| `s6_extreme` | PE 分位 < 15% AND PB 分位 < 15% | 3.0x |
**两版并存的关系**:
- `s4_v2_final`: 单 PE 信号 + 主人档位偏好。简单, 透明度最高, 主人最初拍板的版本。
- `s5_v4_three_indicators`: PE 分位 + 股债比 (1/PE / 10Y 国债) + 60月均线趋势过滤。**这是 2026-08-27 主人进一步要求的版本**, 也是回测下来"绝对收益 + 资金利用率"双优的版本。**默认推荐用这个。**
回测验证 (沪深300, 2014-2026-08, 152 个月, 投入 1000/月):
| 策略 | 终值 | CAGR | 买入月数 |
|---|---|---|---|
| 普通定投 | 220,452 | 2.98% | 152/152 |
| 支付宝风 (PE<30%才买) | 147,660 | 4.08% | 89/152 |
| v2 简化 (PE分位单指标) | 181,009 | 3.72% | 110/152 |
| **v4 三指标 + 趋势** | **207,698** | **3.51%** | **111/152** |
沪深300 全收益指数同期 2602 → 6799, 涨幅 161.3% (年化 7.9%)。
**v4 不是 CAGR 最高, 是绝对收益最高 + 资金利用率最高** —— 这正是主人的偏好: 总收益才是真收益, 年化是给营销看的。后续新对话如果主人再调, 务必 patch 这个表, 不要在脚本里悄悄改。
### 分段表现 (2026-08-27 主人额外要求)
默认 2014-2024 跑完一轮后, **主人会再问"扩到 2026 看近期"** —— 直接加分段表:
| 段 | 时长 | 普通定投 CAGR | v4 CAGR | v4 优势 |
|---|---|---|---|---|
| 慢牛 (2019-2020) | 24 月 | 14.57% | 20.37% | +5.8 pp |
| **阴跌 (2021-2023)** | 36 月 | **-5.90%** | **-2.84%** | **+3.1 pp** |
| 反弹 (2024-2026) | 32 月 | 6.33% | 12.38% | +6.0 pp |
**核心洞察**: v4 在三种不同行情下都跑赢普通定投, **阴跌段损失减半、反弹段收益翻倍**。这是估值信号的真正价值, 不是只看 CAGR 能看出来的。后续回答主人"为什么用 v4"时, 直接引用这段话。
**最近 12 个月 (2025-09 ~ 2026-08) v4 决策 = 全部 0.0 (PE 分位 73-96%)** —— 这就是 v4 在贵市里的"沉默期", 用来回答"算法是不是死了"质疑的最佳证据。
## Common Pitfalls
- ❌ 用 `df['pe'].rolling().rank(pct=True)` —— 那给的是滚动百分位, 不是历史分位, 含义不同。
- ❌ 在回测里"用未来数据算分位"(rolling 包含当日)—— 必须 `x[:-1] <= x[-1]`, 防止前视偏差。
- ❌ 用"全历史百分位"代替滚动窗口 —— 会让分位永远不进入"低估区", 永无扣款, 假阴性。
- ❌ 把"一次性买入"和"定投"放同一张图比较终值 —— 起点偏差巨大。要么同起点比较, 要么标注 "假设你某日开始"。
- ❌ 把 akshare 拉到的"沪深300全收益指数"当成价格指数 —— 它**含分红再投**, 比真实价格指数高, 做严格回测要减分红或换口径。
- ❌ 代理不稳时(`Cannot connect to proxy`)—— 用 `curl --socks5-hostname host.docker.internal:11112`, 或重试一次, 不要无限重试。
## Visualization
回测结论最好落地为一张图: X 轴时间, Y 轴累计投入 / 累计市值, 多策略叠加。
推荐 `matplotlib` + `fill_between`, 例:
```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12, 6))
for s in ['s1', 's2', 's3', 's4']:
cum_value = (month_first[f'{s}_share'].cumsum() * month_first['close']).values
ax.plot(month_first['日期'], cum_value, label=s)
ax.legend()
ax.grid(alpha=0.3)
```
图存到 `/root/new/tmp/<task>/<date>/fig_*.png`, 在对话里直接引用路径。
## 主人偏好 (digoal/德哥) —— 2026-08-27 实战沉淀
实战中主人明确表达的偏好, 后续回测必须满足, 不要替主人切档位:
1. **结果汇报的硬指标** (任一回测必须输出):
- 累计投入、累计份额、终值、收益额 (不要只给 CAGR)
- CAGR、买入月数 / 总月数 (验证策略是否触发)
- 必要时给一次性买入作为起点敏感性参考, **但分两栏列, 不要混着看**
2. **分档偏好**: 30-50% 区间 0.5x, **50-70% 直接跳过(0)**。主人原话: "调整一项, 中性区间(分位 50-70%) 跳过不买. 30~50% 0.5倍"。这是有意设计的更严纪律, 不是疏漏。
3. **结论偏好**: 主人关注"绝对收益(终值)"远高于"年化"。回测总结话术要强调"投了 X 万, 终值 Y 万, 资金利用率 Z%", 不要只喊 CAGR。
4. **报告节奏**: 结论先行, 一张总表 + 3-5 条犀利观察, 不堆理论。详细数据落 CSV, 让主人自己复盘。
5. **回测分段**: 默认 2014-01 ~ 2024-12 覆盖完整牛熊, 但**额外加 2014-2018 / 2019-2024 拆分**, 让主人看清"近期是否仍适用"。
6. **设计先于自动化 (2026-08-27 主人明确纠正)**:
- 主人原话: "先别急cron, 你给个比较优的算法先."
- **触发模式**: 主人提出"我有个策略 / 我在用 X, 帮我评估 / 落 cron"时, **不要第一时间就搭 cron**。先用 1-2 轮对话设计/论证/调优算法, 等主人明确说"落 cron"才进入部署阶段。
- **判断信号**: 主人没说"现在就建" / "落 cron" / "跑起来" 时, 默认不要进入 cron/job/state script 阶段。
- **错踩后果**: 上来就建 cron 会被主人打回, 浪费一轮; 上来就回答"好, 落 cron"是替主人切档位, 违反主人"先设计后部署"的偏好。
## 已知数据源陷阱 (实战踩坑沉淀)
- **`ak.stock_zh_index_value_csindex` (中证指数估值)** 返回的 xls URL (`oss-ch.csindex.com.cn/static/html/...`) 已 404, 实际是占位 JPEG 图片。**不要用这个接口拿历史 PE/PB/股息率**。直接走 `stock_zh_index_hist_csindex`。
- **10 年国债日频**: 用 `ak.bond_zh_us_rate()`, 含 1990-2026 全历史, 列名 `中国国债收益率10年`。akshare 的 `bond_china_yield()` 只覆盖到 2021, 别用。
- **乐咕乐股 PE 静态版** (`ak.stock_index_pe_lg`): 挂了 csrf, 直接放弃, 别尝试。雪球 PE 也有 csrf / 反爬, 不推荐。
## 从回测到生产: cron 部署阶段 (2026-08-27 主人拍板落地)
回测结论跑通 + 主人明确说"落 cron"后, 进入此阶段。**默认推荐用 `--script` + `--no-agent` 纯脚本模式**, 不是 agentic 模式:
- daily-decision 是 if-then-else 表格, 不需要 LLM 阅读理解
- 节省 token, 跑得快 (< 5s)
- 用 `hermes-cron-orchestrator` 描述的 agentic 模式是浪费 (那 skill 主要服务"数据 + LLM 解读"型 cron)
完整模板 / 部署流程 / 时区换算 / 7 步走 / 踩坑速查表, 看 `references/production-deployment.md`。关键提醒:
- 脚本必须放 `/root/.hermes/profiles/digoal/scripts/` (per-profile, 不是全局)
- `--script` 参数用纯文件名 (不要绝对路径)
- workdir 必须在 `/root/new/work/digoal/` 下
- exit code 透传给 cron, 失败不要静默
## Reference Files
- `references/akshare-pe-data-sources.md`: 中证 / 乐咕乐股 / 雪球 / 万得 各源对比, 哪个能用、哪个有 csrf、字段含义。
- `references/valuation-strategy-design-notes.md`: 估值定投档位选择背后的依据, 三指标 / 趋势过滤 / EPS 一致预期修正等高级条件的取舍逻辑。
- `references/production-deployment.md`: 回测结论跑通后的生产化部署模式 (hermes cron + script + no-agent), 包括 wrapper.sh 模板、workdir 布局、history.json 格式、常见踩坑。
- `scripts/dca_backtest_template.py`: 一个可直接 copy 修改的回测模板。
GitHubで見る