malkhut(T5): risk gate + ASEx + IPC + storage + venue adapter

Risk gate (risk/gate.py): hard invariants — leverage, self-trade, post-only.
ASEx integration (execution/asex_integration.py): validate-before-mutate,
single-writer, zero-lock state serialization.
IPC (ipc/): Zinc SHM (zinc_plane.py) + control plane (control_plane.py),
UVZINC01 seqlock framing.
ClickHouse storage (storage/ch_store.py): 5 tables, HTTP API.
BingX venue adapter (venue/bingx/adapter.py): wraps DITAv2, order tracking.
This commit is contained in:
Codex
2026-07-11 10:31:31 +02:00
parent 863a4cc8c9
commit 6fb55dadcb
12 changed files with 1391 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
from malkhut.execution.asex_integration import (
GuardedFulfilmentState,
GuardedRiskState,
FulfilmentWorker,
RiskWorker,
FulfilmentWatch,
create_sharded_fulfilment,
BookUpdate,
AccountUpdate,
IntentUpdate,
OrderAction,
RiskCheck,
PolicyReload,
)

View File

@@ -0,0 +1,418 @@
"""
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)