""" Unit tests: Risk gate. Mutation litmus: remove kill switch check; if no test breaks, the gate is untested. """ import pytest from malkhut.state import ( AccountState, FulfilmentPolicyParams, MarketWorldState, Mode, OrderBookState, PriceLevel, VenueRules, ) from malkhut.risk.gate import RiskGate from malkhut.actions import ActionKind, FulfilmentAction, OrderType, PlannedPolicy, Side def _state() -> MarketWorldState: return MarketWorldState( ts_ns=1_000_000_000, 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_000_000_000, symbol="BTCUSDT", bids=(PriceLevel(50000.0, 1.0),), asks=(PriceLevel(50001.0, 1.0),), ), account=AccountState( ts_ns=1_000_000_000, equity=10000.0, wallet_balance=10000.0, available_balance=10000.0, margin_used=0.0, total_notional=0.0, ), ) def _params() -> FulfilmentPolicyParams: return FulfilmentPolicyParams( version="test", ucb_c=1.414, max_sims=32, max_depth=2, rollout_depth=1, root_temperature=0.5, min_root_entropy=0.25, quote_offsets_ticks=(0, 1, 2), quote_size_fractions=(0.10, 0.25, 0.50), 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, ) class TestRiskGate: def test_noop_always_approved(self): gate = RiskGate() action = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(action,), probabilities=(1.0,), selected_action=action, diagnostics={}) decision = gate.validate(_state(), planned, _params()) assert decision.approved assert decision.reason == "noop" def test_post_only_cross_block(self): gate = RiskGate() # offset=-10 => price = best_bid + 10*tick = 50001.0 >= best_ask => crosses action = FulfilmentAction( ActionKind.PLACE, Side.BUY, OrderType.POST_ONLY, -10, 0.10, 200, post_only=True, ) planned = PlannedPolicy(actions=(action,), probabilities=(1.0,), selected_action=action, diagnostics={}) decision = gate.validate(_state(), planned, _params()) assert not decision.approved assert decision.reason == "post_only_cross" def test_leverage_block(self): gate = RiskGate() state = MarketWorldState( ts_ns=1_000_000_000, mode=Mode.LIVE, venue=_state().venue, book=_state().book, account=AccountState( ts_ns=1_000_000_000, equity=1000.0, wallet_balance=1000.0, available_balance=1000.0, margin_used=0.0, total_notional=5000.0, ), ) action = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT, 0, 0.10, 200) planned = PlannedPolicy(actions=(action,), probabilities=(1.0,), selected_action=action, diagnostics={}) decision = gate.validate(state, planned, _params()) assert not decision.approved assert decision.reason == "leverage_limit" def test_approved_action_passes(self): gate = RiskGate() action = FulfilmentAction( ActionKind.PLACE, Side.BUY, OrderType.POST_ONLY, 1, 0.10, 200, post_only=True, ) planned = PlannedPolicy(actions=(action,), probabilities=(1.0,), selected_action=action, diagnostics={}) decision = gate.validate(_state(), planned, _params()) assert decision.approved assert decision.reason == "approved"