Status: DONE (2026-08-15) · Suite: 294 passed, 2 skipped (+13 tests in tests/test_alpaca_risk.py)
What was built
data/alpaca/risk.py — vol-targeted sizing + wall-clock trading guardrails
for the long/flat bar system.
Sizing (no leverage, spot only)
risk_budget = equity * risk_per_trade (config 2%)
qty = risk_budget / ATR (coin units)
notional = min(qty * price, equity * max_position_pct) (cap: 10%)
- A 1-ATR adverse move costs ≤
risk_per_tradeof equity before the position cap; the cap truncates further concentration. min_notionalfloor (default $10): undersized orders are SKIPPED, never rounded up (a crypto min-trade guardrail).qty_for_notionalrejects non-positive notional/qty — long/flat enforced at the boundary.
Guardrails (RiskManager, state machine)
| Gate | Behavior |
|---|---|
daily loss limit (max_drawdown_daily, 5%) | halts until the next UTC day |
total drawdown (max_drawdown_total, 15%) | halts until equity recovers above peak × (1-thresh); computed from running peak, not sticky |
consecutive-loss cooldown (cooldown_after_loss, 4) | after N losses in a row, skip N trades (a win serves one) |
max daily trades (max_daily_trades, 12) | halts for the day |
| no equity | hard halt |
record_trade(pnl, ts)updates equity, running peak, daily counters, and cooldowns;can_trade(ts)returns(go, reason)with the specific gate inreason;halt_reason()strips it for logs.- Timestamps are wall-clock aware: day boundaries roll in UTC; cooldowns are trade-counted, not time-based.
Tests
13 offline tests: exact notional math (cap binding, 1-ATR cost = risk budget when uncapped), min-notional skip, degenerate inputs → 0, non-negative qty, long-only realized PnL, daily gate + UTC day roll, cooldown arming/serving, computed (recoverable) drawdown halt, max-daily-trades, peak tracking, config defaults, halt_reason helper.
Integration
Phase 7's paper executor consumes RiskManager + size() per signal before
ordering: gate → size → market/limit order. The Phase-8 monitor reports the
halt reasons; the same guards apply to live trading.