Status: DONE (2026-08-15) · Suite: 267 passed, 2 skipped (+7 tests in tests/test_alpaca_model.py)
What was built
data/alpaca/model.py — rolling walk-forward trainer + honest, fees-aware OOS
evaluation for the long/flat bar system. CLI: python -m data.alpaca.model.
Design (repo lessons honored)
- One model per symbol — never pooled cross-sectional crypto (that failed repeatedly in this repo).
GradientBoostingRegressorper symbol with locked defaults:n_estimators=300, max_depth=3, learning_rate=0.05, subsample=0.8, min_samples_leaf=20, random_state=0(override-able viamodel_kwargs).- Expandable walk-forward: each test block is predicted by a model trained
strictly on PRIOR rows minus the embargo (
gap_bars = config.ml.embargo_bars, PANEL-style 50-bar default). No randomized/CV splitting anywhere. - No forward-looking trains:
fitted_untiltracks the last training bar;leakage_check()(Phase 3) is invoked per split before predictions are kept.
Guardrails
walk_forward_predict: split skipped (no model) when clean training rows <min_train(200) — earlier thin-history blocks become no-trade by design;- training decimation: when clean rows >
max_train_rows(60k), a stride keeps the newest portion representative without O(n) blowup; - predictions NaN on any feature-missing row (never imputed);
AlpacaModel.fitrefuses < 100 clean rows; inf/NaN rows dropped before fit.
Evaluation (gross stats + explicit fee hurdle)
evaluate_oos on out-of-sample rows only:
- n, Pearson IC, Spearman, mean y (bps);
- top/bottom-quintile mean returns + spread + t-stat, gross PF;
- fee hurdle surfaced, never hidden:
maker_rt_bps = 2×fee_maker_pct×1e4(30 bps round trip),taker_rt_bps = 2×fee_taker_pct×1e4(50 bps) fromconfig.risk.fee_maker_pct/fee_taker_pct. Economic pass/fail lives in the Phase-5 backtester; this module only reports numbers honestly.
Persistence (models_dir/alpaca/{SYM}/{TF}/)
h{h}.pkl— fitted models + metrics + fit timestamp;h{h}_oos.parquet— full OOS[timestamp, y, pred];h{h}.json— summary (IC, top-quintile bps, fee hurdles) for reports/UIs.load_pipeline()for reuse (Phase 9 live inference will load h{h}.pkl).
CLI
python -m data.alpaca.model --symbols ETH/USD,SOL/USD --timeframes 1Min,5Min \
--h {config.ml.prediction_horizon} --n-splits {walk_forward_splits} \
--gap {embargo_bars} [--start YYYY-MM-DD] [--end YYYY-MM-DD]
Tests (tests/test_alpaca_model.py, isolated dirs → tmp)
- fit/predict alignment with NaN-on-gap; fit refuses dirty rows;
- walk-forward OOS: injected-signal dataset reaches IC ≈ 1 with positive top-quintile means; noise-only dataset gives IC ≈ 0 (sanity floor);
- early thin splits skipped without breaking the pipeline;
run_pipelinepersists (pkl/json/parquet) andload_pipelinereads back;evaluate_oosrespects configurable maker/taker fees.
Known caveats
- GBR hyperparameters are sensible defaults, not tuned — Phase 5's backtester is the arbitrating cost model; tuning only after the honest baseline exists.
- Very early splits often skip on 1Min history (warmup + min_train) — that is intended: thin-history models are worse than no signal.