399 lines
11 KiB
Python
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
|