malkhut(spec): item 1 mutation-litmus + item 3 maker-fee UNVERIFIED comment
Item 1 — Mutation-litmus test (spec §1 item 3): - test_taker_fee_10x_changes_score: fee change MUST affect score - test_zero_fees_vs_correct_fees: zero vs 5bps must differ - BOTH PASS — confirms fees ARE wired into reward function - If fees were ignored, these tests would go RED Item 3 — Maker fee verification (spec §1 item 5): - Added '# UNVERIFIED — no maker fills on record as of 2026-07-13' to Binance and Bybit exchange profiles - Maker fee sign (positive on BingX, negative rebate on others) is correct after fee fix but unverified from actual fills. Items 2,4-10 remain for implementation.
This commit is contained in:
108
MALKHUT/malkhut/tests/test_mutation_litmus.py
Normal file
108
MALKHUT/malkhut/tests/test_mutation_litmus.py
Normal file
@@ -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.")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user