""" State invariants — frozen dataclasses, immutability, serialization. """ import json import pytest from malkhut.state import ( AccountState, ExecutionIntent, FulfilmentPolicyParams, IntentKind, MarketWorldState, Mode, OpenOrderState, OrderBookState, PositionState, PriceLevel, Side, TradePathState, VenueRules, ) from malkhut.actions import ( ActionKind, CounterpartyAction, FulfilmentAction, OrderType, PlannedPolicy, RiskDecision, ) class TestFrozenInvariants: def test_venue_rules_immutable(self): v = VenueRules( exchange="bingx", symbol="BTCUSDT", tick_size=0.1, lot_size=0.001, min_qty=0.001, min_notional=5.0, maker_fee_bps=-0.2, taker_fee_bps=0.5, post_only_supported=True, reduce_only_supported=True, max_orders_per_second=100, max_cancels_per_minute=120, ) with pytest.raises(AttributeError): v.symbol = "ETHUSDT" def test_price_level_immutable(self): p = PriceLevel(price=50000.0, qty=1.0) with pytest.raises(AttributeError): p.price = 51000.0 def test_order_book_immutable(self): b = OrderBookState( ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0),), ) with pytest.raises(AttributeError): b.bids = () def test_account_state_immutable(self): a = AccountState(ts_ns=1, equity=10000.0, wallet_balance=10000.0, available_balance=10000.0, margin_used=0.0, total_notional=0.0) with pytest.raises(AttributeError): a.equity = 20000.0 def test_fulfilment_action_immutable(self): a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT, 0, 0.1, 200) with pytest.raises(AttributeError): a.qty_fraction = 0.5 def test_market_world_state_immutable(self): s = MarketWorldState( ts_ns=1, mode=Mode.LIVE, venue=VenueRules(exchange="bingx", symbol="BTCUSDT", tick_size=0.1, lot_size=0.001, min_qty=0.001, min_notional=5.0, maker_fee_bps=-0.2, taker_fee_bps=0.5, post_only_supported=True, reduce_only_supported=True, max_orders_per_second=100, max_cancels_per_minute=120), book=OrderBookState(ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0),)), account=AccountState(ts_ns=1, equity=10000.0, wallet_balance=10000.0, available_balance=10000.0, margin_used=0.0, total_notional=0.0), ) with pytest.raises(AttributeError): s.ts_ns = 2 class TestOrderBookProperties: def test_best_bid_highest(self): b = OrderBookState( ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0), PriceLevel(49999.0, 2.0)), asks=(PriceLevel(50001.0, 1.0),), ) assert b.best_bid == 50000.0 def test_best_ask_lowest(self): b = OrderBookState( ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0), PriceLevel(50002.0, 2.0)), ) assert b.best_ask == 50001.0 def test_mid_calculation(self): b = OrderBookState( ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50002.0, 1.0),), ) assert b.mid == 50001.0 def test_spread_calculation(self): b = OrderBookState( ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0),), ) assert b.spread == 1.0 def test_spread_bps(self): b = OrderBookState( ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0),), ) expected_bps = 10_000.0 * 1.0 / 50000.5 assert abs(b.spread_bps - expected_bps) < 1e-6 class TestEnums: def test_side_values(self): assert Side.BUY.value == "BUY" assert Side.SELL.value == "SELL" def test_action_kind_values(self): assert ActionKind.NOOP.value == "NOOP" assert ActionKind.PLACE.value == "PLACE" def test_intent_kind_values(self): assert IntentKind.ENTER_LONG.value == "ENTER_LONG" def test_order_type_values(self): assert OrderType.LIMIT.value == "LIMIT" assert OrderType.POST_ONLY.value == "POST_ONLY" class TestTradePathState: def test_frozen(self): tp = TradePathState( symbol="BTCUSDT", side=Side.BUY, entry_ts_ns=0, now_ts_ns=100, bars_held=5, seconds_held=50.0, pnl_bps=10.0, mae_bps=-5.0, mfe_bps=15.0, distance_from_mfe_bps=5.0, distance_from_entry_bps=10.0, time_to_mfe_s=20.0, time_in_loss_s=10.0, time_in_profit_s=40.0, time_since_last_profit_s=5.0, time_since_deep_mae_s=15.0, loss_to_profit_transitions=1, deep_loss_recoveries=0, failed_recovery_count=0, recovery_velocity_bps_per_s=1.0, adverse_velocity_bps_per_s=-0.5, dolphin_regime_score=0.5, jericho_signal_strength=0.3, volatility_bps=15.0, orderflow_toxicity=0.3, queue_churn_score=0.2, book_imbalance=0.1, cross_venue_lead_score=0.1, ) with pytest.raises(AttributeError): tp.pnl_bps = 20.0 class TestFulfilmentPolicyParams: def test_frozen(self): p = FulfilmentPolicyParams( version="v1", ucb_c=1.414, max_sims=256, max_depth=3, rollout_depth=3, root_temperature=0.5, min_root_entropy=0.25, quote_offsets_ticks=(0, 1, 2), quote_size_fractions=(0.1, 0.25, 0.5), passive_ttl_ms=200, aggressive_ttl_ms=50, maker_edge_min_bps=0.5, cross_spread_edge_min_bps=5.0, adverse_toxicity_cancel_threshold=0.5, queue_churn_cancel_threshold=0.5, mae_tail_cut_bps=50.0, mfe_giveback_cut_fraction=0.5, max_time_in_loss_s=300.0, failed_recovery_cut_count=3, recovery_velocity_min_bps_per_s=0.0, max_symbol_notional_fraction=0.20, max_single_order_notional_fraction=0.05, reduce_when_global_up_fraction=0.30, session_profit_lock_fraction=0.02, w_expected_pnl=1.0, w_fill_probability=0.5, w_adverse_selection=2.0, w_queue_priority=0.5, w_inventory_risk=1.5, w_tail_loss=5.0, w_fee_quality=0.5, w_time_decay=0.3, w_policy_entropy=0.5, robust_tail_weight=2.0, toxic_counterparty_weight=3.0, low_liquidity_weight=2.0, latency_stress_weight=1.0, ) with pytest.raises(AttributeError): p.version = "v2"