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:
14
MALKHUT/malkhut/execution/__init__.py
Normal file
14
MALKHUT/malkhut/execution/__init__.py
Normal 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,
|
||||
)
|
||||
418
MALKHUT/malkhut/execution/asex_integration.py
Normal file
418
MALKHUT/malkhut/execution/asex_integration.py
Normal 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)
|
||||
Reference in New Issue
Block a user