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