Files
2026-07-13 13:26:28 +08:00

6.9 KiB
Raw Permalink Blame History

Case Study: ETF Cross-Asset Exposures

This case study applies the ML4T workflow to 100 exchange-traded funds spanning equities, fixed income, commodities, currencies, and real estate. ETFs offer a clean laboratory for cross-asset rotation: standardized pricing, deep liquidity, and broad asset-class coverage at a single rebalance cadence.

The configuration is the most cost-favorable in the book — long-only rank-and-rebalance, monthly month-end decisions on a 21-day forward-return label, with a 5--15 bps-per-leg cost model. That cadence makes it the natural setting for the broadest model-family comparison in the book: linear, GBM, tabular DL, sequence DL, latent factors, and causal DML are all trained on the same feature panel. The teaching point is the gap between IC and Sharpe — the family with the highest rank correlation is not the family with the highest portfolio Sharpe — which makes ETFs the canonical setting for the "portfolio construction mediates prediction quality" thread that runs through Ch16--Ch20.

At a Glance

Property Value
Asset Class Multi-asset ETFs
Frequency Daily data, monthly decisions
Universe 100 ETFs across 9 categories
History 2006--2025
Primary Label fwd_ret_21d
CV Folds 8 (10Y train, 1Y val)
Cost Model Material (5--15 bps per leg)

Pipeline

Stage Notebook Chapter Description
Feasibility 01_feasibility_analysis Ch6 Universe breadth, point-in-time eligibility, horizon-cost feasibility, walk-forward demonstration
Labels 02_labels Ch7 21-day and 5-day forward returns with walk-forward splits
Features 03_financial_features Ch8 Momentum, volatility, and cross-asset ranking features
Temporal 04_model_based_features Ch9 ARIMA, HMM, and spectral features from walk-forward fits
Evaluation 05_evaluation Ch7--9 Feature-label IC diagnostics across all engineered features
Linear 06_linear Ch11 Ridge, LASSO, ElasticNet baseline for cross-asset momentum
GBM 07_gbm Ch12 LightGBM with Optuna testing non-linear interactions
Tabular DL 08_tabular_dl Ch12 TabM rank-1 adapter MLP ensemble
LSTM 09_dl_lstm Ch13 Temporal gating over sequential ETF return windows
TSMixer 10_dl_tsmixer Ch13 Cross-asset lead-lag patterns via time-feature mixing
Latent Factors 11_latent_factors Ch14 Factor extraction across the ETF universe
Causal DML 12_causal_dml Ch15 Does momentum cause future ETF returns or reflect confounders?
Model Analysis 13_model_analysis Ch11--15 Cross-family IC comparison, checkpoint sensitivity, fold stability
Backtest 14_backtest Ch16 Strategy simulation with falsification against equal-weight
Portfolio 15_portfolio_management Ch17 Score-weighted, risk-parity, inverse-vol, MVO, HRP, and conformal-weighted allocation
Costs 16_costs Ch18 Transaction cost impact on the momentum edge
Risk 17_risk_management Ch19 Position-level stop-loss, trailing-stop, and time-exit overlays calibrated against the in-sample MAE distribution
Strategy Analysis 18_strategy_analysis Ch20 End-to-end strategy assessment with IC, Sharpe, and cost analysis

Key Results

Signal quality: Daily-pooled IC for the highest-Sharpe configuration is +0.052 [+0.009, +0.095] (HAC t=2.37, p=0.018, excludes zero on the positive side); pct-positive is 56.4%, modestly above coin flip. The rank-correlation prior is statistically resolved at the validation window, with magnitude small but credibly nonzero.

Strategy-stage performance with CIs: The cross-stage rank-1 configuration is deep_learning/lstm_h64 on fwd_ret_21d resolved at the risk-overlay stage (risk-parity top-20 + MAE-calibrated trailing overlay trailing_mae_p25_h20_4p3pct). Validation Sharpe is +1.21 [+0.61, +1.87], PSR p=0.00029 — both Sharpe CI and PSR exclude zero on the positive side. Selection-adjusted DSR (effective-rank) is +0.072 (p=4.1\mathrm{e}{-5}) on the 20-variant family cohort, min_trl_periods is 461 (the 2016-day validation window clears the MinTRL bar by ≈4×), and PBO is 0.157 across 8 folds × 70 combinations — modest in-sample overfitting on the combinatorially shuffled folds but well inside the "low" band; the cross-stage label cohort (714 variants spanning every family × allocator × cost × overlay) carries DSR_ER +0.067 on the same leader.

Holdout closure: Validation→holdout Sharpe difference is -0.14 [-1.87, $+1.53$] (p=0.886, straddles zero with an extremely wide CI — the 481-day holdout cannot resolve decay magnitude under the disjoint-window pairing convention). Against an equal-weight benchmark, the holdout-period Sharpe difference is -0.34 [-1.69, $+0.81$] (p=0.582); the two-sided test does not reject. Holdout EW Sharpe runs at +1.36 — unusually high, driven by the 2024--2025 broad-equity rally where cross-asset rotation toward bonds and commodities gave back ground to a static equity-weighted universe.

Friction floor: Cost sensitivity scans 11 levels from 0 to 50 bps per leg. The highest-Sharpe configuration stays positive across the full grid; median Sharpe across all configurations stays positive through realistic ETF friction (≤5 bps). Both kill gates pass — validation Sharpe lower bound ≥ 0, and holdout strategy CI does not exclude zero negatively.

Running

# From repo root
uv run python case_studies/etfs/01_feasibility_analysis.py
uv run python case_studies/etfs/02_labels.py
uv run python case_studies/etfs/03_financial_features.py
uv run python case_studies/etfs/04_model_based_features.py
uv run python case_studies/etfs/05_evaluation.py
uv run python case_studies/etfs/06_linear.py
uv run python case_studies/etfs/07_gbm.py
uv run python case_studies/etfs/08_tabular_dl.py
uv run python case_studies/etfs/09_dl_lstm.py
uv run python case_studies/etfs/10_dl_tsmixer.py
uv run python case_studies/etfs/11_latent_factors.py
uv run python case_studies/etfs/12_causal_dml.py
uv run python case_studies/etfs/13_model_analysis.py
uv run python case_studies/etfs/14_backtest.py
uv run python case_studies/etfs/15_portfolio_management.py
uv run python case_studies/etfs/16_costs.py
uv run python case_studies/etfs/17_risk_management.py
uv run python case_studies/etfs/18_strategy_analysis.py

Run Log

Model training runs, predictions, and backtest results are tracked in a content-addressed registry under run_log/registry.db.