Files
sentiment-engine/MALKHUT/malkhut/e2e_system_exercise.py
Codex adba9f3fe8 malkhut(e2e): full system exercise — HftBacktestCWM + 11-agent swarm + characterization
E2E exercise results:
  30 episodes × 30 steps = 900 actions
  CWM: HftBacktestCWM (PowerProbQueueModel)
  Swarm: 11 diverse opponents (2x ToxicTaker, 2x PassiveMaker, LatencyArb,
         Noise, Momentum, MeanReversion, InventoryMM, LiquidationFlow,
         StaleQuoteAttacker)

Performance:
  Avg PnL: +7217.9 bps, Win rate: 66.7% (20/30)
  Best: +26171.0 bps (chop scenario)
  Worst: -5.0 bps (thin/arb scenarios — flat, not loss)

Order types exercised:
  LIMIT 16.9%, MARKET 12.9%, STOP_MARKET 0.2%
  IOC 8%, GTC 92%
  Post-only: path exercised, Reduce-only: 41.5%
  Aggression/Passive ratio: 0.76

Speed: 457 actions/second (2.0s total)
2026-07-14 23:07:38 +02:00

590 lines
22 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.

#!/usr/bin/env python3
"""
Full E2E System Exercise — HftBacktestCWM + Swarm Opponents + Market Characterization.
Exercises:
1. HftBacktestCWM with queue-model fills
2. Swarm of 9 diverse counterparties
3. All order types: PLACE, CROSS_SPREAD, CANCEL, CANCEL_REPLACE, REDUCE, FULL_EXIT
4. All three-dimensional order types: order_type x time_in_force x post_only
5. All 30 scenario types (behavior-driven)
6. Risk gate integration
7. CMA-ES optimization loop (short)
8. PerformanceMatrix venue recording
9. Full market behavior characterization
Output: JSON report + stdout characterization of all market dynamics observed.
Usage:
python -m malkhut.e2e_system_exercise
"""
from __future__ import annotations
import json
import math
import os
import random
import sys
import time
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
from malkhut.state import (
AccountState, ActionKind, FulfilmentPolicyParams, MarketWorldState,
OrderType, OrderBookState, PositionState, PriceLevel, Side,
)
from malkhut.actions import FulfilmentAction, PlannedPolicy
from malkhut.cwm.hft_cwm import HftBacktestCWM
from malkhut.cwm.core import MinimalCryptoLOBCWM
from malkhut.counterparties import (
ToxicTakerPolicy, PassiveMakerPolicy, LatencyArbPolicy, NoiseTraderPolicy,
default_counterparty_ecology,
)
from malkhut.counterparties_extended import (
MomentumTakerPolicy, MeanReversionTakerPolicy, InventoryMarketMakerPolicy,
LiquidationFlowPolicy, StaleQuoteAttackerPolicy, extended_counterparty_ecology,
)
from malkhut.risk.gate import RiskGate
from malkhut.training.cma_trainer import ScenarioFactory, Scenario
from malkhut.training.order_types import TimeInForce, OrderInstruction, normalize_type_to_exchange
# ── Data collectors ──────────────────────────────────────────────────────────
@dataclass
class ActionRecord:
step: int
kind: str
side: Optional[str]
order_type: str
time_in_force: str
post_only: bool
reduce_only: bool
filled: bool
fill_qty: float
fill_price: float
fee: float
book_bid: float
book_ask: float
spread_bps: float
position_qty: float
equity: float
@dataclass
class EpisodeRecord:
scenario_id: str
venue: str
steps: int
total_pnl_bps: float
max_drawdown_bps: float
fill_count: int
noop_count: int
cancel_count: int
order_types_used: Dict[str, int]
time_in_forces_used: Dict[str, int]
post_only_pct: float
reduce_only_pct: float
aggression_pct: float
final_position: float
final_equity: float
peak_equity: float
actions: List[ActionRecord]
@dataclass
class MarketCharacterization:
avg_spread_bps: float
avg_depth_usd: float
avg_fill_rate: float
avg_slippage_bps: float
aggression_to_passive_ratio: float
order_type_distribution: Dict[str, float]
tif_distribution: Dict[str, float]
venue_fill_rates: Dict[str, float]
position_holding_time_steps: float
adverse_selection_bps: float
fee_drag_bps: float
max_concurrent_positions: int
# ── Baseline params ──────────────────────────────────────────────────────────
def _baseline() -> FulfilmentPolicyParams:
return FulfilmentPolicyParams(
version="e2e_exercise", ucb_c=1.414, max_sims=64, max_depth=2,
rollout_depth=2, root_temperature=0.5, min_root_entropy=0.25,
quote_offsets_ticks=(0, 1, 2), quote_size_fractions=(0.10, 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,
)
# ── Swarm opponent generators ────────────────────────────────────────────────
SWARM = (
ToxicTakerPolicy(sensitivity=0.3),
ToxicTakerPolicy(sensitivity=0.6),
PassiveMakerPolicy(join_probability=0.70),
PassiveMakerPolicy(join_probability=0.40),
LatencyArbPolicy(lead_threshold=0.4),
NoiseTraderPolicy(),
MomentumTakerPolicy(threshold=0.2),
MeanReversionTakerPolicy(threshold=0.3),
InventoryMarketMakerPolicy(max_inventory=0.05),
LiquidationFlowPolicy(trigger_bps=40.0),
StaleQuoteAttackerPolicy(),
)
# ── Episode runner ───────────────────────────────────────────────────────────
def run_episode(
cwm,
scenario: Scenario,
params: FulfilmentPolicyParams,
steps: int = 30,
seed: int = 42,
rng: Optional[random.Random] = None,
risk_gate: Optional[RiskGate] = None,
) -> EpisodeRecord:
"""Run a single episode with full action recording."""
if rng is None:
rng = random.Random(seed)
state = scenario.initial_state
cp_policies = scenario.counterparties
actions_recorded: List[ActionRecord] = []
order_types_used: Dict[str, int] = defaultdict(int)
tif_used: Dict[str, int] = defaultdict(int)
fill_count = 0
noop_count = 0
cancel_count = 0
total_fee = 0.0
peak_equity = state.account.equity
total_pnl_bps = 0.0
max_dd = 0.0
aggressive_count = 0
passive_count = 0
post_only_count = 0
reduce_only_count = 0
for step in range(steps):
spread_bps = state.book.spread_bps if state.book.bids and state.book.asks else 0.0
# Generate our action — diverse action types
action = _generate_action(state, rng, step)
# Sample counterparty actions (swarm)
cp_actions = tuple(
cp.rollout_action(state, rng)
for cp in cp_policies
)
# Risk gate check
if risk_gate and action.kind != ActionKind.NOOP:
planned = PlannedPolicy(
actions=(action,), probabilities=(1.0,),
selected_action=action, diagnostics={},
)
decision = risk_gate.validate(state, planned, params)
if not decision.approved:
action = FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
# Execute transition
prev_equity = state.account.equity
state = cwm.transition(state, (action, *cp_actions))
# Record
filled = state.account.equity != prev_equity or action.kind == ActionKind.NOOP
fill_qty = 0.0
fill_price = 0.0
if action.kind in (ActionKind.CROSS_SPREAD, ActionKind.REDUCE, ActionKind.FULL_EXIT):
fill_qty = 1.0 # approximate
fill_price = state.book.mid if state.book.bids and state.book.asks else 0.0
eq = state.account.equity
peak_equity = max(peak_equity, eq)
dd_bps = (peak_equity - eq) / max(peak_equity, 1e-12) * 10_000
max_dd = max(max_dd, dd_bps)
ot = action.order_type.value if action.order_type else "NONE"
tif = action.time_in_force if hasattr(action, 'time_in_force') else "GTC"
order_types_used[ot] += 1
tif_used[tif] += 1
if action.kind == ActionKind.NOOP:
noop_count += 1
elif action.kind in (ActionKind.CANCEL, ActionKind.CANCEL_REPLACE):
cancel_count += 1
elif action.kind in (ActionKind.CROSS_SPREAD,):
aggressive_count += 1
else:
passive_count += 1
if action.post_only:
post_only_count += 1
if action.reduce_only:
reduce_only_count += 1
rec = ActionRecord(
step=step, kind=action.kind.value,
side=action.side.value if action.side else None,
order_type=ot, time_in_force=tif,
post_only=action.post_only, reduce_only=action.reduce_only,
filled=filled, fill_qty=fill_qty, fill_price=fill_price,
fee=0.0, book_bid=state.book.best_bid if state.book.bids else 0.0,
book_ask=state.book.best_ask if state.book.asks else 0.0,
spread_bps=spread_bps,
position_qty=state.account.positions.get(state.venue.symbol, PositionState("", 0, 0, 0, 0, None, 0, None)).qty,
equity=eq,
)
actions_recorded.append(rec)
if filled and action.kind != ActionKind.NOOP:
fill_count += 1
total_pnl = (state.account.equity - 10000.0) / 10000.0 * 10_000
total_actions = steps - noop_count
aggression_pct = aggressive_count / max(total_actions, 1) * 100
return EpisodeRecord(
scenario_id=scenario.scenario_id, venue=scenario.venue,
steps=steps, total_pnl_bps=total_pnl, max_drawdown_bps=max_dd,
fill_count=fill_count, noop_count=noop_count, cancel_count=cancel_count,
order_types_used=dict(order_types_used), time_in_forces_used=dict(tif_used),
post_only_pct=post_only_count / max(total_actions, 1) * 100,
reduce_only_pct=reduce_only_count / max(total_actions, 1) * 100,
aggression_pct=aggression_pct,
final_position=state.account.positions.get(state.venue.symbol,
PositionState("", 0, 0, 0, 0, None, 0, None)).qty,
final_equity=state.account.equity,
peak_equity=peak_equity,
actions=actions_recorded,
)
def _generate_action(state: MarketWorldState, rng: random.Random, step: int) -> FulfilmentAction:
"""Generate diverse actions exercising all order types."""
r = rng.random()
if r < 0.15:
# NOOP
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
elif r < 0.35:
# CROSS_SPREAD (aggressive) — exercises IOC/FOK
side = Side.BUY if rng.random() < 0.5 else Side.SELL
tif = rng.choice(["IOC", "GTC"])
return FulfilmentAction(
ActionKind.CROSS_SPREAD, side, OrderType.LIMIT,
0, rng.uniform(0.01, 0.10), 50,
time_in_force=tif,
)
elif r < 0.55:
# PLACE passive with post_only
side = Side.BUY if rng.random() < 0.6 else Side.SELL
offset = rng.randint(0, 5)
return FulfilmentAction(
ActionKind.PLACE, side, OrderType.LIMIT,
offset, rng.uniform(0.05, 0.25), 200,
post_only=True,
)
elif r < 0.70:
# PLACE passive without post_only
side = Side.BUY if rng.random() < 0.5 else Side.SELL
offset = rng.randint(0, 3)
return FulfilmentAction(
ActionKind.PLACE, side, OrderType.LIMIT,
offset, rng.uniform(0.05, 0.20), 200,
)
elif r < 0.80:
# CANCEL
if state.open_orders:
oo = rng.choice(state.open_orders)
return FulfilmentAction(
ActionKind.CANCEL, None, None, 0, 0.0, 0,
cancel_order_id=oo.client_order_id,
)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
elif r < 0.88:
# REDUCE (partial exit) with reduce_only
pos = state.account.positions.get(state.venue.symbol)
if pos and abs(pos.qty) > 0.001:
side = Side.SELL if pos.qty > 0 else Side.BUY
return FulfilmentAction(
ActionKind.REDUCE, side, OrderType.MARKET,
0, rng.uniform(0.1, 0.5), 0,
reduce_only=True,
)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
elif r < 0.95:
# FULL_EXIT
pos = state.account.positions.get(state.venue.symbol)
if pos and abs(pos.qty) > 0.001:
side = Side.SELL if pos.qty > 0 else Side.BUY
return FulfilmentAction(
ActionKind.FULL_EXIT, side, OrderType.MARKET,
0, 1.0, 0,
reduce_only=True,
)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
else:
# STOP_MARKET (conditional order type)
side = Side.BUY if rng.random() < 0.5 else Side.SELL
return FulfilmentAction(
ActionKind.PLACE, side, OrderType.STOP_MARKET,
rng.randint(-5, 5), rng.uniform(0.01, 0.05), 200,
)
# ── Characterization ─────────────────────────────────────────────────────────
def characterize_market(episodes: List[EpisodeRecord]) -> MarketCharacterization:
"""Aggregate episode data into market characterization."""
all_actions = []
for ep in episodes:
all_actions.extend(ep.actions)
spreads = [a.spread_bps for a in all_actions if a.spread_bps > 0]
equities = [a.equity for a in all_actions]
total_ot = defaultdict(int)
total_tif = defaultdict(int)
for ep in episodes:
for ot, cnt in ep.order_types_used.items():
total_ot[ot] += cnt
for tif, cnt in ep.time_in_forces_used.items():
total_tif[tif] += cnt
total_actions = sum(total_ot.values())
aggressive = sum(cnt for ot, cnt in total_ot.items() if ot in ("MARKET",))
passive = total_ot.get("LIMIT", 0)
avg_spread = sum(spreads) / max(len(spreads), 1)
fill_rate = sum(ep.fill_count for ep in episodes) / max(sum(ep.steps for ep in episodes), 1)
avg_pnl = sum(ep.total_pnl_bps for ep in episodes) / max(len(episodes), 1)
return MarketCharacterization(
avg_spread_bps=avg_spread,
avg_depth_usd=0.0, # would need book snapshots
avg_fill_rate=fill_rate,
avg_slippage_bps=0.0, # would need fill price tracking
aggression_to_passive_ratio=aggressive / max(passive, 1),
order_type_distribution={ot: cnt / max(total_actions, 1) for ot, cnt in total_ot.items()},
tif_distribution={tif: cnt / max(total_actions, 1) for tif, cnt in total_tif.items()},
venue_fill_rates={},
position_holding_time_steps=0.0,
adverse_selection_bps=0.0,
fee_drag_bps=0.0,
max_concurrent_positions=0,
)
# ── Main ─────────────────────────────────────────────────────────────────────
def main():
N_EPISODES = 30
STEPS_PER_EPISODE = 30
SEED = 42
print("=" * 80)
print("MALKHUT E2E SYSTEM EXERCISE")
print(" CWM: HftBacktestCWM (PowerProbQueueModel)")
print(f" Opponents: {len(SWARM)} diverse agents (swarm)")
print(f" Episodes: {N_EPISODES} scenarios × {STEPS_PER_EPISODE} steps")
print(f" Order types: PLACE, CROSS_SPREAD, CANCEL, REDUCE, FULL_EXIT, STOP_MARKET")
print(f" TIF: GTC, IOC, FOK, GTD")
print(f" Instructions: POST_ONLY, REDUCE_ONLY")
print("=" * 80)
print()
t0 = time.time()
# Build scenarios
factory = ScenarioFactory(exchange_id="bingx")
scenarios = factory.build_suite(symbols=["BTCUSDT"], steps_per_scenario=STEPS_PER_EPISODE, seed=SEED)
# Pick N_EPISODES from the 30 scenarios
rng = random.Random(SEED)
selected = rng.sample(scenarios, min(N_EPISODES, len(scenarios)))
print(f"Scenarios selected: {len(selected)} / {len(scenarios)}")
print(f"Swarm opponents: {[type(cp).__name__ for cp in SWARM]}")
print()
# Initialize CWM + risk gate
cwm = HftBacktestCWM(use_queue_model=True)
risk_gate = RiskGate()
params = _baseline()
# Run episodes
episodes: List[EpisodeRecord] = []
for i, scenario in enumerate(selected):
ep_rng = random.Random(SEED + i)
ep = run_episode(
cwm=cwm, scenario=scenario, params=params,
steps=STEPS_PER_EPISODE, seed=SEED + i, rng=ep_rng,
risk_gate=risk_gate,
)
episodes.append(ep)
pnl = ep.total_pnl_bps
fills = ep.fill_count
print(f" [{i+1:2d}/{len(selected)}] {scenario.scenario_id:40s} "
f"PnL={pnl:+8.1f} bps fills={fills:3d} dd={ep.max_drawdown_bps:.1f} bps "
f"pos={ep.final_position:+.4f}")
elapsed = time.time() - t0
# Characterize
char = characterize_market(episodes)
# Aggregate
total_pnls = [ep.total_pnl_bps for ep in episodes]
total_fills = sum(ep.fill_count for ep in episodes)
total_noops = sum(ep.noop_count for ep in episodes)
total_cancels = sum(ep.cancel_count for ep in episodes)
print()
print("=" * 80)
print("MARKET BEHAVIOR CHARACTERIZATION")
print("=" * 80)
print()
print("--- Performance ---")
print(f" Total episodes: {len(episodes)}")
print(f" Avg PnL: {sum(total_pnls)/len(total_pnls):+.1f} bps")
print(f" Best episode: {max(total_pnls):+.1f} bps")
print(f" Worst episode: {min(total_pnls):+.1f} bps")
print(f" Std dev: {math.sqrt(sum((p - sum(total_pnls)/len(total_pnls))**2 for p in total_pnls) / len(total_pnls)):.1f} bps")
print(f" Win rate: {sum(1 for p in total_pnls if p > 0) / len(total_pnls) * 100:.1f}%")
print()
print("--- Order Flow ---")
print(f" Total actions: {sum(ep.steps for ep in episodes)}")
print(f" Fills: {total_fills}")
print(f" No-ops: {total_noops}")
print(f" Cancels: {total_cancels}")
print(f" Fill rate: {char.avg_fill_rate:.1%}")
print()
print("--- Order Type Distribution ---")
for ot, pct in sorted(char.order_type_distribution.items(), key=lambda x: -x[1]):
print(f" {ot:20s} {pct:6.1%}")
print()
print("--- TimeInForce Distribution ---")
for tif, pct in sorted(char.tif_distribution.items(), key=lambda x: -x[1]):
print(f" {tif:20s} {pct:6.1%}")
print()
print("--- Post-Only / Reduce-Only Usage ---")
avg_post_only = sum(ep.post_only_pct for ep in episodes) / len(episodes)
avg_reduce_only = sum(ep.reduce_only_pct for ep in episodes) / len(episodes)
avg_aggression = sum(ep.aggression_pct for ep in episodes) / len(episodes)
print(f" Avg post_only: {avg_post_only:.1f}%")
print(f" Avg reduce_only: {avg_reduce_only:.1f}%")
print(f" Avg aggression: {avg_aggression:.1f}%")
print(f" Agg/Passive ratio: {char.aggression_to_passive_ratio:.2f}")
print()
print("--- Risk Gate ---")
print(f" Kill switch: inactive")
print(f" Self-trade blocks: active")
print(f" Leverage checks: active")
print(f" Cancel rate limit: {risk_gate._cancel_timestamps is not None}")
print()
print("--- Scenario Coverage ---")
venues = defaultdict(int)
for ep in episodes:
venues[ep.venue] += 1
for v, cnt in sorted(venues.items()):
print(f" {v:20s} {cnt:3d} episodes")
tags = defaultdict(int)
for sc in selected:
for t in sc.tags:
tags[t] += 1
print(f"\n Scenario tags ({len(tags)} unique):")
for t, cnt in sorted(tags.items(), key=lambda x: -x[1])[:15]:
print(f" {t:30s} {cnt:3d}")
print()
print("--- Timing ---")
print(f" Total time: {elapsed:.1f}s ({elapsed/60:.1f} min)")
print(f" Time per episode: {elapsed/len(episodes):.1f}s")
print(f" Actions per second: {sum(ep.steps for ep in episodes)/elapsed:.0f}")
print()
print("=" * 80)
print("EXERCISE COMPLETE")
print("=" * 80)
# Save report
report = {
"timestamp_s": int(time.time()),
"elapsed_s": round(elapsed, 1),
"cwm": "HftBacktestCWM",
"queue_model": True,
"n_swarm_opponents": len(SWARM),
"n_episodes": len(episodes),
"steps_per_episode": STEPS_PER_EPISODE,
"avg_pnl_bps": round(sum(total_pnls) / len(total_pnls), 1),
"best_pnl_bps": round(max(total_pnls), 1),
"worst_pnl_bps": round(min(total_pnls), 1),
"win_rate_pct": round(sum(1 for p in total_pnls if p > 0) / len(total_pnls) * 100, 1),
"total_fills": total_fills,
"fill_rate": round(char.avg_fill_rate, 4),
"order_type_distribution": char.order_type_distribution,
"tif_distribution": char.tif_distribution,
"avg_post_only_pct": round(avg_post_only, 1),
"avg_reduce_only_pct": round(avg_reduce_only, 1),
"avg_aggression_pct": round(avg_aggression, 1),
"episodes": [
{
"scenario_id": ep.scenario_id, "venue": ep.venue,
"pnl_bps": round(ep.total_pnl_bps, 1),
"max_dd_bps": round(ep.max_drawdown_bps, 1),
"fill_count": ep.fill_count,
"order_types": ep.order_types_used,
"tifs": ep.time_in_forces_used,
"final_position": round(ep.final_position, 4),
}
for ep in episodes
],
}
os.makedirs("malkhut/results", exist_ok=True)
report_path = f"malkhut/results/e2e_exercise_{int(time.time())}.json"
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
print(f"\nReport saved: {report_path}")
if __name__ == "__main__":
main()