Week 16:量化项目报告、回撤分析与样本外解释
Week 16 的目标是把 Week 15 的 mini backtest 写成一份诚实、可复查的量化项目报告。报告不是宣传策略有多赚钱,而是回答:策略规则是什么、数据怎么来、样本怎么切、成本怎么算、风险有多大、样本外是否仍有效、局限性在哪里。
本文命令默认使用 fish shell。进入项目后激活环境:
source .venv/bin/activate.fish
0. 本周详细教学:语法、规范、验收
本节不是追加在尾部的复习,而是本周正文的入口。先读这里,再做后面的命令和项目。
0.1 本周真正要学会什么
| 维度 | 要求 |
|---|---|
| 知识点 | annual return、vol、Sharpe、max drawdown、成本敏感性 |
| 代码语法 | 能从空文件写出本周核心脚本,而不是只复制运行 |
| 程序规范 | 函数拆分、路径清楚、输入输出明确、错误能解释 |
| 交付物 | reports/week16_quant_report.md |
| 验收方式 | 从 fish 终端运行命令,得到可复查的文件或指标 |
0.2 代码语法精讲
下面的代码不是最终答案,而是本周必须理解的最小骨架:
summary = {
"annual_return": annual_return(net_ret),
"annual_volatility": annual_volatility(net_ret),
"sharpe": sharpe_ratio(net_ret),
"max_drawdown": max_drawdown(net_ret),
}
print(summary)
读代码时按四步检查:输入从哪里来;中间变量的类型和 shape 是什么;函数或脚本输出什么;哪些错误应该显式报出来。
0.3 本周程序规范
- 所有路径用相对路径或 `pathlib.Path`,不要写死 `/home/miku/...`。
- 核心逻辑进 `src/`,notebook 只做探索和解释。
- 每个脚本能从 fish 终端运行,并在 README 写出命令。
- 输出必须落盘到 `reports/`、`figures/` 或 `outputs/`,不能只在屏幕上看。
0.4 本周练习分层
| 层级 | 任务 | 不合格表现 | 合格验收 |
|---|---|---|---|
| 最小练习 | 手写上面的最小骨架 | 只在 notebook 里运行 | 终端运行成功 |
| 标准练习 | 把逻辑拆成函数/模块 | 一个大脚本从头写到尾 | 至少 2 个函数,职责清楚 |
| 项目练习 | 生成本周交付物 reports/week16_quant_report.md | 只有屏幕输出 | 文件落盘,可复查 |
| 复盘练习 | 写 3 个错误和修复 | 只写“已解决” | 写清报错、原因、修复、预防 |
0.5 本周和主线的连接
- 回到总计划:USTC AI / Quant 练习手册
- 查详细练习索引:技术练习详解
- 查质量评分:最终质量门槛
1. 本周学习目标
完成本周后,你应该能产出一份 reports/week16_quant_project_report.md,包含:
- 策略规则和交易假设。
- 数据来源、时间范围、频率、缺失值处理。
- 样本内 / 样本外切分。
- annual return、annual volatility、Sharpe、max drawdown、turnover、交易成本敏感性。
- 净值曲线、回撤曲线、月度收益或分段收益。
- 对“是否样本外有效”的谨慎判断。
- 对过拟合、交易成本、滑点、流动性、实盘约束的限制说明。
建议文件布局:
quant-backtest-mini/
├── data/
│ └── prices.csv
├── figures/
│ ├── equity_curve.png
│ ├── drawdown.png
│ ├── split_equity.png
│ └── cost_sensitivity.png
├── reports/
│ ├── week15_backtest_daily_results.csv
│ ├── week15_backtest_notes.md
│ ├── week16_metrics_by_split.csv
│ ├── week16_cost_sensitivity.csv
│ └── week16_quant_project_report.md
├── src/
│ └── quant_backtest_mini/
│ ├── __init__.py
│ ├── backtest.py
│ ├── metrics.py
│ └── report.py
├── README.md
└── pyproject.toml
如果 Week 15 项目还不完整,先回去补齐回测脚本,再开始写报告。
2. 前置条件
你需要已经完成:
- Week 15 的
backtest.py和metrics.py。 - 至少能生成策略日收益、benchmark 日收益、turnover。
- 至少能画净值曲线和回撤曲线。
- 理解
position.shift(1)为什么必要。
进入 Week 15 项目继续工作:
cd ~/Code/ustc-stat-ai-quant/quant-backtest-mini
source .venv/bin/activate.fish
如果你重新打开了终端,必须重新激活虚拟环境。检查依赖:
python -c "import pandas as pd; import numpy as np; import matplotlib; print('report env ok')"
3. 报告不是 README
README 负责告诉别人如何运行项目;量化报告负责解释研究结果。
区别:
| 文件 | 读者关心什么 |
|---|---|
README.md |
怎么安装、怎么运行、项目结构是什么 |
reports/week16_quant_project_report.md |
策略是否可靠、结果是否可解释、风险在哪里 |
Week 16 先写报告;Week 17 再把整个项目工程化。
4. 创建报告分析脚本
创建 src/quant_backtest_mini/report.py,用于生成分段指标和成本敏感性表。
from pathlib import Pathimport matplotlib.pyplot as plt import pandas as pd
from quant_backtest_mini.backtest import load_prices, run_backtest from quant_backtest_mini.metrics import drawdown, equity_curve, summarize
FIGURE_DIR = Path(“figures”) REPORT_DIR = Path(“reports”) FIGURE_DIR.mkdir(exist_ok=True) REPORT_DIR.mkdir(exist_ok=True)
def split_periods(result: pd.DataFrame) -> dict[str, pd.DataFrame]: n = len(result) train_end = int(n * 0.6) test_start = int(n * 0.8) return { “in_sample”: result.iloc[
], “validation”: result.iloc[train_end], “out_of_sample”: result.iloc[test_start:], “full_period”: result, }def metrics_by_split(result: pd.DataFrame) -> pd.DataFrame: rows = [] for name, part in split_periods(result).items(): strategy = summarize(part[“net_strategy_return”], part[“turnover”]) benchmark = summarize(part[“benchmark_return”], pd.Series(0, index=part.index)) rows.append({“period”: name, “portfolio”: “strategy”, **strategy}) rows.append({“period”: name, “portfolio”: “benchmark”, **benchmark}) return pd.DataFrame(rows)
def cost_sensitivity(prices: pd.DataFrame, lookback: int) -> pd.DataFrame: rows = [] for cost_bps in [0, 5, 10, 20, 50]: result = run_backtest(prices, lookback=lookback, cost_bps=cost_bps) summary = summarize(result[“net_strategy_return”], result[“turnover”]) rows.append({“cost_bps”: cost_bps, **summary}) return pd.DataFrame(rows)
def plot_split_equity(result: pd.DataFrame) -> None: fig, ax = plt.subplots(figsize=(10, 5)) for name, part in split_periods(result).items(): if name == “full_period”: continue equity_curve(part[“net_strategy_return”]).plot(ax=ax, label=name) ax.set_title(“Strategy Equity by Split”) ax.set_ylabel(“growth of 1 within split”) ax.legend() fig.tight_layout() fig.savefig(FIGURE_DIR / “split_equity.png”, dpi=150) plt.close(fig)
def plot_cost_sensitivity(table: pd.DataFrame) -> None: fig, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True) axes[0].plot(table[“cost_bps”], table[“annual_return”], marker=“o”) axes[0].set_title(“Annual Return vs Transaction Cost”) axes[0].set_ylabel(“annual return”)
plain axes[1].plot(table[“cost_bps”], table[“sharpe_ratio”], marker=“o”) axes[1].set_title(“Sharpe vs Transaction Cost”) axes[1].set_xlabel(“cost bps”) axes[1].set_ylabel(“sharpe”)
plain fig.tight_layout() fig.savefig(FIGURE_DIR / “cost_sensitivity.png”, dpi=150) plt.close(fig)
def main() -> None: prices = load_prices() result = run_backtest(prices, lookback=20, cost_bps=5)
plain split_metrics = metrics_by_split(result) split_metrics.to_csv(REPORT_DIR / “week16_metrics_by_split.csv”, index=False) print(split_metrics)
plain cost_table = cost_sensitivity(prices, lookback=20) cost_table.to_csv(REPORT_DIR / “week16_cost_sensitivity.csv”, index=False) print(cost_table)
plain plot_split_equity(result) plot_cost_sensitivity(cost_table) print(“saved Week 16 report tables and figures”)
if name == “main”: main()
运行:
python -m quant_backtest_mini.report
如果模块运行失败,可以临时使用:
PYTHONPATH=src python src/quant_backtest_mini/report.py
但最终报告中建议保留模块运行方式,便于 Week 17 工程化。
5. 指标表怎么写进报告
打开生成的指标表:
python -c "import pandas as pd; print(pd.read_csv('reports/week16_metrics_by_split.csv').round(4))"
报告中不要只复制一堆数字,要解释数字背后的含义。
示例解释方式:
样本内策略 Sharpe 高于 benchmark,但样本外 Sharpe 明显下降,说明策略可能依赖前期市场结构。样本外最大回撤接近 benchmark,没有明显风险优势。因此不能认为该动量规则已经稳定有效。
如果策略表现很差,也不要美化。量化报告里诚实说明失败原因,比硬说有效更专业。
6. 最大回撤分析
最大回撤不是一个抽象数字,它代表真实持有体验。
要回答:
- 最大回撤发生在什么时间段?
- 回撤持续多久?
- 当时 benchmark 是否也在回撤?
- 策略是否比 benchmark 更早恢复?
- 如果是自己真实资金,是否能承受?
可以加入辅助代码:
def drawdown_details(daily_returns: pd.Series) -> dict[str, object]:
equity = equity_curve(daily_returns)
dd = drawdown(equity)
trough = dd.idxmin()
peak = equity.loc[:trough].idxmax()
recovery_candidates = equity.loc[trough:][equity.loc[trough:] >= equity.loc[peak]]
recovery = recovery_candidates.index[0] if not recovery_candidates.empty else None
return {
"peak": peak,
"trough": trough,
"recovery": recovery,
"max_drawdown": float(dd.loc[trough]),
}
在报告中写成自然语言:
策略最大回撤从 YYYY-MM-DD 的净值高点开始,到 YYYY-MM-DD 达到最低点。样本结束前是否恢复:是/否。这个回撤说明策略在连续震荡或趋势反转时承压明显。
7. 样本切分:不要只看全样本
报告必须包含三段:
in_sample:用于观察和开发策略
validation:用于比较参数或模型
out_of_sample:最终检验,不反复调参
写报告时避免这种说法:
全样本收益很好,所以策略有效。
更专业的说法:
全样本表现不能单独证明策略有效。样本外表现更接近未来未知数据,因此本文主要依据 out_of_sample 指标判断策略稳定性。
如果样本外变差,可以写:
样本外年化收益和 Sharpe 均下降,说明策略可能对样本内走势过拟合,或该动量规则在后期市场状态中不适用。
8. 成本敏感性
查看成本敏感性表:
python -c "import pandas as pd; print(pd.read_csv('reports/week16_cost_sensitivity.csv').round(4))"
打开图:
xdg-open figures/cost_sensitivity.png
报告必须回答:
- 0 bps 时是否有效?
- 5 bps 时是否仍有效?
- 20 bps 时是否完全失效?
- 策略换手率是否太高?
一个常见结论:
策略在无成本条件下略优于 benchmark,但随着成本从 0 bps 增加到 20 bps,年化收益和 Sharpe 快速下降,说明该策略对交易成本敏感,不适合高频换仓或高摩擦市场。
9. 报告正文模板
创建报告:
code reports/week16_quant_project_report.md
推荐结构:
# Mini Quant Backtest Report1. Research question
本文研究一个简单 N 日动量规则是否能在样本外获得稳定风险调整收益。
2. Data
- 标的:
- 数据来源:
- 日期范围:
- 频率:日频
- 缺失值处理:
3. Strategy rule
- 过去 N 日收益为正:持有
- 否则:空仓
- t 日收盘生成信号,t+1 日持仓
4. Backtest assumptions
- 初始资金:1
- 交易成本:5 bps
- 不考虑滑点
- 不考虑停牌、涨跌停、成交量限制
- benchmark:buy and hold
5. Sample split
- in_sample:
- validation:
- out_of_sample:
6. Metrics
插入 metrics_by_split 表格,并解释 annual return、volatility、Sharpe、max drawdown、turnover。
7. Equity and drawdown
引用 equity_curve、drawdown、split_equity 图,并解释最大回撤时间段。
8. Cost sensitivity
引用 cost_sensitivity 表格和图片。
9. Findings
- 样本内是否有效?
- 样本外是否有效?
- 是否优于 benchmark?
- 是否对成本敏感?
10. Limitations
- 数据样本有限
- 策略过于简单
- 未考虑真实交易滑点和流动性
- 未考虑参数挖掘惩罚
- 未进行多标的验证
11. Conclusion
用谨慎语言判断:可以继续研究、需要改进,或暂不认为有效。
10. 如何写限制,而不是找借口
限制不是失败借口,而是研究边界。
好的限制写法:
本回测只使用单一标的日频数据,无法证明策略在其它市场或更长周期中有效。交易成本仅用固定 bps 近似,没有考虑滑点、成交量、涨跌停和资金容量,因此实盘表现可能显著低于回测。
不好的写法:
如果以后优化一下,策略肯定会更好。
量化研究中,未来改进只能说“可能”,不能说“肯定”。
11. 图表引用建议
在 Markdown 报告中可以用相对路径引用图:



如果报告渲染工具从项目根目录读取,也可以改成:

关键是你自己打开预览检查图片能显示。
12. 文件布局检查
执行:
pwd
ls reports figures src/quant_backtest_mini
至少应有:
reports/week16_metrics_by_split.csv
reports/week16_cost_sensitivity.csv
reports/week16_quant_project_report.md
figures/split_equity.png
figures/cost_sensitivity.png
src/quant_backtest_mini/report.py
13. 练习
- 在报告中加入 benchmark 的最大回撤时间段,和策略进行比较。
- 把
lookback=20改成lookback=60,重新生成表格,但不要只因为结果更好就替换主结论。 - 增加一张月度收益表,检查收益是否集中在少数月份。
- 在成本敏感性中加入 100 bps,观察策略是否完全失效。
- 写一段“如果实盘会遇到什么问题”,至少包含滑点、流动性、心理承受、参数失效。
- 用三句话向非量化同学解释最大回撤。
14. 验收检查
在项目根目录执行:
source .venv/bin/activate.fish
python -m quant_backtest_mini.report
python -c "from pathlib import Path; required = ['reports/week16_metrics_by_split.csv', 'reports/week16_cost_sensitivity.csv', 'reports/week16_quant_project_report.md', 'figures/split_equity.png', 'figures/cost_sensitivity.png']; missing = [p for p in required if not Path(p).exists()]; print('missing:', missing); raise SystemExit(1 if missing else 0)"
通过标准:
- 报告明确写出策略规则和执行假设。
- 报告包含样本内、验证集、样本外指标。
- 报告包含最大回撤解释,而不只是一个数字。
- 报告包含交易成本敏感性。
- 报告明确写出局限性和是否可能过拟合。
- 结论使用谨慎语言,不夸大收益。
15. 常见错误
错误 1:报告像广告
不要写“策略收益优秀,未来可期”。要写证据、风险、限制。
错误 2:只报告全样本
全样本结果容易掩盖样本外失效。必须拆分。
错误 3:最大回撤只写数字
最大回撤应解释发生时间、持续时间、是否恢复,以及投资者是否可能承受。
错误 4:忽略交易成本
量化策略如果没有成本敏感性分析,报告不完整。
错误 5:结论超过证据
如果只测了一个模拟数据或一个标的,就不能说“策略有效”。最多说“在当前样本和假设下表现如何”。
16. 下一步
进入 Week 17:项目工程化、CLI 入口、uv 依赖与可复现运行。下一周会把 Week 15-16 的回测项目整理成别人可以 clone、安装、运行、复查的干净仓库。
plain
喜欢的话,留下你的评论吧~