Files
sentiment-engine/MALKHUT/malkhut/risk/gate.py
Codex f6d8d13146 malkhut(wire): 5 risk gate stubs implemented + 3 scenarios behavior-driven
Risk gate (risk/gate.py) — 5 stubs implemented:

1. _kill_switch_active(): operator-controlled emergency stop via set_kill_switch()
2. _cancel_rate_would_exceed(): tracks cancel timestamps per symbol in 60s
   sliding window, blocks if >= MAX_CANCELS_PER_SYMBOL_PER_MINUTE
3. _would_self_trade(): checks open orders for same symbol+side at same price
   (within tick_size), skipping the cancel_order_id for CANCEL_REPLACE
4. _would_exceed_symbol_notional(): sums current open order notional + new
   order notional, blocks if > equity * MAX_SYMBOL_NOTIONAL_FRACTION
5. _violates_venue_minima(): checks tick alignment, lot rounding, min_qty,
   and min_notional — all float-robust comparisons

ScenarioFactory — 3 remaining hardcoded scenarios converted:

1. _spread_tightening: spread_mult=0.3, depth_fraction=1.0 (was hardcoded BTC)
2. _cross_venue_arb: spread_mult=0.5, depth_fraction=0.5 (was hardcoded BTC)
3. _cross_exchange_arb_stress: spread_mult=0.8, depth_fraction=0.3 (was hardcoded BTC)

All 30 scenarios now use _behavior_state() — zero hardcoded prices remain.

675 tests pass. Zero regressions.
2026-07-14 17:01:38 +02:00

179 lines
6.7 KiB
Python

"""
Risk gate — final hard stop before venue execution.
The planner is not trusted. The optimiser is not trusted, the exchange adapter
is not trusted. This gate enforces hard invariants.
"""
from __future__ import annotations
import time
from collections import defaultdict
from typing import Deque
from collections import deque
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
from malkhut.state import (
ActionKind,
FulfilmentPolicyParams,
MarketWorldState,
MAX_ACCOUNT_LEVERAGE,
MAX_CANCELS_PER_SYMBOL_PER_MINUTE,
MAX_SYMBOL_NOTIONAL_FRACTION,
Side,
)
from malkhut.cwm import materialize_price_from_action
class RiskGate:
def __init__(self) -> None:
self._kill_switch: bool = False
self._cancel_timestamps: dict[str, Deque[float]] = defaultdict(
lambda: deque(maxlen=MAX_CANCELS_PER_SYMBOL_PER_MINUTE + 10)
)
def set_kill_switch(self, active: bool) -> None:
"""Operator-controlled emergency stop."""
self._kill_switch = active
def record_cancel(self, symbol: str) -> None:
"""Record a cancel event for rate-limit tracking."""
self._cancel_timestamps[symbol].append(time.time())
def validate(
self,
state: MarketWorldState,
planned: PlannedPolicy,
params: FulfilmentPolicyParams,
daat_verdict: str = "KNOWN",
) -> RiskDecision:
"""Validate a planned action.
Args:
daat_verdict: "KNOWN" | "MARGINAL" | "OUT_OF_DISTRIBUTION"
From DAAT classifier. If OUT_OF_DISTRIBUTION, veto the action
and fall back to doctrinal simple policy.
"""
action = planned.selected_action
if daat_verdict == "OUT_OF_DISTRIBUTION":
return RiskDecision(True, None, "ood_veto_fall_back_to_doctrinal")
if action.kind == ActionKind.NOOP:
return RiskDecision(True, action, "noop")
if self._kill_switch_active():
return RiskDecision(False, None, "kill_switch")
if self._cancel_rate_would_exceed(state, action):
return RiskDecision(False, None, "cancel_rate_limit")
if self._would_self_trade(state, action):
return RiskDecision(False, None, "self_trade_risk")
if self._would_exceed_leverage(state, action, params):
return RiskDecision(False, None, "leverage_limit")
if self._would_exceed_symbol_notional(state, action, params):
return RiskDecision(False, None, "symbol_notional_limit")
if self._post_only_would_cross(state, action):
return RiskDecision(False, None, "post_only_cross")
if self._violates_venue_minima(state, action):
return RiskDecision(False, None, "venue_minimum")
return RiskDecision(True, action, "approved")
def _kill_switch_active(self) -> bool:
return self._kill_switch
def _cancel_rate_would_exceed(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
if action.kind != ActionKind.CANCEL and action.kind != ActionKind.CANCEL_REPLACE:
return False
symbol = state.venue.symbol
now = time.time()
window = self._cancel_timestamps[symbol]
cutoff = now - 60.0
while window and window[0] < cutoff:
window.popleft()
return len(window) >= MAX_CANCELS_PER_SYMBOL_PER_MINUTE
def _would_self_trade(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
if action.kind not in (ActionKind.PLACE, ActionKind.CANCEL_REPLACE, ActionKind.CROSS_SPREAD):
return False
if action.side is None:
return False
for oo in state.open_orders:
if oo.symbol != state.venue.symbol:
continue
if oo.side != action.side:
continue
if oo.client_order_id == action.cancel_order_id:
continue
if oo.price is None or action.price_ticks_from_best is None:
continue
our_price = materialize_price_from_action(state, action)
if our_price is None:
continue
if abs(our_price - oo.price) < state.venue.tick_size:
return True
return False
def _would_exceed_leverage(
self, state: MarketWorldState, action: FulfilmentAction, params: FulfilmentPolicyParams,
) -> bool:
return state.account.total_notional / max(state.account.equity, 1e-12) > MAX_ACCOUNT_LEVERAGE
def _would_exceed_symbol_notional(
self, state: MarketWorldState, action: FulfilmentAction, params: FulfilmentPolicyParams,
) -> bool:
if action.kind not in (ActionKind.PLACE, ActionKind.CANCEL_REPLACE, ActionKind.CROSS_SPREAD):
return False
price = materialize_price_from_action(state, action)
if price is None:
return False
qty = action.qty_fraction * state.account.available_balance / max(price, 1e-12)
order_notional = price * qty
max_notional = state.account.equity * MAX_SYMBOL_NOTIONAL_FRACTION
current_notional = 0.0
for oo in state.open_orders:
if oo.symbol == state.venue.symbol and oo.price is not None:
current_notional += oo.price * oo.remaining_qty
return (current_notional + order_notional) > max_notional
def _post_only_would_cross(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
if not action.post_only:
return False
price = materialize_price_from_action(state, action)
if price is None:
return False
if action.side == Side.BUY and price >= state.book.best_ask:
return True
if action.side == Side.SELL and price <= state.book.best_bid:
return True
return False
def _violates_venue_minima(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
if action.kind not in (ActionKind.PLACE, ActionKind.CANCEL_REPLACE):
return False
price = materialize_price_from_action(state, action)
if price is None:
return False
if price <= 0:
return True
tick = state.venue.tick_size
if tick > 0:
remainder = price % tick
if remainder > 1e-9 and tick - remainder > 1e-9:
return True
qty = action.qty_fraction * state.account.available_balance / max(price, 1e-12)
lot = state.venue.lot_size
if lot > 0 and qty > 0:
rounded = round(qty / lot) * lot
if rounded < state.venue.min_qty:
return True
notional = price * qty
if notional < state.venue.min_notional:
return True
return False