"""Tests for asset-faithful book generation.""" from __future__ import annotations import os import math import random import tempfile from malkhut.training.asset_book_profile import ( AssetBookProfile, BookGenerationConfig, BookGenerator, build_profile_from_behavior, _intraday_multiplier, ) from malkhut.training.asset_registry import AssetRegistry, RuntimeProfileCache from malkhut.state import PriceLevel class TestIntradayMultiplier: def test_peak_is_max(self): m = _intraday_multiplier(15, 15, 19, 7.4) assert m == 7.4 def test_trough_is_min(self): m = _intraday_multiplier(19, 15, 19, 7.4) assert m == 1.0 def test_midpoint_between_peak_and_trough(self): m = _intraday_multiplier(17, 15, 19, 4.0) assert 1.0 < m < 4.0 def test_all_hours_bounded(self): for h in range(24): m = _intraday_multiplier(h, 15, 19, 7.4) assert 1.0 <= m <= 7.4, f"hour={h} mult={m}" class TestAssetBookProfile: def test_build_from_btc(self): p = build_profile_from_behavior("BTCUSDT") assert p.symbol == "BTCUSDT" assert p.depth_amplitude_usd == 750_000 assert p.depth_alpha == 0.70 assert p.depth_fragility == 0.10 assert p.spread_normal_bps == 0.01 assert p.spread_stress_mult == 50.0 assert p.typical_num_levels > 0 assert p.avg_level_size_usd > 0 def test_build_from_doge(self): p = build_profile_from_behavior("DOGEUSDT") assert p.symbol == "DOGEUSDT" assert p.depth_amplitude_usd == 22_000 assert p.spread_normal_bps == 1.35 assert p.depth_alpha == 1.00 def test_build_from_unknown_raises(self): try: build_profile_from_behavior("FAKEUSDT") assert False, "Should have raised ValueError" except ValueError: pass def test_roundtrip_dict(self): p = build_profile_from_behavior("ETHUSDT") d = p.to_dict() p2 = AssetBookProfile.from_dict(d) assert p2.symbol == p.symbol assert p2.depth_amplitude_usd == p.depth_amplitude_usd assert p2.spread_normal_bps == p.spread_normal_bps class TestBookGenerationConfig: def test_defaults(self): c = BookGenerationConfig() assert c.use_asset_faithful_depth is True assert c.use_asset_faithful_spread is True assert c.use_intraday_clock is True assert c.use_weekend_mode is True assert c.worst_case_mode is False def test_worst_case_overrides(self): c = BookGenerationConfig(worst_case_mode=True) assert c.worst_case_mode is True def test_independent_toggles(self): c = BookGenerationConfig( use_asset_faithful_depth=True, use_intraday_clock=False, use_weekend_mode=False, use_stress_mode=True, ) assert c.use_asset_faithful_depth is True assert c.use_intraday_clock is False assert c.use_weekend_mode is False assert c.use_stress_mode is True class TestBookGenerator: def test_generate_btc_book(self): p = build_profile_from_behavior("BTCUSDT") gen = BookGenerator(p, BookGenerationConfig()) book = gen.generate_initial_book(64000.0, 0.1, ts_ns=1_000_000) assert len(book.bids) > 0 assert len(book.asks) > 0 assert book.bids[0].price < book.asks[0].price assert book.mid > 0 def test_generate_doge_book(self): p = build_profile_from_behavior("DOGEUSDT") gen = BookGenerator(p, BookGenerationConfig()) book = gen.generate_initial_book(0.07, 0.00001, ts_ns=1_000_000) assert len(book.bids) > 0 assert len(book.asks) > 0 spread = book.asks[0].price - book.bids[0].price spread_bps = spread / book.mid * 10_000 assert spread_bps > 0.5 def test_worst_case_wider_spread(self): p = build_profile_from_behavior("BTCUSDT") normal = BookGenerator(p, BookGenerationConfig()) worst = BookGenerator(p, BookGenerationConfig(worst_case_mode=True)) b1 = normal.generate_initial_book(64000.0, 0.1) b2 = worst.generate_initial_book(64000.0, 0.1) s1 = (b1.asks[0].price - b1.bids[0].price) / b1.mid * 10_000 s2 = (b2.asks[0].price - b2.bids[0].price) / b2.mid * 10_000 assert s2 >= s1 * 10 def test_worst_case_thinner_book(self): p = build_profile_from_behavior("BTCUSDT") normal = BookGenerator(p, BookGenerationConfig()) worst = BookGenerator(p, BookGenerationConfig(worst_case_mode=True)) b1 = normal.generate_initial_book(64000.0, 0.1) b2 = worst.generate_initial_book(64000.0, 0.1) assert b2.bids[0].qty < b1.bids[0].qty * 0.2 def test_refresh_preserves_structure(self): p = build_profile_from_behavior("BTCUSDT") gen = BookGenerator(p, BookGenerationConfig()) book = gen.generate_initial_book(64000.0, 0.1) rng = random.Random(42) refreshed = gen.refresh_book(book, 0.1, rng) assert len(refreshed.bids) > 0 assert len(refreshed.asks) > 0 assert refreshed.bids[0].price < refreshed.asks[0].price def test_refresh_multiple_steps(self): p = build_profile_from_behavior("ETHUSDT") gen = BookGenerator(p, BookGenerationConfig()) book = gen.generate_initial_book(1800.0, 0.01) rng = random.Random(42) for _ in range(50): book = gen.refresh_book(book, 0.01, rng) assert len(book.bids) > 0 assert book.mid > 0 def test_worst_case_refresh_even_thinner(self): p = build_profile_from_behavior("DOGEUSDT") normal = BookGenerator(p, BookGenerationConfig()) worst = BookGenerator(p, BookGenerationConfig(worst_case_mode=True)) b1 = normal.generate_initial_book(0.07, 0.00001) b2 = worst.generate_initial_book(0.07, 0.00001) rng1 = random.Random(42) rng2 = random.Random(42) for _ in range(10): b1 = normal.refresh_book(b1, 0.00001, rng1) b2 = worst.refresh_book(b2, 0.00001, rng2) avg_qty1 = sum(l.qty for l in b1.bids) / len(b1.bids) avg_qty2 = sum(l.qty for l in b2.bids) / len(b2.bids) assert avg_qty2 < avg_qty1 * 0.5 def test_no_cross_after_refresh(self): for sym in ["BTCUSDT", "DOGEUSDT", "SOLUSDT", "ADAUSDT"]: p = build_profile_from_behavior(sym) gen = BookGenerator(p, BookGenerationConfig()) ref_p = p.reference_price if p.reference_price > 0 else 100.0 book = gen.generate_initial_book(ref_p, ref_p * 0.0001) rng = random.Random(42) for _ in range(20): book = gen.refresh_book(book, ref_p * 0.0001, rng) assert book.bids[0].price < book.asks[0].price, f"{sym} crossed" def test_different_assets_different_books(self): btc = BookGenerator(build_profile_from_behavior("BTCUSDT"), BookGenerationConfig()) doge = BookGenerator(build_profile_from_behavior("DOGEUSDT"), BookGenerationConfig()) b1 = btc.generate_initial_book(64000.0, 0.1) b2 = doge.generate_initial_book(0.07, 0.00001) s1 = (b1.asks[0].price - b1.bids[0].price) / b1.mid * 10_000 s2 = (b2.asks[0].price - b2.bids[0].price) / b2.mid * 10_000 assert s2 > s1 * 5 class TestAssetRegistry: def test_upsert_and_get(self): with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "test.db") reg = AssetRegistry(db) p = build_profile_from_behavior("BTCUSDT") reg.upsert_profile(p) got = reg.get_profile("BTCUSDT") assert got is not None assert got.symbol == "BTCUSDT" assert got.depth_amplitude_usd == 750_000 reg.close() def test_upsert_all_from_behaviors(self): with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "test.db") reg = AssetRegistry(db) count = reg.upsert_all_from_asset_behaviors() assert count >= 8 syms = reg.list_symbols() assert "BTCUSDT" in syms assert "ETHUSDT" in syms reg.close() def test_upsert_overwrites(self): with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "test.db") reg = AssetRegistry(db) p = build_profile_from_behavior("BTCUSDT") reg.upsert_profile(p) reg.upsert_profile(p) profiles = reg.list_profiles() assert len(profiles) == 1 reg.close() def test_delete_profile(self): with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "test.db") reg = AssetRegistry(db) p = build_profile_from_behavior("BTCUSDT") reg.upsert_profile(p) reg.delete_profile("BTCUSDT") assert reg.get_profile("BTCUSDT") is None reg.close() def test_csv_roundtrip(self): with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "test.db") csv_out = os.path.join(tmp, "export.csv") reg = AssetRegistry(db) reg.upsert_all_from_asset_behaviors() n = reg.export_csv(csv_out) assert n >= 8 assert os.path.exists(csv_out) reg.close() reg2 = AssetRegistry(os.path.join(tmp, "test2.db")) n2 = reg2.upsert_from_csv(csv_out) assert n2 >= 8 assert reg2.get_profile("BTCUSDT") is not None reg2.close() class TestRuntimeProfileCache: def test_put_and_get(self): cache = RuntimeProfileCache() p = build_profile_from_behavior("BTCUSDT") cache.put(p) assert cache.has("BTCUSDT") assert cache.get("BTCUSDT").symbol == "BTCUSDT" def test_load_from_registry(self): with tempfile.TemporaryDirectory() as tmp: db = os.path.join(tmp, "test.db") reg = AssetRegistry(db) reg.upsert_all_from_asset_behaviors() cache = RuntimeProfileCache() n = cache.load_from_registry(reg) assert n >= 8 assert cache.has("BTCUSDT") assert cache.has("ETHUSDT") reg.close() class TestHftCwmWithProfile: def test_cwm_accepts_profile(self): from malkhut.cwm.hft_cwm import HftBacktestCWM p = build_profile_from_behavior("BTCUSDT") cfg = BookGenerationConfig() cwm = HftBacktestCWM( use_queue_model=True, use_dynamic_book=True, book_profile=p, book_config=cfg, ) assert cwm._book_generator is not None def test_cwm_without_profile_fallback(self): from malkhut.cwm.hft_cwm import HftBacktestCWM cwm = HftBacktestCWM(use_queue_model=True, use_dynamic_book=True) assert cwm._book_generator is None def test_cwm_default_backward_compat(self): from malkhut.cwm.hft_cwm import HftBacktestCWM cwm = HftBacktestCWM() assert cwm._use_dynamic_book is False assert cwm._book_generator is None class TestAllAssetsHaveProfiles: def test_all_13_assets(self): symbols = [ "BTCUSDT", "ETHUSDT", "SOLUSDT", "DOGEUSDT", "ADAUSDT", "AVAXUSDT", "UNIUSDT", "LINKUSDT", "BNBUSDT", "MATICUSDT", "AAVEUSDT", "DOTUSDT", "ATOMUSDT", ] for sym in symbols: p = build_profile_from_behavior(sym) assert p.symbol == sym assert p.depth_amplitude_usd > 0 assert p.spread_normal_bps > 0 assert p.typical_num_levels > 0 gen = BookGenerator(p, BookGenerationConfig()) ref_p = p.reference_price if p.reference_price > 0 else 100.0 book = gen.generate_initial_book(ref_p, ref_p * 0.0001) assert len(book.bids) > 0, f"{sym} no bids" assert len(book.asks) > 0, f"{sym} no asks" assert book.mid > 0, f"{sym} no mid"