malkhut(wire): venue tagging + cross-exchange transfer + CWM order type fix

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.
This commit is contained in:
Codex
2026-07-14 15:18:56 +02:00
parent 455a7a5a4e
commit 401d5a70ca
2 changed files with 102 additions and 42 deletions

View File

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

View File

@@ -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,