Status: DONE | Date: 2026-08-15 | Stack: Phase 1 raw store -> data/alpaca/features.py
Scope
Per-symbol feature frames over the raw bar + sparse-quote partitions, built exactly to the feed reality measured in Phase 1: ETH ~3.3k / SOL ~3k quotes per day (~2-3/minute), so most 1Min bars have no quote in-bar. Design choice: every quote feature is the last-known quote at-or-before the bar close, plus an explicit staleness column — we report liquidity honestly instead of backfilling fake freshness.
Feature catalogue (24 columns)
- Price/momentum: r_1/3/5/10/20/60, mom_20/60/120, hi_dist_20, lo_dist_20
- Volatility/shape: ret_std_10/30, atr_14, range_pct, range_ratio_20, body_pct, upper/lower_wick_pct, mp_position
- Volume/intensity: log_vol, vol_ratio_20, log_trade_count, tc_ratio_20, vwap_dev, vol_per_trade_ratio
- Calendar (24/7): hour_sin, hour_cos, dow
- Sparse-quote microstructure (as-of bar close): spread_bps, microprice_dev_bps, imb, log_bid_size, log_ask_size, quote_age_s, quotes_in_bar
Deliberately absent: aggressor-side / order-flow imbalance features — Alpaca crypto trades carry no aggressor side and there is no historical deep book (Phase-1 limitation). Documented, not assumed away.
No-look-ahead guarantees
- All rolling windows trailing with
min_periods;atr_14,mom_*,ret_std_*etc. are NaN on the first rows (never shifted/centered). - Quotes merged with
merge_asof(..., direction="backward"); a quote att+1safter bar-close can only affect bars from the next close onwards (tested explicitly). assert_no_future_leak()structural guard ships alongside; warmup rows are NaN by construction.
Files
| File | Purpose |
|---|---|
data/alpaca/features.py | builders, schema, persist/load, leak guard |
tests/test_alpaca_features.py | 10 offline tests (synthetic bars/quotes injected) |
data/processed/alpaca/features/{SYM}/{TF}/v1.parquet | saved feature frames |
Tests
10 passed new; full suite 251 passed, 2 skipped. Coverage: rolling
first-row NaN, r_1 construction, spread/microprice/imb math, backward-asof
no-future-leak, in-bar quote counting, staleness monotonicity, raw-store-
driven build, persistence roundtrip, strict symbol rejection.
Real-data smoke (3-day live store, 2026-08-12→15)
| Symbol | TF | rows | spread cover | mean quote age | atr_14 mean | r_1 NA |
|---|---|---|---|---|---|---|
| ETH/USD | 1Min | 1,413 | 99.9% | 152s | 0.00063 | 1 |
| ETH/USD | 5Min | 651 | 100% | 203s | 0.00101 | 1 |
| SOL/USD | 1Min | 1,453 | 99.9% | 147s | 0.00090 | 1 |
| SOL/USD | 5Min | 680 | 99.9% | 182s | 0.00151 | 1 |
Row counts exactly reproduce the stored 2,872 1Min bars across both symbols.
Findings / implications
- Spread coverage near 100% even though the feed is sparse — the as-of design pays off — but the mean quote age of ~150-200s on 1Min bars is the feature that actually encodes the sparse feed. Models must learn that stale-quote microstructure ≠ fresh.
- ETH 1Min ATR ~0.06% of price vs SOL ~0.09%; SOL 5Min ATR ~0.15% rounds out Phase-1's spread observation (SOL ~45bps spread). Entry costs dominate SOL at this scale — relevant to the backtest/position-sizing phases.
Next steps
- Phase 3: labels (forward open->open returns), dataset assembly with warmup/embargo trimming, walk-forward splits and a leakage guard.
- Extend the historical store for training (bars 2021→, quotes SOL 2024-10→) — only a 3-day live window is stored so far.