""" Tests for extended counterparties and structured observability. """ import pytest from malkhut.state import ( AccountState, MarketWorldState, Mode, OrderBookState, PriceLevel, PositionState, Side, TradePathState, VenueRules, ) from malkhut.actions import ActionKind, AgentRole from malkhut.counterparties_extended import ( MomentumTakerPolicy, MeanReversionTakerPolicy, InventoryMarketMakerPolicy, LiquidationFlowPolicy, StaleQuoteAttackerPolicy, extended_counterparty_ecology, ) from malkhut.training.structured_obs import StructuredObservability, DecisionMetrics def _venue(): return 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) def _state(**kw): tp = kw.get("trade_path") pos = kw.get("position") positions = {"BTCUSDT": pos} if pos else {} return MarketWorldState( ts_ns=kw.get("ts", 1_000_000_000), mode=Mode.REPLAY_NO_IMPACT, venue=_venue(), book=OrderBookState(ts_ns=1, symbol="BTCUSDT", bids=(PriceLevel(kw.get("bid", 50000.0), 1.0),), asks=(PriceLevel(kw.get("ask", 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, positions=positions), trade_path=tp, open_orders=kw.get("open_orders", ()), ) def _tp(**kw): d = dict(symbol="BTCUSDT", side=Side.BUY, entry_ts_ns=0, now_ts_ns=100_000_000, bars_held=5, seconds_held=50.0, pnl_bps=0.0, mae_bps=-10.0, mfe_bps=15.0, distance_from_mfe_bps=15.0, distance_from_entry_bps=0.0, time_to_mfe_s=20.0, time_in_loss_s=50.0, time_in_profit_s=50.0, time_since_last_profit_s=10.0, time_since_deep_mae_s=10.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) d.update(kw) return TradePathState(**d) # ══════════════════════════════════════════════════════════════════════════════ # EXTENDED COUNTERPARTIES (20 tests) # ══════════════════════════════════════════════════════════════════════════════ class TestExtendedCounterparties: def test_momentum_taker_buy_on_upward(self): p = MomentumTakerPolicy() tp = _tp(pnl_bps=50.0) s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD assert a.side == Side.BUY def test_momentum_taker_sell_on_downward(self): p = MomentumTakerPolicy() tp = _tp(pnl_bps=-50.0) s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD assert a.side == Side.SELL def test_momentum_taker_noop_when_flat(self): p = MomentumTakerPolicy() tp = _tp(pnl_bps=0.0) s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_mean_reversion_buy_on_drop(self): p = MeanReversionTakerPolicy() tp = _tp(pnl_bps=-60.0) # abs(60) > 0.5*100 = 50 s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD assert a.side == Side.BUY def test_mean_reversion_sell_on_rise(self): p = MeanReversionTakerPolicy() tp = _tp(pnl_bps=60.0) # abs(60) > 0.5*100 = 50 s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD assert a.side == Side.SELL def test_inventory_mm_reduces_high_inventory(self): p = InventoryMarketMakerPolicy(max_inventory=0.05) pos = PositionState(symbol="BTCUSDT", qty=0.1, avg_entry=50000.0, unrealized_pnl=0.0, realized_pnl=0.0, liquidation_price=None, leverage=0.5, side=Side.BUY) s = _state(position=pos) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.PLACE assert a.side == Side.SELL def test_inventory_mm_noop_when_balanced(self): p = InventoryMarketMakerPolicy(max_inventory=0.2) s = _state() rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_liquidation_flow_triggers_on_deep_loss(self): p = LiquidationFlowPolicy(trigger_bps=50.0) tp = _tp(mae_bps=-60.0) s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD assert a.side == Side.SELL assert a.toxicity == 0.9 def test_liquidation_flow_noop_when_no_loss(self): p = LiquidationFlowPolicy(trigger_bps=50.0) tp = _tp(mae_bps=-10.0) s = _state(trade_path=tp) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_stale_quote_attacker_attacks(self): p = StaleQuoteAttackerPolicy() from malkhut.state import OpenOrderState oo = OpenOrderState(client_order_id="c1", venue_order_id="v1", symbol="BTCUSDT", side=Side.BUY, order_type=OrderType.LIMIT, price=50000.0, qty=0.001, remaining_qty=0.001, queue_ahead_estimate=0.001, created_ts_ns=1, last_update_ts_ns=1) s = _state(open_orders=(oo,)) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD def test_stale_quote_attacker_noop_when_no_orders(self): p = StaleQuoteAttackerPolicy() s = _state() rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_extended_ecology_has_9_agents(self): eco = extended_counterparty_ecology() assert len(eco) == 9 def test_extended_ecology_unique_roles(self): eco = extended_counterparty_ecology() roles = [p.role for p in eco] assert len(set(roles)) == 9 def test_all_agents_produce_valid_actions(self): eco = extended_counterparty_ecology() s = _state() rng = random.Random(42) for agent in eco: a = agent.rollout_action(s, rng) assert a.kind is not None # ══════════════════════════════════════════════════════════════════════════════ # STRUCTURED OBSERVABILITY (15 tests) # ══════════════════════════════════════════════════════════════════════════════ class TestStructuredObservability: def test_record_decision(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) decision = RiskDecision(approved=True, action=a, reason="ok") so.record_decision(s, planned, decision, plan_ns=1000, regime="normal") assert so.total_decisions == 1 def test_feature_importance(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) decision = RiskDecision(approved=True, action=a, reason="ok") for _ in range(10): so.record_decision(s, planned, decision, plan_ns=1000, regime="normal") importance = so.get_feature_importance(top_n=5) assert len(importance) > 0 def test_regime_approval_rate(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) # Approved decision = RiskDecision(approved=True, action=a, reason="ok") so.record_decision(s, planned, decision, plan_ns=1000, regime="normal") # Rejected decision2 = RiskDecision(approved=False, action=a, reason="kill") so.record_decision(s, planned, decision2, plan_ns=1000, regime="normal") rate = so.get_regime_approval_rate("normal") assert rate == 0.5 def test_avg_latency(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) decision = RiskDecision(approved=True, action=a, reason="ok") so.record_decision(s, planned, decision, plan_ns=1000) so.record_decision(s, planned, decision, plan_ns=2000) assert so.avg_latency_ns == 1500.0 def test_avg_entropy(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) decision = RiskDecision(approved=True, action=a, reason="ok") so.record_decision(s, planned, decision, plan_ns=1000) so.record_decision(s, planned, decision, plan_ns=1000) assert so.avg_entropy == 0.5 def test_total_decisions(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) decision = RiskDecision(approved=True, action=a, reason="ok") for _ in range(20): so.record_decision(s, planned, decision, plan_ns=1000) assert so.total_decisions == 20 def test_feature_importance_sorted(self): from malkhut.actions import FulfilmentAction, PlannedPolicy, RiskDecision so = StructuredObservability() s = _state() a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0) planned = PlannedPolicy(actions=(a,), probabilities=(1.0,), selected_action=a, diagnostics={"entropy": 0.5, "sims": 10}) decision = RiskDecision(approved=True, action=a, reason="ok") for _ in range(10): so.record_decision(s, planned, decision, plan_ns=1000) importance = so.get_feature_importance(top_n=5) for i in range(len(importance) - 1): assert importance[i][1] >= importance[i+1][1] import random from malkhut.actions import OrderType