Files
sentiment-engine/MALKHUT/malkhut/tests/test_fuzz.py

158 lines
5.5 KiB
Python
Raw Normal View History

"""
Fuzz testing — random state/action sequences through CWM.
Verifies CWM never crashes, always produces valid outputs,
and preserves invariants under random stress.
"""
import random
import math
import pytest
from malkhut.state import (
AccountState, FulfilmentPolicyParams, IntentKind, MarketWorldState,
Mode, OpenOrderState, OrderBookState, PriceLevel, PositionState,
Side, TradePathState, VenueRules,
)
from malkhut.cwm.core import MinimalCryptoLOBCWM
from malkhut.actions import ActionKind, FulfilmentAction, OrderType
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 _random_book(rng):
bid = rng.uniform(100.0, 100000.0)
ask = bid + rng.uniform(0.1, 100.0)
return OrderBookState(
ts_ns=rng.randint(1, 2**62), symbol="BTCUSDT",
bids=(PriceLevel(bid, rng.uniform(0.001, 10.0)),),
asks=(PriceLevel(ask, rng.uniform(0.001, 10.0)),),
)
def _random_action(rng):
kind = rng.choice(list(ActionKind))
side = rng.choice([Side.BUY, Side.SELL, None])
ot = rng.choice(list(OrderType))
offset = rng.randint(-20, 20)
frac = rng.uniform(0.0, 1.0)
ttl = rng.randint(0, 5000)
cancel_id = f"r_{rng.randint(0, 1000)}" if kind in (ActionKind.CANCEL, ActionKind.CANCEL_REPLACE) else None
return FulfilmentAction(
kind=kind, side=side, order_type=ot,
price_ticks_from_best=offset, qty_fraction=frac, ttl_ms=ttl,
cancel_order_id=cancel_id,
)
def _random_state(rng):
book = _random_book(rng)
equity = rng.uniform(100.0, 100000.0)
pos_qty = rng.choice([0.0, rng.uniform(-0.5, 0.5)])
pos = None
positions = {}
if pos_qty != 0.0:
pos = PositionState(
symbol="BTCUSDT", qty=pos_qty, avg_entry=book.mid,
unrealized_pnl=0.0, realized_pnl=0.0,
liquidation_price=None, leverage=abs(pos_qty * book.mid) / max(equity, 1.0),
side=Side.BUY if pos_qty > 0 else Side.SELL,
)
positions["BTCUSDT"] = pos
return MarketWorldState(
ts_ns=rng.randint(1, 2**62), mode=Mode.REPLAY_NO_IMPACT,
venue=_venue(), book=book,
account=AccountState(
ts_ns=rng.randint(1, 2**62), equity=equity,
wallet_balance=equity, available_balance=equity,
margin_used=0.0, total_notional=abs(pos_qty * book.mid),
positions=positions,
),
)
class TestCWMFuzz:
def test_100_random_noop_transitions(self):
cwm = MinimalCryptoLOBCWM()
rng = random.Random(42)
for _ in range(100):
s = _random_state(rng)
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
r = cwm.transition(s, (a,))
assert r.ts_ns >= s.ts_ns
assert r.account.equity >= 0
def test_100_random_action_transitions(self):
cwm = MinimalCryptoLOBCWM()
rng = random.Random(123)
for _ in range(100):
s = _random_state(rng)
a = _random_action(rng)
r = cwm.transition(s, (a,))
assert r.ts_ns >= s.ts_ns
assert r.account.equity >= 0
def test_100_random_multi_counterparty(self):
from malkhut.counterparties import (
ToxicTakerPolicy, PassiveMakerPolicy, NoiseTraderPolicy,
)
cwm = MinimalCryptoLOBCWM()
rng = random.Random(456)
cps = [ToxicTakerPolicy(), PassiveMakerPolicy(), NoiseTraderPolicy()]
for _ in range(100):
s = _random_state(rng)
a = _random_action(rng)
cp_actions = tuple(cp.rollout_action(s, rng) for cp in cps)
r = cwm.transition(s, (a, *cp_actions))
assert r.ts_ns >= s.ts_ns
# Equity can go negative with aggressive counterparties (realistic)
assert isinstance(r.account.equity, float)
def test_50_sequential_transitions(self):
cwm = MinimalCryptoLOBCWM()
rng = random.Random(789)
s = _random_state(rng)
for i in range(50):
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
s = cwm.transition(s, (a,))
assert s.account.equity >= 0
def test_50_sequential_with_actions(self):
cwm = MinimalCryptoLOBCWM()
rng = random.Random(101)
s = _random_state(rng)
for i in range(50):
a = _random_action(rng)
s = cwm.transition(s, (a,))
# Equity can go negative (realistic: overleveraged position)
assert isinstance(s.account.equity, float)
def test_input_never_mutated(self):
cwm = MinimalCryptoLOBCWM()
rng = random.Random(202)
for _ in range(50):
s = _random_state(rng)
orig_ts = s.ts_ns
orig_equity = s.account.equity
a = _random_action(rng)
cwm.transition(s, (a,))
assert s.ts_ns == orig_ts
assert s.account.equity == orig_equity
def test_determinism_under_fuzz(self):
cwm = MinimalCryptoLOBCWM()
rng = random.Random(303)
for _ in range(50):
s = _random_state(rng)
a = _random_action(rng)
r1 = cwm.transition(s, (a,))
r2 = cwm.transition(s, (a,))
assert r1.ts_ns == r2.ts_ns
assert r1.account.equity == r2.account.equity