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
182 lines
5.8 KiB
Python
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
|