Files
sentiment-engine/MALKHUT/malkhut/state.py
Codex aa22529330 malkhut(T1): scaffold — frozen state model, actions, features, engine
T1 scaffold: 42 frozen dataclasses (state.py), action model + PlannedPolicy +
RiskDecision (actions.py), 17-feature extraction (features.py),
FulfilmentEngine hot-path orchestrator (engine.py).
2026-07-11 10:21:27 +02:00

334 lines
8.2 KiB
Python

"""
MALKHUT canonical data model.
All state objects are frozen+slots for:
- deterministic tree search (immutable snapshots)
- GraalVM compatibility (no mutable default hell)
- lock-free shared memory (readers never see partial writes)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
import math
# ==============================================================================
# Enums
# ==============================================================================
class Side(str, Enum):
BUY = "BUY"
SELL = "SELL"
class OrderType(str, Enum):
LIMIT = "LIMIT"
MARKET = "MARKET"
POST_ONLY = "POST_ONLY"
IOC = "IOC"
FOK = "FOK"
REDUCE_ONLY_LIMIT = "REDUCE_ONLY_LIMIT"
REDUCE_ONLY_MARKET = "REDUCE_ONLY_MARKET"
class ActionKind(str, Enum):
NOOP = "NOOP"
PLACE = "PLACE"
CANCEL = "CANCEL"
CANCEL_REPLACE = "CANCEL_REPLACE"
CROSS_SPREAD = "CROSS_SPREAD"
REDUCE = "REDUCE"
FULL_EXIT = "FULL_EXIT"
MOVE_STOP = "MOVE_STOP"
MOVE_TAKE_PROFIT = "MOVE_TAKE_PROFIT"
THROTTLE = "THROTTLE"
class IntentKind(str, Enum):
ENTER_LONG = "ENTER_LONG"
ENTER_SHORT = "ENTER_SHORT"
ADD_LONG = "ADD_LONG"
ADD_SHORT = "ADD_SHORT"
REDUCE_LONG = "REDUCE_LONG"
REDUCE_SHORT = "REDUCE_SHORT"
EXIT_LONG = "EXIT_LONG"
EXIT_SHORT = "EXIT_SHORT"
MAINTAIN = "MAINTAIN"
class AgentRole(str, Enum):
OUR_FULFILMENT = "OUR_FULFILMENT"
PASSIVE_MAKER = "PASSIVE_MAKER"
TOXIC_TAKER = "TOXIC_TAKER"
LATENCY_ARB = "LATENCY_ARB"
MOMENTUM_TAKER = "MOMENTUM_TAKER"
MEAN_REVERSION_TAKER = "MEAN_REVERSION_TAKER"
INVENTORY_MM = "INVENTORY_MM"
LIQUIDATION_FLOW = "LIQUIDATION_FLOW"
NOISE_TRADER = "NOISE_TRADER"
STALE_QUOTE_ATTACKER = "STALE_QUOTE_ATTACKER"
class Mode(str, Enum):
REPLAY_NO_IMPACT = "REPLAY_NO_IMPACT"
ENDOGENOUS_AGENT_SIM = "ENDOGENOUS_AGENT_SIM"
PAPER = "PAPER"
SHADOW_LIVE = "SHADOW_LIVE"
LIVE = "LIVE"
# ==============================================================================
# Core constants
# ==============================================================================
HOT_PATH_BUDGET_MS: int = 100
DEFAULT_PLANNER_BUDGET_MS: int = 25
DEFAULT_TREE_DEPTH: int = 3
DEFAULT_MAX_SIMS: int = 256
DEFAULT_UCB_C: float = 1.41421356237
DEFAULT_MIN_ROOT_POLICY_ENTROPY: float = 0.25
DEFAULT_SELF_PLAY_POOL_MAX: int = 12
DEFAULT_POLICY_PROMOTION_MIN_EDGE_BPS: float = 0.75
DEFAULT_POLICY_PROMOTION_MIN_PVALUE: float = 0.05
MAX_ACCOUNT_LEVERAGE: float = 2.0
MAX_EXCHANGE_LEVERAGE: float = 5.0
MAX_SINGLE_ORDER_NOTIONAL_FRACTION: float = 0.05
MAX_SYMBOL_NOTIONAL_FRACTION: float = 0.20
MAX_CANCELS_PER_SYMBOL_PER_MINUTE: int = 90
TAIL_QUANTILE: float = 0.05
# ==============================================================================
# Frozen data model
# ==============================================================================
@dataclass(frozen=True, slots=True)
class VenueRules:
exchange: str
symbol: str
tick_size: float
lot_size: float
min_qty: float
min_notional: float
maker_fee_bps: float
taker_fee_bps: float
post_only_supported: bool
reduce_only_supported: bool
max_orders_per_second: int
max_cancels_per_minute: int
@dataclass(frozen=True, slots=True)
class PriceLevel:
price: float
qty: float
@dataclass(frozen=True, slots=True)
class OrderBookState:
ts_ns: int
symbol: str
bids: Tuple[PriceLevel, ...]
asks: Tuple[PriceLevel, ...]
last_trade_price: Optional[float] = None
last_trade_qty: Optional[float] = None
last_trade_side: Optional[Side] = None
@property
def best_bid(self) -> float:
return self.bids[0].price
@property
def best_ask(self) -> float:
return self.asks[0].price
@property
def mid(self) -> float:
return 0.5 * (self.best_bid + self.best_ask)
@property
def spread(self) -> float:
return self.best_ask - self.best_bid
@property
def spread_bps(self) -> float:
return 10_000.0 * self.spread / max(self.mid, 1e-12)
@dataclass(frozen=True, slots=True)
class PositionState:
symbol: str
qty: float
avg_entry: float
unrealized_pnl: float
realized_pnl: float
liquidation_price: Optional[float]
leverage: float
side: Optional[Side]
@dataclass(frozen=True, slots=True)
class AccountState:
ts_ns: int
equity: float
wallet_balance: float
available_balance: float
margin_used: float
total_notional: float
positions: Mapping[str, PositionState] = field(default_factory=dict)
@dataclass(frozen=True, slots=True)
class OpenOrderState:
client_order_id: str
venue_order_id: Optional[str]
symbol: str
side: Side
order_type: OrderType
price: Optional[float]
qty: float
remaining_qty: float
queue_ahead_estimate: Optional[float]
created_ts_ns: int
last_update_ts_ns: int
reduce_only: bool = False
post_only: bool = False
@dataclass(frozen=True, slots=True)
class TradePathState:
"""In-trade path encoding for path-aware SL/TP."""
symbol: str
side: Side
entry_ts_ns: int
now_ts_ns: int
bars_held: int
seconds_held: float
pnl_bps: float
mae_bps: float
mfe_bps: float
distance_from_mfe_bps: float
distance_from_entry_bps: float
time_to_mfe_s: float
time_in_loss_s: float
time_in_profit_s: float
time_since_last_profit_s: float
time_since_deep_mae_s: float
loss_to_profit_transitions: int
deep_loss_recoveries: int
failed_recovery_count: int
recovery_velocity_bps_per_s: float
adverse_velocity_bps_per_s: float
dolphin_regime_score: float
jericho_signal_strength: float
volatility_bps: float
orderflow_toxicity: float
queue_churn_score: float
book_imbalance: float
cross_venue_lead_score: float
@dataclass(frozen=True, slots=True)
class ExecutionIntent:
intent_id: str
ts_ns: int
symbol: str
kind: IntentKind
target_qty: float
max_notional: float
urgency: float
alpha_horizon_s: float
alpha_bps: float
max_slippage_bps: float
prefer_maker: bool
reduce_only: bool
ttl_s: float
reason: str
@dataclass(frozen=True, slots=True)
class MarketWorldState:
"""Complete CWM root state. Immutable for safe tree search."""
ts_ns: int
mode: Mode
venue: VenueRules
book: OrderBookState
account: AccountState
open_orders: Tuple[OpenOrderState, ...] = ()
trade_path: Optional[TradePathState] = None
intent: Optional[ExecutionIntent] = None
funding_bps: Optional[float] = None
volatility_state: Optional[float] = None
market_regime: Optional[str] = None
feed_latency_ms: float = 0.0
order_latency_ms: float = 0.0
rng_seed: int = 0
@dataclass(frozen=True, slots=True)
class FulfilmentPolicyParams:
"""
Frozen parameter set loaded by the live planner.
CMA-ES tunes this object offline.
"""
version: str
# Planner
ucb_c: float
max_sims: int
max_depth: int
rollout_depth: int
root_temperature: float
min_root_entropy: float
# Quote menu
quote_offsets_ticks: Tuple[int, ...]
quote_size_fractions: Tuple[float, ...]
passive_ttl_ms: int
aggressive_ttl_ms: int
# Maker/taker thresholds
maker_edge_min_bps: float
cross_spread_edge_min_bps: float
adverse_toxicity_cancel_threshold: float
queue_churn_cancel_threshold: float
# SL/TP/path risk
mae_tail_cut_bps: float
mfe_giveback_cut_fraction: float
max_time_in_loss_s: float
failed_recovery_cut_count: int
recovery_velocity_min_bps_per_s: float
# Inventory/account
max_symbol_notional_fraction: float
max_single_order_notional_fraction: float
reduce_when_global_up_fraction: float
session_profit_lock_fraction: float
# Reward weights
w_expected_pnl: float
w_fill_probability: float
w_adverse_selection: float
w_queue_priority: float
w_inventory_risk: float
w_tail_loss: float
w_fee_quality: float
w_time_decay: float
w_policy_entropy: float
# Scenario robustness
robust_tail_weight: float
toxic_counterparty_weight: float
low_liquidity_weight: float
latency_stress_weight: float