208 lines
8.9 KiB
YAML
208 lines
8.9 KiB
YAML
strategy_id: us_equities_panel
|
||
setup_version: v1
|
||
|
||
decision:
|
||
cadence: daily_close
|
||
snapshot: close
|
||
execution_delay: next_bar_open
|
||
|
||
universe:
|
||
n_assets: 3199
|
||
|
||
# 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.
|
||
#
|
||
# Spec-hash inputs (each invalidates every backtest_hash on change):
|
||
# - execution.initial_cash
|
||
# - execution.share_type
|
||
# - execution.allocator_lookback (CS-level fallback for moment allocators)
|
||
# - per-allocator overrides in backtest.sweep.allocators (e.g. lookback,
|
||
# vol_window, max_weight)
|
||
#
|
||
# ``allocator_lookback`` is the bars-of-underlying-price window applied
|
||
# uniformly to every moment-based allocator. Daily US equity bars → 63
|
||
# ≈ 3 months. ``mvo_ledoit_wolf`` carries an explicit per-allocator
|
||
# override below (126 bars ≈ 6 months) so N/K shrinkage degeneracy doesn't
|
||
# collapse it to identity-target at top_k=50.
|
||
#
|
||
# ``initial_cash`` restored to 1_000_000 (2026-05-16) — the 2026-05-15 SSOT
|
||
# migration drop to 100k caused catastrophic degenerate output here (top_k=50
|
||
# leg suspect; integer-share rounding × high-priced names). See
|
||
# memory/feedback_2026_05_15_equity_sizing_invalidated.md.
|
||
execution:
|
||
initial_cash: 1_000_000 # restores prior validated state
|
||
share_type: integer # US equities trade in whole shares
|
||
allocator_lookback: 63 # 3 months of daily bars (IV/RP/HRP fallback)
|
||
|
||
mapping:
|
||
class: long_short_decile_rebalance
|
||
position_state_space: long_short
|
||
entry_logic: decile_sort_long_top_short_bottom
|
||
sizing: equal_weight_within_decile
|
||
|
||
costs:
|
||
class: material
|
||
model: percentage # Loader path: bps regime via per_leg_cost_bps_range midpoint.
|
||
components: [spread, commission, market_impact, borrow_cost]
|
||
per_leg_cost_bps_range: [5, 20]
|
||
# per_share is the commission rate for the exploratory per-share
|
||
# cost-sensitivity regime (read by Ch18 18_costs.py and the run_sweep
|
||
# planner). IBKR Pro Tiered top tier. NOT used in the headline bps
|
||
# regime — bps regime is the production cost model.
|
||
per_share: 0.0035
|
||
# Documentation-only: loader reads top-level per_leg_cost_bps_range. The
|
||
# era split is qualitative — see feasibility §B.4 for why flat bps suffices
|
||
# given the validation window starts 2000-01-12 (pre-decimal era is ~6.5%
|
||
# of the window).
|
||
era_dependent:
|
||
pre_decimalization:
|
||
period: before 2001-01-29
|
||
per_leg_cost_bps_range: [15, 30]
|
||
note: Tick size 1/16 ($0.0625); wider spreads, higher commissions.
|
||
post_decimalization:
|
||
period: after 2001-01-29
|
||
per_leg_cost_bps_range: [5, 15]
|
||
note: Penny tick regime; electronic trading, lower spreads.
|
||
borrow_cost_note: Long-short requires borrow for the short leg (~50 bps/yr).
|
||
|
||
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_cheap: 0 # score_weighted, inverse_vol — all signal preds
|
||
allocation_expensive: 10 # risk_parity, mvo_ledoit_wolf, hrp — top-10 by signal 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
|
||
# Allocators routed through ``allocation_expensive`` (top-10 by signal Sharpe).
|
||
# All others (equal_weight, score_weighted, inverse_vol) take the cheap path.
|
||
expensive_allocators: [risk_parity, mvo_ledoit_wolf, hrp, conformal_weighted]
|
||
# Ch16 signal-stage selection. Long-short equal-weight top-k on three
|
||
# label horizons (1d primary, 5d/21d variants). The wider top-k grid
|
||
# reflects the larger universe (~3000 names vs ETFs' 100).
|
||
top_k_grid:
|
||
fwd_ret_1d: [20, 50]
|
||
fwd_ret_5d: [20, 50]
|
||
fwd_ret_21d: [20, 50]
|
||
# Ch17 portfolio: reuses top_k_grid above and sweeps over allocators.
|
||
# Moment-based allocators (IV/RP/HRP) use the CS-level
|
||
# ``execution.allocator_lookback`` (63 bars). ``mvo_ledoit_wolf`` gets
|
||
# an explicit 126-bar override (6 months) so N/K ≥ 2.5 at top_k=50 and
|
||
# Ledoit-Wolf shrinkage doesn't collapse to identity-target.
|
||
# SKIP_EXPENSIVE_ALLOC filters mvo_ledoit_wolf and hrp at notebook
|
||
# level when requested.
|
||
# No max_weight cap — see memory/feedback_max_weight_caps_intentionally_absent.md
|
||
# (capping near top_k forces equal-weight and defeats the comparison).
|
||
allocators:
|
||
- {name: equal_weight, method: equal_weight}
|
||
- {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, lookback: 126}
|
||
- {name: hrp, method: hrp}
|
||
- {name: conformal_weighted, method: conformal_weighted}
|
||
# Ch18 cost sensitivity (bps regime — headline). A companion per-share
|
||
# regime is run from Ch18 cost notebooks for regime comparison; for
|
||
# us_equities_panel it is exploratory only because the wide price
|
||
# distribution + adjusted-price confounder make a flat-default
|
||
# half-spread an artifact rather than realistic friction.
|
||
cost_grid_bps: [0, 1, 2, 3, 5, 7, 10, 15, 20, 30, 50]
|
||
# Companion per-share half-spread grid (USD per share). Swept alongside
|
||
# cost_grid_bps by the planner. Values: 0¢, 0.5¢, 1¢, 2.5¢, 5¢, 10¢.
|
||
cost_grid_half_spread_usd: [0.0, 0.005, 0.01, 0.025, 0.05, 0.10]
|
||
# 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}
|
||
|
||
evaluation:
|
||
n_splits: 16
|
||
train_size: 10Y
|
||
val_size: 1Y
|
||
holdout_start: '2016-01-01'
|
||
holdout_end: '2018-03-31'
|
||
calendar: NYSE
|
||
periods_per_year: 252 # NYSE 5d/wk
|
||
|
||
labels:
|
||
primary: fwd_ret_1d
|
||
buffer: 1D
|
||
variants:
|
||
- fwd_ret_5d
|
||
- fwd_ret_21d
|
||
variant_buffers:
|
||
fwd_ret_5d: 5D
|
||
fwd_ret_21d: 21D
|
||
# Vectorized-backtest thinning step per label: number of schedule slots
|
||
# to advance per trade so holding periods don't overlap.
|
||
rebalance_step:
|
||
fwd_ret_1d: 1
|
||
fwd_ret_5d: 5
|
||
fwd_ret_21d: 21
|
||
|
||
modeling:
|
||
gbm:
|
||
libraries: [lightgbm]
|
||
preset: default
|
||
device: cpu
|
||
latent_factors:
|
||
persistent_entities: true
|
||
# Down-tune the SDF schedule on this large-N CS so training time stays
|
||
# in the overnight budget. Paper defaults (256/64/1024) on broad US
|
||
# equities at daily frequency would take many hours per fold.
|
||
model_kwargs:
|
||
sdf:
|
||
n_epochs_unc: 128
|
||
n_epochs_moment: 32
|
||
n_epochs_cond: 512
|
||
burn_in_epochs: 32
|
||
checkpoint_epochs: [128, 256, 384, 512, 640]
|
||
beta_checkpoint_epochs: [256]
|
||
beta_default_checkpoint: 256
|
||
|
||
causal:
|
||
treatment: past_ret_12m_skip
|
||
confounders: [vol_21d, amihud_illiq, volume_ratio]
|
||
method: walk_forward_dml
|
||
|
||
# Read by 20_strategy_analysis.py to gate the headline strategy summary.
|
||
kill_conditions:
|
||
ic_floor: 0.01
|
||
ic_floor_note: Cross-sectional IC below 0.01 across all features.
|
||
edge_to_cost_floor: 1.2
|
||
edge_to_cost_note: Net Sharpe / cost ratio below 1.2x.
|
||
micro_cap_concentration: 0.5
|
||
micro_cap_note: Alpha concentrated >50% in bottom ADV quintile (untradeable).
|
||
net_sharpe_floor: 0.3
|
||
net_sharpe_note: Net Sharpe after borrow costs below 0.3.
|