""" Counterparty ecology — adversarial agent behavior. """ import random import pytest from malkhut.state import ( AccountState, MarketWorldState, Mode, OrderBookState, PriceLevel, Side, TradePathState, VenueRules, ) from malkhut.counterparties import ( ToxicTakerPolicy, PassiveMakerPolicy, LatencyArbPolicy, NoiseTraderPolicy, default_counterparty_ecology, ) from malkhut.actions import ActionKind, AgentRole def _state(**kw): tp_kw = kw.get("trade_path_kw", {}) tp = TradePathState( symbol="BTCUSDT", side=Side.BUY, entry_ts_ns=0, now_ts_ns=100_000_000, bars_held=10, seconds_held=100.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=30.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=20.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=kw.get("toxicity", 0.3), queue_churn_score=0.2, book_imbalance=0.1, cross_venue_lead_score=kw.get("lead", 0.1), ) if kw.get("with_path", True) else None return MarketWorldState( ts_ns=1_000_000_000, mode=Mode.REPLAY_NO_IMPACT, 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, ), trade_path=tp, ) class TestToxicTaker: def test_noop_when_low_toxicity(self): p = ToxicTakerPolicy() s = _state(toxicity=0.3) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_cross_when_high_toxicity(self): p = ToxicTakerPolicy() s = _state(toxicity=0.9) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD def test_always_has_legal_actions(self): p = ToxicTakerPolicy() s = _state() actions = p.legal_actions(s) assert len(actions) == 3 def test_role_is_toxic_taker(self): p = ToxicTakerPolicy() assert p.role == AgentRole.TOXIC_TAKER def test_toxicity_field_set(self): p = ToxicTakerPolicy() s = _state(toxicity=0.9) rng = random.Random(42) a = p.rollout_action(s, rng) if a.kind == ActionKind.CROSS_SPREAD: assert a.toxicity > 0 class TestPassiveMaker: def test_always_has_legal_actions(self): p = PassiveMakerPolicy() s = _state() actions = p.legal_actions(s) assert len(actions) == 4 def test_role_is_passive_maker(self): p = PassiveMakerPolicy() assert p.role == AgentRole.PASSIVE_MAKER def test_rollout_can_place(self): p = PassiveMakerPolicy(join_probability=1.0) s = _state() rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.PLACE def test_rollout_can_noop(self): p = PassiveMakerPolicy(join_probability=0.0) s = _state() rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_deterministic_with_same_seed(self): p = PassiveMakerPolicy() s = _state() a1 = p.rollout_action(s, random.Random(99)) a2 = p.rollout_action(s, random.Random(99)) assert a1.kind == a2.kind class TestLatencyArb: def test_noop_when_low_lead(self): p = LatencyArbPolicy() s = _state(lead=0.3) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.NOOP def test_cross_when_high_lead(self): p = LatencyArbPolicy() s = _state(lead=0.8) rng = random.Random(42) a = p.rollout_action(s, rng) assert a.kind == ActionKind.CROSS_SPREAD def test_always_has_legal_actions(self): p = LatencyArbPolicy() s = _state() actions = p.legal_actions(s) assert len(actions) == 3 class TestNoiseTrader: def test_always_has_legal_actions(self): p = NoiseTraderPolicy() s = _state() actions = p.legal_actions(s) assert len(actions) == 3 def test_role_is_noise_trader(self): p = NoiseTraderPolicy() assert p.role == AgentRole.NOISE_TRADER def test_rollout_can_cross(self): p = NoiseTraderPolicy() s = _state() rng = random.Random(42) crosses = 0 for seed in range(100): a = p.rollout_action(s, random.Random(seed)) if a.kind == ActionKind.CROSS_SPREAD: crosses += 1 assert crosses > 0 class TestDefaultEcology: def test_has_four_agents(self): eco = default_counterparty_ecology() assert len(eco) == 4 def test_unique_roles(self): eco = default_counterparty_ecology() roles = [p.role for p in eco] assert len(set(roles)) == 4 def test_all_have_legal_actions(self): eco = default_counterparty_ecology() s = _state() for p in eco: actions = p.legal_actions(s) assert len(actions) > 0