""" ASEx integration for MALKHUT. Builds ASEx's validate-before-mutate kernel into the core of MALKHUT: - GuardedFulfilmentState: engine state mutations through ASEx - GuardedRiskState: risk gate decisions through ASEx - GuardedVenueState: venue adapter mutations through ASEx - FulfilmentWorker: single-threaded ASExWorker for serialised engine - ShardedFulfilmentWorker: per-symbol ShardedWorker for parallelism - FulfilmentWatch: zero-overhead ASExWatch for hot-path ring buffer The pattern: every mutable state object is an ASExGuardedState. _mutate() calls _validate() first, then _apply() only if valid. One worker thread per state. No locks. No races. No GC pressure. """ from __future__ import annotations import sys import time from dataclasses import dataclass, field from typing import Any, Optional # ASEx is not pip-installable; imported via path _ASEX_SRC = "/mnt/dolphinng5_predict/ASEx/src" if _ASEX_SRC not in sys.path: sys.path.insert(0, _ASEX_SRC) from asex.guarded import ASExGuardedState, ValidationError, SafetyError from asex.worker import ASExWorker from asex.watch import ASExWatch from asex.batch import BatchWorker from asex.sharded import ShardedWorker from asex.daemon import LocalDaemon from asex.client import ASExClientLocal from malkhut.state import ( AccountState, ExecutionIntent, FulfilmentPolicyParams, MarketWorldState, Mode, OpenOrderState, OrderBookState, VenueRules, ) from malkhut.actions import ( ActionKind, FulfilmentAction, PlannedPolicy, RiskDecision, ) # ============================================================================== # Mutation types # ============================================================================== @dataclass(frozen=True, slots=True) class BookUpdate: """Mutation: update the canonical order book.""" ts_ns: int symbol: str bids: tuple # tuple[PriceLevel, ...] asks: tuple # tuple[PriceLevel, ...] @dataclass(frozen=True, slots=True) class AccountUpdate: """Mutation: update account/position state.""" ts_ns: int equity: float wallet_balance: float available_balance: float margin_used: float total_notional: float positions: dict # dict[str, PositionState] @dataclass(frozen=True, slots=True) class IntentUpdate: """Mutation: update the current execution intent.""" intent: Optional[ExecutionIntent] @dataclass(frozen=True, slots=True) class OrderAction: """Mutation: place/cancel/replace an order.""" kind: str # ActionKind value action: Optional[FulfilmentAction] = None cancel_order_id: Optional[str] = None @dataclass(frozen=True, slots=True) class RiskCheck: """Mutation: run risk gate on a planned action.""" state: Any # MarketWorldState planned: PlannedPolicy params: FulfilmentPolicyParams @dataclass(frozen=True, slots=True) class PolicyReload: """Mutation: hot-reload policy parameters.""" params: FulfilmentPolicyParams # ============================================================================== # Guarded state objects # ============================================================================== class GuardedFulfilmentState(ASExGuardedState[Any, Any]): """ ASEx-guarded engine state. All mutations go through validate-before-mutate. Holds: - canonical book state - account state - open orders - trade path - current intent - active policy params Thread safety: owned by exactly one ASExWorker thread. No external mutation allowed. """ def __init__(self, initial_state: Optional[MarketWorldState] = None) -> None: super().__init__() self._state: Optional[MarketWorldState] = initial_state self._params: Optional[FulfilmentPolicyParams] = None self._last_planned: Optional[PlannedPolicy] = None self._last_risk: Optional[RiskDecision] = None self._mutation_count: int = 0 def _validate(self, mutation: Any) -> bool: if isinstance(mutation, BookUpdate): return mutation.ts_ns > 0 and len(mutation.bids) > 0 and len(mutation.asks) > 0 if isinstance(mutation, AccountUpdate): return mutation.ts_ns > 0 and mutation.equity >= 0 if isinstance(mutation, IntentUpdate): return True # intent can be None (no intent) if isinstance(mutation, OrderAction): return mutation.kind in ("PLACE", "CANCEL", "CANCEL_REPLACE", "CROSS_SPREAD", "REDUCE", "FULL_EXIT", "NOOP") if isinstance(mutation, RiskCheck): return mutation.planned is not None and mutation.params is not None if isinstance(mutation, PolicyReload): return mutation.params is not None return False def _apply(self, mutation: Any) -> Any: self._mutation_count += 1 if isinstance(mutation, BookUpdate): if self._state is None: return None from malkhut.state import PriceLevel bids = tuple( PriceLevel(p["price"], p["qty"]) if isinstance(p, dict) else PriceLevel(p[0], p[1]) for p in mutation.bids ) asks = tuple( PriceLevel(p["price"], p["qty"]) if isinstance(p, dict) else PriceLevel(p[0], p[1]) for p in mutation.asks ) new_book = OrderBookState( ts_ns=mutation.ts_ns, symbol=mutation.symbol, bids=bids, asks=asks, last_trade_price=self._state.book.last_trade_price, last_trade_qty=self._state.book.last_trade_qty, last_trade_side=self._state.book.last_trade_side, ) self._state = MarketWorldState( ts_ns=mutation.ts_ns, mode=self._state.mode, venue=self._state.venue, book=new_book, account=self._state.account, open_orders=self._state.open_orders, trade_path=self._state.trade_path, intent=self._state.intent, funding_bps=self._state.funding_bps, volatility_state=self._state.volatility_state, market_regime=self._state.market_regime, ) return new_book if isinstance(mutation, AccountUpdate): if self._state is None: return None from malkhut.state import PositionState positions = {} for sym, pos_data in mutation.positions.items(): positions[sym] = PositionState( symbol=pos_data["symbol"], qty=pos_data["qty"], avg_entry=pos_data["avg_entry"], unrealized_pnl=pos_data.get("unrealized_pnl", 0.0), realized_pnl=pos_data.get("realized_pnl", 0.0), liquidation_price=pos_data.get("liquidation_price"), leverage=pos_data.get("leverage", 0.0), side=pos_data.get("side"), ) new_account = AccountState( ts_ns=mutation.ts_ns, equity=mutation.equity, wallet_balance=mutation.wallet_balance, available_balance=mutation.available_balance, margin_used=mutation.margin_used, total_notional=mutation.total_notional, positions=positions, ) self._state = MarketWorldState( ts_ns=mutation.ts_ns, mode=self._state.mode, venue=self._state.venue, book=self._state.book, account=new_account, open_orders=self._state.open_orders, trade_path=self._state.trade_path, intent=self._state.intent, ) return new_account if isinstance(mutation, IntentUpdate): if self._state is None: return None self._state = MarketWorldState( ts_ns=self._state.ts_ns, mode=self._state.mode, venue=self._state.venue, book=self._state.book, account=self._state.account, open_orders=self._state.open_orders, trade_path=self._state.trade_path, intent=mutation.intent, ) return mutation.intent if isinstance(mutation, PolicyReload): self._params = mutation.params return mutation.params return None @property def state(self) -> Optional[MarketWorldState]: return self._state @property def params(self) -> Optional[FulfilmentPolicyParams]: return self._params @property def mutation_count(self) -> int: return self._mutation_count class GuardedRiskState(ASExGuardedState[Any, RiskDecision]): """ ASEx-guarded risk gate state. Validates risk decisions before they affect the system. """ def __init__(self) -> None: super().__init__() self._kill_switch: bool = False self._cancel_counts: dict[str, int] = {} self._last_decision: Optional[RiskDecision] = None def _validate(self, mutation: Any) -> bool: if isinstance(mutation, RiskCheck): return True if isinstance(mutation, str) and mutation == "KILL_SWITCH_ON": return True if isinstance(mutation, str) and mutation == "KILL_SWITCH_OFF": return True return False def _apply(self, mutation: Any) -> RiskDecision: if mutation == "KILL_SWITCH_ON": self._kill_switch = True return RiskDecision(False, None, "kill_switch_activated") if mutation == "KILL_SWITCH_OFF": self._kill_switch = False return RiskDecision(True, None, "kill_switch_deactivated") if self._kill_switch: return RiskDecision(False, None, "kill_switch") if isinstance(mutation, RiskCheck): from malkhut.risk.gate import RiskGate gate = RiskGate() gate._kill_switch_active = lambda: self._kill_switch decision = gate.validate( mutation.state, mutation.planned, mutation.params, ) self._last_decision = decision return decision return RiskDecision(False, None, "unknown_mutation") @property def kill_switch(self) -> bool: return self._kill_switch @property def last_decision(self) -> Optional[RiskDecision]: return self._last_decision # ============================================================================== # Worker wrappers # ============================================================================== class FulfilmentWorker: """ Single ASExWorker wrapping GuardedFulfilmentState. All engine mutations go through this worker's queue. Serialised, validated, applied on one thread. """ def __init__(self, initial_state: Optional[MarketWorldState] = None) -> None: self._guarded = GuardedFulfilmentState(initial_state) self._worker = ASExWorker(self._guarded, daemon=True) def update_book(self, ts_ns: int, symbol: str, bids: tuple, asks: tuple): from malkhut.state import PriceLevel bid_dicts = [{"price": p.price, "qty": p.qty} for p in bids] ask_dicts = [{"price": p.price, "qty": p.qty} for p in asks] return self._worker.mutate(BookUpdate(ts_ns, symbol, tuple(bid_dicts), tuple(ask_dicts))) def update_account(self, **kwargs): return self._worker.mutate(AccountUpdate(**kwargs)) def update_intent(self, intent: Optional[ExecutionIntent]): return self._worker.mutate(IntentUpdate(intent)) def reload_policy(self, params: FulfilmentPolicyParams): return self._worker.mutate(PolicyReload(params)) @property def state(self) -> Optional[MarketWorldState]: return self._guarded.state @property def params(self) -> Optional[FulfilmentPolicyParams]: return self._guarded.params @property def mutation_count(self) -> int: return self._guarded.mutation_count def close(self, timeout: float = 5.0): self._worker.close(timeout=timeout) class RiskWorker: """ASExWorker wrapping GuardedRiskState for risk gate mutations.""" def __init__(self) -> None: self._guarded = GuardedRiskState() self._worker = ASExWorker(self._guarded, daemon=True) def activate_kill_switch(self): return self._worker.mutate("KILL_SWITCH_ON") def deactivate_kill_switch(self): return self._worker.mutate("KILL_SWITCH_OFF") @property def kill_switch(self) -> bool: return self._guarded.kill_switch def close(self, timeout: float = 5.0): self._worker.close(timeout=timeout) class FulfilmentWatch: """ Zero-overhead ring buffer for hot-path mutations. **DEFERRED from critical path** per T19 ANNEX B pre-condition #2: "ASExWatch stays OUT of the critical path until its heavy-test hangs are fixed (known-deferred)." Producer: claim slot via _free.get(), write, signal via _filled.put(). Consumer: reads _filled.get() directly, calls _apply(), frees slot. When ring is full, mutate() raises queue.Full — never blocks the producer. NOTE: Do NOT use in the live hot path until ASExWatch stability is proven. Use FulfilmentWorker (ASExWorker) instead for production. """ def __init__(self, initial_state: Optional[MarketWorldState] = None, capacity: int = 65536) -> None: self._guarded = GuardedFulfilmentState(initial_state) self._watch = ASExWatch(self._guarded, capacity=capacity) def mutate(self, mutation: Any): return self._watch.mutate(mutation) def poll(self, timeout: float = None) -> int: return self._watch.poll(timeout) def wait(self, timeout: float = None) -> int: return self._watch.wait(timeout) @property def state(self) -> Optional[MarketWorldState]: return self._guarded.state @property def pending(self) -> int: return self._watch.pending def close(self): self._watch.close() def create_sharded_fulfilment(n_partitions: int = 4): """ Create a ShardedWorker for per-symbol fulfilment state. Each symbol gets its own ASExGuardedState + ASExWorker. No two workers ever touch the same accumulator. """ return ShardedWorker(n_partitions, GuardedFulfilmentState)