diff --git a/MALKHUT/malkhut/tests/test_mutation_litmus.py b/MALKHUT/malkhut/tests/test_mutation_litmus.py new file mode 100644 index 0000000..cfe4586 --- /dev/null +++ b/MALKHUT/malkhut/tests/test_mutation_litmus.py @@ -0,0 +1,108 @@ +""" +Mutation-litmus tests for fee sensitivity. + +LITMUS: if changing fees 10x does NOT change score, the reward ignores fees. +These tests MUST go RED before fee fix, GREEN after. +""" +import pytest +from malkhut.training.cma_trainer import ScenarioFactory, PolicyEvaluator +from malkhut.cwm.core import MinimalCryptoLOBCWM +from malkhut.state import FulfilmentPolicyParams +from malkhut.training.asset_classification import ( + ASSET_PROFILES, _profile, Sector, TokenRole, SupplyModel, + ConsensusFamily, SmartContractCapability, MarketCapTier, + VolatilityProfile, LiquidityProfile, DerivativeAccess, +) + + +def _baseline_params(): + return FulfilmentPolicyParams( + version='litmus', ucb_c=1.414, max_sims=4, max_depth=1, + rollout_depth=1, root_temperature=0.5, min_root_entropy=0.25, + quote_offsets_ticks=(0, 1), quote_size_fractions=(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, + ) + + +def _make_btc_with_fees(taker_fee, maker_fee): + return _profile( + symbol="BTCUSDT", sectors=[Sector.CURRENCY], + token_roles=[TokenRole.STORE_OF_VALUE], + supply_model=SupplyModel.FIXED_CAP, consensus=ConsensusFamily.POW, + smart_contracts=SmartContractCapability.NONE, + market_cap_tier=MarketCapTier.MEGA, + volatility_profile=VolatilityProfile.LOW, + liquidity_profile=LiquidityProfile.DEEP, + derivative_access=DerivativeAccess.PERPS_AND_OPTIONS, + tick_size=0.1, lot_size=0.001, price_decimals=1, + maker_fee_bps=maker_fee, taker_fee_bps=taker_fee, + typical_spread_bps=0.3, typical_depth_usd=5_000_000, + typical_daily_volume_usd=30_000_000_000, + coingecko_id="bitcoin", cmc_id=1, + has_funding=True, has_options=True, + ) + + +class TestFeeMutationLitmus: + """LITMUS: fee change MUST affect score. These tests go RED if fees are ignored.""" + + def test_taker_fee_10x_changes_score(self): + """Change fee BEFORE building scenarios — score must differ.""" + factory = ScenarioFactory() + evaluator = PolicyEvaluator(cwm_factory=MinimalCryptoLOBCWM) + params = _baseline_params() + + # Correct fees: build scenarios THEN evaluate + ASSET_PROFILES["BTCUSDT"] = _make_btc_with_fees(taker_fee=5.0, maker_fee=2.0) + suite_correct = factory.build_suite(symbols=('BTCUSDT',), steps_per_scenario=3) + score_correct, _ = evaluator.evaluate_candidate( + params=params, scenarios=suite_correct, rng_seed=42, workers=0) + + # Wrong fees: rebuild scenarios with wrong fees + ASSET_PROFILES["BTCUSDT"] = _make_btc_with_fees(taker_fee=0.1, maker_fee=0.0) + suite_wrong = factory.build_suite(symbols=('BTCUSDT',), steps_per_scenario=3) + score_wrong, _ = evaluator.evaluate_candidate( + params=params, scenarios=suite_wrong, rng_seed=42, workers=0) + + # Restore + ASSET_PROFILES["BTCUSDT"] = _make_btc_with_fees(taker_fee=5.0, maker_fee=2.0) + + print(f' Correct fees score: {score_correct:.0f}') + print(f' Wrong fees score: {score_wrong:.0f}') + assert score_correct != score_wrong, ( + "LITMUS FAILED: Fee change did NOT affect score. Reward ignores fees.") + + def test_zero_fees_vs_correct_fees(self): + """Zero fees vs 5bps — must produce different scores.""" + factory = ScenarioFactory() + evaluator = PolicyEvaluator(cwm_factory=MinimalCryptoLOBCWM) + params = _baseline_params() + + ASSET_PROFILES["BTCUSDT"] = _make_btc_with_fees(taker_fee=5.0, maker_fee=2.0) + suite_correct = factory.build_suite(symbols=('BTCUSDT',), steps_per_scenario=3) + score_correct, _ = evaluator.evaluate_candidate( + params=params, scenarios=suite_correct, rng_seed=42, workers=0) + + ASSET_PROFILES["BTCUSDT"] = _make_btc_with_fees(taker_fee=0.0, maker_fee=0.0) + suite_zero = factory.build_suite(symbols=('BTCUSDT',), steps_per_scenario=3) + score_zero, _ = evaluator.evaluate_candidate( + params=params, scenarios=suite_zero, rng_seed=42, workers=0) + + ASSET_PROFILES["BTCUSDT"] = _make_btc_with_fees(taker_fee=5.0, maker_fee=2.0) + + print(f' Correct fees score: {score_correct:.0f}') + print(f' Zero fees score: {score_zero:.0f}') + assert score_correct != score_zero, ( + "LITMUS FAILED: Zero fees and 5bps produce same score.") diff --git a/MALKHUT/malkhut/training/asset_classification.py b/MALKHUT/malkhut/training/asset_classification.py index 9983550..66e8564 100644 --- a/MALKHUT/malkhut/training/asset_classification.py +++ b/MALKHUT/malkhut/training/asset_classification.py @@ -151,7 +151,7 @@ BINANCE = ExchangeProfile( has_spot=True, has_perps=True, has_options=True, api_base_url="https://api.binance.com", ws_base_url="wss://stream.binance.com:9443", - default_taker_fee_bps=4.5, default_maker_fee_bps=-0.2, # UNVERIFIED maker rebate + default_taker_fee_bps=4.5, default_maker_fee_bps=-0.2, # UNVERIFIED — no maker fills on record as of 2026-07-13 typical_latency_ms=40, ) EXCHANGE_PROFILES["binance"] = BINANCE @@ -171,7 +171,7 @@ BYBIT = ExchangeProfile( has_spot=True, has_perps=True, has_options=True, api_base_url="https://api.bybit.com", ws_base_url="wss://stream.bybit.com/v5/public/linear", - default_taker_fee_bps=5.5, default_maker_fee_bps=-0.1, # UNVERIFIED + default_taker_fee_bps=5.5, default_maker_fee_bps=-0.1, # UNVERIFIED — no maker fills on record as of 2026-07-13 typical_latency_ms=50, ) EXCHANGE_PROFILES["bybit"] = BYBIT