Files
sentiment-engine/MALKHUT/malkhut/tests/test_replay_exhaustive.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

601 lines
23 KiB
Python

"""
Exhaustive replay verification tests.
Categories:
1. ReplayStep construction
2. Deep state comparison (all field types)
3. Tolerance handling (float, int, string)
4. Binary search (first mismatch, no mismatch, boundary)
5. Trajectory recording (record, hash, determinism)
6. ReplayVerifier.verify() (historical mode)
7. ReplayVerifier.verify_determinism() (self-play mode)
8. Edge cases (empty replay, single step, identical states)
9. hftbacktest integration hooks
"""
import pytest
from malkhut.state import (
AccountState, FulfilmentPolicyParams, MarketWorldState, Mode,
OpenOrderState, OrderBookState, PositionState, PriceLevel, Side,
VenueRules,
)
from malkhut.actions import ActionKind, CounterpartyAction, AgentRole, FulfilmentAction, OrderType
from malkhut.cwm.core import MinimalCryptoLOBCWM
from malkhut.cwm.replay_verify import (
ReplayStep, ReplayMismatch, ReplayResult, TrajectoryRecord,
TrajectoryRecorder, ReplayVerifier, _compare_deep, _hash_state,
bisect_first_mismatch,
)
def _venue(**kw):
d = dict(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)
d.update(kw)
return VenueRules(**d)
def _book(bid=50000.0, ask=50001.0, bid_qty=1.0, ask_qty=1.0):
return OrderBookState(
ts_ns=1_000_000_000, symbol="BTCUSDT",
bids=(PriceLevel(bid, bid_qty),),
asks=(PriceLevel(ask, ask_qty),),
)
def _account(equity=10000.0):
return AccountState(
ts_ns=1_000_000_000, equity=equity, wallet_balance=equity,
available_balance=equity, margin_used=0.0, total_notional=0.0,
)
def _state(bid=50000.0, ask=50001.0, equity=10000.0, ts=1_000_000_000, **kw):
return MarketWorldState(
ts_ns=ts, mode=Mode.REPLAY_NO_IMPACT,
venue=kw.get("venue", _venue()),
book=_book(bid, ask),
account=_account(equity),
open_orders=kw.get("open_orders", ()),
trade_path=kw.get("trade_path"),
)
def _noop():
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
def _cross(side, frac=0.1):
return FulfilmentAction(ActionKind.CROSS_SPREAD, side, OrderType.IOC, 0, frac, 50)
# ══════════════════════════════════════════════════════════════════════════════
# 1. REPLAY STEP
# ══════════════════════════════════════════════════════════════════════════════
class TestReplayStep:
def test_construction(self):
s = _state()
a = _noop()
cwm = MinimalCryptoLOBCWM()
after = cwm.transition(s, (a,))
step = ReplayStep(before=s, joint_action=(a,), after_ground_truth=after)
assert step.before.ts_ns == s.ts_ns
assert step.after_ground_truth.ts_ns == after.ts_ns
def test_with_metadata(self):
s = _state()
step = ReplayStep(
before=s, joint_action=(_noop(),), after_ground_truth=s,
metadata={"source": "test"},
)
assert step.metadata["source"] == "test"
def test_frozen(self):
s = _state()
step = ReplayStep(before=s, joint_action=(_noop(),), after_ground_truth=s)
with pytest.raises(AttributeError):
step.step_index = 5
# ══════════════════════════════════════════════════════════════════════════════
# 2. DEEP STATE COMPARISON
# ══════════════════════════════════════════════════════════════════════════════
class TestDeepComparison:
def test_identical_states_no_mismatch(self):
s = _state()
diffs = _compare_deep(0, s, s)
assert len(diffs) == 0
def test_different_ts_detected(self):
s1 = _state(ts=1)
s2 = _state(ts=2)
diffs = _compare_deep(0, s1, s2)
assert any(d.field == "ts_ns" for d in diffs)
def test_different_equity_detected(self):
s1 = _state(equity=10000.0)
s2 = _state(equity=9000.0)
diffs = _compare_deep(0, s1, s2)
assert any(d.field == "account.equity" for d in diffs)
def test_different_bid_detected(self):
s1 = _state(bid=50000.0)
s2 = _state(bid=50001.0)
diffs = _compare_deep(0, s1, s2)
assert any(d.field == "book.best_bid" for d in diffs)
def test_different_ask_detected(self):
s1 = _state(ask=50001.0)
s2 = _state(ask=50002.0)
diffs = _compare_deep(0, s1, s2)
assert any(d.field == "book.best_ask" for d in diffs)
def test_within_tolerance_no_mismatch(self):
s1 = _state(bid=50000.0)
s2 = _state(bid=50000.0001)
diffs = _compare_deep(0, s1, s2, {"book_price": 0.001})
assert len(diffs) == 0
def test_outside_tolerance_mismatch(self):
s1 = _state(bid=50000.0)
s2 = _state(bid=50000.1)
diffs = _compare_deep(0, s1, s2, {"book_price": 0.001})
assert len(diffs) > 0
def test_different_symbol_detected(self):
v1 = _venue(symbol="BTCUSDT")
v2 = _venue(symbol="ETHUSDT")
s1 = _state(venue=v1)
s2 = _state(venue=v2)
diffs = _compare_deep(0, s1, s2)
assert any(d.field == "venue.symbol" for d in diffs)
def test_different_open_order_count(self):
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,
)
s1 = _state(open_orders=(oo,))
s2 = _state(open_orders=())
diffs = _compare_deep(0, s1, s2)
assert any(d.field == "open_orders.count" for d in diffs)
def test_different_position_detected(self):
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,
)
s1 = _state()
s2 = MarketWorldState(
ts_ns=1, mode=Mode.REPLAY_NO_IMPACT, venue=_venue(),
book=_book(), 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={"BTCUSDT": pos},
),
)
diffs = _compare_deep(0, s1, s2)
assert any("positions" in d.field for d in diffs)
def test_missing_position_critical(self):
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,
)
s1 = MarketWorldState(
ts_ns=1, mode=Mode.REPLAY_NO_IMPACT, venue=_venue(),
book=_book(), 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={"BTCUSDT": pos},
),
)
s2 = _state()
diffs = _compare_deep(0, s1, s2)
critical = [d for d in diffs if d.is_critical]
assert len(critical) > 0
# ══════════════════════════════════════════════════════════════════════════════
# 3. HASH FUNCTIONS
# ══════════════════════════════════════════════════════════════════════════════
class TestHashing:
def test_hash_state_deterministic(self):
s = _state()
h1 = _hash_state(s)
h2 = _hash_state(s)
assert h1 == h2
def test_hash_state_different_for_different_states(self):
s1 = _state(bid=50000.0)
s2 = _state(bid=51000.0)
assert _hash_state(s1) != _hash_state(s2)
def test_hash_action_deterministic(self):
a = _noop()
from malkhut.cwm.replay_verify import _hash_action
h1 = _hash_action(a)
h2 = _hash_action(a)
assert h1 == h2
# ══════════════════════════════════════════════════════════════════════════════
# 4. BINARY SEARCH
# ══════════════════════════════════════════════════════════════════════════════
class TestBisect:
def test_bisect_no_mismatch(self):
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
replay = [
ReplayStep(before=s, joint_action=(a,), after_ground_truth=cwm.transition(s, (a,))),
]
result = bisect_first_mismatch(cwm, replay)
assert result is None
def test_bisect_finds_first_mismatch(self):
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
r1 = cwm.transition(s, (a,))
r2 = cwm.transition(r1, (a,))
# Tamper with step 1
bad = MarketWorldState(
ts_ns=r1.ts_ns + 999, mode=r1.mode, venue=r1.venue,
book=r1.book, account=r1.account,
)
replay = [
ReplayStep(before=s, joint_action=(a,), after_ground_truth=r1),
ReplayStep(before=r1, joint_action=(a,), after_ground_truth=bad),
ReplayStep(before=r2, joint_action=(a,), after_ground_truth=cwm.transition(r2, (a,))),
]
result = bisect_first_mismatch(cwm, replay)
assert result is not None
assert result.index == 1
def test_bisect_empty_replay(self):
cwm = MinimalCryptoLOBCWM()
result = bisect_first_mismatch(cwm, [])
assert result is None
def test_bisect_single_step_match(self):
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
replay = [ReplayStep(before=s, joint_action=(a,), after_ground_truth=cwm.transition(s, (a,)))]
result = bisect_first_mismatch(cwm, replay)
assert result is None
def test_bisect_single_step_mismatch(self):
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
bad = MarketWorldState(
ts_ns=999, mode=Mode.REPLAY_NO_IMPACT, venue=_venue(),
book=_book(50000.0, 50001.0), account=_account(9999.0),
)
replay = [ReplayStep(before=s, joint_action=(a,), after_ground_truth=bad)]
result = bisect_first_mismatch(cwm, replay)
assert result is not None
assert result.index == 0
# ══════════════════════════════════════════════════════════════════════════════
# 5. TRAJECTORY RECORDING
# ══════════════════════════════════════════════════════════════════════════════
class TestTrajectoryRecorder:
def test_record_step(self):
rec = TrajectoryRecorder()
s = _state()
a = _noop()
cwm = MinimalCryptoLOBCWM()
after = cwm.transition(s, (a,))
rec.record(0, s, (a,), after)
assert rec.step_count == 1
def test_trajectory_hash_deterministic(self):
rec = TrajectoryRecorder()
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
for i in range(5):
after = cwm.transition(s, (a,))
rec.record(i, s, (a,), after)
s = after
h1 = rec.trajectory_hash()
h2 = rec.trajectory_hash()
assert h1 == h2
def test_max_steps_respected(self):
rec = TrajectoryRecorder(max_steps=3)
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
for i in range(10):
after = cwm.transition(s, (a,))
rec.record(i, s, (a,), after)
s = after
assert rec.step_count == 3
def test_verify_deterministic_passes(self):
rec = TrajectoryRecorder()
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
for i in range(3):
after = cwm.transition(s, (a,))
rec.record(i, s, (a,), after)
s = after
ok, mismatches = rec.verify_deterministic(cwm)
assert ok
assert len(mismatches) == 0
def test_to_replay_steps(self):
rec = TrajectoryRecorder()
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
after = cwm.transition(s, (a,))
rec.record(0, s, (a,), after)
steps = rec.to_replay_steps()
assert len(steps) == 1
assert isinstance(steps[0], ReplayStep)
def test_steps_property(self):
rec = TrajectoryRecorder()
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
after = cwm.transition(s, (a,))
rec.record(0, s, (a,), after)
assert len(rec.steps) == 1
# ══════════════════════════════════════════════════════════════════════════════
# 6. REPLAY VERIFIER
# ══════════════════════════════════════════════════════════════════════════════
class TestReplayVerifier:
def _verifier(self):
return ReplayVerifier()
def _make_replay(self, steps=3):
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
replay = []
for i in range(steps):
after = cwm.transition(s, (a,))
replay.append(ReplayStep(before=s, joint_action=(a,), after_ground_truth=after))
s = after
return replay
def test_verify_passes_for_correct_replay(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(3)
result = v.verify(cwm, replay)
assert result.passed
assert result.steps_verified == 3
def test_verify_fails_for_tampered_replay(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(3)
# Tamper with step 1
bad = MarketWorldState(
ts_ns=999, mode=Mode.REPLAY_NO_IMPACT, venue=_venue(),
book=_book(50000.0, 50001.0), account=_account(9999.0),
)
replay[1] = ReplayStep(
before=replay[1].before, joint_action=replay[1].joint_action,
after_ground_truth=bad,
)
result = v.verify(cwm, replay)
assert not result.passed
assert result.first_mismatch_index == 1
def test_verify_determinism_passes(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(3)
result = v.verify_determinism(cwm, replay)
assert result.passed
def test_verify_result_has_trajectory_hash(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(3)
result = v.verify(cwm, replay)
assert len(result.trajectory_hash) == 16
def test_verify_result_has_timing(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(3)
result = v.verify(cwm, replay)
assert result.duration_ns > 0
def test_verify_empty_replay(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
result = v.verify(cwm, [])
assert result.passed
assert result.steps_verified == 0
def test_verify_single_step(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(1)
result = v.verify(cwm, replay)
assert result.passed
def test_verify_with_tolerance(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
after = cwm.transition(s, (a,))
# Create slightly different ground truth
bad = MarketWorldState(
ts_ns=after.ts_ns, mode=after.mode, venue=after.venue,
book=OrderBookState(
ts_ns=after.book.ts_ns, symbol=after.book.symbol,
bids=(PriceLevel(after.book.best_bid + 0.001, 1.0),),
asks=after.book.asks,
),
account=after.account,
)
replay = [ReplayStep(before=s, joint_action=(a,), after_ground_truth=bad)]
# Tight tolerance → mismatch
result = v.verify(cwm, replay, tolerances={"book_price": 1e-9})
assert not result.passed
# Loose tolerance → match
result2 = v.verify(cwm, replay, tolerances={"book_price": 0.01})
assert result2.passed
def test_bisect_finds_first(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
r1 = cwm.transition(s, (a,))
r2 = cwm.transition(r1, (a,))
bad = MarketWorldState(
ts_ns=r1.ts_ns + 999, mode=r1.mode, venue=r1.venue,
book=r1.book, account=r1.account,
)
replay = [
ReplayStep(before=s, joint_action=(a,), after_ground_truth=r1),
ReplayStep(before=r1, joint_action=(a,), after_ground_truth=bad),
ReplayStep(before=r2, joint_action=(a,), after_ground_truth=cwm.transition(r2, (a,))),
]
result = v.bisect(cwm, replay)
assert result is not None
assert result.index == 1
def test_result_match_rate(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(5)
result = v.verify(cwm, replay)
assert result.match_rate == 1.0
def test_result_critical_count(self):
v = self._verifier()
result = ReplayResult(
passed=False, mismatches=[
ReplayMismatch(0, "x", 1, 2, "critical"),
ReplayMismatch(0, "y", 1, 2, "warning"),
],
steps_verified=1, total_steps=1, first_mismatch_index=0,
duration_ns=0, trajectory_hash="abc",
)
assert result.critical_count == 1
assert result.warning_count == 1
def test_replay_step_index(self):
v = self._verifier()
cwm = MinimalCryptoLOBCWM()
replay = self._make_replay(2)
result = v.verify(cwm, replay)
assert result.steps_verified == 2
# ══════════════════════════════════════════════════════════════════════════════
# 7. REPLAY RESULT
# ══════════════════════════════════════════════════════════════════════════════
class TestReplayResult:
def test_passed_result(self):
r = ReplayResult(
passed=True, mismatches=[], steps_verified=10, total_steps=10,
first_mismatch_index=None, duration_ns=1000, trajectory_hash="abc",
)
assert r.match_rate == 1.0
assert r.critical_count == 0
def test_failed_result(self):
r = ReplayResult(
passed=False,
mismatches=[ReplayMismatch(5, "x", 1, 2, "critical")],
steps_verified=6, total_steps=10,
first_mismatch_index=5, duration_ns=1000, trajectory_hash="abc",
)
assert r.match_rate == 0.6
assert r.first_mismatch_index == 5
assert r.critical_count == 1
# ══════════════════════════════════════════════════════════════════════════════
# 8. INTEGRATION: CWM + REPLAY VERIFIER
# ══════════════════════════════════════════════════════════════════════════════
class TestCWMReplayIntegration:
def test_full_trajectory_verify(self):
"""Record a trajectory through CWM, then verify it matches."""
cwm = MinimalCryptoLOBCWM()
rec = TrajectoryRecorder()
s = _state()
actions = [_noop(), _cross(Side.BUY, 0.1), _noop(), _noop()]
for i, a in enumerate(actions):
after = cwm.transition(s, (a,))
rec.record(i, s, (a,), after)
s = after
# Verify
verifier = ReplayVerifier()
replay = rec.to_replay_steps()
result = verifier.verify(cwm, replay)
assert result.passed
def test_self_play_determinism(self):
"""Record self-play, verify deterministic re-run."""
cwm = MinimalCryptoLOBCWM()
rec = TrajectoryRecorder()
s = _state()
a = _noop()
for i in range(5):
after = cwm.transition(s, (a,))
rec.record(i, s, (a,), after)
s = after
ok, mismatches = rec.verify_deterministic(cwm)
assert ok
def test_cwm_bug_detected_by_replay(self):
"""Inject a bug in CWM, verify replay catches it."""
cwm = MinimalCryptoLOBCWM()
s = _state()
a = _noop()
# Record ground truth
ground_truth = cwm.transition(s, (a,))
# Tamper with ground truth (simulate a bug)
bad_gt = MarketWorldState(
ts_ns=ground_truth.ts_ns + 999,
mode=ground_truth.mode, venue=ground_truth.venue,
book=ground_truth.book, account=ground_truth.account,
)
replay = [ReplayStep(before=s, joint_action=(a,), after_ground_truth=bad_gt)]
verifier = ReplayVerifier()
result = verifier.verify(cwm, replay)
assert not result.passed
assert result.mismatches[0].field == "ts_ns"