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

501 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Exhaustive training subsystem tests.
Categories:
1. CMA Parameter Codec (encode/decode/bounds)
2. SelfPlayPool (add/evict/diversity)
3. Scenario Factory (suite generation)
4. Policy Evaluator (real episodes, metrics)
5. EpisodeResult (fields, computation)
6. Bootstrap CI
7. CMAESTrainer (end-to-end)
8. Promotion logic
"""
import dataclasses
import math
import pytest
from malkhut.state import (
AccountState, ExecutionIntent, FulfilmentPolicyParams, IntentKind,
MarketWorldState, Mode, OrderBookState, PriceLevel, Side, VenueRules,
)
from malkhut.actions import ActionKind, FulfilmentAction, OrderType
from malkhut.cwm.core import MinimalCryptoLOBCWM
from malkhut.counterparties import default_counterparty_ecology
from malkhut.training.cma_trainer import (
CMAParameterCodec, SelfPlayPool, PolicySnapshot,
ScenarioFactory, PolicyEvaluator, EpisodeResult,
CMAESTrainer, bootstrap_ci,
)
def _baseline(**kw):
d = dict(
version="baseline", ucb_c=1.414, max_sims=256, max_depth=3,
rollout_depth=3, root_temperature=0.5, min_root_entropy=0.25,
quote_offsets_ticks=(0, 1, 2), quote_size_fractions=(0.1, 0.25, 0.5),
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,
)
d.update(kw)
return FulfilmentPolicyParams(**d)
# ══════════════════════════════════════════════════════════════════════════════
# 1. CMA PARAMETER CODEC
# ══════════════════════════════════════════════════════════════════════════════
class TestCMAParameterCodec:
def test_initial_vector_length(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
assert len(x0) == len(codec.SPECS)
def test_bounds_length(self):
codec = CMAParameterCodec()
lows, highs = codec.bounds()
assert len(lows) == len(codec.SPECS)
assert len(highs) == len(codec.SPECS)
def test_bounds_ordered(self):
codec = CMAParameterCodec()
lows, highs = codec.bounds()
assert all(l <= h for l, h in zip(lows, highs))
def test_initial_vector_midpoint(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
lows, highs = codec.bounds()
for v, lo, hi in zip(x0, lows, highs):
assert lo <= v <= hi
def test_decode_returns_params(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
p = codec.decode(x0, "v_test")
assert isinstance(p, FulfilmentPolicyParams)
def test_decode_version_preserved(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
p = codec.decode(x0, "my_version")
assert p.version == "my_version"
def test_decode_int_fields_are_integers(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
p = codec.decode(x0, "int_test")
assert isinstance(p.max_depth, int)
assert isinstance(p.passive_ttl_ms, int)
assert isinstance(p.failed_recovery_cut_count, int)
def test_decode_clips_above(self):
codec = CMAParameterCodec()
highs = [s.high for s in codec.SPECS]
x_over = [h + 10.0 for h in highs]
p = codec.decode(x_over, "over")
for spec in codec.SPECS:
val = getattr(p, spec.name)
if spec.kind == "float":
assert val <= spec.high + 1e-9
def test_decode_clips_below(self):
codec = CMAParameterCodec()
lows = [s.low for s in codec.SPECS]
x_under = [l - 10.0 for l in lows]
p = codec.decode(x_under, "under")
for spec in codec.SPECS:
val = getattr(p, spec.name)
if spec.kind == "float":
assert val >= spec.low - 1e-9
def test_decode_idempotent(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
p1 = codec.decode(x0, "a")
p2 = codec.decode(x0, "b")
assert p1.ucb_c == p2.ucb_c
assert p1.max_depth == p2.max_depth
def test_different_vectors_different_params(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
x1 = list(x0)
x1[0] += 0.5
p0 = codec.decode(x0, "a")
p1 = codec.decode(x1, "b")
assert p0.ucb_c != p1.ucb_c
def test_decode_midpoint_values(self):
codec = CMAParameterCodec()
x0 = codec.initial_vector(_baseline())
p = codec.decode(x0, "mid")
# ucb_c midpoint = (0.2 + 3.0) / 2 = 1.6
assert p.ucb_c == pytest.approx(1.6, abs=0.01)
# ══════════════════════════════════════════════════════════════════════════════
# 2. SELF-PLAY POOL
# ══════════════════════════════════════════════════════════════════════════════
class TestSelfPlayPool:
def test_add_and_retrieve(self):
pool = SelfPlayPool(max_size=5)
snap = PolicySnapshot(
params=_baseline(version="v1"), score=10.0,
created_ts_ns=1000, performance_vector=(1.0, 0.5, 0.8, 0.3, 0.1),
)
pool.maybe_add(snap)
assert len(pool.policies()) == 1
assert pool.policies()[0].version == "v1"
def test_evict_lowest_when_full(self):
pool = SelfPlayPool(max_size=3)
for i in range(5):
pool.maybe_add(PolicySnapshot(
params=_baseline(version=f"v{i}"), score=float(i),
created_ts_ns=1000 + i,
performance_vector=(float(i), float(i) * 0.1, 0.5, 0.3, 0.1),
))
assert len(pool.policies()) == 3
# Should keep top 3 by score (diversity eviction keeps diverse ones)
versions = [s.params.version for s in pool.snapshots]
assert "v4" in versions # highest score always kept
def test_diversity_preservation(self):
pool = SelfPlayPool(max_size=3)
# Add 3 diverse policies
pool.maybe_add(PolicySnapshot(
params=_baseline(version="a"), score=10.0,
created_ts_ns=1000, performance_vector=(1.0, 0.0, 0.5, 0.3, 0.1),
))
pool.maybe_add(PolicySnapshot(
params=_baseline(version="b"), score=9.0,
created_ts_ns=1001, performance_vector=(0.0, 1.0, 0.5, 0.3, 0.1),
))
pool.maybe_add(PolicySnapshot(
params=_baseline(version="c"), score=8.0,
created_ts_ns=1002, performance_vector=(0.5, 0.5, 0.5, 0.3, 0.1),
))
# Add a similar one — should not evict diverse ones
pool.maybe_add(PolicySnapshot(
params=_baseline(version="d"), score=7.0,
created_ts_ns=1003, performance_vector=(1.0, 0.0, 0.5, 0.3, 0.1),
))
assert len(pool.policies()) == 3
def test_policies_returns_tuple(self):
pool = SelfPlayPool(max_size=5)
pool.maybe_add(PolicySnapshot(
params=_baseline(version="v1"), score=10.0,
created_ts_ns=1000,
))
assert isinstance(pool.policies(), tuple)
def test_empty_pool(self):
pool = SelfPlayPool(max_size=5)
assert len(pool.policies()) == 0
def test_cosine_similarity(self):
a = (1.0, 0.0, 0.0)
b = (1.0, 0.0, 0.0)
assert SelfPlayPool._cosine_similarity(a, b) == pytest.approx(1.0)
def test_cosine_orthogonal(self):
a = (1.0, 0.0, 0.0)
b = (0.0, 1.0, 0.0)
assert SelfPlayPool._cosine_similarity(a, b) == pytest.approx(0.0)
def test_cosine_empty(self):
assert SelfPlayPool._cosine_similarity((), ()) == 0.0
# ══════════════════════════════════════════════════════════════════════════════
# 3. SCENARIO FACTORY
# ══════════════════════════════════════════════════════════════════════════════
class TestScenarioFactory:
def test_build_suite_returns_scenarios(self):
factory = ScenarioFactory()
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=10)
assert len(suite) >= 30 # 30 real market scenarios per symbol
def test_each_scenario_has_unique_id(self):
factory = ScenarioFactory()
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=10)
ids = [s.scenario_id for s in suite]
assert len(set(ids)) == len(ids)
def test_scenarios_have_different_tags(self):
factory = ScenarioFactory()
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=10)
all_tags = set()
for s in suite:
all_tags.update(s.tags)
assert "normal" in all_tags
assert "thin" in all_tags
assert "toxic" in all_tags
def test_scenario_has_valid_state(self):
factory = ScenarioFactory()
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=10)
for s in suite:
assert s.initial_state.book.best_bid > 0
assert s.initial_state.book.best_ask > 0
assert s.initial_state.account.equity > 0
def test_multi_symbol(self):
factory = ScenarioFactory()
suite = factory.build_suite(symbols=("BTCUSDT", "ETHUSDT"), steps_per_scenario=5)
assert len(suite) >= 60 # 30 per symbol × 2 symbols
def test_scenario_counterparties(self):
factory = ScenarioFactory()
suite = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=10)
for s in suite:
assert len(s.counterparties) > 0
# ══════════════════════════════════════════════════════════════════════════════
# 4. POLICY EVALUATOR
# ══════════════════════════════════════════════════════════════════════════════
class TestPolicyEvaluator:
def _evaluator(self):
return PolicyEvaluator(
cwm_factory=lambda: MinimalCryptoLOBCWM(),
counterparties=default_counterparty_ecology(),
)
def _scenario(self):
factory = ScenarioFactory()
return factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=5)[0]
def test_evaluate_returns_score_and_results(self):
eval_ = self._evaluator()
scenario = self._scenario()
score, results = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
assert isinstance(score, float)
assert len(results) == 1
assert isinstance(results[0], EpisodeResult)
def test_episode_has_steps(self):
eval_ = self._evaluator()
scenario = self._scenario()
_, results = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
assert results[0].steps > 0
def test_episode_has_pnl(self):
eval_ = self._evaluator()
scenario = self._scenario()
_, results = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
assert isinstance(results[0].pnl_bps, float)
def test_episode_has_drawdown(self):
eval_ = self._evaluator()
scenario = self._scenario()
_, results = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
assert results[0].max_drawdown_bps >= 0
def test_episode_has_final_equity(self):
eval_ = self._evaluator()
scenario = self._scenario()
_, results = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
assert results[0].final_equity > 0
def test_deterministic_with_same_seed(self):
eval_ = self._evaluator()
scenario = self._scenario()
_, r1 = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
_, r2 = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
assert r1[0].pnl_bps == r2[0].pnl_bps
def test_different_seeds_different_results(self):
eval_ = self._evaluator()
scenario = self._scenario()
_, r1 = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=42,
)
_, r2 = eval_.evaluate_candidate(
params=_baseline(version="test"), scenarios=[scenario], rng_seed=99,
)
# Different seeds produce different planner distributions (entropy differs)
assert r1[0].policy_entropy_avg != r2[0].policy_entropy_avg
def test_performance_vector(self):
results = [
EpisodeResult(scenario_id="s1", policy_version="v1", seed=1, pnl_bps=10.0, max_drawdown_bps=5.0, fill_ratio=0.8, policy_entropy_avg=0.5, order_count=10, cancel_count=2),
EpisodeResult(scenario_id="s2", policy_version="v1", seed=2, pnl_bps=-5.0, max_drawdown_bps=10.0, fill_ratio=0.6, policy_entropy_avg=0.3, order_count=8, cancel_count=1),
]
vec = PolicyEvaluator.performance_vector(results)
assert len(vec) == 5
assert vec[0] == pytest.approx(2.5, abs=0.01) # mean PnL
# ══════════════════════════════════════════════════════════════════════════════
# 5. BOOTSTRAP CI
# ══════════════════════════════════════════════════════════════════════════════
class TestBootstrapCI:
def test_ci_returns_tuple(self):
result = bootstrap_ci([1.0, 2.0, 3.0, 4.0, 5.0])
assert len(result) == 3
def test_ci_mean_correct(self):
scores = [1.0, 2.0, 3.0, 4.0, 5.0]
mean, lo, hi = bootstrap_ci(scores, n_bootstrap=100)
assert mean == pytest.approx(3.0, abs=0.01)
def test_ci_contains_mean(self):
scores = [1.0, 2.0, 3.0, 4.0, 5.0]
mean, lo, hi = bootstrap_ci(scores, n_bootstrap=100)
assert lo <= mean <= hi
def test_ci_wider_with_fewer_samples(self):
scores = [1.0, 2.0, 3.0]
_, lo1, hi1 = bootstrap_ci(scores, n_bootstrap=100)
scores2 = [1.0] * 100
_, lo2, hi2 = bootstrap_ci(scores2, n_bootstrap=100)
# More uniform data → tighter CI
assert (hi1 - lo1) > (hi2 - lo2)
def test_ci_empty(self):
mean, lo, hi = bootstrap_ci([])
assert mean == 0.0
def test_ci_single_value(self):
mean, lo, hi = bootstrap_ci([5.0])
assert mean == 5.0
# ══════════════════════════════════════════════════════════════════════════════
# 6. PROMOTION LOGIC
# ══════════════════════════════════════════════════════════════════════════════
class TestPromotion:
def _trainer(self):
codec = CMAParameterCodec()
evaluator = PolicyEvaluator(
cwm_factory=lambda: MinimalCryptoLOBCWM(),
counterparties=default_counterparty_ecology(),
)
pool = SelfPlayPool(max_size=5)
return CMAESTrainer(codec=codec, evaluator=evaluator, pool=pool)
def test_promote_better_candidate(self):
trainer = self._trainer()
incumbent = PolicySnapshot(
params=_baseline(version="inc"), score=10.0,
created_ts_ns=1000, evaluation_summary={"mean_pnl": 5.0, "max_dd": 2.0},
performance_vector=(5.0, 2.0, 0.8, 0.3, 0.1),
)
candidate = PolicySnapshot(
params=_baseline(version="cand"), score=12.0,
created_ts_ns=1001, evaluation_summary={"mean_pnl": 7.0, "max_dd": 1.5},
performance_vector=(7.0, 1.5, 0.9, 0.4, 0.05),
)
ok, reason = trainer.promote(candidate, incumbent)
assert ok
assert reason == "promoted"
def test_reject_worse_candidate(self):
trainer = self._trainer()
incumbent = PolicySnapshot(
params=_baseline(version="inc"), score=10.0,
created_ts_ns=1000, evaluation_summary={},
performance_vector=(1.0,),
)
candidate = PolicySnapshot(
params=_baseline(version="cand"), score=9.0,
created_ts_ns=1001, evaluation_summary={},
performance_vector=(0.5,),
)
ok, reason = trainer.promote(candidate, incumbent)
assert not ok
assert reason == "insufficient_edge"
def test_reject_no_performance_vector(self):
trainer = self._trainer()
incumbent = PolicySnapshot(
params=_baseline(version="inc"), score=10.0,
created_ts_ns=1000, evaluation_summary={},
)
candidate = PolicySnapshot(
params=_baseline(version="cand"), score=15.0,
created_ts_ns=1001, evaluation_summary={},
)
ok, reason = trainer.promote(candidate, incumbent)
assert not ok
assert reason == "no_performance_vector"
# ══════════════════════════════════════════════════════════════════════════════
# 7. CMA-ES TRAINER (end-to-end)
# ══════════════════════════════════════════════════════════════════════════════
class TestCMAESTrainer:
def test_train_returns_snapshot(self):
codec = CMAParameterCodec()
evaluator = PolicyEvaluator(
cwm_factory=lambda: MinimalCryptoLOBCWM(),
counterparties=default_counterparty_ecology(),
)
pool = SelfPlayPool(max_size=5)
trainer = CMAESTrainer(codec=codec, evaluator=evaluator, pool=pool)
factory = ScenarioFactory()
scenarios = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=5)
result = trainer.train(
incumbent=_baseline(version="init"),
scenarios=scenarios,
budget_evals=14,
seed=42,
)
assert isinstance(result, PolicySnapshot)
assert result.params is not None
def test_pool_grows_during_training(self):
codec = CMAParameterCodec()
evaluator = PolicyEvaluator(
cwm_factory=lambda: MinimalCryptoLOBCWM(),
counterparties=default_counterparty_ecology(),
)
pool = SelfPlayPool(max_size=5)
trainer = CMAESTrainer(codec=codec, evaluator=evaluator, pool=pool)
factory = ScenarioFactory()
scenarios = factory.build_suite(symbols=("BTCUSDT",), steps_per_scenario=3)
trainer.train(
incumbent=_baseline(version="init"),
scenarios=scenarios,
budget_evals=14,
seed=42,
)
assert len(pool.policies()) > 0