一位 私募 量化 团队 的 资深 研究员 在 原 研究 PR 上 线 半 年 之后 把 报告 递 给 一 位 初级 队友。"重新 跑 一 遍。基金 经理 在 问 这 个 信号 在 2024 年 数据 上 是否 还 work。" 初级 从 共享 盘 拉 出 notebook 打 开,第一 个 错误 立 刻 撞 上 来:ImportError: cannot import name 'X' from 'pandas'。本 机 上 pandas 版 本 是 2.4;notebook 是 1.5 时 写 的。初级 pip install pandas 1.5 —— 另 一 个 单元格 又 挂 了,因为 numpy 版本 现在 不 兼容。两 个 小时 依赖 战争 之后,notebook 跑 通 了,但 算 出 的 数 与 报告 里 不 一致。种子 没 记 录;共享 盘 上 的 数据 文件 自 原始 跑 之后 被 触 动 过;原始 代码 状态 的 git commit SHA 找 不 到。"结果" 无法 复现。资深 研究员 走 回 基金 经理 那 里。"我们 没 有 一 个 可 复现 的 信号。我们 有 一 个 信号 的 记忆。" 本 课 是 模块 的 工程 capstone —— 让 L1 实验 日志 可 审计、让 L2 测试集 上锁 可 执行、让 L3 DSR 半 年 后 可 被 另 一 研究员 验证 的 那 一 层。本 节 结束 后,你 应当 能 产出 一 个 队友 可以 从 单 一 命令 重 跑 的 研究 PR。
六 层 经典 研究 栈
1. notebook vs script - Jupyter for exploration; .py for production; transition within two weeks
2. version-pinned dependencies - pyproject.toml + uv.lock / poetry.lock / requirements.txt with hashes
3. seeded RNGs - every randomness source seeded; seed logged per run
4. git + feature-branch + pull-request workflow - every project a feature branch; result is a PR
5. experiment tracking - mlflow / wandb / SQLite log of hyperparameters / seed / data window / metrics / artefact path / git_commit_sha
6. code-review checklist - five binary checks enforced at PR review
规则:每 一 个 研究 结果 都 必须 能 从 单 条 命令 复现,给 定 锁 定 的 依赖、锁 定 的 数据 快照、记 录 的 种子 与 git commit SHA。六 层 是 让 这 条 规则 可 执行 的 工程 管 道。第 (1) 层 把 想法 生成(notebook)与 结果 主 张(脚本)分 开;第 (2) 层 冻结 运行 时;第 (3) 层 冻结 随机 性;第 (4) 层 把 一 个 结果 打 包 为 带 版本 的 diff;第 (5) 层 记 录 谱 系;第 (6) 层 在 合 并 时 落实 纪律。
研究 PR 的 八 项 artefact
每 一 个 进 入 仿真 交易 的 研究 结果 都 以 pull request 形 式 发 货。PR 按 此 顺序 打 包 八 项 artefact:
(a) notebook - frozen at result-claim moment
(b) production script - regeneratable from CLI
(c) experiment log - CSV / SQLite / mlflow run-ids
(d) pre-registration document - the L1 six-field template, committed at project start
(e) in-sample result
(f) single out-of-sample evaluation result
(g) multiple-testing correction - Bonferroni / BH-FDR / DSR with N counter from the experiment log
(h) write-up - the curated narrative for human review
规则:PR 不 merge 即 研究 结果 不 成 立;代码 评审 清单 不 过 即 PR 不 merge。八 项 artefact 闭 合 前 几 课 的 全 链:(d) 接 L1 预登记;(e) 与 (f) 接 L2 测试集 单 次 触碰 纪律;(g) 接 L3 修正;(c) 是 (g) 的 试验 计数器 源;(a) 与 (b) 是 L4 notebook-vs-脚本 纪律;(h) 是 结果 给 人 看 的 那 一 面。
实验 日志 schema
实验 日志 的 最 小 schema 是 SQLite 里 一 张 runs 表。九 列,按 此 顺序,SQL 类型 固 定:
CREATE TABLE runs (
run_id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
hyperparameters_json TEXT NOT NULL,
seed INTEGER NOT NULL,
data_window TEXT NOT NULL,
metric_name TEXT NOT NULL,
metric_value REAL NOT NULL,
artefact_path TEXT,
git_commit_sha TEXT NOT NULL
);
表 名 runs、九 列 名 按 此 顺序(run_id、timestamp、hyperparameters_json、seed、data_window、metric_name、metric_value、artefact_path、git_commit_sha)以及 SQL 类型 跨 区域 字节 一致。git_commit_sha 列 让 日志 不可 伪造 —— 每 一 个 指标 都 与 一 个 具体 的 代码 状态 绑 定。声 称 在 某 个 git_commit_sha 处 Sharpe 等 于 2.0 的 那 一 行 可以 重 复 验证:git checkout <sha>、恢复 数据 快照、设 种子、重 跑、比 对。指标 对 不 上,那 行 就 是 谎言。这 一 列 的 不可 伪造 性 给 了 下 文 代码 评审 清单 真正 的 牙 齿。
五 项 代码 评审 清单
1. test set evaluated exactly once - search the experiment log for runs that touched the test set; expect exactly one
2. seed logged for every run - search the log for null seeds; expect zero
3. data window justified in pre-registration - the window in the pre-registration document matches the window in the result
4. universe is survivorship-bias-free - the universe definition references `universe(date, symbol)` from 4.1.1 L4
5. multiple-testing correction applied with the experiment-log N - DSR / Bonferroni / BH-FDR reported alongside the headline metric using the actual count of trials from the log
规则:任 一 项 不 过 即 阻 塞 PR 合 并 至 修复;这 是 L1 + L2 + L3 的 工程 落地。检查 (1) 是 L2 测试集 上锁 的 工程 牙 齿。检查 (2) 是 复现 性 地 板 —— 没 有 记 录 种子 的 run 无法 字节 一致 地 重 现。检查 (3) 是 预登记 一致 性 检查 —— 窗口 与 预登记 不 一致 的 结果 要么 是 未 标 注 的 偏离(即 +1 进 多重 检验 修正 的 试验 计数器),要么 是 文档 bug;任 一 情况 PR 都 不能 合 并 直至 差异 解 决。检查 (4) 是 4.1.1 L4 的 survivorship-free universe;检查 (5) 是 用 实验 日志 N 让 L3 修正 可 审计。
代码:seed 一切
def seed_all(seed: int) -> None:
"""Seed every randomness source the project might touch.
Two runs with the same seed must produce byte-identical metrics on the same
hardware and the same lock-file environment.
"""
import os
import random
import numpy as np
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
try:
import torch
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
except ImportError:
pass
try:
import tensorflow as tf
tf.random.set_seed(seed)
except ImportError:
pass
函数 名 seed_all、参数 seed: int、随机 源(random、numpy、PYTHONHASHSEED、torch、torch.cuda、tensorflow)以及 规则 "two runs with the same seed must produce byte-identical metrics on the same hardware and the same lock-file environment" 跨 区域 字节 一致。可 选 依赖(torch、tensorflow)的 try / except ImportError 让 函数 在 缺 库 环境 里 仍 然 安全。
代码:log 一 次 run
def log_run(run_id, hyperparameters, seed, data_window,
metric_name, metric_value, artefact_path):
"""Insert one row into the experiments.db `runs` table.
Retrieves the current git commit SHA via subprocess; binds the metric to the
specific code state so the row cannot be faked retroactively.
"""
import json
import sqlite3
import subprocess
import time
sha = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip()
conn = sqlite3.connect('experiments.db')
conn.execute(
"INSERT INTO runs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
(run_id, time.strftime('%Y-%m-%dT%H:%M:%S%z'),
json.dumps(hyperparameters), seed, data_window,
metric_name, metric_value, artefact_path, sha),
)
conn.commit()
conn.close()
函数 名 log_run、参数 名、SQLite 文件 名 experiments.db、subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode().strip() 取 git commit SHA 与 规则 "每 一 个 写 入 任何 报告 的 指标 都 必须 能 追 溯 回 这 张 表 里 的 一 行" 跨 区域 字节 一致。
代码:pyproject.toml 骨架
[project]
name = "alpha-research-momentum"
version = "0.1.0"
description = "5-day momentum signal research for 510300"
requires-python = ">=3.11"
[project.dependencies]
numpy = ">=1.26"
pandas = ">=2.2"
scikit-learn = ">=1.4"
matplotlib = ">=3.8"
mlflow = ">=2.10"
jupyter = ">=1.0"
[tool.uv]
lock-file = "uv.lock"
YAML 键([project]、[project.dependencies]、[tool.uv])、依赖 名(numpy、pandas、scikit-learn、matplotlib、mlflow、jupyter)以及 Python 版本 要求(>=3.11)跨 区域 字节 一致。规则:lock 文件(uv.lock)和 pyproject.toml 一 起 提交 到 git;部署 用 uv pip sync 对 lock 文件。没 有 lock 文件,"version-pinned dependencies" 只 是 口 号 —— numpy>=1.26 允许 任 何 未 来 numpy 版本 包 括 破坏 性 更新;lock 文件 把 完整 传 递 闭 包 冻 死。
复现 一 个 结果 的 四 项 经典 输入
1. the locked dependency set - uv.lock or poetry.lock or requirements.txt with hashes, committed to git
2. the locked data snapshot - S3 / OSS partitioned parquet with a fixed version tag, referenced in the config
3. the logged seed - from the experiment log
4. the git commit SHA - from the experiment log; reproduce by `git checkout <sha>`
规则:每 一 个 研究 结果 都 必须 能 从 单 条 命令 复现,给 定 这 四 项 输入;另 一 研究员 给 定 这 四 项 输入 仍 不能 复现,那 这 个 结果 就 不可 复现,也 就 不 是 研究 结果。单 条 命令 通常 是 python run_experiment.py --config=<config-id> 或 make repro-<run-id>;四 项 输入 是 让 这 条 命令 在 其中 跑 的 那 个 确定 性 宇宙 的 配置。
经典 capstone 视角:本 课 是 capstone —— 它 把 L1 实验 日志、L2 测试集 上锁、L3 试验 计数器 与 DSR 串 起 来,打 包 进 让 整 个 栈 可 复现 的 工程 管 道 里。下 文 的 capstone 练习 围 绕 这 一 综合 设计。
Formula Explorer
\text{repro\_score} = w_1 \cdot \text{lock} + w_2 \cdot \text{snapshot} + w_3 \cdot \text{seed} + w_4 \cdot \text{sha}纪律 总结
四 项 经典 输入 都 能 从 单 条 命令 恢复 的 研究 项目 是 可 复现 的;输入 散 落 在 个人 笔记本、未 打 tag 的 共享 盘 文件夹、未 锁 版本 的 conda 环境 里 的 项目 不 是。策略 的 夏普比率、相对 基准 的 信息比率、净 值 曲线 的 最大回撤、部署 之后 的 Alpha 衰减、对 价值 / 质量 / 动量 因子 的 因子模型 归因、对 2007-2008 / 2015 股灾 / 2018 trade-war / 2020 疫情 / 2022 房地产 drawdown 的 压力测试、下游 4.4 的 均值方差优化、组合优化 —— 只 有 当 它们 都 绑 定 到 实验 日志 里 的 git_commit_sha 时,每 一 项 指标 才 可 信。没 有 绑 定 时,指标 是 记忆;有 绑 定 时,指标 是 研究 结果。
买 方 在 2015-2018 年 之间 完成 了 这 套 栈 的 采 用 —— 量化 私募 龙头(明汯、幻方 量化、中诚、灵均、九坤 投资)、公募 量化 部门(天弘 / 富国 / 华夏 / 嘉实 / 工银瑞信)以及 卖 方 系统化 桌 子(中信 系统化、华泰 QIS)。2018 年 López de Prado 《Advances in Financial Machine Learning》中文 版 与 2016 年 Harvey-Liu-Zhu 论文 设 定 了 学术 地板;AMAC 中国 证券 投资 基金业 协会 关于 私募 研究 日志 与 信息 披露 的 要求 设 定 了 监管 地板;量化 龙头 的 工程 严格 度 设 定 了 实战 地板。
练习
Exercise
你 正在 给 一 项 关于 510300 沪深 300 ETF 的 5 日 动量 信号 研究 创建 研究 PR。按 顺序 产出 四 项 工程 artefact 并 报告 答案。
(i) 写 一 份 完整 的 pyproject.toml 骨架,列 出 六 项 高 阶 依赖(numpy>=1.26, pandas>=2.2, scikit-learn>=1.4, matplotlib>=3.8, mlflow>=2.10, jupyter>=1.0)与 Python 版本 要求(>=3.11),用 一 句话 说 明 uv.lock 文件 做 了 pyproject.toml 不 做 的 什么 事。
(ii) 写 出 实验 日志 最 小 schema 的 SQL CREATE TABLE runs (...),含 九 列(run_id, timestamp, hyperparameters_json, seed, data_window, metric_name, metric_value, artefact_path, git_commit_sha)与 类型;用 一 句话 说 明 为 什么 git_commit_sha 列 是 不可 伪造 的 那 一 列。
(iii) 写 一 个 Python seed_all(seed: int) 函数,seed random、numpy、PYTHONHASHSEED 以及 可 用 时 的 torch;演示 用 seed=42 跑 两 次 后 再 算 np.random.rand(3) 返回 同 样 的 三 个 数。
(iv) 把 五 项 代码 评审 清单 应 用 到 一 个 假想 研究 PR 上:该 PR 声 称 在 510300 2022-2023 窗口 上 样本外 Sharpe 1.8、N=10 次 试验;对 五 项 检查 每 一 项 说 明 该 主 张 是 PASS 还 是 FAIL(假定 实验 日志 显示:测试集 触碰 1 次、10 次 run 全 部 记 录 种子、数据 窗口 与 预登记 一致、universe 来 自 4.1.1 L4、报告 DSR = 0.92 配 N=10);每 一 项 用 一 句话 论证 裁定。
把 全部 四 个 答案 作 为 一 套 打 包 artefact 报告。
提示
pyproject.toml 声 明 依赖 区间;uv.lock 冻 死 版本 + 哈希 闭 包,保 证 字节 一致 安 装。git_commit_sha 把 指标 与 代码 状态 绑 定,可 由 git checkout <sha> 重新 验证。提示
研究 栈 的 一 天 —— 各 层 如何 组 合
一 个 具体 的 日 常 研究 闭 环 走 法,把 经典 研究 栈 的 各 层 按 序 调 用。研究员 周一 开 一 条 新 特性 分支 —— research/momentum-510300-2024-q2。先 提交 预登记 文档:L1 的 六 字段 模板,填 入 阈值 规则 Sharpe > 1.0 and DSR > 0.95 → paper-trade for one quarter; otherwise abandon,试验 计数器 初 始化 为 1。pyproject.toml 与 uv.lock 同 一 个 commit 提交;下 一 个 人 uv pip sync 即 得 与 原 研究员 完全 相 同 的 numpy、pandas、scikit-learn、matplotlib、mlflow、jupyter 版本。
周二 是 2015-2021 窗口 上 的 样本内 探索。研究员 打 开 EDA notebook;data/test/ 分区 在 文件 系统 层 上锁,测试集 无法 被 意 外 打 开。每 试 一 个 变体,notebook 调 log_run(...) 在 experiments.db 里 插 一 行,含 种子、数据 窗口、指标 与 当前 git commit SHA。到 周五 下午,实验 日志 有 五十 行;预登记 时 计数器 为 1,现在 是 50;增量 是 L3 DSR 公式 看 到 的 N。
下 周 一 研究员 冻结 样本内 调参、破封 测试集 恰好 一 次、用 实验 日志 N=50 计算 样本外 Sharpe + DSR。结果:Sharpe 1.4、DSR 0.88 —— suggestive 不 strong。项目 启动 时 预登记 的 决策 规则 自动 触发:Sharpe > 1.0 → 影子 交易 一 季 度;DSR suggestive → 保守 仓 位。研究员 开 一 个 研究 PR 打 包 八 项 artefact:notebook、生产 脚本、实验 日志 转 储、预登记 文档、样本内 结果、单 次 样本外 结果、配 N=50 的 多重 检验 修正、写 入 报告。PR 模板 要求 五 项 评审 检查 全 部 勾 上;评审 一 确认 单 次 测试集 触碰;评审 二 确认 50 个 种子 全 记 录;评审 三 确认 窗口 与 预登记 一致。PR 周五 合 并;影子 交易 簿 周一 开 仓。
六 个 月 后 基金 经理 问 信号 在 2024 数据 上 是否 还 work。新 研究员 拉 出 合 并 PR,git checkout <sha> 到 合 并 commit,uv pip sync uv.lock,从 实验 日志 取 种子,把 脚本 指 向 新 的 2024 数据 快照,重 跑 生产 脚本。2024 Sharpe 落 在 0.6;信号 已 衰减。决策 规则 再 触 —— abandon。两 次 跑 都 可 从 单 条 命令 复现;两 次 跑 全 链 可 审计。这 是 六 层、八 artefact、SQLite schema、五 项 评审 检查 与 四 项 输入 组 合 起 来 的 产 出。少 任 一 件,六 个 月 后 的 跟 进 都 做 不 到。
参考 卡
本 课 装 配 的 组件,按 序:
- Inline-code listing —— 六 层 经典 研究 工具 栈。
- Inline-code listing —— 研究 PR 的 八 项 artefact。
- Fenced ```sql code block —— 实验 日志 最 小 schema(含 九 列 的
runs表)。 - Inline-code listing —— 五 项 代码 评审 清单。
- Fenced ```python code block ——
seed_all(seed: int)。 - Fenced ```python code block ——
log_run(run_id, hyperparameters, seed, data_window, metric_name, metric_value, artefact_path)。 - Fenced ```yaml code block ——
pyproject.toml骨架 与 六 项 依赖。 - Inline-code listing —— 复现 一 个 结果 的 四 项 经典 输入。
- Exercise —— 四 项 研究 PR artefact 综合 练习,配 两 条 渐进 Hint。
- FormulaExplorer —— 复现 评分 组 合 公式。
模块 结尾
模块 完结。四 节 课 合 成 一 套 持 久 纪律。L1 把 工作流 写 进 书面 合同 —— 七 阶段、六 字段 预登记、四 项 artefact、四 个 诊断 问题。L2 把 数据 纪律 机制 讲 精确 —— 三 分区、四 切 法、四 泄漏 模式、五 项 泄漏 检测 清单。L3 把 试验 计数器 转 成 通缩 概率 —— Bailey-Lopez de Prado 公式、三 项 修正、DSR 阈值 分 层、五 种 p-hacking 形 式。L4 把 上 述 三 课 包 进 工程 —— 六 层 栈、八 项 PR artefact、含 git_commit_sha 的 SQLite schema、五 项 代码 评审 清单、四 项 复现 输入。同 时 尊重 四 课 的 研究 项目 产 出 队友 可 从 单 条 命令 重 跑 的 结果,配 试验 计数器、配 DSR、配 书面 假设。少 任 一 课 的 项目 产 出 一 个 记忆。下 一 个 模块 4.2.2(信号 构 建)讲 的 是 建 什么 信号;本 模块 讲 的 是 怎么 诚 实 地 测试 信号,让 买 方 信任 你 发 货 的 东西。