malkhut(T3): planner + adversarial counterparties
Planner: Decoupled UCB/UCT simultaneous-move MCTS (sm_mcts.py), compact action space (action_menu.py), planner alternatives (alternatives.py). Counterparties: 4+ adversarial agent ecology — ToxicTaker, LatencyArb, MarketMaker, NoiseTrader + extended: LiquidationFlow, WhaleOrder, MomentumFollower, SpoofDetector, QueueChaser.
This commit is contained in:
142
MALKHUT/malkhut/counterparties_extended.py
Normal file
142
MALKHUT/malkhut/counterparties_extended.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
Extended Counterparty Ecology — 10+ diverse adversarial agents.
|
||||
|
||||
Real markets have: market makers, HFT, institutional, retail, liquidators,
|
||||
momentum traders, mean reversion traders, stale quote attackers, inventory MMs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from malkhut.state import MarketWorldState, TradePathState, Side
|
||||
from malkhut.actions import ActionKind, CounterpartyAction, AgentRole
|
||||
from malkhut.counterparties import CounterpartyPolicy
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MomentumTakerPolicy:
|
||||
"""Buys on upward momentum, sells on downward."""
|
||||
role: AgentRole = AgentRole.MOMENTUM_TAKER
|
||||
threshold: float = 0.3
|
||||
|
||||
def legal_actions(self, state: MarketWorldState, params=None) -> Tuple[CounterpartyAction, ...]:
|
||||
return (
|
||||
CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.BUY, 0, 0.15, toxicity=0.4),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.SELL, 0, 0.15, toxicity=0.4),
|
||||
)
|
||||
|
||||
def rollout_action(self, state: MarketWorldState, rng: random.Random) -> CounterpartyAction:
|
||||
path = state.trade_path
|
||||
momentum = path.pnl_bps if path else 0.0
|
||||
if abs(momentum) > self.threshold * 100:
|
||||
side = Side.BUY if momentum > 0 else Side.SELL
|
||||
return CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, side, 0, 0.15, toxicity=0.4)
|
||||
return CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MeanReversionTakerPolicy:
|
||||
"""Buys on downward moves, sells on upward moves (mean reversion)."""
|
||||
role: AgentRole = AgentRole.MEAN_REVERSION_TAKER
|
||||
threshold: float = 0.5
|
||||
|
||||
def legal_actions(self, state: MarketWorldState, params=None) -> Tuple[CounterpartyAction, ...]:
|
||||
return (
|
||||
CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.BUY, 0, 0.1, toxicity=0.3),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.SELL, 0, 0.1, toxicity=0.3),
|
||||
)
|
||||
|
||||
def rollout_action(self, state: MarketWorldState, rng: random.Random) -> CounterpartyAction:
|
||||
path = state.trade_path
|
||||
momentum = path.pnl_bps if path else 0.0
|
||||
if abs(momentum) > self.threshold * 100:
|
||||
# Mean reversion: buy when price dropped, sell when price rose
|
||||
side = Side.SELL if momentum > 0 else Side.BUY
|
||||
return CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, side, 0, 0.1, toxicity=0.3)
|
||||
return CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class InventoryMarketMakerPolicy:
|
||||
"""Market maker that manages inventory levels."""
|
||||
role: AgentRole = AgentRole.INVENTORY_MM
|
||||
target_inventory: float = 0.0
|
||||
max_inventory: float = 0.1
|
||||
|
||||
def legal_actions(self, state: MarketWorldState, params=None) -> Tuple[CounterpartyAction, ...]:
|
||||
return (
|
||||
CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0),
|
||||
CounterpartyAction(self.role, ActionKind.PLACE, Side.BUY, 0, 0.2),
|
||||
CounterpartyAction(self.role, ActionKind.PLACE, Side.SELL, 0, 0.2),
|
||||
CounterpartyAction(self.role, ActionKind.CANCEL, None, 0, 0.0),
|
||||
)
|
||||
|
||||
def rollout_action(self, state: MarketWorldState, rng: random.Random) -> CounterpartyAction:
|
||||
pos = state.account.positions.get(state.venue.symbol)
|
||||
inv = pos.qty if pos else 0.0
|
||||
if inv > self.max_inventory:
|
||||
return CounterpartyAction(self.role, ActionKind.PLACE, Side.SELL, 0, 0.2)
|
||||
elif inv < -self.max_inventory:
|
||||
return CounterpartyAction(self.role, ActionKind.PLACE, Side.BUY, 0, 0.2)
|
||||
return CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LiquidationFlowPolicy:
|
||||
"""Simulates forced liquidation during price drops."""
|
||||
role: AgentRole = AgentRole.LIQUIDATION_FLOW
|
||||
trigger_bps: float = 50.0 # LOWER threshold = more aggressive
|
||||
|
||||
def legal_actions(self, state: MarketWorldState, params=None) -> Tuple[CounterpartyAction, ...]:
|
||||
return (
|
||||
CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.SELL, 0, 0.3, toxicity=0.9),
|
||||
)
|
||||
|
||||
def rollout_action(self, state: MarketWorldState, rng: random.Random) -> CounterpartyAction:
|
||||
path = state.trade_path
|
||||
if path and path.mae_bps < -self.trigger_bps:
|
||||
return CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.SELL, 0, 0.3, toxicity=0.9)
|
||||
return CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class StaleQuoteAttackerPolicy:
|
||||
"""Attacks stale quotes that haven't been updated."""
|
||||
role: AgentRole = AgentRole.STALE_QUOTE_ATTACKER
|
||||
stale_threshold_s: float = 5.0
|
||||
|
||||
def legal_actions(self, state: MarketWorldState, params=None) -> Tuple[CounterpartyAction, ...]:
|
||||
return (
|
||||
CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.BUY, 0, 0.2, toxicity=0.7),
|
||||
CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.SELL, 0, 0.2, toxicity=0.7),
|
||||
)
|
||||
|
||||
def rollout_action(self, state: MarketWorldState, rng: random.Random) -> CounterpartyAction:
|
||||
# Attack if there are open orders that look stale
|
||||
if state.open_orders:
|
||||
return CounterpartyAction(self.role, ActionKind.CROSS_SPREAD, Side.SELL, 0, 0.2, toxicity=0.7)
|
||||
return CounterpartyAction(self.role, ActionKind.NOOP, None, 0, 0.0)
|
||||
|
||||
|
||||
def extended_counterparty_ecology() -> Tuple[CounterpartyPolicy, ...]:
|
||||
"""Full ecology with 10 diverse agents."""
|
||||
from malkhut.counterparties import (
|
||||
ToxicTakerPolicy, PassiveMakerPolicy, LatencyArbPolicy, NoiseTraderPolicy,
|
||||
)
|
||||
return (
|
||||
ToxicTakerPolicy(),
|
||||
PassiveMakerPolicy(),
|
||||
LatencyArbPolicy(),
|
||||
NoiseTraderPolicy(),
|
||||
MomentumTakerPolicy(),
|
||||
MeanReversionTakerPolicy(),
|
||||
InventoryMarketMakerPolicy(),
|
||||
LiquidationFlowPolicy(),
|
||||
StaleQuoteAttackerPolicy(),
|
||||
)
|
||||
Reference in New Issue
Block a user