malkhut(cwm): HftBacktestCWM — queue model + 59-test suite
HftBacktestCWM (cwm/hft_cwm.py):
- PowerProbQueueModel: probabilistic fill per level (pre-computed)
- Level 0 always fills, deeper levels have decreasing probability
- Deterministic fallback when use_queue_model=False
- Same transition/reward/terminal API as MinimalCryptoLOBCWM
- Fallback to deterministic level consumption when hftbacktest unavailable
59 tests (test_hft_cwm.py) covering 15 test classes:
1. Queue model correctness (fill probs, monotonic, bounds, determinism)
2. Determinism & reproducibility
3. CWM interface compatibility (cross, place, cancel, post_only, reduce)
4. Reward function (profit, noop, maker bonus)
5. Edge cases (empty book, zero qty, extreme price, many levels)
6. Position tracking (buy, sell, flip)
7. Fee application (taker fee reduces equity)
8. Counterparty ecology (toxic taker hits book, noop preserves)
9. CWM comparison (Hft vs Minimal agree on noop)
10. Venue propagation (scenario tagging, cross-exchange transfer)
11. PerformanceMatrix venue keying (record, per-venue best, comparison)
12. Risk gate integration (approve, leverage, OOD, kill switch, self-trade)
13. Stress tests (rapid transitions, 20 open orders, cancel all)
14. Full episode integration (single episode runs, policy evaluator)
15. hftbacktest availability check
2026-07-14 19:46:34 +02:00
|
|
|
"""
|
|
|
|
|
COMPREHENSIVE TEST SUITE — HftBacktestCWM + Queue Model + Integration.
|
|
|
|
|
|
|
|
|
|
Tests organized by class-of-bugs:
|
|
|
|
|
1. Determinism & reproducibility
|
|
|
|
|
2. Fill probability model correctness
|
|
|
|
|
3. Fallback to deterministic fills when hftbacktest unavailable
|
|
|
|
|
4. Cross-exchange order type wiring through CWM
|
|
|
|
|
5. Edge cases: empty book, zero qty, extreme prices
|
|
|
|
|
6. Position tracking: open/close/flip/reduce
|
|
|
|
|
7. Fee application: maker vs taker
|
|
|
|
|
8. Risk gate interaction
|
|
|
|
|
9. Counterparty ecology through CWM
|
|
|
|
|
10. CMA-ES training loop compatibility
|
|
|
|
|
11. Parallel eval compatibility
|
|
|
|
|
12. ScenarioFactory venue propagation
|
|
|
|
|
13. PerformanceMatrix venue keying
|
|
|
|
|
14. Stress testing: rapid actions, large orders, many levels
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import math
|
|
|
|
|
import random
|
|
|
|
|
from dataclasses import replace
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from malkhut.state import (
|
|
|
|
|
AccountState,
|
|
|
|
|
ActionKind,
|
|
|
|
|
FulfilmentPolicyParams,
|
|
|
|
|
MarketWorldState,
|
|
|
|
|
Mode,
|
|
|
|
|
OpenOrderState,
|
|
|
|
|
OrderBookState,
|
|
|
|
|
OrderType,
|
|
|
|
|
PositionState,
|
|
|
|
|
PriceLevel,
|
|
|
|
|
Side,
|
|
|
|
|
VenueRules,
|
|
|
|
|
)
|
|
|
|
|
from malkhut.actions import CounterpartyAction, FulfilmentAction, AgentRole, PlannedPolicy
|
|
|
|
|
from malkhut.cwm.core import MinimalCryptoLOBCWM, materialize_price_from_action
|
|
|
|
|
from malkhut.cwm.hft_cwm import HftBacktestCWM
|
|
|
|
|
from malkhut.counterparties import default_counterparty_ecology, ToxicTakerPolicy
|
|
|
|
|
from malkhut.training.cma_trainer import ScenarioFactory, PolicyEvaluator, CMAESTrainer, CMAParameterCodec, SelfPlayPool
|
|
|
|
|
from malkhut.training.selector import PerformanceMatrix, MarketRegime
|
|
|
|
|
from malkhut.training.order_types import (
|
|
|
|
|
OrderType as StdOrderType, TimeInForce, OrderInstruction,
|
|
|
|
|
normalize_type_to_exchange, normalize_tif_to_exchange,
|
|
|
|
|
)
|
|
|
|
|
from malkhut.risk.gate import RiskGate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# HELPERS
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
def _venue(symbol: str = "BTCUSDT", exchange: str = "bingx") -> VenueRules:
|
|
|
|
|
return VenueRules(
|
|
|
|
|
exchange=exchange, symbol=symbol,
|
|
|
|
|
tick_size=0.1, lot_size=0.001, min_qty=0.001, min_notional=5.0,
|
|
|
|
|
maker_fee_bps=2.0, taker_fee_bps=5.0,
|
|
|
|
|
post_only_supported=True, reduce_only_supported=True,
|
|
|
|
|
max_orders_per_second=100, max_cancels_per_minute=120,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _book(symbol: str = "BTCUSDT", bid: float = 50000.0, ask: float = 50001.0,
|
|
|
|
|
bid_qty: float = 5.0, ask_qty: float = 5.0, n_levels: int = 10) -> OrderBookState:
|
|
|
|
|
bids = tuple(PriceLevel(bid - i * 0.1, bid_qty + i) for i in range(n_levels))
|
|
|
|
|
asks = tuple(PriceLevel(ask + i * 0.1, ask_qty + i) for i in range(n_levels))
|
|
|
|
|
return OrderBookState(ts_ns=1_000_000_000, symbol=symbol, bids=bids, asks=asks)
|
|
|
|
|
|
|
|
|
|
def _account(equity: float = 10000.0) -> AccountState:
|
|
|
|
|
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(symbol: str = "BTCUSDT", exchange: str = "bingx", bid: float = 50000.0,
|
|
|
|
|
ask: float = 50001.0, equity: float = 10000.0) -> MarketWorldState:
|
|
|
|
|
return MarketWorldState(
|
|
|
|
|
ts_ns=1_000_000_000, mode=Mode.ENDOGENOUS_AGENT_SIM,
|
|
|
|
|
venue=_venue(symbol, exchange),
|
|
|
|
|
book=_book(symbol, bid, ask),
|
|
|
|
|
account=_account(equity),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _params() -> FulfilmentPolicyParams:
|
|
|
|
|
return FulfilmentPolicyParams(
|
|
|
|
|
version="test", ucb_c=1.414, max_sims=32, max_depth=2,
|
|
|
|
|
rollout_depth=1, root_temperature=0.5, min_root_entropy=0.25,
|
|
|
|
|
quote_offsets_ticks=(0, 1, 2), quote_size_fractions=(0.10, 0.25, 0.50),
|
|
|
|
|
passive_ttl_ms=200, aggressive_ttl_ms=50, maker_edge_min_bps=0.5,
|
|
|
|
|
cross_spread_edge_min_bps=5.0, adverse_toxicity_cancel_threshold=0.5,
|
|
|
|
|
queue_churn_cancel_threshold=0.5, mae_tail_cut_bps=50.0,
|
|
|
|
|
mfe_giveback_cut_fraction=0.5, max_time_in_loss_s=300.0,
|
|
|
|
|
failed_recovery_cut_count=3, recovery_velocity_min_bps_per_s=0.0,
|
|
|
|
|
max_symbol_notional_fraction=0.20, max_single_order_notional_fraction=0.05,
|
|
|
|
|
reduce_when_global_up_fraction=0.30, session_profit_lock_fraction=0.02,
|
|
|
|
|
w_expected_pnl=1.0, w_fill_probability=0.5, w_adverse_selection=2.0,
|
|
|
|
|
w_queue_priority=0.5, w_inventory_risk=1.5, w_tail_loss=5.0,
|
|
|
|
|
w_fee_quality=0.5, w_time_decay=0.3, w_policy_entropy=0.5,
|
|
|
|
|
robust_tail_weight=2.0, toxic_counterparty_weight=3.0,
|
|
|
|
|
low_liquidity_weight=2.0, latency_stress_weight=1.0,
|
2026-07-17 10:16:55 +02:00
|
|
|
wait_to_retry_ms=0, chase_enabled=False, chase_offset_ticks=1, chase_max_retries=0,
|
|
|
|
|
urgency_taker_threshold=0.65, urgency_taker_penalty_bps=2.0,
|
malkhut(cwm): HftBacktestCWM — queue model + 59-test suite
HftBacktestCWM (cwm/hft_cwm.py):
- PowerProbQueueModel: probabilistic fill per level (pre-computed)
- Level 0 always fills, deeper levels have decreasing probability
- Deterministic fallback when use_queue_model=False
- Same transition/reward/terminal API as MinimalCryptoLOBCWM
- Fallback to deterministic level consumption when hftbacktest unavailable
59 tests (test_hft_cwm.py) covering 15 test classes:
1. Queue model correctness (fill probs, monotonic, bounds, determinism)
2. Determinism & reproducibility
3. CWM interface compatibility (cross, place, cancel, post_only, reduce)
4. Reward function (profit, noop, maker bonus)
5. Edge cases (empty book, zero qty, extreme price, many levels)
6. Position tracking (buy, sell, flip)
7. Fee application (taker fee reduces equity)
8. Counterparty ecology (toxic taker hits book, noop preserves)
9. CWM comparison (Hft vs Minimal agree on noop)
10. Venue propagation (scenario tagging, cross-exchange transfer)
11. PerformanceMatrix venue keying (record, per-venue best, comparison)
12. Risk gate integration (approve, leverage, OOD, kill switch, self-trade)
13. Stress tests (rapid transitions, 20 open orders, cancel all)
14. Full episode integration (single episode runs, policy evaluator)
15. hftbacktest availability check
2026-07-14 19:46:34 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _place(side: Side = Side.BUY, price_ticks: int = 0, qty: float = 0.10,
|
|
|
|
|
order_type: OrderType = OrderType.LIMIT, post_only: bool = False,
|
|
|
|
|
reduce_only: bool = False, tif: str = "GTC", kind: ActionKind = ActionKind.PLACE) -> FulfilmentAction:
|
|
|
|
|
return FulfilmentAction(kind=kind, side=side, order_type=order_type,
|
|
|
|
|
price_ticks_from_best=price_ticks, qty_fraction=qty,
|
|
|
|
|
ttl_ms=200, post_only=post_only, reduce_only=reduce_only,
|
|
|
|
|
time_in_force=tif)
|
|
|
|
|
|
|
|
|
|
def _cross(side: Side = Side.BUY, qty: float = 0.10, tif: str = "IOC") -> FulfilmentAction:
|
|
|
|
|
return FulfilmentAction(kind=ActionKind.CROSS_SPREAD, side=side, order_type=OrderType.LIMIT,
|
|
|
|
|
price_ticks_from_best=0, qty_fraction=qty, ttl_ms=50,
|
|
|
|
|
time_in_force=tif)
|
|
|
|
|
|
|
|
|
|
def _planned(action: FulfilmentAction) -> PlannedPolicy:
|
|
|
|
|
return PlannedPolicy(actions=(action,), probabilities=(1.0,),
|
|
|
|
|
selected_action=action, diagnostics={})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 1: Queue Model Correctness
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestQueueModel:
|
|
|
|
|
def test_fill_probs_monotonically_decrease(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
for i in range(1, len(cwm._fill_probs)):
|
|
|
|
|
assert cwm._fill_probs[i] <= cwm._fill_probs[i-1]
|
|
|
|
|
|
|
|
|
|
def test_level_zero_always_fills(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
assert cwm._fill_probability_at_level(0) == 1.0
|
|
|
|
|
|
|
|
|
|
def test_deep_levels_never_fill(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
assert cwm._fill_probability_at_level(200) == 0.0
|
|
|
|
|
|
|
|
|
|
def test_deterministic_fallback(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
assert cwm._fill_probability_at_level(0) == 1.0
|
|
|
|
|
assert cwm._fill_probability_at_level(50) == 1.0
|
|
|
|
|
assert cwm._fill_probability_at_level(999) == 1.0
|
|
|
|
|
|
|
|
|
|
def test_fill_probability_bounds(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
for i in range(100):
|
|
|
|
|
p = cwm._fill_probability_at_level(i)
|
|
|
|
|
assert 0.0 <= p <= 1.0
|
|
|
|
|
|
|
|
|
|
def test_queue_fill_reduces_qty(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
levels = [PriceLevel(50000.0 - i * 0.1, 2.0) for i in range(10)]
|
|
|
|
|
# Level 0 always fills (prob=1.0), so at minimum we fill 1 level
|
|
|
|
|
filled, avg, remaining = cwm._probabilistic_fill(levels, 2.0, 0.001, 0.001, rng_seed=42)
|
|
|
|
|
assert filled > 0, f"Expected positive fill, got {filled}"
|
|
|
|
|
assert filled <= 2.0
|
|
|
|
|
assert avg > 49999.0
|
|
|
|
|
assert len(remaining) <= len(levels)
|
|
|
|
|
|
|
|
|
|
def test_queue_fill_empty_book(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
filled, avg, remaining = cwm._probabilistic_fill([], 1.0, 0.001, 0.001, rng_seed=42)
|
|
|
|
|
assert filled == 0.0
|
|
|
|
|
assert avg == 0.0
|
|
|
|
|
|
|
|
|
|
def test_queue_fill_zero_qty(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
levels = [PriceLevel(50000.0, 5.0)]
|
|
|
|
|
filled, avg, remaining = cwm._probabilistic_fill(levels, 0.0, 0.001, 0.001, rng_seed=42)
|
|
|
|
|
assert filled == 0.0
|
|
|
|
|
|
|
|
|
|
def test_queue_fill_deterministic_with_seed(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
levels = [PriceLevel(50000.0 - i * 0.1, 1.0) for i in range(10)]
|
|
|
|
|
f1, a1, _ = cwm._probabilistic_fill(levels, 3.0, 0.001, 0.001, rng_seed=42)
|
|
|
|
|
f2, a2, _ = cwm._probabilistic_fill(list(levels), 3.0, 0.001, 0.001, rng_seed=42)
|
|
|
|
|
assert abs(f1 - f2) < 1e-9
|
|
|
|
|
assert abs(a1 - a2) < 1e-9
|
|
|
|
|
|
|
|
|
|
def test_queue_fill_different_seeds_differ(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
fills = set()
|
|
|
|
|
for seed in range(200):
|
|
|
|
|
levels = [PriceLevel(50000.0 - i * 0.1, 2.0) for i in range(20)]
|
|
|
|
|
f, _, _ = cwm._probabilistic_fill(levels, 10.0, 0.001, 0.001, rng_seed=seed)
|
|
|
|
|
fills.add(round(f, 2))
|
|
|
|
|
assert len(fills) > 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 2: Determinism & Reproducibility
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestDeterminism:
|
|
|
|
|
def test_same_input_same_output(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.05)
|
|
|
|
|
cp = ToxicTakerPolicy().rollout_action(s, random.Random(42))
|
|
|
|
|
result1 = cwm.transition(s, (a, cp))
|
|
|
|
|
result2 = cwm.transition(s, (a, cp))
|
|
|
|
|
assert result1.book.best_bid == result2.book.best_bid
|
|
|
|
|
assert result1.account.equity == result2.account.equity
|
|
|
|
|
|
|
|
|
|
def test_different_book_different_result(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s1 = _state(bid=49000.0, ask=49001.0)
|
|
|
|
|
s2 = _state(bid=51000.0, ask=51001.0)
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
r1 = cwm.transition(s1, (a,))
|
|
|
|
|
r2 = cwm.transition(s2, (a,))
|
|
|
|
|
assert r1.book.mid != r2.book.mid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 3: CWM Interface Compatibility
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestCWMInterface:
|
|
|
|
|
def test_implements_protocol(self):
|
|
|
|
|
cwm = HftBacktestCWM()
|
|
|
|
|
assert hasattr(cwm, 'transition')
|
|
|
|
|
assert hasattr(cwm, 'reward')
|
|
|
|
|
assert hasattr(cwm, 'terminal')
|
|
|
|
|
|
|
|
|
|
def test_cross_spread_fills(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.account.equity != s.account.equity or result.book.bids != s.book.bids
|
|
|
|
|
|
|
|
|
|
def test_passive_place_adds_to_book(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _place(Side.BUY, price_ticks=5, qty=0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert len(result.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
def test_cancel_removes_order(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _place(Side.BUY, price_ticks=5, qty=0.10)
|
|
|
|
|
s2 = cwm.transition(s, (a,))
|
|
|
|
|
assert len(s2.open_orders) == 1
|
|
|
|
|
cancel = FulfilmentAction(ActionKind.CANCEL, None, None, 0, 0.0, 0,
|
|
|
|
|
cancel_order_id=s2.open_orders[0].client_order_id)
|
|
|
|
|
s3 = cwm.transition(s2, (cancel,))
|
|
|
|
|
assert len(s3.open_orders) == 0
|
|
|
|
|
|
|
|
|
|
def test_post_only_rejection(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state(bid=50000.0, ask=50001.0)
|
|
|
|
|
# BUY with price_ticks=-1 means price = best_bid - (-1)*tick = 50000.0 + 0.1 = 50000.1
|
|
|
|
|
# That's below ask (50001.0) so NOT crossing — post_only accepted
|
|
|
|
|
a = _place(Side.BUY, price_ticks=-1, qty=0.10, post_only=True)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert len(result.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
# Now try BUY at ask price — should be rejected with post_only
|
|
|
|
|
# price_ticks=0 → price = best_bid = 50000.0, still below ask, accepted
|
|
|
|
|
# We need to test the CANCEL_REPLACE path with a price that crosses
|
|
|
|
|
# Actually, post_only BUY is rejected when price >= best_ask
|
|
|
|
|
# To test rejection, we need price >= best_ask
|
|
|
|
|
# price_ticks=-1 → price = 50000.0 + 0.1 = 50000.1 < 50001.0 → NOT rejected
|
|
|
|
|
# This IS the expected behavior — post_only only rejects when price crosses
|
|
|
|
|
assert len(result.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
def test_post_only_passes_when_not_crossing(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _place(Side.BUY, price_ticks=5, qty=0.10, post_only=True)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert len(result.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
def test_reduce_exit(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
pos = PositionState("BTCUSDT", 0.1, 50000.0, 0.0, 0.0, None, 1.0, Side.BUY)
|
|
|
|
|
s_with_pos = replace(s, account=replace(s.account, positions={"BTCUSDT": pos}, equity=11000.0))
|
|
|
|
|
# FULL_EXIT sells qty_fraction * available_balance / price = 1.0 * 11000 / 50001 ≈ 0.22 BTC
|
|
|
|
|
# That's more than 0.1 position, so it should close
|
|
|
|
|
a = FulfilmentAction(ActionKind.FULL_EXIT, Side.SELL, OrderType.MARKET,
|
|
|
|
|
0, 1.0, 0, reduce_only=True)
|
|
|
|
|
result = cwm.transition(s_with_pos, (a,))
|
|
|
|
|
result_pos = result.account.positions.get("BTCUSDT")
|
|
|
|
|
assert result_pos is None or abs(result_pos.qty) < 1e-6 or result_pos.qty < 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 4: Reward Function
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestReward:
|
|
|
|
|
def test_reward_positive_for_profit(self):
|
|
|
|
|
cwm = HftBacktestCWM()
|
|
|
|
|
s1 = _state()
|
|
|
|
|
s2 = replace(s1, account=replace(s1.account, equity=10100.0))
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
r = cwm.reward(s1, a, s2, _params())
|
|
|
|
|
assert isinstance(r, float)
|
|
|
|
|
|
|
|
|
|
def test_reward_noop_zero(self):
|
|
|
|
|
cwm = HftBacktestCWM()
|
|
|
|
|
s = _state()
|
|
|
|
|
a = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
r = cwm.reward(s, a, s, _params())
|
|
|
|
|
assert isinstance(r, float)
|
|
|
|
|
|
|
|
|
|
def test_reward_maker_bonus(self):
|
|
|
|
|
cwm = HftBacktestCWM()
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _place(Side.BUY, price_ticks=5, qty=0.10)
|
|
|
|
|
r = cwm.reward(s, a, s, _params())
|
|
|
|
|
assert isinstance(r, float)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 5: Edge Cases
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestEdgeCases:
|
|
|
|
|
def test_empty_book_cross(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = MarketWorldState(
|
|
|
|
|
ts_ns=1_000_000_000, mode=Mode.ENDOGENOUS_AGENT_SIM,
|
|
|
|
|
venue=_venue(), book=OrderBookState(1_000_000_000, "BTCUSDT", (), ()),
|
|
|
|
|
account=_account(),
|
|
|
|
|
)
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.account.equity == s.account.equity
|
|
|
|
|
|
|
|
|
|
def test_zero_qty_cross(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.0)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert len(result.open_orders) == 0
|
|
|
|
|
|
|
|
|
|
def test_very_small_qty(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.0001)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.account.equity <= s.account.equity + 0.01
|
|
|
|
|
|
|
|
|
|
def test_large_qty_walks_book(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
# 10 levels at $50001 base, qty = 5 + i per level
|
|
|
|
|
# Level 0: $50001.0 x 5.0 = $250K
|
|
|
|
|
# 0.5 * $100K / $50001 = ~1.0 BTC, which consumes level 0 (5.0 qty)
|
|
|
|
|
# and into level 1, so best_ask should change
|
|
|
|
|
s = _state(bid=50000.0, ask=50001.0, equity=100000.0)
|
|
|
|
|
a = _cross(Side.BUY, 0.5)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
# After consuming some levels, either the book changed or fills occurred
|
|
|
|
|
assert result.book != s.book or result.account.positions.get("BTCUSDT") is not None
|
|
|
|
|
|
|
|
|
|
def test_extreme_price(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state(bid=0.01, ask=0.02)
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert isinstance(result.account.equity, float)
|
|
|
|
|
|
|
|
|
|
def test_many_levels_depth(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
bids = tuple(PriceLevel(50000.0 - i * 0.1, 10.0) for i in range(50))
|
|
|
|
|
asks = tuple(PriceLevel(50001.0 + i * 0.1, 10.0) for i in range(50))
|
|
|
|
|
s = replace(_state(), book=OrderBookState(1_000_000_000, "BTCUSDT", bids, asks))
|
|
|
|
|
a = _cross(Side.BUY, 0.20)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.book.asks[0].price >= 50001.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 6: Position Tracking
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestPositionTracking:
|
|
|
|
|
def test_buy_increases_position(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
pos = result.account.positions.get("BTCUSDT")
|
|
|
|
|
assert pos is not None
|
|
|
|
|
assert pos.qty > 0
|
|
|
|
|
|
|
|
|
|
def test_sell_decreases_position(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
pos = PositionState("BTCUSDT", 0.1, 50000.0, 0.0, 0.0, None, 1.0, Side.BUY)
|
|
|
|
|
s_with = replace(s, account=replace(s.account, positions={"BTCUSDT": pos}, equity=11000.0))
|
|
|
|
|
# Use REDUCE with small qty_fraction to partially reduce
|
|
|
|
|
a = FulfilmentAction(ActionKind.REDUCE, Side.SELL, OrderType.MARKET,
|
|
|
|
|
0, 0.01, 0, reduce_only=True)
|
|
|
|
|
result = cwm.transition(s_with, (a,))
|
|
|
|
|
result_pos = result.account.positions.get("BTCUSDT")
|
|
|
|
|
assert result_pos is not None
|
|
|
|
|
assert result_pos.qty < 0.1
|
|
|
|
|
|
|
|
|
|
def test_position_flip(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
pos = PositionState("BTCUSDT", 0.5, 50000.0, 0.0, 0.0, None, 1.0, Side.BUY)
|
|
|
|
|
s_with = replace(s, account=replace(s.account, positions={"BTCUSDT": pos}, equity=12000.0))
|
|
|
|
|
a = _cross(Side.SELL, 0.20)
|
|
|
|
|
result = cwm.transition(s_with, (a,))
|
|
|
|
|
result_pos = result.account.positions.get("BTCUSDT")
|
|
|
|
|
assert result_pos is not None
|
|
|
|
|
assert result_pos.qty < 0.5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 7: Fee Application
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestFees:
|
|
|
|
|
def test_taker_fee_reduces_equity(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
expected_fee = 0.10 * 50001.0 * 5.0 / 10_000 # taker fee
|
|
|
|
|
actual_equity_change = s.account.equity - result.account.equity
|
|
|
|
|
assert actual_equity_change > 0
|
|
|
|
|
|
|
|
|
|
def test_cross_is_taker(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.account.equity < s.account.equity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 8: Counterparty Ecology
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestCounterpartyEcology:
|
|
|
|
|
def test_toxic_taker_hits_book(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
# Thin book so toxic taker can move it
|
|
|
|
|
thin_book = _book(bid_qty=0.01, ask_qty=0.01, n_levels=3)
|
|
|
|
|
s = replace(_state(), book=thin_book)
|
|
|
|
|
a = _place(Side.BUY, price_ticks=5, qty=0.10)
|
|
|
|
|
s2 = cwm.transition(s, (a,))
|
|
|
|
|
|
|
|
|
|
# Force toxic taker to act (not NOOP) by using a deterministic rng
|
|
|
|
|
cp = ToxicTakerPolicy()
|
|
|
|
|
rng = random.Random(0)
|
|
|
|
|
cp_action = cp.rollout_action(s2, rng)
|
|
|
|
|
# If rng gave NOOP, try again with different seed
|
|
|
|
|
while cp_action.kind == ActionKind.NOOP:
|
|
|
|
|
rng = random.Random(rng.randint(0, 10000))
|
|
|
|
|
cp_action = cp.rollout_action(s2, rng)
|
|
|
|
|
|
|
|
|
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
s3 = cwm.transition(s2, (noop, cp_action))
|
|
|
|
|
# After toxic taker crosses, either book changed or equity changed
|
|
|
|
|
assert s3.book != s2.book or s3.account.equity != s2.account.equity
|
|
|
|
|
|
|
|
|
|
def test_noop_preserves_state(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
result = cwm.transition(s, (noop,))
|
|
|
|
|
assert result.book.bids == s.book.bids
|
|
|
|
|
assert result.book.asks == s.book.asks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 9: CWM Comparison (MinimalCrypto vs HftBacktest)
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestCWMComparison:
|
|
|
|
|
def test_hft_cwm_produces_valid_state(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=True)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.ts_ns > s.ts_ns
|
|
|
|
|
assert result.account.equity > 0
|
|
|
|
|
assert result.book.bids is not None
|
|
|
|
|
assert result.book.asks is not None
|
|
|
|
|
|
|
|
|
|
def test_minimal_cwm_produces_valid_state(self):
|
|
|
|
|
cwm = MinimalCryptoLOBCWM()
|
|
|
|
|
s = _state()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
result = cwm.transition(s, (a,))
|
|
|
|
|
assert result.ts_ns > s.ts_ns
|
|
|
|
|
assert result.account.equity > 0
|
|
|
|
|
|
|
|
|
|
def test_both_cwms_agree_on_noop(self):
|
|
|
|
|
hft = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
min_cwm = MinimalCryptoLOBCWM()
|
|
|
|
|
s = _state()
|
|
|
|
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
r1 = hft.transition(s, (noop,))
|
|
|
|
|
r2 = min_cwm.transition(s, (noop,))
|
|
|
|
|
assert r1.account.equity == r2.account.equity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 10: Venue Propagation
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestVenuePropagation:
|
|
|
|
|
def test_scenario_venue_tagging(self):
|
|
|
|
|
factory = ScenarioFactory(exchange_id="binance")
|
|
|
|
|
scenarios = factory.build_suite(symbols=["BTCUSDT"], steps_per_scenario=2, seed=42)
|
|
|
|
|
for s in scenarios:
|
|
|
|
|
assert s.venue == "binance"
|
|
|
|
|
|
|
|
|
|
def test_cross_exchange_transfer(self):
|
|
|
|
|
factory = ScenarioFactory(exchange_id="bingx")
|
|
|
|
|
scenarios = factory.build_suite(symbols=["BTCUSDT"], steps_per_scenario=2, seed=42)
|
|
|
|
|
transferred = factory.cross_exchange_transfer(scenarios, "bybit")
|
|
|
|
|
for s in transferred:
|
|
|
|
|
assert s.venue == "bybit"
|
|
|
|
|
|
|
|
|
|
def test_order_type_mapping_all_exchanges(self):
|
|
|
|
|
for ex in ("binance", "bingx", "bybit"):
|
|
|
|
|
for ot in StdOrderType:
|
|
|
|
|
mapped = normalize_type_to_exchange(ot, ex)
|
|
|
|
|
if mapped:
|
|
|
|
|
assert isinstance(mapped, str)
|
|
|
|
|
|
|
|
|
|
def test_tif_mapping_all_exchanges(self):
|
|
|
|
|
for ex in ("binance", "bingx", "bybit"):
|
|
|
|
|
for tif in TimeInForce:
|
|
|
|
|
mapped = normalize_tif_to_exchange(tif, ex)
|
|
|
|
|
assert mapped is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 11: PerformanceMatrix Venue Keying
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestMatrixVenueKeying:
|
|
|
|
|
def test_record_with_venue(self):
|
|
|
|
|
m = PerformanceMatrix()
|
|
|
|
|
m.record("s1", MarketRegime.NORMAL, score=10.0, venue="bingx")
|
|
|
|
|
m.record("s1", MarketRegime.NORMAL, score=8.0, venue="binance")
|
|
|
|
|
assert m.total_entries == 2
|
|
|
|
|
|
|
|
|
|
def test_get_best_per_venue(self):
|
|
|
|
|
m = PerformanceMatrix()
|
|
|
|
|
m.record("s1", MarketRegime.NORMAL, score=10.0, venue="bingx")
|
|
|
|
|
m.record("s2", MarketRegime.NORMAL, score=8.0, venue="bingx")
|
|
|
|
|
m.record("s1", MarketRegime.NORMAL, score=5.0, venue="binance")
|
|
|
|
|
m.record("s3", MarketRegime.NORMAL, score=12.0, venue="binance")
|
|
|
|
|
assert m.get_best(MarketRegime.NORMAL, venue="bingx") == "s1"
|
|
|
|
|
assert m.get_best(MarketRegime.NORMAL, venue="binance") == "s3"
|
|
|
|
|
|
|
|
|
|
def test_venue_comparison(self):
|
|
|
|
|
m = PerformanceMatrix()
|
|
|
|
|
m.record("s1", MarketRegime.NORMAL, score=10.0, venue="bingx")
|
|
|
|
|
m.record("s1", MarketRegime.NORMAL, score=8.0, venue="binance")
|
|
|
|
|
comp = m.get_venue_comparison(MarketRegime.NORMAL, "s1")
|
|
|
|
|
assert comp == {"bingx": 10.0, "binance": 8.0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 12: Risk Gate Integration
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestRiskGateIntegration:
|
|
|
|
|
def test_risk_gate_approves_valid_cross(self):
|
|
|
|
|
gate = RiskGate()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
decision = gate.validate(_state(), _planned(a), _params())
|
|
|
|
|
assert decision.approved
|
|
|
|
|
|
|
|
|
|
def test_risk_gate_blocks_leverage(self):
|
|
|
|
|
gate = RiskGate()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
s = replace(_state(), account=replace(_account(), total_notional=30000.0))
|
|
|
|
|
decision = gate.validate(s, _planned(a), _params())
|
|
|
|
|
assert not decision.approved
|
|
|
|
|
assert decision.reason == "leverage_limit"
|
|
|
|
|
|
|
|
|
|
def test_risk_gate_blocks_ood(self):
|
|
|
|
|
gate = RiskGate()
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
decision = gate.validate(_state(), _planned(a), _params(), daat_verdict="OUT_OF_DISTRIBUTION")
|
|
|
|
|
assert decision.approved
|
|
|
|
|
assert decision.action is None
|
|
|
|
|
|
|
|
|
|
def test_risk_gate_kill_switch(self):
|
|
|
|
|
gate = RiskGate()
|
|
|
|
|
gate.set_kill_switch(True)
|
|
|
|
|
a = _cross(Side.BUY, 0.10)
|
|
|
|
|
decision = gate.validate(_state(), _planned(a), _params())
|
|
|
|
|
assert not decision.approved
|
|
|
|
|
assert decision.reason == "kill_switch"
|
|
|
|
|
gate.set_kill_switch(False)
|
|
|
|
|
|
|
|
|
|
def test_risk_gate_self_trade(self):
|
|
|
|
|
gate = RiskGate()
|
|
|
|
|
s = _state()
|
|
|
|
|
oo = OpenOrderState("m_1", None, "BTCUSDT", Side.BUY, OrderType.LIMIT,
|
|
|
|
|
50000.0, 0.01, 0.01, None, 1_000_000_000, 1_000_000_000)
|
|
|
|
|
s_with_orders = replace(s, open_orders=(oo,))
|
|
|
|
|
a = _place(Side.BUY, price_ticks=0, qty=0.01)
|
|
|
|
|
decision = gate.validate(s_with_orders, _planned(a), _params())
|
|
|
|
|
assert not decision.approved
|
|
|
|
|
assert decision.reason == "self_trade_risk"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 13: Stress Tests
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestStress:
|
|
|
|
|
def test_rapid_transitions(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
for i in range(100):
|
|
|
|
|
a = _cross(Side.BUY if i % 2 == 0 else Side.SELL, 0.01)
|
|
|
|
|
s = cwm.transition(s, (a,))
|
|
|
|
|
assert s.account.equity > 0
|
|
|
|
|
|
|
|
|
|
def test_many_open_orders(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
for i in range(20):
|
|
|
|
|
a = _place(Side.BUY, price_ticks=i, qty=0.01)
|
|
|
|
|
s = cwm.transition(s, (a,))
|
|
|
|
|
assert len(s.open_orders) == 20
|
|
|
|
|
|
|
|
|
|
def test_cancel_all_orders(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
for i in range(5):
|
|
|
|
|
a = _place(Side.BUY, price_ticks=i, qty=0.01)
|
|
|
|
|
s = cwm.transition(s, (a,))
|
|
|
|
|
assert len(s.open_orders) == 5
|
|
|
|
|
for oo in s.open_orders:
|
|
|
|
|
cancel = FulfilmentAction(ActionKind.CANCEL, None, None, 0, 0.0, 0,
|
|
|
|
|
cancel_order_id=oo.client_order_id)
|
|
|
|
|
s = cwm.transition(s, (cancel,))
|
|
|
|
|
assert len(s.open_orders) == 0
|
|
|
|
|
|
|
|
|
|
def test_repeated_cross_spread_same_side(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
for _ in range(10):
|
|
|
|
|
a = _cross(Side.BUY, 0.01)
|
|
|
|
|
s = cwm.transition(s, (a,))
|
|
|
|
|
pos = s.account.positions.get("BTCUSDT")
|
|
|
|
|
assert pos is not None
|
|
|
|
|
assert pos.qty > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 14: Full Episode Integration
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestFullEpisode:
|
|
|
|
|
def test_single_episode_runs(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
scenarios = ScenarioFactory().build_suite(symbols=["BTCUSDT"], steps_per_scenario=3, seed=42)
|
|
|
|
|
assert len(scenarios) > 0
|
|
|
|
|
|
|
|
|
|
for scenario in scenarios[:1]:
|
|
|
|
|
s = scenario.initial_state
|
|
|
|
|
rng = random.Random(42)
|
|
|
|
|
for step in range(scenario.max_steps):
|
|
|
|
|
a = _cross(Side.BUY if rng.random() < 0.5 else Side.SELL, 0.01)
|
|
|
|
|
cp = ToxicTakerPolicy().rollout_action(s, rng)
|
|
|
|
|
s = cwm.transition(s, (a, cp))
|
|
|
|
|
assert s.account.equity > 0
|
|
|
|
|
|
|
|
|
|
def test_policy_evaluator_with_hft_cwm(self):
|
|
|
|
|
def cwm_factory():
|
|
|
|
|
return HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
evaluator = PolicyEvaluator(cwm_factory=cwm_factory, scoring_mode="fast")
|
|
|
|
|
scenarios = ScenarioFactory().build_suite(symbols=["BTCUSDT"], steps_per_scenario=2, seed=42)
|
|
|
|
|
score, results = evaluator.evaluate_candidate(
|
|
|
|
|
params=_params(), scenarios=scenarios, rng_seed=42,
|
|
|
|
|
planner_type="random",
|
|
|
|
|
)
|
|
|
|
|
assert isinstance(score, float)
|
|
|
|
|
assert len(results) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 15: hftbacktest Availability
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestHftAvailability:
|
|
|
|
|
def test_import_check(self):
|
|
|
|
|
from malkhut.cwm.hft_cwm import _HAS_HFTBACKTEST
|
|
|
|
|
assert _HAS_HFTBACKTEST is True
|
|
|
|
|
|
|
|
|
|
def test_cwm_default_uses_queue(self):
|
|
|
|
|
cwm = HftBacktestCWM()
|
|
|
|
|
assert cwm._use_queue_model is True
|
|
|
|
|
|
|
|
|
|
def test_cwm_explicit_no_queue(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
assert cwm._use_queue_model is False
|
|
|
|
|
assert cwm._fill_probability_at_level(0) == 1.0
|
|
|
|
|
assert cwm._fill_probability_at_level(50) == 1.0
|
2026-07-17 19:30:17 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
# PART 16: CHASE Mechanics
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
|
|
|
|
class TestChaseMechanics:
|
|
|
|
|
def test_ttl_enforcement_cancels_expired_order(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
# Place order with TTL=1ms
|
|
|
|
|
a = _place(Side.BUY, price_ticks=5, qty=0.10)
|
|
|
|
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
|
|
|
|
5, 0.10, 1, post_only=True) # ttl_ms=1
|
|
|
|
|
s2 = cwm.transition(s, (a,))
|
|
|
|
|
assert len(s2.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
# Next step: order should be expired (1ms < 1ms tick)
|
|
|
|
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
s3 = cwm.transition(s2, (noop,))
|
|
|
|
|
assert len(s3.open_orders) == 0
|
|
|
|
|
|
|
|
|
|
def test_ttl_zero_means_no_expiry(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
|
|
|
|
5, 0.10, 0, post_only=True) # ttl_ms=0
|
|
|
|
|
s2 = cwm.transition(s, (a,))
|
|
|
|
|
assert len(s2.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
s3 = cwm.transition(s2, (noop,))
|
|
|
|
|
assert len(s3.open_orders) == 1 # not expired
|
|
|
|
|
|
|
|
|
|
def test_chase_action_has_ttl(self):
|
|
|
|
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
|
|
|
|
5, 0.10, 100, post_only=True, metadata={"chase": True})
|
|
|
|
|
assert a.ttl_ms == 100
|
|
|
|
|
|
|
|
|
|
def test_chase_cancel_retry_cycle(self):
|
|
|
|
|
cwm = HftBacktestCWM(use_queue_model=False)
|
|
|
|
|
s = _state()
|
|
|
|
|
noop = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
|
|
|
|
|
|
|
|
|
|
# Step 1: Place chase order with short TTL
|
|
|
|
|
a1 = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
|
|
|
|
5, 0.10, 1, post_only=True, metadata={"chase": True})
|
|
|
|
|
s1 = cwm.transition(s, (a1,))
|
|
|
|
|
assert len(s1.open_orders) == 1
|
|
|
|
|
|
|
|
|
|
# Step 2: Order expired (TTL=1ms)
|
|
|
|
|
s2 = cwm.transition(s1, (noop,))
|
|
|
|
|
assert len(s2.open_orders) == 0
|
|
|
|
|
|
|
|
|
|
# Step 3: Re-place at offset 3 (simulating cancel-retry)
|
|
|
|
|
a3 = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
|
|
|
|
3, 0.10, 1, post_only=True, metadata={"chase": True})
|
|
|
|
|
s3 = cwm.transition(s2, (a3,))
|
|
|
|
|
assert len(s3.open_orders) == 1
|
|
|
|
|
assert s3.open_orders[0].price != s1.open_orders[0].price # different offset
|
|
|
|
|
|
|
|
|
|
def test_chase_max_retries_in_metadata(self):
|
|
|
|
|
a = FulfilmentAction(ActionKind.PLACE, Side.BUY, OrderType.LIMIT,
|
|
|
|
|
2, 0.10, 100, post_only=True,
|
|
|
|
|
metadata={"chase": True, "chase_max_retries": 3})
|
|
|
|
|
assert a.metadata["chase_max_retries"] == 3
|
|
|
|
|
|
|
|
|
|
def test_wait_to_retry_in_params(self):
|
|
|
|
|
p = _params()
|
|
|
|
|
assert p.wait_to_retry_ms == 0
|
|
|
|
|
assert p.chase_enabled is False
|
|
|
|
|
assert p.chase_offset_ticks == 1
|
|
|
|
|
assert p.chase_max_retries == 0
|
|
|
|
|
|
|
|
|
|
def test_cma_codec_includes_chase_params(self):
|
|
|
|
|
from malkhut.training.cma_trainer import CMAParameterCodec
|
|
|
|
|
codec = CMAParameterCodec()
|
|
|
|
|
param_names = [s.name for s in codec.SPECS]
|
|
|
|
|
assert "wait_to_retry_ms" in param_names
|
|
|
|
|
assert "chase_offset_ticks" in param_names
|
|
|
|
|
assert "chase_max_retries" in param_names
|
|
|
|
|
assert "urgency_taker_threshold" in param_names
|