The Risk Layer
Four small, pure components — a sizer, a vol-targeter, an exposure gatekeeper, and a kill-switch — composed inside every strategy and consulted on every bar. Signal never reaches the broker without passing through them.
In plain terms
Even a good idea can blow up if you bet too big or too often. The risk layer decides how much to trade, refuses trades that would over-concentrate the book, and pulls a master switch that flattens everything if losses get too deep.
Technically
Each Strategy owns one PositionSizer, RiskGatekeeper, and
KillSwitch (momentum also a vol_target). They are plain Python, unit-tested
in isolation — there is no separate Nautilus "risk actor"; the layer is composed into the
strategy so backtest and live behave identically.
Four guards
| Component | Role | Key call | Default limits |
|---|---|---|---|
PositionSizer | fixed-fractional size from the stop distance | calculate(equity, entry, stop) | risk 1% / trade |
vol_target | scale size toward a target volatility | vol_scale(realized, target) | 0.25×–2.0× |
RiskGatekeeper | hard exposure / concentration / loss caps | check_order(...) | gross 80% · net 60% · conc 25% |
KillSwitch | portfolio drawdown halt (latches) | update_equity(equity) | 25% drawdown |
The order path
When a strategy decides to enter, sizing and risk run in a fixed order: size from the stop, scale by volatility, cap by concentration and budget, then submit to the gatekeeper. Only an allowed check reaches the OMS.
_enter_long order path. The gatekeeper is a hard backstop wider than the
sizer's own budget — belt and suspenders. A blocked check is logged and the order is skipped, never forced.# Fixed-fractional: risk a fixed % of equity on the distance to the stop qty = int( equity * risk_per_trade / (entry - stop) ) # Gatekeeper caps checked in order (all must pass): # trade_risk / equity <= max_loss_per_trade (2%) # |gross| <= max_gross_exposure (80%) # |net| <= max_net_exposure (60%) # instrument <= max_concentration (25%)
The kill-switch
Every bar updates the high-water mark and current drawdown. Cross the threshold and the switch latches — it flattens the book and stays triggered until a human resets it. In live trading its state is persisted atomically across restarts.
None (clean, in-memory per run).Hard Rule 5: a strategy may not enter without a defined stop, and the risk layer is obeyed before any signal. The per-bar ordering is always kill-switch → manage open position (hard stop first) → then evaluate a new entry.
References
- src/risk/position_sizer.py —
PositionSizer.calculate(fixed-fractional, notional ceiling). - src/risk/vol_target.py —
vol_scale,annualized_vol,returns_from_equity(fails neutral at 1.0×). - src/risk/gatekeeper.py —
RiskGatekeeper.check_order,RiskCheck, exposure/concentration/loss caps. - src/risk/kill_switch.py —
KillSwitch.update_equity,is_triggered,reset, atomic state persistence. - src/strategies/cross_sectional_momentum.py —
on_barordering and the_enter_longorder path.