第 38 章/ 45
金融机器学习管线(Financial ML Pipeline)
主线:把“特征在何时可知、标签覆盖哪段收益、模型在何时拟合”写成一份契约,再让划分、预处理、调参和评价都服从这份契约。 先修:第23章截面研究(IC、分组、面板)、第31章模型验证(标签区间、purging、embargo、walk-forward)、第37章。完成后你能:为一个日频面板写出每行的 signal_time、trade_time 与标签区间;在给定拟合时点手算哪些训练行可用;用训练内 Pipeline 完成预处理和调参;解释为什么随机打乱的 K 折会把纯噪声特征评成“有效”。
1. 本章目标
- 为面板的每一行登记六个时点:特征可用时点 available_at、signal_time、trade_time、label_start、label_end、label_available_at,并据此手算标签。
- 证明:在拟合时点
,信号日 的行可用于训练当且仅当 ;无缺失时被排除的是测试前最后 个信号日。说明它与“按区间重叠 purging”的差别。 - 用日期分组的 walk-forward 划分面板:同一日期的全部资产行在同一侧。
- 区分只用当日截面信息的变换(无需训练拟合)与跨日期拟合的变换(必须只在训练行上拟合);用
Pipeline实现后者。 - 把超参数选择放在外层训练集内部,并说明内层折为何也要服从同一可用时点规则。
- 分开报告预测层(
、IC、校准斜率)与组合层(成本后收益,第39章与 Project6)。 - 用三组可运行反例量化错误做法的代价:随机打乱 K 折、全样本特征选择、把标签起点设在信号收盘。
完成条件:第15–17节可独立完成;能追溯第8–10节冻结输入与输出,并解释第9节新运行与冻结回放的身份边界;本批不运行或重训。
2. 为什么 Quant 需要它
一个研究员拿来下面的结果:40 只股票、5 年日频数据、22 个特征,用 5 折交叉验证得到样本外
错误往往不在模型,而在时间:特征什么时候能拿到,标签覆盖哪段时间,模型在什么时点拟合。本章把这些时间写成契约,然后让每一步都检查契约。第31章已经给出单序列的标签区间、purging 和 walk-forward;本章处理面板(多资产同日)和完整的预处理—调参—评价链条,并把实现放进 quant_course.ml_pipeline,供第39章和 Project6 共用。
3. 直觉
想象你在 3 月 2 日(周一)收盘后建模。你手里有到周一收盘为止的全部价格。你要预测“明天收盘买入、持有 2 天”的收益。
- 上周三(
)的样本:它的标签是从周四收盘到下周一收盘的收益。周一收盘你已经看到了,这一行可以训练。 - 上周四的样本:标签从周五收盘到本周二收盘,周二还没到,不能训练。
- 上周五的样本:标签从周一收盘到周三收盘,同样不能训练。
所以,即使这些样本的信号日都在你建模之前,它们的标签还没有发生完,不能进入训练。把它们放进去,就是让模型看见了未来收益。被排除的日期数由“执行滞后 + 持有期”决定,不是凭感觉设一个间隔。
面板还多一层:同一天有 40 只股票。它们的特征同时可得、标签覆盖同一段时间。如果把 3 月 2 日的 20 只股票放进训练、另 20 只放进测试,测试时模型已经从同一天的训练股票里学到了“3 月 2 日之后大盘涨了”。所以划分的单位是日期,不是行。
4. 数学基础
| 符号 | 含义 | 单位/维度 |
|---|---|---|
| 交易日,每个日期代表该日收盘 | 日期 | |
| 资产 | — | |
| 执行滞后:信号收盘到成交收盘相隔的交易日数 | 交易日 | |
| 预测区间(持有期) | 交易日 | |
| 资产 |
各特征自身单位 | |
| 收盘价 | 货币/股 | |
| 标签:从成交收盘起 |
小数 | |
| 标签区间(左开右闭) | — | |
| 拟合时点的日期下标:模型在 |
— |
时间约定:signal_time
日期与精确决策时刻分开。 signal_time 记的是信号日期
一天之内的时钟:先取快照,再拟合,再发信号
以下为独立教学构造,采用交易所当地时区,除末行的下一交易日成交外,盘内与盘后时点均属于交易日
| 时点 | 发生的事 | 本次拟合或信号如何处理 |
|---|---|---|
| 16:00 | 收盘;某历史训练行的标签区间刚结束 | 尚须等待标签入库,结束不等于可读 |
| 16:08 | 当日价格派生特征入库 | 可用于16:10信号;16:09拟合快照也已可读 |
| 16:09 | fit_time:锁定训练快照并开始拟合 | 只用此前已可读的历史特征和成熟标签 |
| 16:09:40 | 拟合完成 | 赶上信号截点;其后到达数据不追加进已拟合模型 |
| 16:10 | 用已拟合变换处理16:08已入库的当日特征 | |
| 16:20 | 上述16:00结束的标签入库 | 晚于fit_time,不能进入这次训练 |
| 16:30 | 财报发布(假定同时入库) | 晚于 |
| 本例 |
标签从这一成交收盘起算,不从16:10起算 |
本次训练行是
三个检查各守一扇门:训练数据可读时点不晚于 fit_time,预测特征可读时点不晚于
用到的先修:第31章的区间重叠条件
5. 正式定义
特征(feature)与特征可用时点(available_at)。 特征是信号日可以计算的量
特征工程(feature engineering)。 从原始数据构造特征的过程。按信息来源分两类:
- 截面变换(cross-sectional transform):只用同一信号日的数据,如当日截面排名、当日截面 z-score、当日行业去均值(第23、28章)。它在每个日期独立计算,不需要“训练拟合”,也不会把别的日期的信息带进来。
- 跨日期变换(pooled / time-series transform):参数由多个日期的数据估计,如全样本均值方差标准化、缺失值用全样本中位数填补、PCA、特征选择、分位数截断阈值。它的参数必须只用训练行估计。
标签(label)与预测区间(prediction horizon)。 标签
标签可用时点(label_available_at)。 标签值第一次可被训练程序读取的时点,在本章日粒度构造中假定与 label_end 同一收盘可得;实际系统须登记入库时刻,不能默认相等。它与 signal_time 是两回事:信号日早,不代表标签已知。
截面面板(cross-sectional panel)。 以 (signal_time, asset) 为主键的长表。同一 signal_time 的行共享同一组时点。
拟合时点(fit_time)与可用训练集。 外层折在
按日期分组的 walk-forward(date-grouped walk-forward)。 把唯一日期排序,测试块是连续的若干日期
Purging(按标签区间排除)。 第31章的 purging 删除与测试标签区间重叠的训练行。本章把规则写成可用时点
Gap 与 embargo。 gap 是训练末尾与测试开始之间额外空出的日期;embargo 是在训练行位于测试块之后时(K 折),删除紧接测试块之后、特征回看窗口会覆盖测试标签区间的训练行。二者都应由数据的实际时间结构推出:gap 由标签区间与可用时点推出,embargo 长度由特征回看窗口推出,不能用“统一空 5 天”代替(6.2 节)。
预处理(preprocessing)与管线(Pipeline)。 预处理是拟合模型前对特征做的跨日期变换。sklearn.pipeline.Pipeline 把若干变换和一个估计器串成一个对象,fit 时各步依次只在传入的训练数据上拟合,predict 时用已拟合的参数变换新数据(官方文档 SK_PIPE_DOC)。
超参数选择(hyperparameter selection)。 选择正则强度、特征个数等不由训练直接估计的量。规则:只用外层训练行;在外层训练行内部再做日期 walk-forward(内层折),内层同样服从可用时点规则;选好后用全部外层训练行重拟合,再预测外层测试块。外层测试标签不进入任何拟合、标准化或选择。
评价(evaluation)。 分两层。预测层:
模型校准(model calibration)。 预测值与实现值在数量上是否一致。回归预测的校准斜率 SK_CALIBRATION)。
6. 推导
6.1 可用时点规则、排除日期数,以及它与重叠 purging 的关系
假设:日期等间隔编号;无缺失;
第一步,可用条件。信号日
第二步,排除日期数。候选训练日期为
个日期,乘以资产数
第三步,可用性蕴含不重叠。测试行最早的标签区间是
第四步,只做重叠 purging 不够。训练行
用途:实现时按每行的 label_available_at 过滤,排除数量自然由区间决定;若数据有缺失日或不同资产的标签区间不同,规则依然逐行成立,而“固定空 ml_pipeline.date_walk_forward 按此实现,并把“训练区间与测试区间不得重叠”作为防御断言。
一个更保守的约定:若规定标签必须在拟合时点之前的收盘就已入库(label_available_at
6.2 gap 与 embargo 何时需要、该多长
walk-forward(训练全在测试之前):6.1 节的可用时点规则已经处理了标签跨界,不需要额外 embargo。若你的系统规定“拟合后至少隔
sklearn 的 TimeSeriesSplit(gap=g):gap 是在训练集末尾与测试集之间排除的样本个数(SK_TSS)。在面板上,样本是行而不是日期:每天 40 行时,gap=5 只空出八分之一天。即使改成 gap=5*40,只要某天缺一只股票,行数与日期就错位。所以面板上用日期规则,不用行数 gap。
purged K 折(训练块可以在测试块之后):先按真实标签区间purge,未来训练行也可能与测试标签重叠,不能因其信号日在后就跳过。再检查剩余未来训练行的特征窗口。若特征为过去
第31章的窗口规则须用其明示坐标和半开端点。相对最后标签终点 rev_5对应5日,mom_60_5的最早价格依赖到60日前,可取60日保守协议;EMA无精确有限回看终点,季度变量也不能简单等同有限收益窗口。walk-forward没有测试之后的训练行,标签成熟规则已处理过去端跨界,无此未来端embargo。
本章CLI的日期块诊断实际传入信号日期ordinal与label_end,purge的是保守的(signal_date,label_end]包络,而非本节真实(label_start,label_end];embargo参数默认0。其数值表是离线诊断,不代表完整特征隔离或部署评估;date_walk_forward另按真实标签区间与成熟时点过滤。
6.3 训练内标准化:何时影响结果,何时不影响
命题(三个层次):带截距 OLS,训练设计矩阵
- 训练拟合值总不变:无论
是否满列秩,两种设计下的 OLS 拟合值相同。 - 总存在对应的系数表示:任一组标准化系数
都可写成原坐标系数 、 ,两者对任何新点给出相同的预测。 - 对新点的预测:满列秩时不变,秩亏时不保证:若
满列秩( ,因此 ,且没有任何特征是其他特征与常数项的精确线性组合),OLS 解唯一,两种设计的拟合结果互相对应,对任何新点 的预测相同。若 秩亏,训练数据不能确定系数在零空间方向上的分量,软件默认的解(scikit-learn LinearRegression先按训练均值中心化处理截距,再返回斜率的最小范数解)在两种坐标下取了不同的“最小范数”,对离开训练数据线性关系的新点,预测一般不同;新点满足训练中的同一线性关系时可保证相同;其他新点也可能偶然得到相同预测,但不能普遍保证。
读这三个层次时,区分“我把同一组系数换了坐标”与“软件重新求解”。第2层是代数恒等式,对任意新点都成立;第3层问重新拟合后软件是否必然选中这组对应系数。满列秩保证唯一性,秩亏则只保证训练投影唯一。这里满列秩的是含截距的增广矩阵
证明:(1) LinearRegression 的最小范数解在原坐标下最小化斜率
秩亏反例(教学构造):训练
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X = np.array([[1.0, 2.0], [2.0, 4.0], [3.0, 6.0]]); y = np.array([1.0, 2.0, 3.0]); new = np.array([[1.0, 5.0]])
raw = LinearRegression().fit(X, y)
std = make_pipeline(StandardScaler(), LinearRegression()).fit(X, y)
print(np.round(raw.predict(X), 6), np.round(std.predict(X), 6)) # identical training fit
print(round(raw.predict(new)[0], 4), round(std.predict(new)[0], 4)) # 2.2 1.75
两个模型的训练拟合值都是
所以在满列秩时,“用全样本均值方差标准化”对 OLS 的预测没有影响;秩亏时连 OLS 也可能受影响(上例)。标准化通常可能影响下面几类方法:
- Ridge、Lasso:惩罚
依赖特征尺度, 改变可能使解改变(第39章)。 - kNN、聚类:距离依赖尺度。
- 阈值规则、截断、缺失填补、特征选择:参数本身由数据估计,全样本估计直接使用了测试期信息。
在本章构造案例中,特征选择产生了显著泄漏;不同预处理的泄漏大小依数据与设计而变,不能由此作普遍排名,第8.3节的数值显示:从 200 个纯噪声特征里按全样本相关性挑 10 个,walk-forward 的
6.4 随机打乱 K 折为何把噪声评成信号
设定:特征为 AR(1) 慢变量,日相关
随机打乱后,测试行
线性模型(Ridge)不能“查表”,同一数据上打乱 K 折给出约
6.5 预测误差的“水平—截面”分解与校准
假设:每个日期
推导:
金融含义:第一项是“猜全市场这周涨多少”的误差,第二项是“猜谁比谁好”的误差。美元中性组合(权重和为 0)的收益只取决于第二项对应的信息;Spearman IC只衡量去均值后不变的秩;截面SSE和截面
校准:池化的校准斜率
7. 金融解释
时间契约就是风控。 回测里多用了一天未来收益,实盘里没有那一天。把每个时点写进数据字典,等于把“我在
为什么按日期分组。 同一天的股票受同一个市场冲击。随机拆分同一天的行,模型从训练股票里学到当天的市场方向,再在测试股票上“预测”出来;部署时你在信号日收盘拿不到这一信息。
水平与截面。 很多股票选择策略是美元中性的:权重和为0使共同水平项在
调参不稳定是信息。 第8.4节的岭回归在 12 个外层折里选出的正则强度从 inner_scores 的MSE曲线与稳定性;参数跳动本身不能单独证明曲线平坦或唯一成因。把它当成“模型很稳”是误读;它提示你看重 IC 的稳定性和组合层结果,而不是某个具体超参数。
校准与交易阈值。 若交易规则是“预测收益超过单边成本才交易”,正斜率但幅度过大的预测(
8. Numerical Example
8.1 手算:3 资产 × 12 日, 、
(教学构造。)收盘价:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| A | 100 | 101 | 103 | 102 | 104 | 106 | 105 | 107 | 110 | 108 | 111 | 112 |
| B | 50 | 50.5 | 50 | 49.5 | 49 | 49.5 | 50 | 51 | 51.5 | 52 | 51 | 52.5 |
| C | 20 | 20 | 20.2 | 20.4 | 20.2 | 20 | 20.4 | 20.6 | 20.6 | 20.8 | 21 | 20.8 |
标签
| 信号日 |
label_end | |||
|---|---|---|---|---|
| 1 | 0.009901 | −0.019802 | 0.020000 | |
| 2 | 0.009709 | −0.020000 | 0.000000 | |
| 3 | 0.039216 | 0.000000 | −0.019608 | |
| 4 | 0.009615 | 0.020408 | 0.009901 | |
| 5 | 0.009434 | 0.030303 | 0.030000 | |
| 6 | 0.047619 | 0.030000 | 0.009804 | |
| 7 | 0.009346 | 0.019608 | 0.009709 | |
| 8 | 0.009091 | −0.009709 | 0.019417 | |
| 9 | 0.037037 | 0.009615 | 0.000000 | |
| 10–12 | 超出样本 | 缺失 | 缺失 | 缺失 |
一个折:训练日期
8.2 随机 K 折、日期块 K 折与 walk-forward(纯噪声)
构造面板:40 只资产、1,260 个交易日、22 个慢变特征(AR(1),
| 方法 | Ridge( |
kNN(5 近邻) |
|---|---|---|
| 随机打乱 5 折(错误) | +0.05% | +56.67% |
| 日期块 purged 5 折(第31章,诊断用) | −0.73% | −43.31% |
| 日期 walk-forward(11 折,部署口径) | −1.65% | −40.73% |
日期块 K 折去掉了随机打乱带来的“同股相邻日查表”,但训练块可能在测试块之后,仍不是部署口径;walk-forward 才对应“只用过去”。纯噪声特征下,后两者都为负:模型在拟合噪声,样本外比零预测差。
8.3 全样本特征选择(纯噪声)
30 只资产、756 日、200 个纯噪声特征,选 10 个,Ridge,日期 walk-forward(种子 3802):
| 做法 | 平均逐日 Spearman IC | |
|---|---|---|
| 全样本按 |
+0.617% | +0.0476 |
Pipeline 内训练集选择(SelectKBest) |
−1.214% | −0.0101 |
错误做法在纯噪声上做出了“IC 约 0.05”,量级正好落在第37章所说的“真实但微弱的信号”范围,最难被肉眼识破。
8.4 完整面板案例
构造面板:40 只资产、1,260 日;两条真实特征 rev_5、mom_60_5、vol_20);
- 每个外层折排除 200 行
,与 6.1 节一致;首个外层折因 mom_60_5需要 60 日历史,丢弃 2,400 行缺失()。 - 选出的
: 。 - 预测层(30,000 个测试行、750 个日期):
相对零 ,相对训练均值 ;截面(逐日去均值) ;逐日 Spearman IC 均值 0.0255,Newey–West (滞后 4);方向命中率 49.7%。 - 校准:池化斜率 0.36(水平预测过度分散),截面斜率 1.18(截面预测略保守)。按日期内预测十分位分组,最高组减最低组的平均去均值 5 日收益约
,中间各组不单调。
用 6.5 节分解读这些数字:水平误差使池化
9. Python Implementation
核心模块:src/quant_course/ml_pipeline.py(第39章、Project6 共用)。主要接口:
make_forward_labels(close, horizon, execution_lag=1):返回 (signal_time, asset) 长表,含 trade_time、label_start、label_end、label_available_at、y;样本末尾标签为缺失。date_walk_forward(signal_time, label_start, label_end, label_available_at, *, min_train_dates, test_dates, step=None, expanding=True, max_train_dates=None):日期分组的 walk-forward,训练行满足 label_available_atfit_time;记录 n_excluded_unavailable;它不接收真实供应商的特征available_at,特征PIT版本与精确fit快照仍由调用者保证;训练与测试区间重叠时抛RuntimeError。fit_select_predict(...)/walk_forward_predict(...):StandardScaler+ 线性估计器的 Pipeline;内层日期 walk-forward 选参(平均验证 MSE 最小,平局取网格中靠前者);zero、train_mean两个基准。prediction_metrics(labels, pred, horizon, train_mean_pred=None):(相对零与训练均值)、IC(复用第23章 ic_series/ic_summary,nw_lags=h-1)、命中率。simulate_return_panel、price_features、stack_features:构造面板与特征(教学用)。
缺失值策略:训练行特征或标签缺失则丢弃并计数;测试行特征缺失则不预测并计数;出现 inf 直接报错。
章节脚本:code/ch38_ml_pipeline.py。历史工作脚本的默认图路径可能覆盖章图,不应直接运行来验证冻结数字。接受I3读者Notebook为fresh构造计算,写入唯一运行目录并生成一张标签时间线图;其英文语言层留BIL23。本批未执行CLI、Notebook或课程API。
种子 3801–3803;输出 ch38_results.json(拒绝覆盖)和三张图。第8.2、8.3节的“错误”做法只用于度量偏差,代码中标注 WRONG,从不用于选择模型。测试:tests/test_ml_pipeline.py(22 项,含逐行边界、未来扰动、测试标签扰动、缺失值和与第29章账本联用)。
手算例子可以这样复现:
import pandas as pd
from quant_course import ml_pipeline as mp
close = pd.DataFrame({
"A": [100, 101, 103, 102, 104, 106, 105, 107, 110, 108, 111, 112],
"B": [50, 50.5, 50, 49.5, 49, 49.5, 50, 51, 51.5, 52, 51, 52.5],
"C": [20, 20, 20.2, 20.4, 20.2, 20, 20.4, 20.6, 20.6, 20.8, 21, 20.8],
}, index=pd.bdate_range("2026-03-02", periods=12), dtype=float)
labels = mp.make_forward_labels(close, horizon=2, execution_lag=1)
fold = mp.date_walk_forward(labels.index.get_level_values(0), labels["label_start"], labels["label_end"],
labels["label_available_at"], min_train_dates=6, test_dates=3)[0]
used = sorted({close.index.get_loc(d) + 1 for d in labels.index.get_level_values(0)[fold.train_rows]})
print(round(labels.loc[(close.index[0], "A"), "y"], 6), used, fold.n_excluded_unavailable)
预期输出:0.009901 [1, 2, 3, 4] 6。
10. Visualization

图38-1:横轴为交易日下标

图38-2:横轴为三种验证方法,纵轴为相对零预测的样本外

图38-3:左图横轴为池化十分位的平均预测(%,5 日),纵轴为该组平均实现 5 日收益(%),虚线为 45 度线;右图横轴为每个日期内按预测分的十分位(每组 4 只资产),纵轴为该组平均的日期去均值 5 日收益(%)。观察点:左图的点与 45 度线相去甚远(水平预测不可靠),右图最高组与最低组之差约 0.43%,但中间组不单调。构造面板,种子 3803。
11. Real Market Example
本章没有真实市场的实测结果。Project6 的真实数据状态为 blocked:课程尚无可用于内部培训的、经许可的日频个股价格面板(见 projects/project06/data_manifest.json 的字段与授权要求)。获得数据后,本章管线在真实数据上需要额外处理三件事,构造数据里没有出现:
- 公司行为:标签用复权收益,成交与账本用未复权价格加分红拆股事件(第29、30章),两者不能混用。
- 上市退市与股票池:股票池按信号日的成分冻结,退市收益进入标签(第30章生存偏差)。
- 非同步数据:财务特征按公告时点入库(第12章),available_at 可能晚于报告期数月。
真实结果出来后与本章构造结果分开报告,构造结果不作为证据。
12. Common Mistakes
12.1 在面板上用行数 gap
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
n_dates, n_assets, h = 30, 4, 5
rows = np.repeat(np.arange(n_dates), n_assets) # date id of each panel row
tr, te = list(TimeSeriesSplit(n_splits=3, gap=h).split(rows))[0]
last_d, first_d = rows[tr[-1]], rows[te[0]]
print("train rows on its last date:", int((rows[tr] == last_d).sum()), "of", n_assets)
print("date gap between train and test:", first_d - last_d) # WRONG: 1 date, contract needs lag+h-1 = 5
预期输出:train rows on its last date: 1 of 4 与 date gap between train and test: 1。原因:gap 数的是行(本机 scikit-learn 1.9.1 文档:从每个训练集末尾排除的样本数)。本例训练末行24属于日期6,首测试行30属于日期7,日期编号差为1;成熟条件要求最近训练信号日比首测试日至少早6日,即排除其间5个完整信号日期(ml_pipeline.date_walk_forward,按日期分组并按 label_available_at 过滤。
12.2 全样本特征选择或全样本填补
错误:先 SelectKBest(...).fit(X_all, y_all) 再做 walk-forward。数值代价见第8.3节:纯噪声得到 IC 0.048。修复:把选择放进 Pipeline,每个训练折重新拟合。
12.3 随机打乱 K 折
错误:KFold(5, shuffle=True) 用于重叠标签的面板。代价见第8.2节(kNN +56.7%)。修复:日期 walk-forward;只在明确为诊断目的时使用日期块 purged K 折并配合 embargo(6.2 节)。
12.4 标签从信号收盘起算
import pandas as pd
close = pd.Series([20.0, 20.0, 20.2, 20.4, 20.2, 20.0])
k, lag, h = 2, 1, 2 # signal at index 2 (k=3 in 1-based terms)
wrong = close[k + h] / close[k] - 1 # WRONG: starts at the signal close
right = close[k + lag + h] / close[k + lag] - 1 # starts at the trade close
print(round(wrong, 6), round(right, 6)) # 0.0 -0.019608
原因:收盘信号不能在同一收盘成交(第29章 lag>=1)。把标签起点设在信号收盘,标签就包含了你无法交易的那一天收益。修复:make_forward_labels(..., execution_lag=1)。
12.5 把 5 日标签当逐日持有收益
错误:每天按 5 日标签预测调仓,再把 5 日标签的平均当成每日组合收益。原因:相邻 5 日标签重叠 4 天,每天调仓的组合实际持有 1 天。修复:组合层用逐日价格计 NAV(第29章账本),调仓频率与标签区间分开声明;Project6 每 5 个信号日调仓一次,NAV 逐日记账。
12.6 在测试集上校准或调参
错误:在测试期估计校准斜率,再用它评价同一测试期。修复:校准系数与超参数一样,只在内层验证集上估计。tests/test_ml_pipeline.py 中“把测试标签改成荒谬值、预测不变”的测试守住这一点。
13. Quant Research Application
把本章写成 Project6 的管线规格(构造数据只验证规格本身):
- 数据字典:每个字段的 event_time、available_at、单位与缺失规则;signal_time、trade_time、label 区间、label_available_at、频率(
projects/project06/data_dictionary.md)。 - 标签:
, ,从成交收盘起算的简单收益。 - 划分:日期分组的扩张窗 walk-forward;首个训练窗、测试块长度事前冻结;可用时点规则自动排除
个日期。 - 预处理:截面变换在当日计算;跨日期变换全部放进 Pipeline。
- 调参:外层训练内的日期 walk-forward,网格事前冻结并记录每折选择。
- 评价:预测层报告
(两种基准)、截面 、IC 与 NW 、校准斜率;组合层按明示规则(截面去均值、毛暴露 1、每 5 日调仓、 执行、比例成本)计成本后收益、换手和回撤。 - 基准:零预测、训练均值(截面上全同,组合空仓)。
- 反证条件:截面 IC 的 NW
不显著;成本后收益不为正;结果依赖某一折。
14. Chapter Summary
信号日早于拟合时点,不等于标签已知;面板上的每一行都要按 label_available_at
15. Exercises
38-E01(可用时点)
按本书默认截点
38-E02(手算标签与可用行)
用第8.1节价格,
38-E03(排除日期数)
分别对
38-E04(embargo)
某研究用日期块 purged K 折,特征含 rev_5(回看 5 日)与 mom_60_5(回看 60 日),标签
38-E05(标准化不变性及其条件)
(a) 证明:带截距 OLS 的训练拟合值对仿射标准化不变;若训练设计
38-E06(随机 K 折的查表机制)
特征为 AR(1),
38-E07(嵌套调参的行)
外层折拟合时点
38-E08(水平—截面分解)
两个日期各两只资产:日期1
38-T01(新增:成熟、快照与信号)
沿用第4节精确时间线。(a) 历史行A的标签15:55入库,历史行B的标签16:20入库,两行特征均已于前一日入库;哪行可用于16:09拟合?(b) 当日特征16:08入库、财报16:30入库,哪项可用于16:10信号?(c) 若拟合16:12完成,如何处理16:10的信号?(d) 为何不能只检查 label_end 的日期?
16. Programming Exercises
38-P01(标签与时点)
用 make_forward_labels 复现第8.1节表格。验收:(1) 9 个信号日 × 3 资产的 y 与表格一致(容差 y 为缺失;(3) execution_lag=0、horizon=True 抛 ValueError;(4) 把价格中任一值改为 0 或 inf 抛错。
38-P02(Pipeline 内的特征选择)
在 simulate_return_panel(n_noise=200, signal_loading=0) 上比较全样本选择与 Pipeline 内选择。验收:两者使用同一组 date_walk_forward 折;输出两种做法的
38-P03(行数 gap 的反例)
构造一个不平衡面板(某些日期缺 1–2 只股票),比较 TimeSeriesSplit(gap=h*N) 与 date_walk_forward 的训练末尾日期和测试开始日期。验收:找到至少一个折,TimeSeriesSplit 让同一日期出现在训练和测试两侧,或训练末行的 label_end 晚于测试首个信号日。
17. Challenge Problems
38-C01(purged K 折的 embargo 推导)
设特征最长回看窗口为
38-C02(校准与交易阈值)
内层验证集上截面校准斜率
18. Solutions / Solution Hints
38-E01
默认截点
38-E02
(a) B,
38-E03
可用时点规则排除
38-E04
先purge标签重叠;再相对最后测试label_end采用最长60日特征回看的保守embargo协议。若严格按半开收益增量区间计,删除至
38-E05
(a) 证明见 6.3 节:列空间相同推出训练拟合值相同(不需满秩);满列秩时 OLS 解唯一,系数对应关系推出新点预测相同。(b) 6.3 节秩亏例:LinearRegression 预测 2.20、StandardScaler 后预测 1.75。原因:解集为
import numpy as np
from sklearn.linear_model import Ridge
x = np.array([[1.0], [2.0], [3.0], [4.0]]); y = np.array([1.0, 2.0, 3.0, 5.0])
p1 = Ridge(alpha=1.0).fit(x, y).predict([[5.0]])[0]
p2 = Ridge(alpha=1.0).fit(10 * x, y).predict([[50.0]])[0]
print(round(p1, 4), round(p2, 4)) # 5.4583 5.9935 -> different
38-E06
38-E07
外层训练日期 date_walk_forward 在 700 日单资产序列上可以复现这些下标。
38-E08
总误差:日期1
38-P01
完整参考方法:用第9节的3×12价格创建 labels;对每只资产及 close.iloc[k+2]/close.iloc[k]-1(此处Python的 iloc[k]即成交日),与按信号位置 k-1 取出的标签比较;末3日检查缺失。更直接的零基断言如下:
for j in range(9):
for asset in close:
got = labels.loc[(close.index[j], asset)]
expected = close[asset].iloc[j+3] / close[asset].iloc[j+1] - 1
assert abs(got['y'] - expected) < 1e-6
assert got['label_start'] == close.index[j+1]
assert got['label_end'] == close.index[j+3]
assert labels.loc[(close.index[9:], slice(None)), 'y'].isna().all()
再分别调用 execution_lag=0、horizon=True,以及价格单元改0/inf的副本,用 try/except ValueError 核对拒绝输入;若没报错就显式 raise AssertionError。这些是读者参考方法,本批未运行课程API;既有测试位置为 tests/test_ml_pipeline.py::test_01_hand_computed_labels。
38-P02
参考实现:code/ch38_ml_pipeline.py::selection_leak;本书运行全样本选择 X_all,y_all 上拟合 SelectKBest,再逐折训练;正确支路每折建立 Pipeline([StandardScaler, SelectKBest, Ridge]) 并仅向fit传入训练行。两支路都把预测写回原索引,最后在同一有效评价集上算指标。扰动检查须固定原折、特征与内层规则,仅替换该折外层测试标签:
changed = labels.copy()
changed.iloc[fold.test_rows, changed.columns.get_loc('y')] = 5.0
# fitted transformations and selection use fold.train_rows only
p1 = fit_training_only_pipeline(X, labels, fold).predict(X.iloc[fold.test_rows])
p2 = fit_training_only_pipeline(X, changed, fold).predict(X.iloc[fold.test_rows])
np.testing.assert_array_equal(p1, p2)
这里 fit_training_only_pipeline 表示上述每折训练内选择函数,内部只读取 X.iloc[fold.train_rows] 与对应非缺失训练标签;不能把测试标签用于选k或调参。已有完整工程参考为 selection_leak 和 tests/test_ml_pipeline.py::test_host_predictions_do_not_depend_on_test_labels;本批未重算模拟。
38-P03
承接I3已应用校订:用 rows=np.repeat(np.arange(30),4)[1:] 删去date0一行,得到119行;TimeSeriesSplit(n_splits=3,gap=20) 首折末训练行11对应原日期3,首测试行32对应原日期8。
rows = np.repeat(np.arange(30), 4)[1:]
tr, te = next(TimeSeriesSplit(n_splits=3, gap=20).split(rows))
assert len(rows) == 119
assert (tr[-1], te[0], rows[tr[-1]], rows[te[0]]) == (11, 32, 3, 8)
assert rows[tr[-1]] + 1 + 5 > rows[te[0]]
日期版本先按唯一日期成块,再逐行用 label_available_at <= fit_time 过滤;核对训练/测试日期不交、全部保留标签已成熟及训练与当前测试跨集合的真实标签区间无重叠(各集合内部标签允许重叠)。参考接口见第9节;不要用行gap推断成熟。
38-C01 参考论证
先purge真实标签重叠。剩余未来训练行的有限收益增量支持为
38-C02 评分准则
(a)
38-T01 完整解答
(a) A满足15:55≤16:09,且历史特征可读,可进入训练;B的16:20晚于16:09,不可进入,即使其标签16:00已结束。(b) 16:08特征满足 available_at≤16:10,可用;16:30财报不可用。当日特征只用于预测,模型的训练参数仍来自16:09锁定的历史训练行。(c) 16:12晚于截点,不能称其生成了16:10信号;依事前协议延后信号并重写成交约定,或采用已完成的旧模型。两者不能在看结果后择优。(d) label_end说明收益区间结束,label_available_at说明程序第一次能读取标签;供应商延迟使两者分离。日粒度的
19. Further Reading
- Must Know:第31章(标签区间、purging、embargo、walk-forward、嵌套选择),本章在其单序列版本上增加面板与可用时点。
- Must Know:scikit-learn《Common pitfalls》(
SK_PITFALLS)的泄漏与 Pipeline 部分;Pipeline与GridSearchCV用户指南(SK_PIPE_DOC、SK_GRID_DOC),读 fit 只作用于训练数据的说明。 - Should Know:Cawley, Talbot(2010)关于模型选择过拟合与选择偏差(
CH38_CT2010),读摘要与第5节,理解为什么调参与评价要分开。 - Should Know:López de Prado(2018)第7章(purged K 折与 embargo,
CH38_PRADO2018);作者只核对了出版商目录,未读正文。 - Optional:scikit-learn 概率校准指南(
SK_CALIBRATION),分类概率的可靠性图。
20. References
SK_TSS:scikit-learnTimeSeriesSplit。支持 6.2 节gap按样本计数的说明(侦察报告读 1.9.1 docstring;作者在本机 1.9.1 中核对gap参数说明,见 source_notes)。SK_PITFALLS:scikit-learn, Common pitfalls and recommended practices。支持第5节预处理泄漏、第12.2节。SK_PIPE_DOC、SK_GRID_DOC:scikit-learn Pipeline/组合估计器与超参数搜索用户指南(1.9.1)。支持第5节 Pipeline 定义。SK_CALIBRATION:scikit-learn Probability calibration 用户指南。支持第5节可靠性图的分类版本。CH38_CT2010:Cawley, G. C., Talbot, N. L. C. (2010). On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation. JMLR 11, 2079–2107。支持第5节“调参与评价分开”。CH38_PRADO2018:López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. 第7章目录。只作延伸阅读。- 第31章
quant_course.validation(intervals_overlap、purged_kfold_splits、embargo)与第23章quant_course.factor_research(ic_series、ic_summary)。核读记录见course/zh/chapters/ch38/source_notes.md。