strategy_id: nasdaq100_microstructure setup_version: v1 universe: symbols: - AAL - AAPL - ADBE - ADI - ADP - ADSK - AEP - ALGN - ALXN - AMAT - AMD - AMGN - AMZN - ANSS - ASML - ATVI - AVGO - BIDU - BIIB - BKNG - BMRN - CDNS - CDW - CERN - CHKP - CHTR - CMCSA - COST - CPRT - CSCO - CSGP - CSX - CTAS - CTSH - CTXS - DLTR - DOCU - DXCM - EA - EBAY - EXC - EXPE - FAST - FB - FISV - FOX - FOXA - GILD - GOOG - GOOGL - IDXX - ILMN - INCY - INTC - INTU - ISRG - JD - KDP - KHC - KLAC - LBTYA - LBTYK - LRCX - LULU - MAR - MCHP - MDLZ - MELI - MNST - MRNA - MRVL - MSFT - MTCH - MU - MXIM - NFLX - NTAP - NTES - NVDA - NXPI - OKTA - ORLY - PAYX - PCAR - PDD - PEP - PTON - PYPL - QCOM - REGN - ROST - SBUX - SGEN - SIRI - SNPS - SPLK - SWKS - TEAM - TMUS - TSLA - TTWO - TXN - UAL - ULTA - VRSK - VRSN - VRTX - WBA - WDAY - WDC - WLTW - XEL - XLNX - ZM n_assets: 114 eligibility_rule: nasdaq100_membership # Cost-feasible universe: the cheapest-to-trade names by round-trip cost, # frozen per split. Provenance: _build_cost_feasible_universe.py # (round-trip proxy 2*(per_share/mean_price)*1e4 + 2*median_half_spread_bps). # The validation list is profiled on quote bars strictly before the # validation window (2020-01-01 -> 2020-06-30); the holdout list from the # pre-holdout liquidity profile (< 2021-07-01). No look-ahead. Selected via # strategy.signal.universe_filter='cost_feasible'; the split-specific list is # chosen at backtest time by resolving the prediction set's split (NOT a # spec-hash input — like the 'liquid' filter, the hash carries only the # filter name). The full 114 universe collapses out of sample (featured # config holdout -0.42) while the cost-feasible universe is positive; the # screen is load-bearing. See 16_costs.py for the full-vs-screened contrast. cost_feasible: validation: - AAPL - MSFT - FB - SBUX - PEP - ATVI - INTC - PYPL - MDLZ - AMD - QCOM - TXN - GILD - MU - NVDA - AMAT - CSCO - TMUS - WBA - COST - JD - CSX - XEL - CMCSA - EXC - EBAY - MNST - FISV - FAST - EA - CTSH - NFLX - XLNX - AMZN - ADBE - PAYX - CDNS - AMGN - MXIM - BIDU - PCAR - CERN - UAL - ADP - KHC - FOXA - WDC - TSLA - FOX - CTXS holdout: - AAPL - MSFT - FB - SBUX - PEP - GILD - AMD - QCOM - INTC - MDLZ - ATVI - AMAT - MU - PYPL - AEP - TXN - JD - CSX - MRVL - COST - TMUS - CMCSA - EBAY - CSCO - NVDA - XEL - FISV - WBA - EXC - FAST - TSLA - CTSH - MXIM - ADI - AMZN - NFLX - AMGN - EA - PAYX - CERN - ADBE - ADP - KHC - CDNS - XLNX - MNST - KDP - BIDU - DLTR - UAL decision: bar_frequency: 15_minute decision_snapshot: bar_close execution_delay: 1_bar execution_price_assumption: next_bar_open_or_vwap # 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 # # ``allocator_lookback`` is the bars-of-underlying-price window applied # uniformly to every moment-based allocator. 15-min bars (26 bars/day); # 520 bars ≈ 20 trading days ≈ 1 month of intraday coverage. ``periods_per_year`` # annualizes Sharpe at daily-equivalent grain (252); allocator windows are # measured in raw 15-min bars regardless. # # Expensive allocators (RP/HRP/MVO_LW) are dropped via expensive_allocators_skip # below — covariance estimation at 15-min cadence is prohibitive and # empirically degenerate. MVO_LW carries no per-allocator lookback override # here for that reason; revisit if expensive allocators are reintroduced # (a 6-month equivalent at 15-min would need ~3,300 bars). # # ``initial_cash`` restored to 1_000_000 (2026-05-16) — the 2026-05-15 SSOT # migration drop to 100k caused catastrophic degenerate output here # (avg Sharpe -11 across 339 signal runs; integer-share rounding × the # nasdaq-100 high-priced tail (BKNG≈$5k, MSTR/NFLX/AVGO $700-1.5k)). 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: 520 # ~20 trading days of 15-min bars mapping: class: intraday_rank_and_trade position_state_space: long_short entry_logic: rank_or_threshold sizing: dollar_neutral_or_beta_neutral costs: class: dominant # Per-share commission plus measured per-asset half-spread slippage from # AlgoSeek NBBO close quotes over the training window. Integer share # sizing. The asset_spreads_source parquet is loaded by the backtest # cost-preset builder and joined per symbol; default_half_spread_usd is # the universe p75 fallback for symbols not in the profile. model: per_share_plus_spread per_share: 0.0035 # IBKR Pro Tiered top tier (<=300K shares/mo) minimum: 0.0 asset_spreads_source: liquidity_profile.parquet asset_spreads_column: median_half_spread_usd default_half_spread_usd: 0.025 spread_convention: half_spread source: AlgoSeek minute-bar NBBO close quotes, training window 2020-01-02 to 2021-07-01. friction_floor_bps: 5 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: # Canonical strategy universe. The 15-min round-trip cost on the # cost-expensive tail of the 114-name panel consumes the intraday edge: # the featured config collapses out of sample on the full universe # (holdout -0.42) but is positive on the cost-feasible subset (the # cheapest-to-trade names, frozen per split — see universe.cost_feasible). # The full-universe variant is NOT a canonical rank-1 / cohort / DSR # candidate; it lives only in the 16_costs.py full-vs-screened comparison, # mirroring sp500_options' liquid screen. Read by # case_studies.utils.sweep_config.get_universe_filters_for. universe_filter: cost_feasible # 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: 0 # nasdaq has no expensive allocators (see expensive_allocators below) cost_sensitivity: 1 # top-1 of {signal+allocation} per label risk_overlay: 1 # top-1 of {signal+allocation} per label # No expensive allocators on nasdaq. At 15-min frequency the 1.3M-bar # panel makes covariance estimation on the rebalance schedule # prohibitive; empirically every expensive allocator (risk_parity, # mvo_ledoit_wolf, hrp) produces deeply negative Sharpes (-7 to -10) # on every nasdaq label and equal_weight ties at the top. The 15-min # cadence is a costs-dominated regime where signal-stage selection # carries the entire story. expensive_allocators_skip: true expensive_allocators: [] # Ch16 signal-stage selection. Long-short equal-weight top-k on the # 15-min schedule across four labels (5m/15m/60m continuous + 15m # direction). The classification label fwd_dir_15m is a first-class # signal alongside the continuous labels and carries the same top_k # grid. top_k_grid: fwd_ret_5m: [5, 10, 20] fwd_ret_15m: [5, 10, 20] fwd_ret_60m: [5, 10, 20] fwd_dir_15m: [5, 10, 20] # Ch17 portfolio: reuses top_k_grid above and sweeps over allocators. # Cheap tier only on nasdaq (see ``expensive_allocators`` above). # No max_weight cap on inverse_vol — see # memory/feedback_max_weight_caps_intentionally_absent.md (would force # equal-weight at top_k=5 and defeat the moment-allocator comparison). allocators: - {name: equal_weight, method: equal_weight} - {name: score_weighted, method: score_weighted} - {name: inverse_vol, method: inverse_vol} # Ch18 cost sensitivity (bps regime). cost_grid_bps: [0, 1, 2, 3, 5, 7, 10, 15, 20, 30, 50] # Companion per-share cost regime: dollars per share half-spread. Swept # alongside cost_grid_bps for CSes whose declared cost model is # per_share_plus_spread. 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] # Alternative rebalance cadences explored by Ch18's cadence × cost # heatmap (16_costs.py Section 4). The first entry must be the CS's # default cadence (``decision.bar_frequency``). Each cadence reuses the # ``cost_grid_half_spread_usd`` grid above. Tokens are the same cadence # vocabulary recognized by the engine; the notebook pulls the list from # here, never hardcoded. cadence_sweep: [15_minute, 30_minute, 1_hour, 4_hour] # nasdaq100 v4 slot-mechanism sweep (Ch16 signal stage replacement). # # Adds two selection methods to the canonical Ch16 expansion: # - slot_persistent_signal_exit: per-symbol rolling-percentile entry, # fixed weight-per-slot allocation (slots ARE the allocation — # Ch17 allocator stage is skipped for these rows), max-hold + # optional signal-exit (rolling lower quantile crossover) for # position lifecycle. See case_studies/utils/slot_strategy.py for # the mechanism. Sub-params live under selection_method_config in # the flattened scheme dict. # - eq_w_topk: the canonical equal-weight top-K baseline, kept as # a within-sweep comparison. Direction grid covers long_only, # long_short. # # slot × long_short is intentionally dropped — slot books are # single-direction by construction (one symbol cannot occupy a long # AND short slot simultaneously). The cross-asset long-short story # belongs to eq_w_topk / quintile_long_short. # # Per-prediction cardinality: # slot: long_q(3) × max_slots(3) × hold_bars(3) × exit_signal_q(4) # × bars_per_day_grid(1) = 108 (long_only only) # eq_w_topk: top_k(3) × direction(2) = 6 # ~114 per prediction × ~12 representative preds ≈ ~1,400 backtests # for the signal stage on val (less than the original 2,800 estimate # because slot × long_short is dropped). signal_nasdaq100: selection_method: [slot_persistent_signal_exit, eq_w_topk] long_q: [0.90, 0.95, 0.99] direction: [long_only, long_short] max_slots: [5, 10, 20] hold_bars: [8, 16, 32] # 2h, 4h, 8h at 15-min grid exit_signal_q: [null, 0.30, 0.50, 0.70] # null disables signal-exit pred_freshness_max_min: 14 # variant-A: freshest 1m pred per 15m bar bars_per_day_grid: [14] # 15m only; 1m cadence failed comprehensively top_k_grid: [5, 10, 20] lookback_days: 21 # 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: 2 train_size: 6M val_size: 6M holdout_start: '2021-07-01' holdout_end: '2021-12-31' calendar: NYSE periods_per_year: 252 # NYSE 5d/wk (daily MTM despite intraday signal cadence) labels: primary: fwd_ret_15m buffer: 15min variants: - fwd_ret_5m - fwd_ret_60m - fwd_dir_15m variant_buffers: fwd_ret_5m: 5min fwd_ret_60m: 60min fwd_dir_15m: 15min # Vectorized-backtest thinning step per label: number of schedule slots # to advance per trade so holding periods don't overlap. rebalance_step: fwd_ret_15m: 1 fwd_ret_5m: 1 fwd_ret_60m: 4 # 15min schedule, 60min horizon -> ceil(60/15) = 4 fwd_dir_15m: 1 # Continuous return that each classification label is derived from. classification_eval_label: fwd_dir_15m: fwd_ret_15m modeling: gbm: libraries: [lightgbm] preset: default device: cpu causal: treatment: signed_vol_share confounders: [rel_spread_close, rv_5m, r1m] method: walk_forward_dml