Week 16:量化项目报告、回撤分析与样本外解释

发布于 2026-07-26 16:00 4366 字 22 min read

Week 16:量化项目报告、回撤分析与样本外解释。fish-first 终端教学,面向 CachyOS、VS Code、uv 和 Python 学习路线。
Oh My Pi / weekly tutorial / week16-quant-project-report
miku@cachyos:~/Code/python-learning$ omp teach week16-quant-project-report --fish-first --step-by-step
source487 行教学文档
weekWeek 16
shellfish-first 命令版
backlink20 周计划

Week 16:量化项目报告、回撤分析与样本外解释

返回总计划:USTC 统计 + AI / 量化 20 周成长计划

Week 16 的目标是把 Week 15 的 mini backtest 写成一份诚实、可复查的量化项目报告。报告不是宣传策略有多赚钱,而是回答:策略规则是什么、数据怎么来、样本怎么切、成本怎么算、风险有多大、样本外是否仍有效、局限性在哪里。

本文命令默认使用 fish shell。进入项目后激活环境:

ompfish
source .venv/bin/activate.fish

0. 本周详细教学:语法、规范、验收

本节不是追加在尾部的复习,而是本周正文的入口。先读这里,再做后面的命令和项目。

0.1 本周真正要学会什么

维度要求
知识点annual return、vol、Sharpe、max drawdown、成本敏感性
代码语法能从空文件写出本周核心脚本,而不是只复制运行
程序规范函数拆分、路径清楚、输入输出明确、错误能解释
交付物reports/week16_quant_report.md
验收方式从 fish 终端运行命令,得到可复查的文件或指标

0.2 代码语法精讲

下面的代码不是最终答案,而是本周必须理解的最小骨架:

omppython
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 本周和主线的连接

1. 本周学习目标

完成本周后,你应该能产出一份 reports/week16_quant_project_report.md,包含:

  1. 策略规则和交易假设。
  2. 数据来源、时间范围、频率、缺失值处理。
  3. 样本内 / 样本外切分。
  4. annual return、annual volatility、Sharpe、max drawdown、turnover、交易成本敏感性。
  5. 净值曲线、回撤曲线、月度收益或分段收益。
  6. 对“是否样本外有效”的谨慎判断。
  7. 对过拟合、交易成本、滑点、流动性、实盘约束的限制说明。

建议文件布局:

ompprompt
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.pymetrics.py
  • 至少能生成策略日收益、benchmark 日收益、turnover。
  • 至少能画净值曲线和回撤曲线。
  • 理解 position.shift(1) 为什么必要。

进入 Week 15 项目继续工作:

ompfish
cd ~/Code/ustc-stat-ai-quant/quant-backtest-mini
source .venv/bin/activate.fish

如果你重新打开了终端,必须重新激活虚拟环境。检查依赖:

ompfish
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,用于生成分段指标和成本敏感性表。

omppython
from pathlib import Path

import 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()

运行:

ompfish
python -m quant_backtest_mini.report

如果模块运行失败,可以临时使用:

ompfish
PYTHONPATH=src python src/quant_backtest_mini/report.py

但最终报告中建议保留模块运行方式,便于 Week 17 工程化。

5. 指标表怎么写进报告

打开生成的指标表:

ompfish
python -c "import pandas as pd; print(pd.read_csv('reports/week16_metrics_by_split.csv').round(4))"

报告中不要只复制一堆数字,要解释数字背后的含义。

示例解释方式:

ompprompt
样本内策略 Sharpe 高于 benchmark,但样本外 Sharpe 明显下降,说明策略可能依赖前期市场结构。样本外最大回撤接近 benchmark,没有明显风险优势。因此不能认为该动量规则已经稳定有效。

如果策略表现很差,也不要美化。量化报告里诚实说明失败原因,比硬说有效更专业。

6. 最大回撤分析

最大回撤不是一个抽象数字,它代表真实持有体验。

要回答:

  1. 最大回撤发生在什么时间段?
  2. 回撤持续多久?
  3. 当时 benchmark 是否也在回撤?
  4. 策略是否比 benchmark 更早恢复?
  5. 如果是自己真实资金,是否能承受?

可以加入辅助代码:

omppython
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]),
    }

在报告中写成自然语言:

ompprompt
策略最大回撤从 YYYY-MM-DD 的净值高点开始,到 YYYY-MM-DD 达到最低点。样本结束前是否恢复:是/否。这个回撤说明策略在连续震荡或趋势反转时承压明显。

7. 样本切分:不要只看全样本

报告必须包含三段:

ompprompt
in_sample:用于观察和开发策略
validation:用于比较参数或模型
out_of_sample:最终检验,不反复调参

写报告时避免这种说法:

ompprompt
全样本收益很好,所以策略有效。

更专业的说法:

ompprompt
全样本表现不能单独证明策略有效。样本外表现更接近未来未知数据,因此本文主要依据 out_of_sample 指标判断策略稳定性。

如果样本外变差,可以写:

ompprompt
样本外年化收益和 Sharpe 均下降,说明策略可能对样本内走势过拟合,或该动量规则在后期市场状态中不适用。

8. 成本敏感性

查看成本敏感性表:

ompfish
python -c "import pandas as pd; print(pd.read_csv('reports/week16_cost_sensitivity.csv').round(4))"

打开图:

ompfish
xdg-open figures/cost_sensitivity.png

报告必须回答:

  • 0 bps 时是否有效?
  • 5 bps 时是否仍有效?
  • 20 bps 时是否完全失效?
  • 策略换手率是否太高?

一个常见结论:

ompprompt
策略在无成本条件下略优于 benchmark,但随着成本从 0 bps 增加到 20 bps,年化收益和 Sharpe 快速下降,说明该策略对交易成本敏感,不适合高频换仓或高摩擦市场。

9. 报告正文模板

创建报告:

ompfish
code reports/week16_quant_project_report.md

推荐结构:

ompprompt
# Mini Quant Backtest Report

1. 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. 如何写限制,而不是找借口

限制不是失败借口,而是研究边界。

好的限制写法:

ompprompt
本回测只使用单一标的日频数据,无法证明策略在其它市场或更长周期中有效。交易成本仅用固定 bps 近似,没有考虑滑点、成交量、涨跌停和资金容量,因此实盘表现可能显著低于回测。

不好的写法:

ompprompt
如果以后优化一下,策略肯定会更好。

量化研究中,未来改进只能说“可能”,不能说“肯定”。

11. 图表引用建议

在 Markdown 报告中可以用相对路径引用图:

ompprompt
![Equity Curve](../figures/equity_curve.png)
![Drawdown](../figures/drawdown.png)
![Cost Sensitivity](../figures/cost_sensitivity.png)

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

ompprompt
![Equity Curve](figures/equity_curve.png)

关键是你自己打开预览检查图片能显示。

12. 文件布局检查

执行:

ompfish
pwd
ls reports figures src/quant_backtest_mini

至少应有:

ompprompt
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. 练习

  1. 在报告中加入 benchmark 的最大回撤时间段,和策略进行比较。
  2. lookback=20 改成 lookback=60,重新生成表格,但不要只因为结果更好就替换主结论。
  3. 增加一张月度收益表,检查收益是否集中在少数月份。
  4. 在成本敏感性中加入 100 bps,观察策略是否完全失效。
  5. 写一段“如果实盘会遇到什么问题”,至少包含滑点、流动性、心理承受、参数失效。
  6. 用三句话向非量化同学解释最大回撤。

14. 验收检查

在项目根目录执行:

ompfish
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

喜欢的话,留下你的评论吧~