QuantWeft量化经纬
English
本章小节

第 38 章/ 45

金融机器学习管线(Financial ML Pipeline)

主线:把“特征在何时可知、标签覆盖哪段收益、模型在何时拟合”写成一份契约,再让划分、预处理、调参和评价都服从这份契约。 先修:第23章截面研究(IC、分组、面板)、第31章模型验证(标签区间、purging、embargo、walk-forward)、第37章。完成后你能:为一个日频面板写出每行的 signal_time、trade_time 与标签区间;在给定拟合时点手算哪些训练行可用;用训练内 Pipeline 完成预处理和调参;解释为什么随机打乱的 K 折会把纯噪声特征评成“有效”。

1. 本章目标

  1. 为面板的每一行登记六个时点:特征可用时点 available_at、signal_time、trade_time、label_start、label_end、label_available_at,并据此手算标签。
  2. 证明:在拟合时点 ss,信号日 kk 的行可用于训练当且仅当 k+ℓ+h≤sk+\ell+h\le s;无缺失时被排除的是测试前最后 ℓ+h−1\ell+h-1 个信号日。说明它与“按区间重叠 purging”的差别。
  3. 用日期分组的 walk-forward 划分面板:同一日期的全部资产行在同一侧。
  4. 区分只用当日截面信息的变换(无需训练拟合)与跨日期拟合的变换(必须只在训练行上拟合);用 Pipeline 实现后者。
  5. 把超参数选择放在外层训练集内部,并说明内层折为何也要服从同一可用时点规则。
  6. 分开报告预测层(ROS2R^2_{\text{OS}}、IC、校准斜率)与组合层(成本后收益,第39章与 Project6)。
  7. 用三组可运行反例量化错误做法的代价:随机打乱 K 折、全样本特征选择、把标签起点设在信号收盘。

完成条件:第15–17节可独立完成;能追溯第8–10节冻结输入与输出,并解释第9节新运行与冻结回放的身份边界;本批不运行或重训。

2. 为什么 Quant 需要它

一个研究员拿来下面的结果:40 只股票、5 年日频数据、22 个特征,用 5 折交叉验证得到样本外 R2=56.7%R^2=56.7\%。该构造的特征真实载荷为0,因此这个大正值不能当作真实可用信号的证据;应检查数据生成与泄漏路径,不能仅凭经验量级断言任何金融问题里的高 R2R^2 都数学上不可能。问题出在管线上:每天一个 5 日收益标签,相邻标签共享 4 天收益;特征是慢变的公司特征,今天的值和昨天几乎相同;随机打乱后,测试行的“邻居”就是同一只股票前后一两天的训练行,它们的特征几乎一样、标签高度重叠。模型在测试集上做的事是查表,不是预测。第8节会在纯噪声特征上复现这个 56.7%。

错误往往不在模型,而在时间:特征什么时候能拿到,标签覆盖哪段时间,模型在什么时点拟合。本章把这些时间写成契约,然后让每一步都检查契约。第31章已经给出单序列的标签区间、purging 和 walk-forward;本章处理面板(多资产同日)和完整的预处理—调参—评价链条,并把实现放进 quant_course.ml_pipeline,供第39章和 Project6 共用。

3. 直觉

想象你在 3 月 2 日(周一)收盘后建模。你手里有到周一收盘为止的全部价格。你要预测“明天收盘买入、持有 2 天”的收益。

  • 上周三(kk)的样本:它的标签是从周四收盘到下周一收盘的收益。周一收盘你已经看到了,这一行可以训练。
  • 上周四的样本:标签从周五收盘到本周二收盘,周二还没到,不能训练。
  • 上周五的样本:标签从周一收盘到周三收盘,同样不能训练。

所以,即使这些样本的信号日都在你建模之前,它们的标签还没有发生完,不能进入训练。把它们放进去,就是让模型看见了未来收益。被排除的日期数由“执行滞后 + 持有期”决定,不是凭感觉设一个间隔。

面板还多一层:同一天有 40 只股票。它们的特征同时可得、标签覆盖同一段时间。如果把 3 月 2 日的 20 只股票放进训练、另 20 只放进测试,测试时模型已经从同一天的训练股票里学到了“3 月 2 日之后大盘涨了”。所以划分的单位是日期,不是行。

4. 数学基础

符号 含义 单位/维度
D1<D2<⋯<DTD_1<D_2<\dots<D_T 交易日,每个日期代表该日收盘 日期
i=1,…,Ni=1,\dots,N 资产 —
ℓ≥1\ell\ge1 执行滞后:信号收盘到成交收盘相隔的交易日数 交易日
h≥1h\ge1 预测区间(持有期) 交易日
xi,k∈Rpx_{i,k}\in\mathbb R^p 资产 ii 在信号日 DkD_k 收盘可得的特征 各特征自身单位
Pi,kP_{i,k} 收盘价 货币/股
yi,k=Pi,k+ℓ+h/Pi,k+ℓ−1y_{i,k}=P_{i,k+\ell+h}/P_{i,k+\ell}-1 标签:从成交收盘起 hh 日简单收益 小数
(Dk+ℓ,Dk+ℓ+h](D_{k+\ell},D_{k+\ell+h}] 标签区间(左开右闭) —
ss 拟合时点的日期下标:模型在 DsD_s 收盘后拟合,并对信号日 DsD_s 起的测试行预测 —

时间约定:signal_time =Dk=D_k;trade_time == label_start =Dk+ℓ=D_{k+\ell};label_end == label_available_at =Dk+ℓ+h=D_{k+\ell+h}。本书假设同一收盘的价格与特征在收盘后同时可得;若你的数据系统晚一天入库,把 available_at 和 label_available_at 都往后推,下面的推导照样适用。

日期与精确决策时刻分开。 signal_time 记的是信号日期 DkD_k。本书默认的精确决策时刻 τk\tau_k 是该日收盘价定稿后的信号截点,例如收盘 16:00、截点 16:10(交易所当地时区)。特征可用于 DkD_k 的信号,当且仅当 available_at ≤τk\le\tau_k。晚于截点公布的数据(例如 16:30 的财报)不能用于 DkD_k 的信号,最早用于 Dk+1D_{k+1}。盘后信号变体:若研究明确把截点设在当晚(例如 20:00)并保证数据在此之前入库,则 16:30 的数据可以用于 DkD_k 的信号,但必须在数据字典写明截点、时区与 available_at 的映射;成交仍在 Dk+ℓD_{k+\ell}。第38–39章与 Project6 的构造数据只用默认截点:全部特征由收盘价派生或规定在收盘可得,不受这一变体影响。

一天之内的时钟:先取快照,再拟合,再发信号

以下为独立教学构造,采用交易所当地时区,除末行的下一交易日成交外,盘内与盘后时点均属于交易日 DsD_s。日期索引只负责排序,16:00、16:08 等才说明程序能否读取数据;DsD_s 不等于一个任意的盘后时刻。

时点 发生的事 本次拟合或信号如何处理
16:00 收盘;某历史训练行的标签区间刚结束 尚须等待标签入库,结束不等于可读
16:08 当日价格派生特征入库 可用于16:10信号;16:09拟合快照也已可读
16:09 fit_time:锁定训练快照并开始拟合 只用此前已可读的历史特征和成熟标签
16:09:40 拟合完成 赶上信号截点;其后到达数据不追加进已拟合模型
16:10 τs\tau_s:生成并冻结信号 用已拟合变换处理16:08已入库的当日特征
16:20 上述16:00结束的标签入库 晚于fit_time,不能进入这次训练
16:30 财报发布(假定同时入库) 晚于τs\tau_s,不能进入这次预测
Ds+1D_{s+1} 收盘 本例ℓ=1\ell=1的成交点 标签从这一成交收盘起算,不从16:10起算

本次训练行是 k<sk<s、历史特征按各自信号截点的PIT版本固定且在拟合快照中可读、label_available_at 不晚于16:09的行;当日 k=sk=s 行只用于预测。16:08的当日特征并未加入尚无标签的训练行,也没有事后回填到16:09之前的快照。若另一历史行的标签于15:55入库且历史特征完整,它可进入训练;16:20才入库的那行则排除,即使两行的 label_end 都不晚于当天收盘。

三个检查各守一扇门:训练数据可读时点不晚于 fit_time,预测特征可读时点不晚于 τs\tau_s,拟合完成时点不晚于 τs\tau_s。若模型16:12才拟合完,16:10不能发布这个模型的信号;应按事前协议延后信号或使用此前已完成的模型,不能把运行时间倒写。第37章手算用下一开盘成交,本章、第39章与P06用下一收盘成交;两套例子保留各自的收益起点。

用到的先修:第31章的区间重叠条件 max⁡(t1,t2)<min⁡(e1,e2)\max(t_1,t_2)<\min(e_1,e_2);第8章 OLS 的投影性质;第23章的逐日 IC。

5. 正式定义

特征(feature)与特征可用时点(available_at)。 特征是信号日可以计算的量 xi,kx_{i,k}。每个特征登记 available_at,要求 available_at ≤τk\le\tau_k(信号日 DkD_k 的决策时刻,见第4节)。收盘价派生的特征(过去 5 日收益、过去 20 日波动)在当日收盘定稿后、截点之前可得;收盘后 16:30 才公布的财报,即使“报告期”早于信号日,available_at 也是公布时点,在默认截点下不能用于当日信号(第12章 PIT)。引入它是为了让“特征是否用了未来”成为一条可以断言的规则。

特征工程(feature engineering)。 从原始数据构造特征的过程。按信息来源分两类:

  • 截面变换(cross-sectional transform):只用同一信号日的数据,如当日截面排名、当日截面 z-score、当日行业去均值(第23、28章)。它在每个日期独立计算,不需要“训练拟合”,也不会把别的日期的信息带进来。
  • 跨日期变换(pooled / time-series transform):参数由多个日期的数据估计,如全样本均值方差标准化、缺失值用全样本中位数填补、PCA、特征选择、分位数截断阈值。它的参数必须只用训练行估计。

标签(label)与预测区间(prediction horizon)。 标签 yi,ky_{i,k} 是模型要预测的量;本书默认从成交收盘起算的 hh 日简单收益,hh 即预测区间。标签区间 (Dk+ℓ,Dk+ℓ+h](D_{k+\ell},D_{k+\ell+h}]。

标签可用时点(label_available_at)。 标签值第一次可被训练程序读取的时点,在本章日粒度构造中假定与 label_end 同一收盘可得;实际系统须登记入库时刻,不能默认相等。它与 signal_time 是两回事:信号日早,不代表标签已知。

截面面板(cross-sectional panel)。 以 (signal_time, asset) 为主键的长表。同一 signal_time 的行共享同一组时点。

拟合时点(fit_time)与可用训练集。 外层折在 DsD_s 收盘拟合。训练集候选为信号日早于 DsD_s 的行。精确时钟下,历史特征与标签均须在 fit_time 前或当时可读;下面的日期公式只在收盘同步可得的构造假设下把 label_available_at ≤\le fit_time 简写为 label_available_at ≤Ds\le D_s:

U(s)={(i,k):  k<s,  k+ℓ+h≤s,  xi,k 与 yi,k 均非缺失}. \mathcal U(s)=\{(i,k):\;k<s,\;k+\ell+h\le s,\;x_{i,k}\text{ 与 }y_{i,k}\text{ 均非缺失}\}.

按日期分组的 walk-forward(date-grouped walk-forward)。 把唯一日期排序,测试块是连续的若干日期 Ds,…,Ds+m−1D_s,\dots,D_{s+m-1},训练日期为 D1,…,Ds−1D_1,\dots,D_{s-1}(扩张窗)或最近 LL 个日期(滚动窗);每块之后整体前移。同一日期的全部资产行总在同一侧。

Purging(按标签区间排除)。 第31章的 purging 删除与测试标签区间重叠的训练行。本章把规则写成可用时点 k+ℓ+h≤sk+\ell+h\le s,6.1 节证明它包含重叠条件,并且更严格。

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)。 分两层。预测层:ROS2R^2_{\text{OS}}(相对零与训练均值)、逐日 Spearman IC 及其 Newey–West tt 值(滞后 h−1h-1)、校准斜率。组合层:把预测按明示规则变成仓位、滞后执行、扣成本后的收益、换手、回撤(第29章账本;第39章与 Project6)。

模型校准(model calibration)。 预测值与实现值在数量上是否一致。回归预测的校准斜率 b=Cov⁡(y,y^)/Var⁡(y^)b=\operatorname{Cov}(y,\hat y)/\operatorname{Var}(\hat y);Var⁡(y^)>0\operatorname{Var}(\hat y)>0 时,在线性投影意义下,0<b<10<b<1 表示预测相对线性校准均值的幅度过大,b>1b>1 表示幅度过小;b=0b=0 表示无线性信息,b<0b<0 表示反向关系,不能仅理解为置信度缩放。例:预测 (−1,0,1)(-1,0,1)、实现 (1,0,−1)(1,0,-1) 有 b=−1b=-1 且两边方差相等;还须检查截距 a=E[y]−bE[y^]a=E[y]-bE[\hat y]。即使 a=0,b=1a=0,b=1,也不保证 E[y∣y^]=y^E[y\mid\hat y]=\hat y,非线性或分组偏差仍可能存在。按预测分组的可靠性图(reliability diagram)在每组比较平均预测与平均实现值(分类概率的版本见 SK_CALIBRATION)。

6. 推导

6.1 可用时点规则、排除日期数,以及它与重叠 purging 的关系

假设:日期等间隔编号;无缺失;ℓ,h≥1\ell,h\ge1;拟合时点 DsD_s,第一个测试行的信号日为 DsD_s。

第一步,可用条件。信号日 kk 的标签在 Dk+ℓ+hD_{k+\ell+h} 收盘已知。拟合时点为 DsD_s 收盘,所以可用当且仅当

k+ℓ+h≤s  ⟺  k≤s−ℓ−h. k+\ell+h\le s\iff k\le s-\ell-h.

第二步,排除日期数。候选训练日期为 k=1,…,s−1k=1,\dots,s-1,可用的是 k≤s−ℓ−hk\le s-\ell-h,被排除的是 k=s−ℓ−h+1,…,s−1k=s-\ell-h+1,\dots,s-1,共

(s−1)−(s−ℓ−h)=ℓ+h−1 (s-1)-(s-\ell-h)=\ell+h-1

个日期,乘以资产数 NN 即排除行数。ℓ=1,h=5\ell=1,h=5 时排除 5 个信号日。

第三步,可用性蕴含不重叠。测试行最早的标签区间是 (Ds+ℓ,Ds+ℓ+h](D_{s+\ell},D_{s+\ell+h}],其余测试区间更晚。可用的训练行满足 k+ℓ+h≤s<s+ℓk+\ell+h\le s<s+\ell,其区间右端不超过任何测试区间左端,由第31章条件 max⁡(t1,t2)<min⁡(e1,e2)\max(t_1,t_2)<\min(e_1,e_2) 不成立,二者不重叠。

第四步,只做重叠 purging 不够。训练行 (Dk+ℓ,Dk+ℓ+h](D_{k+\ell},D_{k+\ell+h}] 与第一个测试区间 (Ds+ℓ,Ds+ℓ+h](D_{s+\ell},D_{s+\ell+h}] 重叠当且仅当 s+ℓ<k+ℓ+hs+\ell<k+\ell+h,即 k>s−hk>s-h。重叠 purging 只排除 h−1h-1 个日期;可用时点规则排除 ℓ+h−1\ell+h-1 个。多出来的 ℓ\ell 个日期,其标签区间在测试区间开始之前结束,但结束于 DsD_s 之后,拟合时还没发生。

用途:实现时按每行的 label_available_at 过滤,排除数量自然由区间决定;若数据有缺失日或不同资产的标签区间不同,规则依然逐行成立,而“固定空 hh 天”会出错。ml_pipeline.date_walk_forward 按此实现,并把“训练区间与测试区间不得重叠”作为防御断言。

一个更保守的约定:若规定标签必须在拟合时点之前的收盘就已入库(label_available_at <Ds<D_s),则多排除 1 个日期。两种约定都不泄漏;本书选择与特征相同的“同一收盘可得”。

6.2 gap 与 embargo 何时需要、该多长

walk-forward(训练全在测试之前):6.1 节的可用时点规则已经处理了标签跨界,不需要额外 embargo。若你的系统规定“拟合后至少隔 gg 日才能上线”,那是部署延迟,应写进 trade_time,而不是在训练端随意删行。

sklearn 的 TimeSeriesSplit(gap=g):gap 是在训练集末尾与测试集之间排除的样本个数(SK_TSS)。在面板上,样本是行而不是日期:每天 40 行时,gap=5 只空出八分之一天。即使改成 gap=5*40,只要某天缺一只股票,行数与日期就错位。所以面板上用日期规则,不用行数 gap。

purged K 折(训练块可以在测试块之后):先按真实标签区间purge,未来训练行也可能与测试标签重叠,不能因其信号日在后就跳过。再检查剩余未来训练行的特征窗口。若特征为过去 LL 个收益增量,其支持为 (Dk−L,Dk](D_{k-L},D_k];测试标签区间并集最后结束于 DeD_e,设连续测试标签并集为 (Da,De](D_a,D_e],一般交集条件为 k>ak>a 且 k−L<ek-L<e;只有额外限定 k>ek>e 时,后一条件才已足够,此时删除 e<k≤e+L−1e<k\le e+L-1。若测试并集不连续,应逐段检查,使用外包络是保守协议。若把价格端点也计作共享信息,或采用 k≤e+Lk\le e+L,会额外多删一个日期,这是保守协议,不能同时称严格最小。

第31章的窗口规则须用其明示坐标和半开端点。相对最后标签终点 ee 计数,有限增量回看长度由 LL 控制;若改从最后测试信号日计数,ee 本身随 ℓ,h\ell,h 改变,不能说总间隔无条件与 hh 无关。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,训练设计矩阵 Z=[1,X]∈Rn×(p+1)Z=[\mathbf 1,X]\in\mathbb R^{n\times(p+1)};标准化 x~=S−1(x−μ)\tilde x=S^{-1}(x-\mu)(SS 为可逆对角阵,μ,S\mu,S 是任意固定常数),对应 Z~=[1,X~]\tilde Z=[\mathbf 1,\tilde X],X~=(X−1μ⊤)S−1\tilde X=(X-\mathbf 1\mu^\top)S^{-1}。

  1. 训练拟合值总不变:无论 ZZ 是否满列秩,两种设计下的 OLS 拟合值相同。
  2. 总存在对应的系数表示:任一组标准化系数 (a~,b~)(\tilde a,\tilde b) 都可写成原坐标系数 b=S−1b~b=S^{-1}\tilde b、a=a~−μ⊤S−1b~a=\tilde a-\mu^\top S^{-1}\tilde b,两者对任何新点给出相同的预测。
  3. 对新点的预测:满列秩时不变,秩亏时不保证:若 ZZ 满列秩(rank⁡Z=p+1\operatorname{rank}Z=p+1,因此 n≥p+1n\ge p+1,且没有任何特征是其他特征与常数项的精确线性组合),OLS 解唯一,两种设计的拟合结果互相对应,对任何新点 xx 的预测相同。若 ZZ 秩亏,训练数据不能确定系数在零空间方向上的分量,软件默认的解(scikit-learn LinearRegression 先按训练均值中心化处理截距,再返回斜率的最小范数解)在两种坐标下取了不同的“最小范数”,对离开训练数据线性关系的新点,预测一般不同;新点满足训练中的同一线性关系时可保证相同;其他新点也可能偶然得到相同预测,但不能普遍保证。

读这三个层次时,区分“我把同一组系数换了坐标”与“软件重新求解”。第2层是代数恒等式,对任意新点都成立;第3层问重新拟合后软件是否必然选中这组对应系数。满列秩保证唯一性,秩亏则只保证训练投影唯一。这里满列秩的是含截距的增广矩阵 ZZ,不是仅看特征列数;SS 可逆排除了零标准差特征。

证明:(1) Z~=ZT\tilde Z=ZT,其中 T=(1−μ⊤S−10S−1)T=\begin{pmatrix}1&-\mu^\top S^{-1}\\0&S^{-1}\end{pmatrix} 可逆,所以 col⁡(Z~)=col⁡(Z)\operatorname{col}(\tilde Z)=\operatorname{col}(Z)。OLS 拟合值是 yy 在列空间上的正交投影,投影唯一,与列空间的基无关,故相同(不需要满秩)。(2) 直接代入:a~+b~⊤S−1(x−μ)=a+b⊤x\tilde a+\tilde b^\top S^{-1}(x-\mu)=a+b^\top x。(3) 满列秩时 Z~⊤Z~=T⊤Z⊤ZT\tilde Z^\top\tilde Z=T^\top Z^\top ZT 可逆,唯一解 β~=T−1β^\tilde\beta=T^{-1}\hat\beta,由 (2) 的对应关系,新点预测相同。秩亏时,正规方程的解集是 β^+ker⁡Z\hat\beta+\ker Z;新点预测 z⊤βz^\top\beta 在解集上不唯一,除非 zz 正交于 ker⁡Z\ker Z(即新点满足训练数据中的同一线性关系)。LinearRegression 的最小范数解在原坐标下最小化斜率 ∥b∥\|b\|,在标准化坐标下最小化 ∥b~∥=∥Sb∥\|\tilde b\|=\|Sb\|,两个范数不同,选出的解一般不同。□\square

秩亏反例(教学构造):训练 X=(122436)X=\begin{pmatrix}1&2\\2&4\\3&6\end{pmatrix},y=(1,2,3)y=(1,2,3),第二列恰好是第一列的 2 倍;新点 x=(1,5)x=(1,5) 不满足这一关系。

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

两个模型的训练拟合值都是 (1,2,3)(1,2,3)。原坐标在约束 b1+2b2=1b_1+2b_2=1 下最小化 ∥b∥\|b\|,得 b=(0.2,0.4)b=(0.2,0.4)、截距 0,新点预测 0.2+0.4×5=2.200.2+0.4\times5=2.20;标准化坐标(训练标准差 s=(0.8165,1.6330)s=(0.8165,1.6330))下最小化 ∥Sb∥\|Sb\|,换回原单位是 b=(0.5,0.25)b=(0.5,0.25)、截距 0,新点预测 0.5+0.25×5=1.750.5+0.25\times5=1.75。两者都满足训练数据唯一能确定的组合 b1+2b2=1b_1+2b_2=1,差别全在零空间方向 (2,−1)(2,-1) 上,数据对它没有信息。实践含义:OLS 的“标准化不改变预测”只在满列秩时成立;面板数据里常见的精确共线(例如把同一特征的两种单位都放进去、虚拟变量陷阱)会破坏它。无论是否满秩,标准化参数仍只在训练行上估计。

所以在满列秩时,“用全样本均值方差标准化”对 OLS 的预测没有影响;秩亏时连 OLS 也可能受影响(上例)。标准化通常可能影响下面几类方法:

  • Ridge、Lasso:惩罚 ∥b∥\|b\| 依赖特征尺度,μ,S\mu,S 改变可能使解改变(第39章)。
  • kNN、聚类:距离依赖尺度。
  • 阈值规则、截断、缺失填补、特征选择:参数本身由数据估计,全样本估计直接使用了测试期信息。

在本章构造案例中,特征选择产生了显著泄漏;不同预处理的泄漏大小依数据与设计而变,不能由此作普遍排名,第8.3节的数值显示:从 200 个纯噪声特征里按全样本相关性挑 10 个,walk-forward 的 ROS2R^2_{\text{OS}} 是 +0.62%+0.62\%、IC 4.8%;把选择放进训练内是 −1.21%-1.21\%。规则统一为:凡是有参数的跨日期变换,都放进 Pipeline,在每个训练折上重新拟合。

6.4 随机打乱 K 折为何把噪声评成信号

设定:特征为 AR(1) 慢变量,日相关 ρ=0.98\rho=0.98;标签为 5 日收益,相邻日标签相关约 (h−j)/h(h-j)/h(第37章 6.4 节);特征与标签真实无关。

随机打乱后,测试行 (i,k)(i,k) 的训练集中几乎总有 (i,k±1)(i,k\pm1)、(i,k±2)(i,k\pm2)。两行特征距离的平方期望为每个特征 E(xi,k−xi,k+j)2=2(1−ρj)E(x_{i,k}-x_{i,k+j})^2=2(1-\rho^j),j=1j=1 时 0.04;不同资产之间为 2。kNN 的近邻几乎全是同一资产的相邻日期,它们的标签与测试标签相关 0.8、0.6。预测值约等于“邻近日期标签的平均”,与测试标签高度相关。这是重叠泄漏(第37章 12.5 节):模型通过特征认出“这是哪只股票的哪几天”,再借用重叠标签。

线性模型(Ridge)不能“查表”,同一数据上打乱 K 折给出约 +0.05%+0.05\%,walk-forward 给出 −1.65%-1.65\%。差别小,但方向相同:随机划分让训练和测试共享同一时期的伪相关。灵活的模型(第40章的树、近邻)会把这一点放大成第8节的 56.7%。

6.5 预测误差的“水平—截面”分解与校准

假设:每个日期 dd 有 NdN_d 个资产。记日期均值 yˉd,y^ˉd\bar y_d,\bar{\hat y}_d,去均值 y∘=y−yˉdy^\circ=y-\bar y_d、y^∘=y^−y^ˉd\hat y^\circ=\hat y-\bar{\hat y}_d。

∑d∑i(yi,d−y^i,d)2=∑dNd(yˉd−y^ˉd)2+∑d∑i(yi,d∘−y^i,d∘)2. \sum_{d}\sum_{i}(y_{i,d}-\hat y_{i,d})^2=\sum_d N_d(\bar y_d-\bar{\hat y}_d)^2+\sum_d\sum_i(y^\circ_{i,d}-\hat y^\circ_{i,d})^2.

推导:y−y^=(yˉd−y^ˉd)+(y∘−y^∘)y-\hat y=(\bar y_d-\bar{\hat y}_d)+(y^\circ-\hat y^\circ),平方后交叉项 2(yˉd−y^ˉd)∑i(yi,d∘−y^i,d∘)=02(\bar y_d-\bar{\hat y}_d)\sum_i(y^\circ_{i,d}-\hat y^\circ_{i,d})=0,因为去均值后每个日期内的和为 0。

金融含义:第一项是“猜全市场这周涨多少”的误差,第二项是“猜谁比谁好”的误差。美元中性组合(权重和为 0)的收益只取决于第二项对应的信息;Spearman IC只衡量去均值后不变的秩;截面SSE和截面 R2R^2 仍依赖预测幅度与尺度。一个模型可以在第一项上很差(水平预测被市场噪声主导,ROS2R^2_{\text{OS}} 为负),在第二项上有用(截面 R2R^2 为正、IC 为正)。第8.4节的构造面板就是这样。

校准:池化的校准斜率 bb 混合了两项;截面斜率 b∘=∑y^∘y∘/∑(y^∘)2b^\circ=\sum\hat y^\circ y^\circ/\sum(\hat y^\circ)^2 只看第二项。若要按“预测收益是否超过成本”决定交易(第13节),需要截面校准:先在内层验证集上估计 b∘b^\circ,在相应线性校准模型成立时,再用 b∘y^∘b^\circ\hat y^\circ 作为期望超额收益的线性近似;池化斜率不能直接当作每只资产、每种状态的真实条件期望。用测试集估计校准系数是选择泄漏。

7. 金融解释

时间契约就是风控。 回测里多用了一天未来收益,实盘里没有那一天。把每个时点写进数据字典,等于把“我在 tt 时点知道什么”交给代码检查,而不是交给研究员的记忆。

为什么按日期分组。 同一天的股票受同一个市场冲击。随机拆分同一天的行,模型从训练股票里学到当天的市场方向,再在测试股票上“预测”出来;部署时你在信号日收盘拿不到这一信息。

水平与截面。 很多股票选择策略是美元中性的:权重和为0使共同水平项在 w⊤yw^\top y 中抵消,但不保证beta中性,也不意味着所有这类组合只依赖排序。实际接口把去均值预测按L1绝对和归一:共同正比例缩放和加同一常数不改权重,任意同秩变换一般会改变权重。例:预测 (−1,0,1)(-1,0,1) 给权重 (−1/2,0,1/2)(-1/2,0,1/2);同秩预测 (−1,−0.9,1)(-1,-0.9,1) 给 (−7/26,−3/13,1/2)(-7/26,-3/13,1/2)。Spearman IC只看秩,截面 R2R^2 还看平方误差与尺度。

调参不稳定是信息。 第8.4节的岭回归在 12 个外层折里选出的正则强度从 10410^4 跳到 10710^7。这提示应检查实际内层 inner_scores 的MSE曲线与稳定性;参数跳动本身不能单独证明曲线平坦或唯一成因。把它当成“模型很稳”是误读;它提示你看重 IC 的稳定性和组合层结果,而不是某个具体超参数。

校准与交易阈值。 若交易规则是“预测收益超过单边成本才交易”,正斜率但幅度过大的预测(0<b<10<b<1)会让你频繁交易、付出成本;过于保守(b>1b>1)会让你错过机会。截面校准斜率在明示线性假设下提供尺度换算,仍须检查截距、分组偏差及规则对应的真实成本。

8. Numerical Example

8.1 手算:3 资产 × 12 日,ℓ=1\ell=1、h=2h=2

(教学构造。)收盘价:

kk 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

标签 yi,k=Pi,k+3/Pi,k+1−1y_{i,k}=P_{i,k+3}/P_{i,k+1}-1,标签区间 (Dk+1,Dk+3](D_{k+1},D_{k+3}]。例如 A 在 k=1k=1:成交日 D2D_2(101),终点 D4D_4(102),y=102/101−1=0.009901y=102/101-1=0.009901;B 在 k=1k=1:49.5/50.5−1=−0.01980249.5/50.5-1=-0.019802。C 在 k=3k=3:成交日 D4D_4(20.4),终点 D6D_6(20.0),y=20.0/20.4−1=−0.019608y=20.0/20.4-1=-0.019608;若把信号日 D3D_3 的 20.2 当起点、终点 D5D_5(20.2),会得到 0,这是第12.4节的错误。

信号日 kk label_end yAy_A yBy_B yCy_C
1 D4D_4 0.009901 −0.019802 0.020000
2 D5D_5 0.009709 −0.020000 0.000000
3 D6D_6 0.039216 0.000000 −0.019608
4 D7D_7 0.009615 0.020408 0.009901
5 D8D_8 0.009434 0.030303 0.030000
6 D9D_9 0.047619 0.030000 0.009804
7 D10D_{10} 0.009346 0.019608 0.009709
8 D11D_{11} 0.009091 −0.009709 0.019417
9 D12D_{12} 0.037037 0.009615 0.000000
10–12 超出样本 缺失 缺失 缺失

一个折:训练日期 k=1,…,6k=1,\dots,6,测试日期 k=7,8,9k=7,8,9,拟合时点 D7D_7。可用条件 k+3≤7k+3\le7,即 k≤4k\le4。可用训练行:k=1,…,4k=1,\dots,4 共 4×3=124\times3=12 行;排除 k=5,6k=5,6 共 (ℓ+h−1)×N=2×3=6(\ell+h-1)\times N=2\times3=6 行。只做重叠 purging 时:与第一个测试区间 (D8,D10](D_8,D_{10}] 重叠当且仅当 k+3>8k+3>8,只排除 k=6k=6。k=5k=5 的区间 (D6,D8](D_6,D_8] 不与测试重叠,但它在 D8D_8 收盘才结束,D7D_7 拟合时未知。图38-1 把这些区间画在同一条时间轴上。

8.2 随机 K 折、日期块 K 折与 walk-forward(纯噪声)

构造面板:40 只资产、1,260 个交易日、22 个慢变特征(AR(1),ρ=0.98\rho=0.98),真实载荷全部为 0;5 日标签;种子 3801。ROS2R^2_{\text{OS}} 相对零预测:

方法 Ridge(α=1\alpha=1) 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):

做法 ROS2R^2_{\text{OS}}(相对零) 平均逐日 Spearman IC
全样本按 ∣\lvert相关∣\rvert 选特征(错误) +0.617% +0.0476
Pipeline 内训练集选择(SelectKBest) −1.214% −0.0101

错误做法在纯噪声上做出了“IC 约 0.05”,量级正好落在第37章所说的“真实但微弱的信号”范围,最难被肉眼识破。

8.4 完整面板案例

构造面板:40 只资产、1,260 日;两条真实特征 q1,q2q_1,q_2(日预期收益载荷 0.04%、0.02% 每单位)、20 条噪声特征、三个价格特征(rev_5、mom_60_5、vol_20);ℓ=1\ell=1、h=5h=5;扩张窗 walk-forward,首个训练窗 504 个日期,测试块 63 个日期,共 12 折(首个拟合时点 2016-12-08);Ridge,α∈{102,…,107}\alpha\in\{10^2,\dots,10^7\},内层 2 个日期折、每折 63 个验证日期、内层最少 252 个训练日期;种子 3803。

  • 每个外层折排除 200 行 =(ℓ+h−1)×N=5×40=(\ell+h-1)\times N=5\times40,与 6.1 节一致;首个外层折因 mom_60_5 需要 60 日历史,丢弃 2,400 行缺失(60×4060\times40)。
  • 选出的 α\alpha:106,107,106,107,107,105,105,104,105,104,107,10510^6,10^7,10^6,10^7,10^7,10^5,10^5,10^4,10^5,10^4,10^7,10^5。
  • 预测层(30,000 个测试行、750 个日期):ROS2R^2_{\text{OS}} 相对零 −0.325%-0.325\%,相对训练均值 +0.089%+0.089\%;截面(逐日去均值)R2=+0.092%R^2=+0.092\%;逐日 Spearman IC 均值 0.0255,Newey–West t=2.46t=2.46(滞后 4);方向命中率 49.7%。
  • 校准:池化斜率 0.36(水平预测过度分散),截面斜率 1.18(截面预测略保守)。按日期内预测十分位分组,最高组减最低组的平均去均值 5 日收益约 0.096%−(−0.332%)=0.43%0.096\%-(-0.332\%)=0.43\%,中间各组不单调。

用 6.5 节分解读这些数字:水平误差使池化 R2R^2 为负,截面部分有正的排序信息。命中率 49.7% 低于 50%,同时 IC 为正,这再次说明第37章的观点:方向准确率不是这类策略的评价指标。

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_at ≤\le fit_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):ROS2R^2_{\text{OS}}(相对零与训练均值)、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-1:横轴为交易日下标 kk(每个日期是一个收盘),纵轴为行的信号日;圆点为信号收盘,横条为标签区间(成交收盘,标签终点]。竖虚线为拟合时点 D7D_7 收盘。绿色为可用训练行(标签在 D7D_7 前已结束),橙色为被排除的训练候选(标签在 D7D_7 之后结束),红色为测试行。观察点:k=5k=5 的橙色区间不与任何红色区间重叠,却仍被排除。构造数据。

图38-2 纯噪声特征下三种验证方法的结果

图38-2:横轴为三种验证方法,纵轴为相对零预测的样本外 R2R^2(%),两种颜色分别为 Ridge(α=1\alpha=1)与 5 近邻回归。特征真实载荷为 0。观察点:随机打乱 K 折给 kNN 报出 +56.7%,两种按日期的方法都为负。构造数据,种子 3801。

图38-3 水平校准与截面排序

图38-3:左图横轴为池化十分位的平均预测(%,5 日),纵轴为该组平均实现 5 日收益(%),虚线为 45 度线;右图横轴为每个日期内按预测分的十分位(每组 4 只资产),纵轴为该组平均的日期去均值 5 日收益(%)。观察点:左图的点与 45 度线相去甚远(水平预测不可靠),右图最高组与最低组之差约 0.43%,但中间组不单调。构造面板,种子 3803。

11. Real Market Example

本章没有真实市场的实测结果。Project6 的真实数据状态为 blocked:课程尚无可用于内部培训的、经许可的日频个股价格面板(见 projects/project06/data_manifest.json 的字段与授权要求)。获得数据后,本章管线在真实数据上需要额外处理三件事,构造数据里没有出现:

  1. 公司行为:标签用复权收益,成交与账本用未复权价格加分红拆股事件(第29、30章),两者不能混用。
  2. 上市退市与股票池:股票池按信号日的成分冻结,退市收益进入标签(第30章生存偏差)。
  3. 非同步数据:财务特征按公告时点入库(第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个完整信号日期(ℓ+h−1=5\ell+h-1=5)。代码WRONG注释混用了编号差与排除数,不能照该比较理解契约。训练末日期只有1只股票进入训练,同一日期的行也可能分布在空隔与测试块。修复: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 的管线规格(构造数据只验证规格本身):

  1. 数据字典:每个字段的 event_time、available_at、单位与缺失规则;signal_time、trade_time、label 区间、label_available_at、频率(projects/project06/data_dictionary.md)。
  2. 标签:ℓ=1\ell=1,h=5h=5,从成交收盘起算的简单收益。
  3. 划分:日期分组的扩张窗 walk-forward;首个训练窗、测试块长度事前冻结;可用时点规则自动排除 ℓ+h−1\ell+h-1 个日期。
  4. 预处理:截面变换在当日计算;跨日期变换全部放进 Pipeline。
  5. 调参:外层训练内的日期 walk-forward,网格事前冻结并记录每折选择。
  6. 评价:预测层报告 ROS2R^2_{\text{OS}}(两种基准)、截面 R2R^2、IC 与 NW tt、校准斜率;组合层按明示规则(截面去均值、毛暴露 1、每 5 日调仓、ℓ=1\ell=1 执行、比例成本)计成本后收益、换手和回撤。
  7. 基准:零预测、训练均值(截面上全同,组合空仓)。
  8. 反证条件:截面 IC 的 NW tt 不显著;成本后收益不为正;结果依赖某一折。

14. Chapter Summary

信号日早于拟合时点,不等于标签已知;面板上的每一行都要按 label_available_at ≤\le fit_time 过滤,排除日期数 ℓ+h−1\ell+h-1 由区间推出,比只按区间重叠排除多 ℓ\ell 天。划分以日期为单位。跨日期估计参数的变换必须在训练折内拟合;满列秩时 OLS 对标准化不敏感(秩亏时训练拟合不变、新点预测可能改变),正则化、近邻和特征选择都敏感,本章特征选择案例展示了显著泄漏,大小仍依具体设计。随机打乱 K 折让慢变特征和重叠标签“查表”,纯噪声也能得到大的正 R2R^2。评价时把水平误差与截面误差分开:美元中性消掉共同水平;实际连续权重仍依赖去均值预测的相对幅度,校准也应在截面上、在内层验证集上完成。

15. Exercises

38-E01(可用时点)

按本书默认截点 τk\tau_k(DkD_k 收盘价定稿后 16:10)写出下列数据的 available_at,判断能否用于 DkD_k 的信号,再说明改用“当晚 20:00 截点”时哪几项结论会变:(a) DkD_k 收盘价;(b) DkD_k 全天成交量;(c) 公司在 DkD_k 16:30 发布的季报利润;(d) 指数公司在 DkD_k 盘后宣布、Dk+5D_{k+5} 生效的成分调整。

38-E02(手算标签与可用行)

用第8.1节价格,ℓ=1,h=2\ell=1,h=2。(a) 计算 B 在 k=5k=5 和 C 在 k=6k=6 的标签及标签区间。(b) 若拟合时点改为 D8D_8、测试日期 k=8,9,10k=8,9,10,哪些信号日的行可用于训练?排除多少行?

38-E03(排除日期数)

分别对 (ℓ,h)=(1,5)(\ell,h)=(1,5)、(2,3)(2,3)、(1,1)(1,1) 计算可用时点规则排除的日期数与只做重叠 purging 排除的日期数。h=1h=1 时为什么重叠 purging 一行都不排除,可用时点规则仍排除 ℓ\ell 个日期?

38-E04(embargo)

某研究用日期块 purged K 折,特征含 rev_5(回看 5 日)与 mom_60_5(回看 60 日),标签 h=5h=5。embargo 应取多少个交易日?若改用扩张窗 walk-forward,还需要 embargo 吗?

38-E05(标准化不变性及其条件)

(a) 证明:带截距 OLS 的训练拟合值对仿射标准化不变;若训练设计 [1,X][\mathbf 1,X] 满列秩,对任何新点的预测也不变。(b) 给出一个秩亏反例,说明训练拟合相同而新点预测不同,并解释原因。(c) 给出一个反例说明 Ridge 的预测会变。

38-E06(随机 K 折的查表机制)

特征为 AR(1),ρ=0.98\rho=0.98,5 日重叠标签。计算同一资产相隔 1 日、3 日的特征差平方的期望(每个单位方差特征),以及对应标签的相关系数。解释 kNN 在随机打乱 K 折上得到高 R2R^2 的原因。

38-E07(嵌套调参的行)

外层折拟合时点 D600D_{600},ℓ=1,h=5\ell=1,h=5,扩张窗训练日期为 D1,…,D599D_1,\dots,D_{599}。内层取 2 个验证块、每块 63 日、内层最少训练 252 日。写出两个内层折的验证日期范围、各自的拟合时点,以及内层训练行必须满足的条件。

38-E08(水平—截面分解)

两个日期各两只资产:日期1 y=(0.02,0.00)y=(0.02,0.00)、y^=(0.03,0.01)\hat y=(0.03,0.01);日期2 y=(−0.01,0.01)y=(-0.01,0.01)、y^=(0.00,−0.02)\hat y=(0.00,-0.02)。计算总平方误差、水平项、截面项,并判断该模型对美元中性组合有没有用。

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 与表格一致(容差 10−610^{-6});(2) k≥10k\ge10 的行 label_end 为缺失、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 折;输出两种做法的 ROS2R^2_{\text{OS}} 与平均 IC;断言 Pipeline 内选择的测试预测不随测试标签改变(把测试标签换成常数 5.0 后预测逐元素相同)。

38-P03(行数 gap 的反例)

构造一个不平衡面板(某些日期缺 1–2 只股票),比较 TimeSeriesSplit(gap=h*N) 与 date_walk_forward 的训练末尾日期和测试开始日期。验收:找到至少一个折,TimeSeriesSplit 让同一日期出现在训练和测试两侧,或训练末行的 label_end 晚于测试首个信号日。

17. Challenge Problems

38-C01(purged K 折的 embargo 推导)

设特征最长回看窗口为 LL 个交易日,标签 (ℓ,h)(\ell,h)。对位于测试块之后的训练行,写出“特征回看窗口与任何测试标签区间相交”的条件,推导最小 embargo 长度,分别说明从标签终点 ee 和最后信号日计数时,长度如何依赖 LL 与 hh。再讨论:如果特征是慢变的公司特征(如账面市值比,按季度更新),embargo 能否消除泄漏?额外先修:第31章。评分:条件与推导 50%,两种坐标及依赖关系的论证 20%,慢变特征讨论 30%。

38-C02(校准与交易阈值)

内层验证集上截面校准斜率 b∘=0.6b^\circ=0.6;单边成本 0.10%;某日从较大截面抽取两只资产,其去均值预测为 +0.25%+0.25\%、−0.05%-0.05\%(5 日)。(a) 校准后的期望超额收益是多少?(b) 规则“仅当校准后的 ∣y^∘∣|\hat y^\circ| 超过往返成本 0.20% 时交易”是否交易这两只资产?(c) 讨论:为什么这一规则需要截面校准而排序组合不需要?开放题,评分见第18节。

18. Solutions / Solution Hints

38-E01

默认截点 τk\tau_k=16:10:(a) 收盘价定稿(约 16:00)后即可得,可用。(b) 全天成交量随收盘定稿;若数据商在 16:10 前送达,可用,否则 available_at 为实际送达时点,不能用于 DkD_k。(c) available_at 为 16:30 公布(及入库)时点,晚于截点,不能用于 DkD_k 的信号;最早用于 Dk+1D_{k+1} 的信号、Dk+2D_{k+2} 成交。(d) available_at 为盘后公告时点,晚于截点,不能用于 DkD_k;生效日 Dk+5D_{k+5} 是事件日,不是可用时点。改用当晚 20:00 截点且数据在此前入库:(c)(d) 变为可用于 DkD_k 的信号,成交仍在 Dk+1D_{k+1};(a)(b) 不变。无论哪种截点,都要在数据字典写明截点、时区与 available_at,不能按日期粗算。

38-E02

(a) B,k=5k=5:成交日 D6D_6(49.5),终点 D5+1+2=D8D_{5+1+2}=D_8(51),y=51/49.5−1=0.030303y=51/49.5-1=0.030303,区间 (D6,D8](D_6,D_8]。C,k=6k=6:D7D_7(20.4)到 D9D_9(20.6),y=0.009804y=0.009804,区间 (D7,D9](D_7,D_9]。(b) 拟合时点 D8D_8,可用条件 k+3≤8k+3\le8,k≤5k\le5;候选 k=1,…,7k=1,\dots,7,可用 k=1,…,5k=1,\dots,5(15 行),排除 k=6,7k=6,7 共 6 行。

38-E03

可用时点规则排除 ℓ+h−1\ell+h-1:(1,5)→5(1,5)\to5,(2,3)→4(2,3)\to4,(1,1)→1(1,1)\to1。重叠 purging 排除 h−1h-1:4、2、0。h=1h=1 时训练区间 (Dk+ℓ,Dk+ℓ+1](D_{k+\ell},D_{k+\ell+1}] 与测试区间 (Ds+ℓ,… ](D_{s+\ell},\dots] 重叠需 k>s−1k>s-1,候选里没有这样的 kk;但 k=s−1k=s-1 的标签在 Ds+ℓD_{s+\ell} 结束,晚于拟合时点 DsD_s,未知。

38-E04

先purge标签重叠;再相对最后测试label_end采用最长60日特征回看的保守embargo协议。若严格按半开收益增量区间计,删除至 e+59e+59;含价格端点或 k≤e+60k\le e+60 会多排除一个日期,须明示坐标(6.2节)。扩张窗 walk-forward 的训练行都在测试之前,不需要 embargo;可用时点规则已处理标签跨界。

38-E05

(a) 证明见 6.3 节:列空间相同推出训练拟合值相同(不需满秩);满列秩时 OLS 解唯一,系数对应关系推出新点预测相同。(b) 6.3 节秩亏例:XX 第二列为第一列的 2 倍,训练拟合都为 (1,2,3)(1,2,3);新点 (1,5)(1,5) 上原始 LinearRegression 预测 2.20、StandardScaler 后预测 1.75。原因:解集为 β^+ker⁡Z\hat\beta+\ker Z,两种坐标下的最小范数解在零空间方向 (2,−1)(2,-1) 上取值不同,新点不满足训练中的线性关系 x2=2x1x_2=2x_1,所以预测依赖于这一无法识别的分量。(c) 反例:一个特征 x=(1,2,3,4)x=(1,2,3,4)、y=(1,2,3,5)y=(1,2,3,5),Ridge α=1\alpha=1、不带标准化与把 xx 乘以 10 后再拟合,对 x=5x=5 的预测不同(惩罚 αb2\alpha b^2 中 bb 随单位缩放)。可以用 sklearn 验证:

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

E(xk−xk+j)2=2(1−ρj)E(x_k-x_{k+j})^2=2(1-\rho^j):j=1j=1 为 0.04,j=3j=3 为 2(1−0.983)=0.11762(1-0.98^3)=0.1176。标签相关 (5−j)/5(5-j)/5:0.8 与 0.4。随机打乱后,测试行的近邻几乎都是同一资产的相邻日期(距离远小于不同资产间的 2),它们的标签与测试标签高度相关,预测就是重叠标签的平均。

38-E07

外层训练日期 1..5991..599,但外层可用行只到 k≤600−6=594k\le600-6=594。内层在这 594 个日期上从第 253 个日期起每 63 日切一个验证块:253..315253..315、316..378316..378、379..441379..441、442..504442..504、505..567505..567;再往后不足 63 日(568..594568..594),不构成验证块。最后两个内层折的验证日期为 k=442..504k=442..504 与 k=505..567k=505..567,拟合时点 D442D_{442}、D505D_{505};内层训练行须满足 k+6≤442k+6\le442(或 ≤505\le505),即训练到 k=436k=436(或 499499)。外层测试 k≥600k\ge600 的标签从不出现。用 date_walk_forward 在 700 日单资产序列上可以复现这些下标。

38-E08

总误差:日期1 (−0.01)2+(−0.01)2=0.0002(-0.01)^2+(-0.01)^2=0.0002;日期2 (−0.01)2+(0.03)2=0.0010(-0.01)^2+(0.03)^2=0.0010;合计 0.0012。水平项:日期1 2×(0.01−0.02)2=0.00022\times(0.01-0.02)^2=0.0002,日期2 2×(0−(−0.01))2=0.00022\times(0-(-0.01))^2=0.0002,合计 0.0004。截面项:日期1 去均值 y∘=(0.01,−0.01)y^\circ=(0.01,-0.01)、y^∘=(0.01,−0.01)\hat y^\circ=(0.01,-0.01),误差 0;日期2 y∘=(−0.01,0.01)y^\circ=(-0.01,0.01)、y^∘=(0.01,−0.01)\hat y^\circ=(0.01,-0.01),误差 0.0008;合计 0.0008。0.0004+0.0008=0.00120.0004+0.0008=0.0012。日期1 排序完全正确,日期2 完全相反,这两日平均截面 IC 为0;按对称等额多空权重,两日平均毛优势为0,正费用后更差。这两次观察不足以证明总体完全无信息。

38-P01

完整参考方法:用第9节的3×12价格创建 labels;对每只资产及 k=1,…,9k=1,\ldots,9,独立计算 close.iloc[k+2]/close.iloc[k]-1(此处Python的 kk 从1起写作循环变量,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;本书运行全样本选择 +0.617%+0.617\% / IC 0.0476,Pipeline 内 −1.214%-1.214\% / IC −0.0101-0.0101。完整方法:先固定同一组日期折;错误支路在 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。ℓ=1,h=5\ell=1,h=5 时末训练标签到日期9才结束,拟合日8尚未知,因此满足第二种失败条件。每日期最多4行、gap20不会让同一日期同时跨训练和测试,不能虚构第一种失败。

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真实标签重叠。剩余未来训练行的有限收益增量支持为 (Dk−L,Dk](D_{k-L},D_k],连续测试标签并集为 (Da,De](D_a,D_e],相交当且仅当 k>ak>a 且 k−L<ek-L<e。限定 k>ek>e 后才可只用后一条件,删除 e<k≤e+L−1e<k\le e+L-1;一般未来信号行不能无条件全删到此处并称严格最小。若测试并集不连续须逐段检查,外包络为保守协议。例:单测试信号0、ℓ=h=L=1\ell=h=L=1,测试标签 (D1,D2](D_1,D_2];未来 k=1k=1 的标签 (D2,D3](D_2,D_3] 与特征 (D0,D1](D_0,D_1] 均不重叠,可保留;k=2k=2 特征 (D1,D2](D_1,D_2] 重叠,须删除。若连价格端点也排除,或采用 k≤e+Lk\le e+L,这是多删一个日期的保守协议。相对标签终点计数只需 LL;相对最后测试信号日,标签终点随 ℓ+h\ell+h 移动,不能说间隔不依赖 hh。季度更新的公司变量不是有限收益窗口,EMA也无精确有限kill窗口;须按真实PIT与价格依赖检查。评分按条件/坐标推导、两种计数区别、慢变量边界给出。

38-C02 评分准则

(a) 0.6×0.25%=0.15%0.6\times0.25\%=0.15\%,0.6×(−0.05%)=−0.03%0.6\times(-0.05\%)=-0.03\%。(b) 两者都不超过 0.20%,都不交易(未校准时第一只会交易,属于被过度分散的预测诱导的交易)。(c) 纯秩组合只依赖次序;实际连续权重并非纯秩,但共同正比例缩放在去均值/L1归一后也抵消。任意同秩变换不保证权重不变。阈值规则比较有单位的收益与成本,需要明示线性校准假设,单个斜率不保证完整校准。评分:(a)(b) 各 30%,(c) 40%;用测试集估计 b∘b^\circ 扣全部 (c) 分。

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说明程序第一次能读取标签;供应商延迟使两者分离。日粒度的k+ℓ+h≤sk+\ell+h\le s仅在收盘同步假设下等价于成熟检查。

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-learn TimeSeriesSplit。支持 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。

搜索教材

输入关键词开始搜索。