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.
|
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.
|
is not trusted. This gate enforces hard invariants.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
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.actions import FulfilmentAction, PlannedPolicy, RiskDecision
|
||||||
from malkhut.state import (
|
from malkhut.state import (
|
||||||
ActionKind,
|
ActionKind,
|
||||||
@@ -13,12 +18,27 @@ from malkhut.state import (
|
|||||||
MarketWorldState,
|
MarketWorldState,
|
||||||
MAX_ACCOUNT_LEVERAGE,
|
MAX_ACCOUNT_LEVERAGE,
|
||||||
MAX_CANCELS_PER_SYMBOL_PER_MINUTE,
|
MAX_CANCELS_PER_SYMBOL_PER_MINUTE,
|
||||||
|
MAX_SYMBOL_NOTIONAL_FRACTION,
|
||||||
Side,
|
Side,
|
||||||
)
|
)
|
||||||
from malkhut.cwm import materialize_price_from_action
|
from malkhut.cwm import materialize_price_from_action
|
||||||
|
|
||||||
|
|
||||||
class RiskGate:
|
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(
|
def validate(
|
||||||
self,
|
self,
|
||||||
state: MarketWorldState,
|
state: MarketWorldState,
|
||||||
@@ -35,7 +55,6 @@ class RiskGate:
|
|||||||
"""
|
"""
|
||||||
action = planned.selected_action
|
action = planned.selected_action
|
||||||
|
|
||||||
# DAAT OOD veto — highest priority
|
|
||||||
if daat_verdict == "OUT_OF_DISTRIBUTION":
|
if daat_verdict == "OUT_OF_DISTRIBUTION":
|
||||||
return RiskDecision(True, None, "ood_veto_fall_back_to_doctrinal")
|
return RiskDecision(True, None, "ood_veto_fall_back_to_doctrinal")
|
||||||
|
|
||||||
@@ -66,12 +85,38 @@ class RiskGate:
|
|||||||
return RiskDecision(True, action, "approved")
|
return RiskDecision(True, action, "approved")
|
||||||
|
|
||||||
def _kill_switch_active(self) -> bool:
|
def _kill_switch_active(self) -> bool:
|
||||||
return False
|
return self._kill_switch
|
||||||
|
|
||||||
def _cancel_rate_would_exceed(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
|
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:
|
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
|
return False
|
||||||
|
|
||||||
def _would_exceed_leverage(
|
def _would_exceed_leverage(
|
||||||
@@ -82,7 +127,19 @@ class RiskGate:
|
|||||||
def _would_exceed_symbol_notional(
|
def _would_exceed_symbol_notional(
|
||||||
self, state: MarketWorldState, action: FulfilmentAction, params: FulfilmentPolicyParams,
|
self, state: MarketWorldState, action: FulfilmentAction, params: FulfilmentPolicyParams,
|
||||||
) -> bool:
|
) -> 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:
|
def _post_only_would_cross(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
|
||||||
if not action.post_only:
|
if not action.post_only:
|
||||||
@@ -97,4 +154,25 @@ class RiskGate:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _violates_venue_minima(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
|
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
|
return False
|
||||||
|
|||||||
@@ -645,7 +645,7 @@ class ScenarioFactory:
|
|||||||
return Scenario(
|
return Scenario(
|
||||||
scenario_id=f"tighten_{symbol}_{seed}",
|
scenario_id=f"tighten_{symbol}_{seed}",
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
initial_state=self._make_state(symbol, bid=49950.0, ask=50050.0, bid_qty=2.0, ask_qty=2.0, exchange_id=self.exchange_id),
|
initial_state=self._behavior_state(symbol, spread_mult=0.3, depth_fraction=1.0, exchange_id=self.exchange_id),
|
||||||
counterparties=self.counterparties,
|
counterparties=self.counterparties,
|
||||||
max_steps=steps,
|
max_steps=steps,
|
||||||
tags=("spread_tightening", "competition"),
|
tags=("spread_tightening", "competition"),
|
||||||
@@ -726,7 +726,7 @@ class ScenarioFactory:
|
|||||||
return Scenario(
|
return Scenario(
|
||||||
scenario_id=f"arb_{symbol}_{seed}",
|
scenario_id=f"arb_{symbol}_{seed}",
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
initial_state=self._make_state(symbol, bid=49990.0, ask=50010.0, bid_qty=0.5, ask_qty=0.5, exchange_id=self.exchange_id),
|
initial_state=self._behavior_state(symbol, spread_mult=0.5, depth_fraction=0.5, exchange_id=self.exchange_id),
|
||||||
counterparties=(LatencyArbPolicy(lead_threshold=0.3), ToxicTakerPolicy(sensitivity=0.4)),
|
counterparties=(LatencyArbPolicy(lead_threshold=0.3), ToxicTakerPolicy(sensitivity=0.4)),
|
||||||
max_steps=steps,
|
max_steps=steps,
|
||||||
tags=("arbitrage", "cross_venue", "price_discovery"),
|
tags=("arbitrage", "cross_venue", "price_discovery"),
|
||||||
@@ -834,7 +834,7 @@ class ScenarioFactory:
|
|||||||
return Scenario(
|
return Scenario(
|
||||||
scenario_id=f"arb_stress_{symbol}_{seed}",
|
scenario_id=f"arb_stress_{symbol}_{seed}",
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
initial_state=self._make_state(symbol, bid=49980.0, ask=50020.0, bid_qty=0.3, ask_qty=0.3, exchange_id=self.exchange_id),
|
initial_state=self._behavior_state(symbol, spread_mult=0.8, depth_fraction=0.3, exchange_id=self.exchange_id),
|
||||||
counterparties=(LatencyArbPolicy(lead_threshold=0.2), ToxicTakerPolicy(sensitivity=0.4)),
|
counterparties=(LatencyArbPolicy(lead_threshold=0.2), ToxicTakerPolicy(sensitivity=0.4)),
|
||||||
max_steps=steps,
|
max_steps=steps,
|
||||||
tags=("cross_exchange", "arb_stress", "price_discovery"),
|
tags=("cross_exchange", "arb_stress", "price_discovery"),
|
||||||
|
|||||||
Reference in New Issue
Block a user