Files
sentiment-engine/MALKHUT/malkhut/state.py

399 lines
11 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):
"""Standardized order types — FIX/CCXT-aligned, multi-exchange.
THREE ORTHOGONAL DIMENSIONS (not one flat enum):
1. Order Type (FIX Tag 40): what the order IS — this enum
2. TimeInForce (FIX Tag 59): how long it LIVES — separate parameter
3. Instructions (FIX Tag 18): behavioral modifiers — separate parameter
CRITICAL: IOC, FOK, POST_ONLY are NOT order types.
IOC/FOK = TimeInForce on a LIMIT order.
POST_ONLY = ExecInst modifier on a LIMIT order.
"""
# Core order types (FIX Tag 40)
MARKET = "MARKET"
LIMIT = "LIMIT"
STOP_MARKET = "STOP_MARKET"
STOP_LIMIT = "STOP_LIMIT"
TRIGGER_MARKET = "TRIGGER_MARKET"
TRIGGER_LIMIT = "TRIGGER_LIMIT"
TRAILING_STOP = "TRAILING_STOP"
OCO = "OCO"
TP_SL = "TP_SL"
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 FillQuality:
"""Fill quality metrics — the CORE optimization target of MALKHUT.
MALKHUT is an execution improvement engine. Fill quality IS the primary aim.
Every transition records these metrics. The reward function weights them heavily.
The PerformanceMatrix tracks them per (regime, strategy, venue).
"""
filled: bool = False
fill_qty: float = 0.0
fill_price: float = 0.0
requested_qty: float = 0.0
# How close to mid did we fill? (for aggressive: positive = slipped)
slippage_bps: float = 0.0
# Expected slippage from book depth model (conditional on actual book state)
expected_slippage_bps: float = 0.0
# For passive fills: how much better than best bid/ask? (positive = improvement)
price_improvement_bps: float = 0.0
# How many levels deep was the fill?
levels_consumed: int = 0
# Was this a maker (passive) or taker (aggressive) fill?
is_maker_fill: bool = False
# Fill rate rolling window (updated each transition)
rolling_fill_rate: float = 0.0
# Adverse selection: price movement after fill (negative = adverse)
post_fill_adverse_bps: float = 0.0
# Fill value score: composite metric for optimization
# = fill_rate * price_quality - adverse_selection - slippage
fill_value_score: float = 0.0
@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
fill_quality: Optional[FillQuality] = None
@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
# Chase mechanics (cancel → wait → retry)
wait_to_retry_ms: int = 0 # ms to wait before re-quoting after cancel
chase_enabled: bool = False # enable chase-follow behavior
chase_offset_ticks: int = 1 # ticks from target price to chase
chase_max_retries: int = 3 # max cancel-retry cycles
# Urgency-driven maker/taker decision (CMA-ES optimizable)
urgency_taker_threshold: float = 0.65 # above this urgency, prefer taker
urgency_taker_penalty_bps: float = 2.0 # penalty for taker at low urgency