Files
sentiment-engine/MALKHUT/malkhut/tests/test_state_invariants.py

171 lines
6.8 KiB
Python
Raw Normal View History

"""
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"