单元 10:完成第一份量化研究报告
前面的实验已经留下不少结果:3 日移动平均策略在样本内胜出,却在样本外亏损;仓位管理降低了历史波动和回撤;止损在一条路径中有帮助,在反弹路径中却会拖累收益。
如果只挑出一张漂亮的净值图,这些互相牵制的证据很容易消失。本单元要做的,是把研究问题、实验过程、结果和局限连接起来,生成一份别人能够检查、质疑和复现的研究报告。
本单元目标
完成本单元后,你将能够:
- 把模糊想法改写成有明确判定条件的研究假设;
- 在生成报告前自动检查关键输入与中间结果;
- 使用统一口径比较基准、基础策略和风险管理方案;
- 区分数据支持的结论与超出证据范围的推断;
- 记录运行环境、输入文件和内容哈希,使结果可以追溯;
- 阅读一份研究报告时,找到最值得质疑的环节。
本单元会沿用单元 08 和单元 09 的固定种子虚构数据。先从项目根目录依次运行:
uv run python examples/unit08/analyze_overfitting.py
uv run python examples/unit09/manage_risk.py
uv run python examples/unit10/build_research_report.py前两条命令重建报告所依赖的实验,最后一条命令检查输入并生成报告。
前置条件
场景:报告从一个可能失败的问题开始
“寻找一个赚钱策略”不是一个容易检验的研究问题。只要不断修改参数、区间和规则,总能找到某个看起来赚钱的版本,却很难判断它是规律还是偶然。
本次研究把问题收窄为:
仅使用过去收盘价形成的移动平均趋势信号,在考虑交易成本后,能否把样本内选择结果推广到随后未见的数据?
对应的主要假设是:
在 2~60 日窗口中,仅按样本内总收益选出的窗口,扣除 0.1% 单边交易成本后,样本外总收益大于 0。
这是一条可证伪(Falsifiable)的假设:它不仅说清了期待看到什么,还提前规定了什么结果会让期待落空。窗口选择只能查看前 240 个观测;参数锁定后,后 120 个观测的收益若不大于 0,假设便不成立。
实际结果是:3 日窗口在样本内获得 44.2920%,在样本外获得 -18.4671%。因此报告明确写出“主要假设不成立”。一次失败的检验仍然是有效的研究结果,它阻止我们把样本内冠军误当成可推广的规律。
先写判定条件,再揭晓结果
如果看完样本外表现才决定“什么算成功”,判定条件就可能跟着答案移动。研究假设、候选参数范围、时间切分和主要指标最好在揭晓样本外结果前固定。
把完整证据链写进报告
一份研究报告不是若干图表的合集。每个结果都应该能够沿着下面的关系向前追问:
flowchart LR
Q[研究问题] --> D[数据与时间范围]
D --> R[策略与参数规则]
R --> A[成交、成本与仓位假设]
A --> E[样本内外证据]
E --> C[结论与局限]
C --> P[复现信息]例如,看到“样本外收益 -18.4671%”时,读者还需要知道:这个样本何时被隔离、参数如何选出、信号何时形成、收益何时发生、成本怎样扣除。缺少其中任何一环,同一个数字都可能具有完全不同的含义。
程序生成的 research_report.md 因此包含:
- 研究问题与可证伪假设;
- 数据来源、范围、字段和质量检查;
- 策略、参数选择、成交、成本与仓位规则;
- 基准、指标口径、样本内外和稳健性结果;
- 风险管理证据、局限和可能失效的场景;
- 环境、执行命令、输入清单和内容哈希。
这些内容不是为了让报告显得正式,而是让结论受到相同约束。别人应该能从结论追溯到数据,也能从数据重新得到结论。
先检查证据,再组织文字
自动生成报告有一个危险:如果上游文件错位,程序也可能把错误数字排成整齐的表格。validate_inputs() 在写报告前执行 16 项检查,覆盖四类问题:
| 检查对象 | 需要确认的事实 |
|---|---|
| 表格结构 | 八份输入的必要字段都存在 |
| 数据与时间 | 价格为正、日期升序且唯一,240/120 切分正确 |
| 研究过程 | 参数完整覆盖 2~60,样本内选中窗口 3,滚动验证没有越过最终测试边界 |
| 结果对账 | 五种风险方案齐全,逐日结果与价格对齐,收益、末端净值和最大回撤互相一致 |
检查失败时,程序直接停止,而不是继续生成一份貌似完整的报告。例如逐日风险结果的日期与价格日期错开后,即使行数仍然相同,仓位和收益也不再属于同一时点。
checks = validate_inputs(tables)
metrics = build_unified_metrics(tables)
report = render_report(tables, metrics, checks, manifest, environment)这个顺序表达了一个重要原则:文字只能总结已经通过验证的证据。自动检查无法证明研究思想正确,但能及时拦住字段缺失、时间错位和汇总数字对不上等机械错误。
用同一张表比较同一件事
报告把价格型买入并持有基准与五种策略放入一张指标表。所有策略使用同一段虚构日期、相同的 0.1% 单边成本和相同年化期数;基准不扣交易成本、不计分红,只表示虚构价格本身的变化。
| 方案 | 全样本收益 | 样本外收益 | 年化波动率 | 最大回撤 | 夏普比率 | 平均现金比例 |
|---|---|---|---|---|---|---|
| 价格型买入并持有 | 7.8690% | 3.5561% | 8.5437% | -23.0550% | 0.6650 | 0.0000% |
| 全仓信号 | 17.6454% | -18.4671% | 6.2827% | -19.9944% | 1.8474 | 46.6667% |
| 固定 50% | 8.5416% | -9.6775% | 3.1413% | -10.5272% | 1.8474 | 73.3333% |
| 波动率仓位,无调仓阈值 | 11.2880% | -12.5678% | 4.3505% | -13.8739% | 1.7476 | 62.7055% |
| 波动率仓位,10% 阈值 | 11.3215% | -12.5481% | 4.3479% | -13.8458% | 1.7535 | 62.9094% |
| 波动率仓位,加止损 | 12.8236% | -11.3681% | 4.2709% | -12.6832% | 2.0047 | 63.1331% |
这张表需要结合口径阅读:
- 全样本包含用于选择参数的样本内区间,17.6454% 不能代替样本外证据;
- 基准与策略的成本假设不同,因此它是价格参照,不是完全可交易的公平竞赛;
- 风险方案持有更多现金,波动与回撤较低的一部分来自承担更少的资产暴露;
- 所有策略样本外收益仍为负,风险管理改善了亏损幅度,却没有恢复信号的预测能力。
报告图把参数扫描、净值和回撤放在一起。上图揭示样本内外表现的分离,中图显示最终样本外开始后的净值下降,下图则显示仓位控制怎样改变回撤路径。三幅图回答的是不同问题,不能只保留最有利的一幅。
结论只能走到证据允许的位置
程序没有根据正负收益自动拼出一句“策略有效”或“策略无效”,而是把支持与不支持的陈述分开。
本次数据支持:
- 3 日窗口是预定候选范围内的样本内收益冠军;
- 参数锁定后,3 日窗口的样本外扣费收益为负;
- 仓位缩放在这条路径中降低了历史波动和最大回撤;
- 止损的结果取决于触发后的价格路径。
本次数据不支持:
- 这条规则对真实 ETF 具有稳定预测能力;
- 样本内 44.2920% 可以外推为未来收益;
- 波动率目标能够保证未来波动不超过 6%;
- 止损能够保证按阈值成交或避免大额亏损。
“不支持”不等于已经证明相反命题。例如,这份报告不能证明所有移动平均策略永远无效;它只能说明当前数据、候选范围和实验设计没有支持原假设。把结论限制在证据范围内,是研究报告必须守住的边界。
风险较低不等于规律有效
仓位缩放可以减少资金暴露,从而缩小波动和回撤;这不表示方向信号更准确。风险管理与预测能力必须分开评价。
可复现不只是“代码能运行”
同一份代码读取了不同文件,或者依赖版本改变,结果都可能不同。因此程序同时保存:
| 产物 | 用途 |
|---|---|
research_report.md | 完整研究报告 |
research_metrics.csv | 基准与五种策略的统一指标 |
validation_checks.csv | 关键中间结果检查 |
artifact_manifest.csv | 八份输入的路径、大小和 SHA-256 |
environment.json | Python、pandas、matplotlib 和操作系统信息 |
research_overview.png | 参数、净值和回撤概览 |
SHA-256 是根据文件内容计算出的长字符串,可以把它理解为文件的“内容指纹”。内容只要发生变化,哈希通常也会变化。复现者可以借此判断双方是否使用了相同输入,而不必逐行肉眼比较 CSV。
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as file:
for block in iter(lambda: file.read(65536), b""):
digest.update(block)
return digest.hexdigest()哈希只能确认内容是否一致,不能证明内容正确。一个带有未来函数的数据文件同样能拥有稳定哈希。可复现性(Reproducibility)回答“别人能否得到相同结果”,研究有效性还需要回答“这些结果是否由合理的数据和方法得到”。
依赖则由项目的锁文件固定。完整复现时可以先执行:
uv sync --locked然后重新运行单元 08、09 和 10。这样,代码版本、依赖版本、参数、输入文件与执行命令共同构成可追溯的研究环境。
运行并审查报告
执行:
uv run python examples/unit10/build_research_report.py核心输出为:
自动检查: 16 项,全部通过
基础策略全样本收益: 17.6454%
基础策略样本外收益: -18.4671%
主要假设: 不成立
研究报告: outputs/unit10/research_report.md
统一指标: outputs/unit10/research_metrics.csv
报告图表: outputs/unit10/research_overview.png打开报告后,不要只确认数字与本页相同。可以按以下问题逐层审查:
- 研究假设有没有明确的失败条件?
- 数据来源、日期、频率和价格口径是否足够清楚?
- 参数选择是否接触了最终样本外数据?
- 信号、成交、收益和成本的时间关系是否可实现?
- 基准与策略是否在可解释的口径下比较?
- 结论是否同时保留了不利证据和失败结果?
- 局限是否说明了虚构数据与真实交易的差距?
- 其他人能否根据命令、环境和输入清单重建结果?
研究报告的价值不在于让策略显得无懈可击,而在于让薄弱环节容易被发现。
常见陷阱
先有结论,再挑证据
如果目标变成“证明策略有效”,研究者很容易只保留有利区间、参数和图表。应该先固定可失败的假设,再完整报告结果。
隐藏失败的样本外结果
样本外失败不是应被删除的异常,而是对推广能力最直接的证据。根据它修改规则后,这段数据也不能继续被称为新策略的样本外测试。
混用全样本和样本外指标
全样本结果包含已经参与开发的数据,往往比真正未见数据更乐观。表格、文字和图表都应清楚标注时间范围。
只保存截图,不保存数据口径
截图无法说明计算公式、输入版本和参数,也不便于复核。图表应当能由代码和结构化数据重新生成。
把成功复现当成正确性证明
错误的时间关系、偏差数据和不现实成交假设也能被完美复现。复现减少的是“结果从哪里来”的不确定性,不能替代方法审查。
验证结果
完成标准:
- 报告生成前的 16 项输入检查全部通过;
- 主要假设、失败条件和 -18.4671% 的样本外结果同时写入报告;
- 基准与五种策略使用明确且可比较的指标口径;
- 报告分别列出数据支持和不支持的陈述,并保留虚构数据与成交简化等局限;
- 六份报告产物能够由同一组命令重新生成;
- 输入清单包含八份前置文件的路径、大小和 SHA-256;
- 能解释内容哈希、可复现性和研究有效性之间的区别。
小结
本单元把前面的实验整理成一条可审查的研究链:
- 主要假设提前规定样本外收益必须大于 0,实际结果使其不成立;
- 16 项自动检查在报告生成前核对数据、时间边界和汇总结果;
- 基准与五种策略在统一表格中展示,同时说明成本和现金暴露差异;
- 结论明确区分数据支持与不支持的陈述,没有把风险降低写成预测能力;
- 环境记录、输入清单和 SHA-256 让研究结果能够追溯和复现。
一份可信的量化研究报告不需要每次都得到好消息,但必须让问题、证据、结论和局限彼此对得上。
练习
基础练习:补充一个风险假设
在报告的主要假设旁增加一个次要风险假设,并提前写出它的判定指标。
进阶练习:验证检查与指标口径
修改一份前置 CSV 的日期,观察程序在哪项检查停止,再恢复文件并重新生成报告。然后为统一指标表加入样本外最大回撤,说明它与全样本最大回撤回答的问题有何不同。
延伸练习:观察内容哈希变化
重新生成相同输入的清单,比较 SHA-256;再只修改一个数值,观察哈希如何变化。