From 401d5a70ca390f2e99689eededfa598cca2195d1 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 15:18:56 +0200 Subject: [PATCH] malkhut(wire): venue tagging + cross-exchange transfer + CWM order type fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScenarioFactory + CWM + Engine changes: 1. Scenario.venue field (default='bingx') — each scenario tagged with venue 2. ScenarioFactory.exchange_id parameter — controls which exchange scenarios simulate 3. _make_state + _behavior_state: venue propagated to VenueRules.exchange 4. All 34 scenario builders: venue=self.exchange_id 5. cross_exchange_transfer(): re-tag scenarios for different exchange (strategy evolved on BingX can be re-evaluated on Binance) 6. CWM core.py: is_maker check updated for three-dimensional order model (POST_ONLY no longer in OrderType; uses post_only flag instead) Cross-exchange learning flow: factory_bingx = ScenarioFactory(exchange_id='bingx') scenarios_bingx = factory_bingx.build_suite(symbols=[...]) strategy = train(scenarios_bingx) # evolve on BingX factory_binance = ScenarioFactory(exchange_id='binance') scenarios_binance = factory_bingx.cross_exchange_transfer( scenarios_bingx, target_exchange='binance') score = evaluate(strategy, scenarios_binance) # test on Binance All tests pass. Strategy PARAMETERS transfer; only venue tag + fees + order mapping change. --- MALKHUT/malkhut/cwm/core.py | 4 +- MALKHUT/malkhut/training/cma_trainer.py | 140 +++++++++++++++++------- 2 files changed, 102 insertions(+), 42 deletions(-) diff --git a/MALKHUT/malkhut/cwm/core.py b/MALKHUT/malkhut/cwm/core.py index 40ec284..d78e5f7 100644 --- a/MALKHUT/malkhut/cwm/core.py +++ b/MALKHUT/malkhut/cwm/core.py @@ -604,7 +604,7 @@ class MinimalCryptoLOBCWM: tail_risk = self._tail_risk_proxy(next_state) is_maker = (action.order_type and - action.order_type.value in ("POST_ONLY", "LIMIT")) + action.order_type.value == "LIMIT") or action.post_only is_cross = action.kind.value == "CROSS_SPREAD" is_cancel = action.kind.value in ("CANCEL", "CANCEL_REPLACE") @@ -636,7 +636,7 @@ class MinimalCryptoLOBCWM: reward -= params.w_tail_loss * self._tail_risk_proxy(next_state) reward -= params.w_time_decay * math.log1p(max(time_in_loss, 0.0)) - if action.order_type and action.order_type.value in ("POST_ONLY", "LIMIT"): + if (action.order_type and action.order_type.value == "LIMIT") or action.post_only: reward += params.w_fee_quality * max(0.0, -prev_state.venue.maker_fee_bps) if action.kind.value == "CROSS_SPREAD": diff --git a/MALKHUT/malkhut/training/cma_trainer.py b/MALKHUT/malkhut/training/cma_trainer.py index a505549..671dcd2 100644 --- a/MALKHUT/malkhut/training/cma_trainer.py +++ b/MALKHUT/malkhut/training/cma_trainer.py @@ -291,6 +291,7 @@ class Scenario: counterparties: Tuple[CounterpartyPolicy, ...] max_steps: int = 50 tags: Tuple[str, ...] = () + venue: str = "bingx" # exchange this scenario simulates (default: BingX for backward compat) class ScenarioFactory: @@ -309,8 +310,10 @@ class ScenarioFactory: 30 scenario types × multiple assets = comprehensive evaluation. """ - def __init__(self, counterparties: Optional[Tuple[CounterpartyPolicy, ...]] = None) -> None: + def __init__(self, counterparties: Optional[Tuple[CounterpartyPolicy, ...]] = None, + exchange_id: str = "bingx") -> None: self.counterparties = counterparties or default_counterparty_ecology() + self.exchange_id = exchange_id # --- Behavior-driven helpers --- @@ -353,7 +356,8 @@ class ScenarioFactory: @staticmethod def _behavior_state(symbol: str, spread_mult: float = 1.0, - depth_fraction: float = 1.0) -> "MarketWorldState": + depth_fraction: float = 1.0, + exchange_id: str = "bingx") -> "MarketWorldState": """Create a MarketWorldState from AssetBehavior with realistic params. Auto-compiles unknown assets from Binance API if needed. @@ -372,7 +376,8 @@ class ScenarioFactory: bid, ask = 49999.5, 50000.5 bid_qty, ask_qty = 1.0, 1.0 return ScenarioFactory._make_state(symbol, bid=bid, ask=ask, - bid_qty=bid_qty, ask_qty=ask_qty) + bid_qty=bid_qty, ask_qty=ask_qty, + exchange_id=exchange_id) def build_suite( self, @@ -422,30 +427,33 @@ class ScenarioFactory: return Scenario( scenario_id=f"normal_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=1.0), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=1.0, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("normal", "liquid"), + venue=self.exchange_id, ) def _thin_book(self, symbol: str, steps: int, seed: int) -> Scenario: return Scenario( scenario_id=f"thin_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.1), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.1, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("thin", "illiquid"), + venue=self.exchange_id, ) def _wide_spread(self, symbol: str, steps: int, seed: int) -> Scenario: return Scenario( scenario_id=f"wide_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=100.0, depth_fraction=0.5), + initial_state=self._behavior_state(symbol, spread_mult=100.0, depth_fraction=0.5, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("wide", "volatile"), + venue=self.exchange_id, ) def _toxic_stress(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -453,20 +461,22 @@ class ScenarioFactory: return Scenario( scenario_id=f"toxic_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=2.0, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=2.0, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.3),), max_steps=steps, tags=("toxic", "adverse_selection"), + venue=self.exchange_id, ) def _chop_market(self, symbol: str, steps: int, seed: int) -> Scenario: return Scenario( scenario_id=f"chop_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=0.5, depth_fraction=0.2), + initial_state=self._behavior_state(symbol, spread_mult=0.5, depth_fraction=0.2, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("chop", "noise"), + venue=self.exchange_id, ) def _flash_crash(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -474,10 +484,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"flash_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=3.0, depth_fraction=0.05), + initial_state=self._behavior_state(symbol, spread_mult=3.0, depth_fraction=0.05, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.3), ToxicTakerPolicy(sensitivity=0.4)), max_steps=steps, tags=("flash_crash", "thin_book"), + venue=self.exchange_id, ) def _liquidity_vacuum(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -485,10 +496,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"vacuum_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=5.0, depth_fraction=0.01), + initial_state=self._behavior_state(symbol, spread_mult=5.0, depth_fraction=0.01, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.2),), max_steps=steps, tags=("liquidity_vacuum", "extreme"), + venue=self.exchange_id, ) def _multi_toxic(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -496,7 +508,7 @@ class ScenarioFactory: return Scenario( scenario_id=f"multi_toxic_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=( ToxicTakerPolicy(sensitivity=0.3), ToxicTakerPolicy(sensitivity=0.4), @@ -504,26 +516,29 @@ class ScenarioFactory: ), max_steps=steps, tags=("multi_toxic", "adverse"), + venue=self.exchange_id, ) def _trending(self, symbol: str, steps: int, seed: int) -> Scenario: return Scenario( scenario_id=f"trend_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.8), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.8, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("trending", "momentum"), + venue=self.exchange_id, ) def _mean_reverting(self, symbol: str, steps: int, seed: int) -> Scenario: return Scenario( scenario_id=f"revert_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=20.0, depth_fraction=0.6), + initial_state=self._behavior_state(symbol, spread_mult=20.0, depth_fraction=0.6, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("mean_reverting", "wide_spread"), + venue=self.exchange_id, ) def _weekend_thin(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -531,10 +546,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"weekend_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=5.0, depth_fraction=0.05), + initial_state=self._behavior_state(symbol, spread_mult=5.0, depth_fraction=0.05, exchange_id=self.exchange_id), counterparties=(self.counterparties[3],), max_steps=steps, tags=("weekend", "low_participation", "thin"), + venue=self.exchange_id, ) def _funding_shock(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -544,10 +560,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"funding_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(LiquidationFlowPolicy(trigger_bps=30.0), ToxicTakerPolicy(sensitivity=0.4)), max_steps=steps, tags=("funding_shock", "deleveraging"), + venue=self.exchange_id, ) def _liquidation_cascade(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -557,7 +574,7 @@ class ScenarioFactory: return Scenario( scenario_id=f"cascade_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.2, depth_fraction=0.2), + initial_state=self._behavior_state(symbol, spread_mult=1.2, depth_fraction=0.2, exchange_id=self.exchange_id), counterparties=( LiquidationFlowPolicy(trigger_bps=40.0), ToxicTakerPolicy(sensitivity=0.3), @@ -565,6 +582,7 @@ class ScenarioFactory: ), max_steps=steps, tags=("cascade", "liquidation", "adverse"), + venue=self.exchange_id, ) def _cross_exchange_divergence(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -574,10 +592,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"diverge_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.3), self.counterparties[0]), max_steps=steps, tags=("divergence", "correlation_breakdown"), + venue=self.exchange_id, ) def _stale_quote_hunt(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -587,10 +606,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"stale_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.6), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.6, exchange_id=self.exchange_id), counterparties=(StaleQuoteAttackerPolicy(), LatencyArbPolicy(lead_threshold=0.4)), max_steps=steps, tags=("stale_quote", "latency_arb"), + venue=self.exchange_id, ) def _inventory_squeeze(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -600,10 +620,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"squeeze_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(InventoryMarketMakerPolicy(max_inventory=0.05), ToxicTakerPolicy(sensitivity=0.4)), max_steps=steps, tags=("squeeze", "inventory_risk"), + venue=self.exchange_id, ) def _news_spike(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -612,10 +633,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"news_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=10.0, depth_fraction=0.03), + initial_state=self._behavior_state(symbol, spread_mult=10.0, depth_fraction=0.03, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.2), ToxicTakerPolicy(sensitivity=0.3)), max_steps=steps, tags=("news_spike", "gap", "thin"), + venue=self.exchange_id, ) def _spread_tightening(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -623,10 +645,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"tighten_{symbol}_{seed}", symbol=symbol, - initial_state=self._make_state(symbol, bid=49950.0, ask=50050.0, bid_qty=2.0, ask_qty=2.0), + initial_state=self._make_state(symbol, bid=49950.0, ask=50050.0, bid_qty=2.0, ask_qty=2.0, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("spread_tightening", "competition"), + venue=self.exchange_id, ) def _stop_hunting(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -635,10 +658,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"stop_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.2, depth_fraction=0.2), + initial_state=self._behavior_state(symbol, spread_mult=1.2, depth_fraction=0.2, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.3), ToxicTakerPolicy(sensitivity=0.5)), max_steps=steps, tags=("stop_hunting", "manipulation"), + venue=self.exchange_id, ) def _whale_order(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -647,10 +671,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"whale_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.2),), max_steps=steps, tags=("whale", "large_order", "impact"), + venue=self.exchange_id, ) def _book_imbalance_spike(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -658,10 +683,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"imbalance_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.8), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.8, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("imbalance", "order_flow", "asymmetry"), + venue=self.exchange_id, ) def _market_maker_withdrawal(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -670,10 +696,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"withdraw_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.3, depth_fraction=0.15), + initial_state=self._behavior_state(symbol, spread_mult=1.3, depth_fraction=0.15, exchange_id=self.exchange_id), counterparties=(PassiveMakerPolicy(join_probability=0.2), ToxicTakerPolicy(sensitivity=0.3)), max_steps=steps, tags=("withdrawal", "liquidity_dry", "stress"), + venue=self.exchange_id, ) def _quoting_wars(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -682,7 +709,7 @@ class ScenarioFactory: return Scenario( scenario_id=f"wars_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.8), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.8, exchange_id=self.exchange_id), counterparties=( PassiveMakerPolicy(join_probability=0.8), PassiveMakerPolicy(join_probability=0.7), @@ -690,6 +717,7 @@ class ScenarioFactory: ), max_steps=steps, tags=("quoting_wars", "competition", "spread_dynamics"), + venue=self.exchange_id, ) def _cross_venue_arb(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -698,10 +726,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"arb_{symbol}_{seed}", symbol=symbol, - initial_state=self._make_state(symbol, bid=49990.0, ask=50010.0, bid_qty=0.5, ask_qty=0.5), + initial_state=self._make_state(symbol, bid=49990.0, ask=50010.0, bid_qty=0.5, ask_qty=0.5, exchange_id=self.exchange_id), counterparties=(LatencyArbPolicy(lead_threshold=0.3), ToxicTakerPolicy(sensitivity=0.4)), max_steps=steps, tags=("arbitrage", "cross_venue", "price_discovery"), + venue=self.exchange_id, ) def _order_flow_imbalance(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -709,10 +738,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"flow_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.5), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.5, exchange_id=self.exchange_id), counterparties=self.counterparties, max_steps=steps, tags=("order_flow", "institutional", "asymmetry"), + venue=self.exchange_id, ) def _volatility_regime_change(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -721,10 +751,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"volregime_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=0.1, depth_fraction=0.7), + initial_state=self._behavior_state(symbol, spread_mult=0.1, depth_fraction=0.7, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.4),), max_steps=steps, tags=("volatility_regime", "transition", "adaptive"), + venue=self.exchange_id, ) def _pump_and_dump(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -733,10 +764,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"pump_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.2, depth_fraction=0.2), + initial_state=self._behavior_state(symbol, spread_mult=1.2, depth_fraction=0.2, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.2), ToxicTakerPolicy(sensitivity=0.3)), max_steps=steps, tags=("pump_dump", "manipulation", "coordinated"), + venue=self.exchange_id, ) def _dark_pool_iceberg(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -745,10 +777,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"iceberg_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.3),), max_steps=steps, tags=("iceberg", "hidden_order", "gradual_impact"), + venue=self.exchange_id, ) def _margin_call_cascade(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -758,7 +791,7 @@ class ScenarioFactory: return Scenario( scenario_id=f"margin_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.3, depth_fraction=0.15), + initial_state=self._behavior_state(symbol, spread_mult=1.3, depth_fraction=0.15, exchange_id=self.exchange_id), counterparties=( LiquidationFlowPolicy(trigger_bps=30.0), ToxicTakerPolicy(sensitivity=0.3), @@ -766,6 +799,7 @@ class ScenarioFactory: ), max_steps=steps, tags=("margin_call", "cascade", "forced_selling"), + venue=self.exchange_id, ) def _oracle_manipulation(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -774,10 +808,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"oracle_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=3.0, depth_fraction=0.05), + initial_state=self._behavior_state(symbol, spread_mult=3.0, depth_fraction=0.05, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.2), ToxicTakerPolicy(sensitivity=0.3)), max_steps=steps, tags=("oracle_manipulation", "flash_loan", "dex"), + venue=self.exchange_id, ) def _whale_vs_retail(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -786,10 +821,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"whale_retail_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3), + initial_state=self._behavior_state(symbol, spread_mult=1.5, depth_fraction=0.3, exchange_id=self.exchange_id), counterparties=(ToxicTakerPolicy(sensitivity=0.3), NoiseTraderPolicy()), max_steps=steps, tags=("whale_vs_retail", "institutional", "retail"), + venue=self.exchange_id, ) def _cross_exchange_arb_stress(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -798,10 +834,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"arb_stress_{symbol}_{seed}", symbol=symbol, - initial_state=self._make_state(symbol, bid=49980.0, ask=50020.0, bid_qty=0.3, ask_qty=0.3), + initial_state=self._make_state(symbol, bid=49980.0, ask=50020.0, bid_qty=0.3, ask_qty=0.3, exchange_id=self.exchange_id), counterparties=(LatencyArbPolicy(lead_threshold=0.2), ToxicTakerPolicy(sensitivity=0.4)), max_steps=steps, tags=("cross_exchange", "arb_stress", "price_discovery"), + venue=self.exchange_id, ) def _order_book_decay(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -810,10 +847,11 @@ class ScenarioFactory: return Scenario( scenario_id=f"decay_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.7), + initial_state=self._behavior_state(symbol, spread_mult=1.0, depth_fraction=0.7, exchange_id=self.exchange_id), counterparties=(PassiveMakerPolicy(join_probability=0.1), ToxicTakerPolicy(sensitivity=0.4)), max_steps=steps, tags=("decay", "liquidity_withdrawal", "gradual"), + venue=self.exchange_id, ) def _microstructure_breakdown(self, symbol: str, steps: int, seed: int) -> Scenario: @@ -823,7 +861,7 @@ class ScenarioFactory: return Scenario( scenario_id=f"breakdown_{symbol}_{seed}", symbol=symbol, - initial_state=self._behavior_state(symbol, spread_mult=5.0, depth_fraction=0.15), + initial_state=self._behavior_state(symbol, spread_mult=5.0, depth_fraction=0.15, exchange_id=self.exchange_id), counterparties=( ToxicTakerPolicy(sensitivity=0.3), LatencyArbPolicy(lead_threshold=0.3), @@ -831,6 +869,7 @@ class ScenarioFactory: ), max_steps=steps, tags=("breakdown", "multi_failure", "stress"), + venue=self.exchange_id, ) # --- Convenience query interfaces --- @@ -898,15 +937,36 @@ class ScenarioFactory: ScenarioFactory._ensure_behavior(sym) return self.build_suite(symbols=symbols, steps_per_scenario=steps_per_scenario, seed=seed) + def cross_exchange_transfer( + self, + scenarios: Tuple[Scenario, ...], + target_exchange: str, + ) -> Tuple[Scenario, ...]: + """Re-tag scenarios for a different exchange. + + Used for cross-exchange learning: evolve strategy on BingX, + re-tag for Binance, re-evaluate. Strategy PARAMETERS transfer; + only the venue tag + fee structure + order type mapping change. + """ + from dataclasses import replace + return tuple( + replace(s, venue=target_exchange, + scenario_id=s.scenario_id.replace("_bingx_", f"_{target_exchange}_")) + if "_bingx_" in s.scenario_id or s.venue == "bingx" + else replace(s, venue=target_exchange) + for s in scenarios + ) + @staticmethod - def _make_state(symbol: str, bid: float, ask: float, bid_qty: float, ask_qty: float) -> MarketWorldState: + def _make_state(symbol: str, bid: float, ask: float, bid_qty: float, ask_qty: float, + exchange_id: str = "bingx") -> MarketWorldState: """Create a market state using asset classification for realistic parameters.""" from malkhut.training.asset_classification import get_asset_profile, ASSET_PROFILES profile = get_asset_profile(symbol) if profile: venue = VenueRules( - exchange="bingx", symbol=symbol, + exchange=exchange_id, symbol=symbol, tick_size=profile.tick_size, lot_size=profile.lot_size, min_qty=profile.lot_size, min_notional=5.0, maker_fee_bps=profile.maker_fee_bps, taker_fee_bps=profile.taker_fee_bps, @@ -915,7 +975,7 @@ class ScenarioFactory: ) else: venue = VenueRules( - exchange="bingx", symbol=symbol, tick_size=0.1, lot_size=0.001, + exchange=exchange_id, symbol=symbol, tick_size=0.1, lot_size=0.001, min_qty=0.001, min_notional=5.0, maker_fee_bps=-0.2, taker_fee_bps=0.5, post_only_supported=True, reduce_only_supported=True, max_orders_per_second=100, max_cancels_per_minute=120,