2026-07-11 10:31:31 +02:00
|
|
|
"""
|
|
|
|
|
Risk gate — final hard stop before venue execution.
|
|
|
|
|
|
2026-07-14 17:01:38 +02:00
|
|
|
The planner is not trusted. The optimiser is not trusted, the exchange adapter
|
2026-07-11 10:31:31 +02:00
|
|
|
is not trusted. This gate enforces hard invariants.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-14 17:01:38 +02:00
|
|
|
import time
|
|
|
|
|
from collections import defaultdict
|
|
|
|
|
from typing import Deque
|
|
|
|
|
from collections import deque
|
|
|
|
|
|
2026-07-11 10:31:31 +02:00
|
|
|
from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision
|
|
|
|
|
from malkhut.state import (
|
|
|
|
|
ActionKind,
|
|
|
|
|
FulfilmentPolicyParams,
|
|
|
|
|
MarketWorldState,
|
|
|
|
|
MAX_ACCOUNT_LEVERAGE,
|
|
|
|
|
MAX_CANCELS_PER_SYMBOL_PER_MINUTE,
|
2026-07-14 17:01:38 +02:00
|
|
|
MAX_SYMBOL_NOTIONAL_FRACTION,
|
2026-07-11 10:31:31 +02:00
|
|
|
Side,
|
|
|
|
|
)
|
|
|
|
|
from malkhut.cwm import materialize_price_from_action
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RiskGate:
|
2026-07-14 17:01:38 +02:00
|
|
|
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())
|
|
|
|
|
|
2026-07-11 10:31:31 +02:00
|
|
|
def validate(
|
|
|
|
|
self,
|
|
|
|
|
state: MarketWorldState,
|
|
|
|
|
planned: PlannedPolicy,
|
|
|
|
|
params: FulfilmentPolicyParams,
|
malkhut(spec): items 5-10 — manifold, actuals, OOD, query, book fidelity
Item 5 — PerformanceMatrix manifold:
RegimeStrategyScore: added confidence, support_count, distance_to_nearest
record() populates confidence from episode count (more evidence = more confidence)
Item 6 — ActualsLoader:
ActualsSnapshot: 12-field frozen dataclass for live market data
ActualsLoader: reads CH tables (obf_universe, exf_data, maras_fingerprint, etc.)
Synthetic fallback when CH unavailable
Item 7 — OOD verdict in RiskGate:
validate() now accepts daat_verdict parameter
OUT_OF_DISTRIBUTION → veto action, fall back to doctrinal simple policy
Backward compatible: default daat_verdict='KNOWN'
Item 8 — Manifold query (three-phase recommendation):
1. DAAT classify live state (KNOWN/MARGINAL/OOD)
2. If KNOWN: find nearest regime in PerformanceMatrix → best strategy
3. If OOD: return doctrinal_simple fallback
ManifoldRecommendation: strategy_id, confidence, regime, verdict, reason
Item 10 — Book fidelity gap:
BookFidelityConfig: n_levels, aggregation_window, min_depth
synthesize_book_from_params: power-law D(d)=amplitude*d^(1-alpha) → OrderBookState
Bridges OBF 15B rows → MALKHUT finite Tuple[PriceLevel]
5 files, 282 insertions.
2026-07-14 06:11:37 +02:00
|
|
|
daat_verdict: str = "KNOWN",
|
2026-07-11 10:31:31 +02:00
|
|
|
) -> RiskDecision:
|
malkhut(spec): items 5-10 — manifold, actuals, OOD, query, book fidelity
Item 5 — PerformanceMatrix manifold:
RegimeStrategyScore: added confidence, support_count, distance_to_nearest
record() populates confidence from episode count (more evidence = more confidence)
Item 6 — ActualsLoader:
ActualsSnapshot: 12-field frozen dataclass for live market data
ActualsLoader: reads CH tables (obf_universe, exf_data, maras_fingerprint, etc.)
Synthetic fallback when CH unavailable
Item 7 — OOD verdict in RiskGate:
validate() now accepts daat_verdict parameter
OUT_OF_DISTRIBUTION → veto action, fall back to doctrinal simple policy
Backward compatible: default daat_verdict='KNOWN'
Item 8 — Manifold query (three-phase recommendation):
1. DAAT classify live state (KNOWN/MARGINAL/OOD)
2. If KNOWN: find nearest regime in PerformanceMatrix → best strategy
3. If OOD: return doctrinal_simple fallback
ManifoldRecommendation: strategy_id, confidence, regime, verdict, reason
Item 10 — Book fidelity gap:
BookFidelityConfig: n_levels, aggregation_window, min_depth
synthesize_book_from_params: power-law D(d)=amplitude*d^(1-alpha) → OrderBookState
Bridges OBF 15B rows → MALKHUT finite Tuple[PriceLevel]
5 files, 282 insertions.
2026-07-14 06:11:37 +02:00
|
|
|
"""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.
|
|
|
|
|
"""
|
2026-07-11 10:31:31 +02:00
|
|
|
action = planned.selected_action
|
|
|
|
|
|
malkhut(spec): items 5-10 — manifold, actuals, OOD, query, book fidelity
Item 5 — PerformanceMatrix manifold:
RegimeStrategyScore: added confidence, support_count, distance_to_nearest
record() populates confidence from episode count (more evidence = more confidence)
Item 6 — ActualsLoader:
ActualsSnapshot: 12-field frozen dataclass for live market data
ActualsLoader: reads CH tables (obf_universe, exf_data, maras_fingerprint, etc.)
Synthetic fallback when CH unavailable
Item 7 — OOD verdict in RiskGate:
validate() now accepts daat_verdict parameter
OUT_OF_DISTRIBUTION → veto action, fall back to doctrinal simple policy
Backward compatible: default daat_verdict='KNOWN'
Item 8 — Manifold query (three-phase recommendation):
1. DAAT classify live state (KNOWN/MARGINAL/OOD)
2. If KNOWN: find nearest regime in PerformanceMatrix → best strategy
3. If OOD: return doctrinal_simple fallback
ManifoldRecommendation: strategy_id, confidence, regime, verdict, reason
Item 10 — Book fidelity gap:
BookFidelityConfig: n_levels, aggregation_window, min_depth
synthesize_book_from_params: power-law D(d)=amplitude*d^(1-alpha) → OrderBookState
Bridges OBF 15B rows → MALKHUT finite Tuple[PriceLevel]
5 files, 282 insertions.
2026-07-14 06:11:37 +02:00
|
|
|
if daat_verdict == "OUT_OF_DISTRIBUTION":
|
|
|
|
|
return RiskDecision(True, None, "ood_veto_fall_back_to_doctrinal")
|
|
|
|
|
|
2026-07-11 10:31:31 +02:00
|
|
|
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:
|
2026-07-14 17:01:38 +02:00
|
|
|
return self._kill_switch
|
2026-07-11 10:31:31 +02:00
|
|
|
|
|
|
|
|
def _cancel_rate_would_exceed(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
|
2026-07-14 17:01:38 +02:00
|
|
|
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
|
2026-07-11 10:31:31 +02:00
|
|
|
|
|
|
|
|
def _would_self_trade(self, state: MarketWorldState, action: FulfilmentAction) -> bool:
|
2026-07-14 17:01:38 +02:00
|
|
|
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
|
2026-07-11 10:31:31 +02:00
|
|
|
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:
|
2026-07-14 17:01:38 +02:00
|
|
|
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
|
2026-07-11 10:31:31 +02:00
|
|
|
|
|
|
|
|
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
|
2026-07-15 00:11:57 +02:00
|
|
|
if not state.book.bids or not state.book.asks:
|
|
|
|
|
return False
|
2026-07-11 10:31:31 +02:00
|
|
|
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:
|
2026-07-14 17:01:38 +02:00
|
|
|
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
|
2026-07-11 10:31:31 +02:00
|
|
|
return False
|