Files
sentiment-engine/MALKHUT/malkhut/tests/test_counterparties.py
Codex 4c239f7774 malkhut(tests): 1140 test functions across 46 test files
CWM (103): core mechanics, exhaustive edge cases, numba, exchange mechanics
Replay (118): exhaustive verification, microstructure, trajectory
Training (190): asset classification, phase0 extensive, pipeline, exhaustive
DSL (102): v2 syntax, expanded, new features
ASEx (33): validate-before-mutate, single-writer
Planner (48): MCTS, alternatives, hooks
Counterparties (19): 9 adversarial agent policies
Clock (30): event-driven reactor
BingX (28): venue adapter
IPC (8): Zinc SHM
Storage (9): ClickHouse
Risk (4): hard invariants
State (17): frozen dataclass invariants
Integration: E2E, concurrency, sync/async seams, hypothesis, fuzz, adversarial
2026-07-11 10:46:12 +02:00

182 lines
5.8 KiB
Python

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