Files
sentiment-engine/MALKHUT/malkhut/tests/test_asset_book_profile.py
Codex 6990ff3bee malkhut: asset-faithful book generation with composable toggles
Three independently toggleable features:
  1. Asset-faithful depth/spread: levels sized by OB study power-law per asset
  2. Intraday volume clock: depth scales by time-of-day (peak/trough)
  3. Realistic spread: per-asset spread from OB study + Flight7

Composable via BookGenerationConfig toggles:
  use_asset_faithful_depth, use_asset_faithful_spread, use_intraday_clock,
  use_weekend_mode, use_stress_mode, use_fragility, worst_case_mode

worst_case_mode overrides everything for max adversarial learning:
  spread * stress_mult, depth * fragility, no intraday/weekend.

DuckDB registry for online updates:
  AssetRegistry: upsert/get/list/delete/query
  RuntimeProfileCache: hot-reload during CWM runs
  upsert_from_csv/export_csv: pipeline support
  upsert_all_from_asset_behaviors(): seed from OB study

Results (8 assets):
  BTC: spread 0.031 bps, depth $350M (normal) / $4.9M (worst)
  DOGE: spread 2.86 bps, depth $2M (normal) / $132K (worst)
  ADA: spread 11.8 bps, depth $10M (normal) / $511K (worst)
  Intraday: BTC peak/trough = 2.8x depth ratio

All 99 tests green (31 new + 68 existing).
2026-07-20 19:06:24 +02:00

319 lines
12 KiB
Python

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