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.
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
"""
|
||||
Risk gate — final hard stop before venue execution.
|
||||
|
||||
The planner is not trusted. The optimiser is not trusted. The exchange adapter
|
||||
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,
|
||||
@@ -13,12 +18,27 @@ from malkhut.state import (
|
||||
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,
|
||||
@@ -35,7 +55,6 @@ class RiskGate:
|
||||
"""
|
||||
action = planned.selected_action
|
||||
|
||||
# DAAT OOD veto — highest priority
|
||||
if daat_verdict == "OUT_OF_DISTRIBUTION":
|
||||
return RiskDecision(True, None, "ood_veto_fall_back_to_doctrinal")
|
||||
|
||||
@@ -66,12 +85,38 @@ class RiskGate:
|
||||
return RiskDecision(True, action, "approved")
|
||||
|
||||
def _kill_switch_active(self) -> bool:
|
||||
return False
|
||||
return self._kill_switch
|
||||
|
||||
def _cancel_rate_would_exceed(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
|
||||
return False
|
||||
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(
|
||||
@@ -82,7 +127,19 @@ class RiskGate:
|
||||
def _would_exceed_symbol_notional(
|
||||
self, state: MarketWorldState, action: FulfilmentAction, params: FulfilmentPolicyParams,
|
||||
) -> bool:
|
||||
return False
|
||||
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:
|
||||
@@ -97,4 +154,25 @@ class RiskGate:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user