Concevoir un cadre de recherche diversifié multi-actifs sur les contrats à terme CME
Résumé
Cette configuration définit un univers de recherche hebdomadaire sur les contrats à terme couvrant les indices boursiers, les obligations d’État, l’énergie, les métaux, les devises, les produits agricoles et l’élevage. Elle fixe le règlement du vendredi comme instantané de décision et l’ouverture du lundi comme point d’exécution. Le cadre de stratégie classe les produits selon le carry ou le momentum et prend en charge des positions long-short avec une pondération à risque égal ou en notionnel. Les hypothèses de coûts varient selon la liquidité du produit et incluent les commissions, les spreads et le slippage de rollover.
La configuration précise également les fenêtres de caractéristiques pour le momentum, la volatilité, le carry, la tendance et les mesures connexes ; les rangs et composites en centiles par date ; les choix d’allocation de portefeuille ; la sensibilité aux coûts ; ainsi que les contrôles de risque par stop, stop suiveur et durée. Elle exclut deux séances, car des fichiers boursiers manquants laissent des univers de produits incomplets, rendant les rangs transversaux incomparables. La configuration documente les contraintes pratiques de mise en œuvre, comme les contrats entiers, la marge par produit, les coûts de trading et les seuils de rééquilibrage. Elle définit un cadre expérimental plutôt que de présenter les performances d’une stratégie ; ses choix de paramètres ne démontrent donc pas la rentabilité d’un signal ou d’une méthode d’allocation.
Idées clés
- L’univers combine des produits à terme 30 CME couvrant sept grands secteurs de marché.
- Les signaux sont évalués chaque semaine à partir des cours de règlement, l’exécution étant reportée à l’ouverture suivante du lundi.
- Les rangs de carry et de momentum constituent la base déclarée des positions long-short, tandis que la pondération peut se faire selon le risque ou le notionnel.
- Les séances où l’univers de produits est incomplet sont exclues, car les rangs en centiles calculés sur un univers partiel ne seraient pas comparables.
- La configuration explicite les coûts, la marge, la granularité des contrats, les méthodes d’allocation et les contrôles de risque, mais ne fournit aucun élément sur les performances.
Étiquettes
Texte intégral
# setup.yaml
```yaml
strategy_id: cme_futures
setup_version: v1
# 30 CME products grouped by sector. Tickers are CME root symbols.
universe:
product_groups:
equity_index:
- ES # E-mini S&P 500
- NQ # E-mini Nasdaq-100
- YM # E-mini Dow Jones Industrial Average
- RTY # E-mini Russell 2000
treasuries:
- ZN # 10-year T-Note
- ZB # 30-year T-Bond
- ZF # 5-year T-Note
- ZT # 2-year T-Note
energy:
- CL # WTI Crude Oil
- NG # Henry Hub Natural Gas
- HO # NY Harbor ULSD (heating oil)
- RB # RBOB Gasoline
metals:
- GC # Gold
- SI # Silver
- HG # Copper
- PL # Platinum
currencies:
- 6E # Euro / US Dollar
- 6J # Japanese Yen / US Dollar
- 6B # British Pound / US Dollar
- 6A # Australian Dollar / US Dollar
- 6C # Canadian Dollar / US Dollar
- 6S # Swiss Franc / US Dollar
agriculture:
- ZC # Corn
- ZS # Soybeans
- ZW # Wheat (Chicago SRW)
- ZM # Soybean Meal
- ZL # Soybean Oil
livestock:
- LE # Live Cattle
- HE # Lean Hogs
- GF # Feeder Cattle
n_products: 30
# Two sessions where one clearing venue's settlement file is absent. On each, the
# missing products are missing at every tenor, and the two missing sets partition the
# universe exactly: 2020-02-28 carries no CBOT, NYMEX or COMEX product (YM, the four
# treasuries, five grains, four energy, four metals) and 2014-06-13 carries no CME
# product (three equity index, six currencies, three livestock). Neighbouring sessions
# trade normally, so this is a missing file rather than a thin market. Eleven of the
# levels in stage 03 are carried as a percentile within the decision date, and a
# percentile taken over one venue's products is not comparable with one taken over
# thirty, so both dates are dropped rather than ranked against a partial universe.
excluded_sessions:
- 2020-02-28
- 2014-06-13
decision:
cadence: weekly_friday_close
snapshot: settlement_price
execution_delay: monday_open
# Engine-level execution defaults. Single source of truth — Ch16-19 notebooks
# read these via get_backtest_config(); never declare a local INITIAL_CASH
# or share_type constant. Changing values here invalidates every existing
# backtest_hash for this case study (cash + share_type are spec-hash inputs).
#
# ``initial_cash`` is sized so the full 30-product CME universe can fill
# positions under integer-share rules across the top_k_grid (5, 10) in
# long-short mode. The binding constraint is the high-notional equity-
# index e-minis: at top_k=10 long-short the per-position dollar budget
# is 5% × cash, and integer-rounding zeros out any product whose notional
# exceeds that budget. ES holdout notional ≈ $347k and NQ ≈ $513k drive
# the floor: $10M (per-position $500k) clears ES in the holdout and NQ
# in the validation window. NQ in the very-late holdout (Dec 2025,
# notional $513k) still integer-rounds out for that one product at
# top_k=10 LS — a residual integer-share footnote documented in the
# README "Margin model" section.
#
# Per-product initial+maintenance margin (futures_specs.yaml::
# {initial,maintenance}_margin_pct → ContractSpec.margin_pct) is wired
# through Engine.from_config(contract_specs=...); the engine auto-
# populates the broker's margin_pct_schedule so the margin draw scales
# with each bar's notional rather than the account-wide 50% default.
# allow_leverage is set in config/backtest/base.yaml.
#
# ``allocator_lookback`` is the bars-of-underlying-price window applied
# uniformly to every moment-based allocator (inverse_vol, risk_parity, hrp,
# mvo_ledoit_wolf). CME daily settlement bars → 63 ≈ 3 months. Keeps
# allocators comparable.
execution:
initial_cash: 10_000_000 # sized for full 30-product universe at top_k_grid
share_type: integer # Futures contracts are integer
allocator_lookback: 63 # 3 months of daily CME settlement bars
mapping:
class: long_short_carry_rank
position_state_space: long_short
entry_logic: rank_by_carry_or_momentum
sizing: equal_risk_or_notional
costs:
class: material
components: [commission, spread, roll_slippage]
commission_per_contract: 2.0
spread_ticks:
liquid: 1
illiquid: 2
# Products charged the illiquid spread: live cattle, feeder cattle and soybean oil
# quote wider than the rest of the universe. Everything else pays the liquid one tick.
illiquid_products: [LE, GF, ZL]
backtest:
rebalance:
# A rebalance is skipped when the per-asset weight change is below
# min_weight_change AND the resulting trade notional is below
# min_trade_value. The benchmark profile disables thresholds so that
# full-universe equal-weight (1/N per asset) rebalances at all.
default:
min_weight_change: 0.005
min_trade_value: 100.0
benchmark:
min_weight_change: 0.0
min_trade_value: 0.0
sweep:
# Iteration controls per stage. ``signal: 0`` means "all predictions";
# downstream stages take the top-N from the upstream stage's rank-1.
# Notebooks read these via get_top_n_predictions(case_study, stage).
top_n_predictions:
signal: 0 # all signal predictions (eq-weight baseline)
allocation: 10 # top-10 model configs by equal-weight baseline Sharpe
cost_sensitivity: 1 # top-1 of {signal+allocation} per label
risk_overlay: 1 # top-1 of {signal+allocation} per label
# Skip MVO/HRP when allocator runtime is the bottleneck (intraday CSes).
expensive_allocators_skip: false
# All others (equal_weight, score_weighted, inverse_vol) take the cheap path.
# Ch16 signal-stage selection. Long-short equal-weight top-k on both
# label horizons; long-short is enforced at the backtest config level
# (allow_short_selling=true).
# 30-product universe with long_short=True: top_k=20 selects 20 longs ∪
# 20 shorts (40 positions) from 30 products → degenerate (collapses to
# ~zero trades). Cap at 10 = floor(30/3) to keep selections non-degenerate.
top_k_grid:
fwd_ret_5d: [5, 10]
fwd_ret_21d: [5, 10]
# Ch17 portfolio: reuses top_k_grid above and sweeps over allocators.
# Moment-based allocators (IV/RP/HRP/MVO_LW) all use the CS-level
# ``execution.allocator_lookback`` — no per-allocator vol_window /
# lookback fields. No max-weight cap.
# No equal_weight entry. Equal weight IS the Chapter 16 baseline, and `stage` is not
# part of `backtest_hash`, so an equal-weight run at the allocation stage is a no-op
# reweight that hashes identically to its baseline parent - whichever writes first
# claims the row and the other is lost. Measured in this case study's own pre-rebuild
# store: 48 rows carrying `stage='signal'` with `allocation.method='equal_weight'`,
# and zero `allocation`/`equal_weight` rows, which downstream tooling then reported
# as both missing and unexpected trials. The baseline is already in the pool that the
# later stages select over.
allocators:
- {name: score_weighted, method: score_weighted}
- {name: inverse_vol, method: inverse_vol}
- {name: risk_parity, method: risk_parity}
- {name: mvo_ledoit_wolf, method: mvo_ledoit_wolf}
- {name: hrp, method: hrp}
- {name: conformal_weighted, method: conformal_weighted}
# Ch18 cost sensitivity (bps; commission + exchange fees + slippage
# combined).
cost_grid_bps: [0, 1, 2, 3, 5, 7, 10, 15, 20, 30, 50]
# Ch19 risk overlays.
risk_controls:
position:
- {name: stop_loss_3pct, type: stop_loss, threshold: 0.03}
- {name: stop_loss_5pct, type: stop_loss, threshold: 0.05}
- {name: stop_loss_10pct, type: stop_loss, threshold: 0.10}
- {name: stop_loss_15pct, type: stop_loss, threshold: 0.15}
- {name: trailing_1pct, type: trailing_stop, threshold: 0.01}
- {name: trailing_2pct, type: trailing_stop, threshold: 0.02}
- {name: trailing_3pct, type: trailing_stop, threshold: 0.03}
- {name: trailing_5pct, type: trailing_stop, threshold: 0.05}
- {name: trailing_10pct, type: trailing_stop, threshold: 0.10}
- {name: trailing_15pct, type: trailing_stop, threshold: 0.15}
- {name: trailing_20pct, type: trailing_stop, threshold: 0.20}
- {name: time_exit_10, type: time_exit, bars: 10}
- {name: time_exit_20, type: time_exit, bars: 20}
- {name: time_exit_40, type: time_exit, bars: 40}
# The feature specification for 03_financial_features. Every window the notebook
# uses is declared here and read from here, and each family carries the ten fields
# the register renders: what it reads, how far back, with what delay, and what it
# is claimed to do. ``role`` separates a signal — something the carry thesis says
# ranks products against each other — from a state variable describing the
# environment a ranking is formed in. Windows are CME settlement sessions.
features:
windows:
momentum: [21, 63, 126, 252] # each carries a risk-adjusted twin
short_return: 5 # the weekly return, no Sharpe twin
volatility: [21, 63, 126]
skip_recent: 21 # skip-month momentum runs t-252 to t-21
skip_start: 252
carry_smoothing: 21
carry_zscore: [63, 126]
carry_momentum: [5, 21]
trend_sign: [63, 126, 252]
moving_average: [21, 50, 200]
high: 252 # dist_from_52w_high
low: 126 # dist_from_6m_low
rsi: 14
yang_zhang: 21
variance_ratio: {horizon: 5, window: 63}
# Source column -> the name its within-date percentile is written under. The
# names predate this register and later stages select by them, so the mapping
# is explicit rather than a suffix rule.
ranked:
ret_21d: mom_rank_21d
ret_63d: mom_rank_63d
ret_126d: mom_rank_126d
ret_252d: mom_rank_252d
sharpe_63d: sharpe_rank_63d
sharpe_126d: sharpe_rank_126d
sharpe_252d: sharpe_rank_252d
vol_21d: vol_rank
rsi_14: rsi_rank
carry_pct: carry_rank
curve_curvature_norm: curvature_rank
composites:
momentum_composite: [mom_rank_63d, mom_rank_126d, mom_rank_252d]
sharpe_composite: [sharpe_rank_63d, sharpe_rank_126d, sharpe_rank_252d]
thresholds:
carry_regime_band: 0.01 # carry, on the x12 scale, separating the two regimes from flat
signal_long: 60 # percentile of the carry-momentum composite
signal_short: 40
roll_proximity_day: 25 # calendar day from which the roll week is flagged
# Planting, harvest, heating and cooling cycles are documented for these two
# sectors and not for the financial ones.
seasonal_sectors: [agriculture, energy]
families:
- name: term structure
pattern: carry_pct|carry_21d|carry_momentum_*|carry_zscore_*|carry_regime_num|curve_curvature_norm|curvature_21d
role: signal
hypothesis: A product whose curve is in backwardation earns the roll the shape implies
inputs: unadjusted front, second and third settlement prices
lookback: 146
lag: 0
frame: term structure
representation: front-to-next spread on a x12 scale, its smoothed level, z-score and change
failure_mode: the spread inverts around a supply shock faster than the smoothing follows
- name: momentum
pattern: ret_*d|skip_month_mom|mom_accel_*|ts_mom_*
role: signal
hypothesis: Trends in futures persist over horizons of a quarter to a year
inputs: roll-adjusted settlement price
lookback: 252
lag: 0
frame: time series
representation: simple return, its sign, and differences between horizons
failure_mode: reverses over the most recent month, which skip-month momentum drops
- name: risk-adjusted momentum
pattern: sharpe_[0-9]*d
role: signal
hypothesis: A trend earned with less dispersion repeats more reliably than a raw one
inputs: log return
lookback: 252
lag: 0
frame: time series
representation: mean log return over its own dispersion, annualized
failure_mode: unbounded as dispersion approaches zero
- name: volatility and regime
pattern: vol_[0-9]*d|vol_ratio_*|vol_yz_*|vr_*d
role: state
hypothesis: Dispersion and the trending-or-reverting regime set what a ranking can earn
inputs: OHLC settlement bars, log return
lookback: 126
lag: 0
frame: time series
representation: annualized standard deviation, ratios of windows, variance ratio
failure_mode: close-to-close misses the overnight gap Yang-Zhang is here to catch
- name: trend and range
pattern: rsi_[0-9]*|ma_ratio_*|dist_from_*
role: signal
hypothesis: Price against its own recent path separates a trend from an exhausted one
inputs: roll-adjusted settlement price
lookback: 252
lag: 0
frame: time series
representation: bounded oscillator, ratio to a moving average or a rolling extreme
failure_mode: saturates in a sustained trend, where every product reads the same
- name: calendar and season
pattern: month_*|day_of_year_norm|quarter|roll_proximity|is_seasonal_sector
role: state
hypothesis: Agricultural and energy curves move with planting, harvest and heating cycles
inputs: the session date and the product's sector
lookback: 1
lag: 0
frame: calendar
representation: circular month encoding, quarter, roll-week and sector flags
failure_mode: says when in the year it is and never which product to prefer
- name: cross-sectional position
pattern: '*_rank|mom_rank_*|sharpe_rank_*|carry_rank_sector'
role: signal
hypothesis: Only relative standing is tradable in a long-short cross-sectional strategy
inputs: the levels this register's other families produce
lookback: 252
lag: 0
frame: cross-section
representation: percentile within the decision date and contract position
failure_mode: discards the level the state families carry instead
- name: composite
pattern: momentum_composite|sharpe_composite|carry_mom_composite|carry_mom_interaction|risk_adj_score|ls_signal
role: signal
hypothesis: Carry and momentum are separate premia, so their agreement is itself information
inputs: the percentiles of the cross-sectional family
lookback: 252
lag: 0
frame: cross-section
representation: mean of percentiles, their product, and a banded discretization
failure_mode: hides which of the two components moved the score
# What `04_model_based_features` decides, declared here for the same reason the
# feature windows above are: an estimation window is part of a fitted feature's
# definition, so it belongs where the definition lives rather than inside the
# notebook that runs it. Every count is in trading sessions.
#
# A fitted feature is bounded by the schedule below and not by a walk-forward
# period. The parameters behind a value are estimated from sessions strictly
# before it, refreshed on the cadence given, and the same value comes out
# whichever period later selects the row - so the artifact carries no fold column.
#
# The spectral features in section C.2 have no entry here. A Fourier transform of
# a window estimates nothing, so it has no estimation window to declare; its
# lookback is a lookback like the ones under `features.windows`.
model_based:
arima:
# Sessions of a product's own carry z-score spent before the first forecast.
# A trading year, so the first order is chosen from a full year of the series
# rather than from a few weeks of it. The walk was already causal at this
# cadence before the schedule was declared here; what moved is where the two
# numbers live.
burnin: 252
# How often the weights are re-estimated. Monthly.
refit_every: 21
# (p, d, q), declared rather than selected per window.
#
# This replaced AutoARIMA's stepwise search, and the measurement that decided it is
# worth stating because it is the reason rather than the justification. Sampling
# eight refit cutoffs on each of the 30 eligible products, 240 searches in all,
# AutoARIMA returned 34 distinct orders and NO product held a single order across
# its own walk. The modal choice (2,0,1) took 11.7% of searches and (2,0,2) 10.4%;
# the best any product managed was 6 of its 8 cutoffs on one order. An order that
# changes almost every month on an expanding window of the same series is the
# information criterion moving with the sample, not structure being found, and it
# makes "the ARIMA carry forecast" a different quantity in every block.
#
# (2,0,1) is the modal selection and the more parsimonious of the two that are
# indistinguishable from each other.
#
# d=0 is by construction, not by search: carry_zscore is already a rolling z-score
# clipped to +/-5, so it is stationary before this model sees it. 83% of the searches
# agreed (199 of 240 chose d=0); the 17% that differenced it were over-differencing a
# bounded series, which is a known failure of automatic selection rather than a
# property of carry.
order: [2, 0, 1]
hmm:
# Backwardation and contango.
n_states: 2
# Sessions of the book-wide carry average before the regime chain is fitted.
# Two years. A two-state chain has to see both states switch several times
# before its transition matrix means anything, and this is one market-level
# series rather than a panel, so the burn-in is paid once for the whole
# notebook rather than once per product.
#
# The average itself begins 2011-04-27, after the carry smoothing and z-score
# windows, and the oldest period trains from the first session of the panel -
# so this burn-in comes out of that period's training window rather than
# ahead of it. The first regime value lands 2013-04-09: 504 of the 1,978
# sessions in that period's training window, and no evaluation session, since
# the earliest evaluation window opens 2019-01-03. The walk makes 44
# estimates before the holdout freezes it.
burnin: 504
# How often the chain is re-estimated. A quarter, as in etfs and fx_pairs:
# regime parameters are the slowest-moving thing this notebook fits.
refit_every: 63
evaluation:
n_splits: 5
train_size: 8Y
val_size: 1Y
holdout_start: '2024-01-01'
holdout_end: '2025-12-31'
calendar: CME
periods_per_year: 252 # CME 5d/wk
labels:
primary: fwd_ret_5d
buffer: 5D
variants:
- fwd_ret_21d
variant_buffers:
fwd_ret_21d: 21D
# Vectorized-backtest thinning step per label: number of schedule slots
# to advance per trade so holding periods don't overlap. Authored from
# (schedule cadence, label horizon); add an entry here for any new label.
rebalance_step:
fwd_ret_5d: 1 # weekly_friday schedule, 5d horizon <= 7d gap
fwd_ret_21d: 3 # weekly_friday schedule, 21d horizon -> ceil(21/7) = 3
modeling:
gbm:
libraries: [lightgbm]
preset: default
device: cpu
# LightGBM's own CPU default. 63 is the GPU default and was carried over with the
# device when these runs moved off the GPU, so every CPU fit was quantizing the design
# matrix into a quarter of the bins the library would have used. Coarser bins are
# faster and lose split points; the reader running this on a CPU gets what the
# documentation describes.
max_bin: 255
latent_factors:
persistent_entities: true
# Declared, not inferred. Without this key case_study.py:93 falls back to
# preferred_latent_device(), which is `cuda` where torch sees a GPU and `cpu` where it
# does not. device enters computation.runtime and computation.numerical_runtime, both
# inside the hashed computation, so the same declared configuration resolves a different
# training identity on a GPU host than on a CPU one and each publishes under the same
# population name. 10b is neural and takes this; 10a overrides it to cpu, because
# PCAModel is numpy and scipy with no device argument and recording cuda there would
# describe a computation that did not happen.
device: cuda
model_kwargs:
sdf:
checkpoint_epochs: [256, 512, 768, 1024] # conditional-relative; publishes global 256..1280
beta_checkpoint_epochs: [256]
beta_default_checkpoint: 256
causal:
treatment: carry_pct
# Bars the treatment's own construction window spans, which is what the placebo block has
# to cover: permuting carry_pct in blocks shorter than this destroys the serial
# dependence the refutation exists to preserve, and the resulting p-value reads like a
# refutation without being one. Declared here rather than inferred, because guessing which
# element of a window list a column was built from puts a wrong number behind a right-looking
# one. Derived from the construction, not chosen:
#
# `(c0_price - c1_price) / c0_price * 12` in 04_model_based_features, from two prices at
# the same timestamp. Nothing rolls, so the construction window spans one bar, which is what
# this field declares. It is not a claim that the column is serially independent: carry_pct is
# highly persistent, and 12_model_analysis measures that profile. Where the construction window
# is the shorter of the two scales, as it is here, the label buffer is what sizes the block -
# `block_size = max(label_buffer, treatment_window)`.
treatment_window: 1
confounders: [vol_21d, momentum_composite, carry_rank]
method: walk_forward_dml
```Reproduit dans son intégralité avec attribution, conformément à la licence de la source. Licence: MIT
Ce résumé a été rédigé par l’agent de recherche de Stratmill à partir de la source originale ; il n’en est pas une copie.