machine-learning-for-trading 案例研究:NASDAQ-100 微观结构 —— 15 分钟频率下的成本主导失败与两种纪律性修复
【免费下载链接】machine-learning-for-tradingCode for Machine Learning for Trading, 3rd edition — from data sourcing to live execution.项目地址: https://gitcode.com/GitHub_Trending/ma/machine-learning-for-trading
本篇指南系统拆解开源仓库 machine-learning-for-trading 中频率最高、最具"成本教育"意义的案例:NASDAQ-100 微观结构(case_studies/nasdaq100_microstructure)。它以 AlgoSeek TAQ 派生的 15 分钟 bar 检验订单流、报价陈旧度、相对价差等微观结构信号能否产生可交易的日内 alpha,完整演示了"全宇宙朴素构建必然亏损 → 通过成本可行性宇宙筛选与集成模型选择两步修复 → holdout 由负转正"的全过程。读完你既能复现这条 20 个阶段的完整流水线,也能理解为何日内频率下成本地板会吞没边际收益、以及"把模型选择当作不确定下的估计"究竟意味着什么。
At a Glance:案例研究一览
| 属性 | 值 |
|---|---|
| 资产类别 | 美股(NASDAQ-100 成分股) |
| 频率 | 15 分钟 bar |
| 标的池 | 114 只股票(config/setup.yaml中声明 115 个符号,含 1 个保留项) |
| 历史区间 | 2020–2021 |
| 主标签 | fwd_ret_15m |
| CV 折数 | 2(6 个月训练 + 6 个月验证) |
| 成本模型 | per_share_plus_spread(每股 $0.0035 佣金 + 实测半价差;5 bps 摩擦地板) |
该案例是全书关于成本纪律与选择纪律的集中体现:一个在完整标的池上被摩擦成本支配的原始日内横截面信号,通过两步纪律性调整被重新激活。朴素构建(完整 NASDAQ-100、不做价差分析)下策略在全 sweep 中亏损,holdout Sharpe 为−0.89;而仔细的成本分析表明边际收益真实存在、但集中在以该节奏交易过于昂贵的标的上——筛出成本可行标的池并放弃"单一最佳模型"改用集成后,holdout 从−0.89 → −0.21 → +0.53。修复后的数字依然温和、置信区间依然很宽——该案例的教训是"纪律性调整如何改变答案",而非宣称一个可部署的稳定超额收益。
流水线:从可行性到策略评估的 20 个阶段
完整流水线见case_studies/nasdaq100_microstructure/目录,每个阶段均有.ipynb(jupytext 配对的.py版本)与章节对应:
| 阶段 | Notebook | 章节 | 职责 | 产出 |
|---|---|---|---|---|
| 可行性 | 01_feasibility_analysis | Ch6 | 按符号实测往返成本、各 horizon 的宽度与波动清除能力、收益持续性、walk-forward 演示 | liquidity_profile.parquet |
| 标签 | 02_labels | Ch7 | 15 分钟、5 分钟、60 分钟前向收益 | labels/下每个标签一个 parquet |
| 特征 | 03_financial_features | Ch8 | 订单流、价差、波动率与微观结构特征 | features/financial.parquet |
| 时序特征 | 04_model_based_features | Ch9 | 日内模式的 walk-forward 时序特征 | features/model_based.parquet |
| 评估 | 05_evaluation | Ch7–9 | 66 个财务/时序特征的 特征-标签 IC 诊断 | evaluation/triage_ledger.parquet、evaluation/ic_timeseries.parquet |
| 线性 | 06_linear | Ch11 | 全书最丰富特征空间上的 Ridge 基线 | run_log/registry.db中的训练与预测记录 |
| GBM | 07_gbm | Ch12 | 1500 万+训练样本的 15 分钟频率 LightGBM | booster、learning_curves.parquet、fold_metrics.parquet |
| NLinear | 08_dl_nlinear | Ch13 | 最小时序基线 | run_log/training/deep_learning/检查点 |
| LSTM | 09_dl_lstm | Ch13 | 短期订单流/价差动态的循环记忆 | 同上 |
| TCN | 10_dl_tcn | Ch13 | 日内时序模式的膨胀因果卷积 | 同上 |
| PatchTST | 11_dl_patchtst | Ch13 | 分钟 bar 序列上的多尺度 patch attention | 同上 |
| 因果 DML | 12_causal_dml | Ch15 | 带符号成交量占比是否"导致"未来 15 分钟收益? | registry 中一行causal_runs |
| 模型分析 | 13_model_analysis | -- | 跨模型 IC 对比与折稳定性诊断 | 只读 registry,不写产物 |
| 回测 | 14_backtest | Ch16 | 演示成本驱动失败的回测 | run_log/backtest/{hash}/下daily_returns.parquet、weights.parquet、trades.parquet、fills.parquet、equity.parquet、portfolio_state.parquet、spec.json |
| 组合 | 15_portfolio_management | Ch17 | 美元中性日内约束下的配置方法 | 每配置方法一个回测,产物布局同上 |
| 风控 | 16_risk_management | Ch19 | 日内风控与持仓级退出规则 | 每 overlay 变体一个回测 |
| 成本 | 17_costs | Ch18 | 旗舰成本分析:价差、冲击、佣金分解 | 每成本档一个回测 |
| Holdout 预测 | 18_holdout_predictions | Ch20 | 在标签 buffer 前重训所选配置,对 holdout 窗输出预测 | 一个训练运行 + 一个split='holdout'预测集 |
| Holdout 回测 | 19_holdout_backtest | Ch20 | 以既定 sizing 与 overlay 只交易一次该预测 | 一个stage='holdout'回测 |
| 策略分析 | 20_strategy_analysis | Ch20 | IC、Sharpe、成本分析的端到端策略评估 | results/strategy_assessment.json、20_strategy_synthesis/output/nasdaq100_microstructure/nasdaq100_microstructure_tearsheet.html |
配置中心:setup.yaml 中的实验骨架
与多数案例一致,本案例把一切策略假设都声明在case_studies/nasdaq100_microstructure/config/setup.yaml,notebook 只读取、不重复,保证代码与声明永不冲突。几组关键设置:
- 历史切分(
evaluation):样本从 2020-01-01 到 2021-12-31,最后 6 个月为 holdout(holdout_start: '2021-07-01');n_splits: 2、train_size: 6M、val_size: 6M,periods_per_year: 252(按日 MTM)。 - 标的池规则(
universe):eligibility_rule: nasdaq100_membership_with_pre_holdout_session——只有至少贡献了一个 holdout 前交易时段的符号才进入标的池。AlgoSeek 归档实际携带 123 个符号,其中 8 个在 2021-07-01 之后才首次报价,因此从不被拟合、其 spread 也从未被测量。01_feasibility_analysis会对声明列表与归档做双向断言,任何新增/丢失符号都会让 notebook 停下而不是悄悄改变全部横截面统计。 - 决策节奏(
decision):bar_frequency: 15_minute,decision_snapshot: bar_close,execution_delay: 1_bar,execution_price_assumption: next_bar_open_or_vwap——在一个 bar 收盘打分、在下一个 bar 成交,避免用已经使用过的价格做交易决策。注意该字段是决策节奏而非面板 bar 尺寸:观测是 1 分钟一根,03_financial_features保留每第 15 根构成决策计划;把它误读成 bar 尺寸会把答案缩小 15 倍且结果毫无异常提示。 - 成本模型(
costs):model: per_share_plus_spread,per_share: 0.0035(IBKR Pro Tiered 顶级费率,≤30 万股/月),asset_spreads_source: liquidity_profile.parquet,asset_spreads_column: median_half_spread_usd,default_half_spread_usd: 0.025(未入 profile 符号的 p75 兜底),friction_floor_bps: 5。回测为每个符号按median_half_spread_usd单独计费,而非用一个平均价差。 - 执行与组合(
execution/mapping/backtest):initial_cash: 1_000_000,share_type: integer,allocator_lookback: 520(1 分钟价格帧的 520 行,约 1.3 个交易日);mapping.class: intraday_rank_and_trade,position_state_space: long_short,sizing: dollar_neutral_or_beta_neutral;top_k_grid四个标签均为[5, 10, 20],配置器仅保留三个快配置器(equal_weight、score_weighted、inverse_vol),expensive_allocators_skip: true——15 分钟节奏下 130 万 bar 的面板使协方差估计不现实,经验上每个昂贵配置器(risk_parity、mvo_ledoit_wolf、hrp)在全部标签上都产生 −7 到 −10 的深度负 Sharpe。 - 成本 sweep(
backtest.sweep):cost_grid_bps: [0, 1, 2, 3, 5, 7, 10, 15, 20, 30, 50],每股半价差档cost_grid_half_spread_usd: [0.0, 0.005, 0.01, 0.025, 0.05, 0.10],节奏 sweepcadence_sweep: [15_minute, 30_minute, 1_hour, 4_hour]。 - 风控 overlay(
backtest.sweep.risk_controls):15 个持仓级变体,覆盖stop_loss(3%/5%/10%/15%)、trailing_stop(1%–20%)、time_exit(10/20/40 bars)。
值得注意的成本可行性列表本身也有可复现出处:case_studies/nasdaq100_microstructure/_build_cost_feasible_universe.py用往返成本代理2*(per_share/mean_price)*1e4 + 2*median_half_spread_bps在每个 split 前冻结 top-50——验证期列表基于严格早于验证窗(2020-01-01 → 2020-06-30)的报价 bar 画像,holdout 列表基于早于 2021-07-01 的 pre-holdout 流动性画像,两处均无前视。setup.yaml注释明确写道:完整 115 标的池样本外坍塌(featured config holdout −0.42),而成本可行池为正——该筛选是承重墙而非装饰。
核心结果:−0.89 → −0.21 → +0.53 的三级跳
信号质量
主标签fwd_ret_15m上最高 IC 的配置是 gbm/leaves_7_mae,IC +0.0060(HAC CI [+0.0026, +0.0094],排除零);线性 ridge_a1000000.0 紧随其后 +0.0049(CI [+0.0022, +0.0076],排除零)。IC 向短端单调增强——最高 IC 的 GBM 配置在fwd_ret_5m上达 +0.0104;而在fwd_ret_60m上 ridge_a1000000.0 的 IC 为 +0.0069,HAC CI [−0.0021, +0.0159](t=1.50, p=0.13),点估计勉强为正但 CI 横跨零。每个 horizon 上收缩最彻底的 regularizer 都取得最高 IC:小叶 MAE GBM 与 α ∈ {10⁶, 10⁷} 的 ridge。
两步调整,各只做一件事
每一步都是单一刻意的改动,且每一步都只对所选配置做一次 holdout 咨询(全部可从run_log/registry.db复现):
| 步骤 | 改动 | Holdout Sharpe | CI95 |
|---|---|---|---|
| 1. 朴素基线 | 全宇宙 + 单一最佳模型 | −0.89 | [−3.59, +2.32] |
| 2. 成本可行宇宙 | 筛到最便宜可交易标的名(每 split 冻结) | −0.21 | [−2.37, +3.64] |
| 3. 集成选择 | 用 12 模型集合的平均替代单一最佳选择 | +0.53 | [−1.94, +3.07] |
调整 1 是微观结构课:115 只面板中成本昂贵的尾部消耗掉日内边际收益,因此全宇宙样本外坍塌、而成本可行子集没有(全量 vs 筛选对比见17_costs.py)。调整 2 是选择课:各模型的验证 Sharpe 置信区间巨大且完全重叠,所以"单一最佳"的选择本身就是噪声——对模型集合取平均(集成)是在选择不确定性下的估计。集成是对抗选择噪声的稳健性装置,而非收益放大器:它把单一最佳选择的样本外亏损(−0.21)转化为一个诚实的温和正值,而不是增加 alpha。
每个 holdout CI 都很宽且横跨零(n≈128 天);这次修复是把点估计从明确为负救到勉强为正,不是显著性结果。另外朴素基线行使用方向标签fwd_dir_15m,而筛选步骤使用fwd_ret_60m;该阶梯在方向上诚实——全宇宙无论用哪个标签都深度为负——但它不是同一个策略就地调参的结果。
Holdout 闭合(集成载体)
部署配置是成本可行集成,holdout Sharpe+0.53[−1.94, +3.07](n=128 个交易日)——点估计为正,而朴素全宇宙基线(−0.89)与单一最佳成本可行选择(−0.21)均为负。但它仍低于同期对同一筛选篮子的被动等权买入持有:这次修复是走向"可行",不是走向"跑赢"——主动信号爬回正值,但打不过直接持有成本可行标的。该案例的backtest_paired_metrics没有生产者,因此策略 vs 等权配对差区间在 registry 中未填充,kill gate 2 读到的是no data而非虚假通过——gate 辅助函数把缺失 bootstrap 视为"无证据"而不是绿灯。诚实的结论是:集成恢复了正的点估计,而不是统计显著,也不是跑赢市场的边际;上文每个区间都横跨零。
摩擦地板:朴素构建为何失败
从17_costs.py的 bps 成本轨迹看(naive-lineage 预测1c4327c80284,即 linear/ridge_a1000000.0/fwd_ret_60m):Sharpe 从零成本时的 −0.78 单调走到 50 bps 尾部的 −10,CI 上界只在零 / 1 / 2 / 3 bps 档位才夹住正值。现实 NQ100 大盘股的半价差为 1–3 bps,加上每股 $0.0035 的佣金下限(约 5 bps 摩擦地板),恰好落在负值带内。这正是调整 1 背后的诊断:全宇宙下成本地板淹没边际收益,因此筛出最便宜可交易标的是承重墙而非表面功夫——昂贵尾部正是成本所在,剔除它才让 holdout 脱离地板。
(仅位置级 overlay 做不到这一点:20 行/标签的风控 sweep——trailing_stop、stop_loss、time_exit——在朴素血统的三个回归标签上全部为负。)组合级 kill switch(最大回撤熔断、日亏损上限)不参与模型选择的 sweep——其永久停机语义在早期试跑中产生了零标准差 Sharpe 伪影;它们保留为 Ch19 §19.8 的治理工具。
可行性分析:成本是"决定答案"的量
01_feasibility_analysis.py是全流水线的第一站,其核心论断是:在 15 分钟 horizon 上,价格波动与捕捉它的成本是同一数量级,成本数字错 2 倍就会改变答案。它只做测量、不拟合任何模型,回答三个问题:
- 策略交易时标的池存在吗?每 15 分钟换仓一次,因此在每个决策 bar 上必须有足够多合格标的同时报价以填满两端(最多每侧 20 只,共 40 只)。实测:声明宇宙每决策 bar 101–103 只,合格簿 46–50 只,在全部 9,778 个决策 bar 上都高于 40 只下限。
- 典型价格波动大于捕获它的成本吗?成本按符号实测而非假定——一美分价差对 $25 股票是 4 bps、对 $200 股票是 0.5 bps。实测 115 个声明符号的往返成本从最窄的 1.16 bps 到最宽的 28.17 bps,中位数 6.16 bps——超过 20 倍的离散度,单一成本水平根本不够用。在允许持有的 50 只上,5/15/60 分钟绝对中点波动的中位数分别为 9.1/15.8/30.6 bps,清除自身往返成本的比例为 0.757/0.856/0.926。
- 历史足够诚实评估吗?有效样本量按"决策 bar"而非行数计算——同一时刻观测的一百只股票共享同一市场冲击,共同部分是一个观测而不是一百个。130 万+行面板的真实分母是约 9,778 个决策 bar,比行数小三个数量级。
该 notebook 还给出三条直击日内策略的方法论要点:用中点收益而非成交价(成交价在 bid/offer 间交替产生锯齿伪波动);面板自相关必须在单个实体内部、单个交易时段内计算(跨实体合并测量的是历史衔接处,跨 session 边界测量的是滞后计数不知道的 17 小时隔夜缺口);把每笔波动除以它所属符号自己的往返成本再比较 horizon,让盈亏平衡对每只股票都为 1,一条曲线即可读全横截面。
结果标签(tags=["results"])给出的结论与前述一致:往返成本中位数 6.16 bps 高于 setup.yaml 声明的 5 bps 摩擦地板,即该地板是本宇宙收费的乐观端而非典型情形;2 折在 13,094 个决策 bar 上生成,最后一段验证止于 2021-06-30,holdout 未被读取。
成本分析:两个可归因的杠杆
17_costs.py(Ch18)是旗舰成本 notebook,把修复拆成两个可单独归因的杠杆:
- 交易哪些标的:限制到最便宜可交易标的同时改变每笔支付的费用与排序引发的交易量——昂贵标的往往排序变动最剧烈。
- 多久交易一次:降低重平衡频率把每次入场成本摊到更长的持有期上。信号不变,变的是它被执行的次数。
notebook 分三部分:Sections 1–3 在完整宇宙配置组合上做标准 bps 成本网格,追踪 Sharpe-vs-成本衰减曲线;Section 4 做全宇宙 vs 成本可行筛选的对比(第一个杠杆,直接读 featured slot 设计的 registry 既有行);Section 5 做节奏 × 每股成本的二维 sweep(第二个杠杆,也是发表级发现)——用更贴近股票的每股成本模型($/share 而非 bps)跨重平衡频率扫描。15 分钟节奏下每天约 26 个 bar,即使每腿 1 bps 也会复利成显著的年度拖累;该案例的盈亏平衡成本预期极低——总量 1–3 bps——只有做市商/自营交易台级别的机构执行质量,或把持有期延长到 4–8 bars 摊薄每笔成本,才有可能存活。
实现细节上值得注意两点:成本池取自STAGE_SEQUENCE除cost_sensitivity外的全部阶段(风险 overlay 是独立的策略候选,四个已完成的案例中有两个的 rank-1 验证配置就是risk_overlay);且池被钉死在规范宇宙上——_on_canonical_universe逐行读spec_json中的universe_filter,防止全宇宙变体(case study 明确排除的"非 canonical rank-1 / cohort / DSR 候选")混入成本曲线。
GBM 配置:交互发现 vs 线性先验
07_gbm.py指出本案例选择 GBM 的动机:订单流不平衡在不同价差状态下信息量不同——簿紧时单边流推动价格,簿宽时同样的不平衡可能是会反转的流动性提供。线性系数对不平衡只能给一个数,而树可以先按价差分裂、再在两侧分别读取不平衡。三个拨盘被同时变化:容量(num_leaves7–63,config/training/fwd_ret_15m.yaml中的leaves_7_mae到leaves_63_mse共 15 个 GBM 配置)、损失函数(mse/mae/huber,决定尾部在多大程度上操纵拟合)、何时停(每配置沿训练轨迹在 10 个检查点打分——检查点是配置的一部分,把每个配置的"最佳迭代"当作一个候选就是偷偷把十个数的最大值报成单个数)。训练配置在 config/training/fwd_ret_15m.yaml,完整 grid 覆盖 21 个线性配置(ols、13 个 ridge α、2 个 lasso、2 个 enet)、15 个 GBM 配置、4 个深度配置与 1 个因果 DML。
运行方式
从仓库根目录用 uv 依序执行(.py与.ipynb一一对应,jupytext 配对):
# 前置:uv sync 安装环境(见 envs/ 与 pyproject.toml) uv run python case_studies/nasdaq100_microstructure/01_feasibility_analysis.py uv run python case_studies/nasdaq100_microstructure/02_labels.py uv run python case_studies/nasdaq100_microstructure/03_financial_features.py uv run python case_studies/nasdaq100_microstructure/04_model_based_features.py uv run python case_studies/nasdaq100_microstructure/05_evaluation.py uv run python case_studies/nasdaq100_microstructure/06_linear.py uv run python case_studies/nasdaq100_microstructure/07_gbm.py uv run python case_studies/nasdaq100_microstructure/08_dl_nlinear.py uv run python case_studies/nasdaq100_microstructure/09_dl_lstm.py uv run python case_studies/nasdaq100_microstructure/10_dl_tcn.py uv run python case_studies/nasdaq100_microstructure/11_dl_patchtst.py uv run python case_studies/nasdaq100_microstructure/12_causal_dml.py uv run python case_studies/nasdaq100_microstructure/13_model_analysis.py uv run python case_studies/nasdaq100_microstructure/14_backtest.py uv run python case_studies/nasdaq100_microstructure/15_portfolio_management.py uv run python case_studies/nasdaq100_microstructure/16_risk_management.py uv run python case_studies/nasdaq100_microstructure/17_costs.py uv run python case_studies/nasdaq100_microstructure/20_strategy_analysis.py(18_holdout_predictions、19_holdout_backtest在选定配置后执行,用于 holdout 闭合评估。)模型训练运行、预测与回测结果均记录在内容寻址 registryrun_log/registry.db中。
结论:把教训带出案例
这个案例的迁移价值不在于 +0.53 本身,而在于三层方法论:第一,日内频率下成本假设决定结果,必须逐符号测量成本、用每个符号自己的成本缩放波动、用决策 bar 而非行数统计有效样本;第二,当模型间差异落在噪声带内时,"选最佳"是把噪声当信号,集成的价值是稳健性而非收益;第三,验证与选择分离——07_gbm的注释明言"选择发生在 14_backtest,不在这里"——每一次 holdout 咨询都必须是一次刻意的、可复现的改动,这正是从 README 到 setup.yaml 再到每个 notebook 反复强化的纪律。
【免费下载链接】machine-learning-for-tradingCode for Machine Learning for Trading, 3rd edition — from data sourcing to live execution.项目地址: https://gitcode.com/GitHub_Trending/ma/machine-learning-for-trading
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考