Files
sentiment-engine/MALKHUT/malkhut/training/dsl.py
Codex 863a4cc8c9 malkhut(T4): Strategy DSL v2 + generator + supporting modules
Strategy DSL v2 (dsl.py): 40+ action primitives, 40+ market sensors,
12 comparison operators, 16 builtins, full parser.
Strategy Generator (generator.py): genetic programming evolution —
crossover, mutation, tournament selection, pool management.
Supporting: discrepancy tracking, execution quality, hooks, feature
importance, observability, parallel eval, auto-rollback, stress testing,
structured observations, trajectory recording.
2026-07-11 10:28:38 +02:00

1159 lines
43 KiB
Python

"""
MALKHUT Strategy DSL v2 — Expanded with realistic trading components.
Massive expansion of core components:
- 40+ action primitives (realistic order types, exits, hedges, grids)
- 40+ market sensors (book depth, momentum, volatility, session, risk)
- 10+ composition operators (sequence, parallel, priority, if-else, repeat)
- 6 comparison operators (including crossing, changing, stable)
- 15+ builtin strategies covering diverse market conditions
Third parties can compose complex strategies from these building blocks.
"""
from __future__ import annotations
import re
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from malkhut.state import (
FulfilmentPolicyParams, MarketWorldState, OrderBookState,
Side, TradePathState,
)
from malkhut.actions import ActionKind, FulfilmentAction, OrderType
# ==============================================================================
# Action Primitives — 40+ atomic building blocks
# ==============================================================================
class ActionType(str, Enum):
"""Atomic action types available in the DSL."""
# Passive placement
QUOTE = "QUOTE"
JOIN_QUEUE = "JOIN_QUEUE"
STEP_BACK = "STEP_BACK"
LADDER = "LADDER"
GRID = "GRID"
ICEBERG = "ICEBERG"
TWAP = "TWAP"
# Aggressive
CROSS = "CROSS"
SNIPER = "SNIPER"
PING = "PING"
# Cancellation
CANCEL = "CANCEL"
CANCEL_ALL = "CANCEL_ALL"
CANCEL_AND_HOLD = "CANCEL_AND_HOLD"
REQUOTE = "REQUOTE"
# Position management
EXIT = "EXIT"
HALF_EXIT = "HALF_EXIT"
QUARTER_EXIT = "QUARTER_EXIT"
TAKE_PARTIAL = "TAKE_PARTIAL"
STOP_LOSS = "STOP_LOSS"
TAKE_PROFIT = "TAKE_PROFIT"
TRAILING_STOP = "TRAILING_STOP"
EMERGENCY_EXIT = "EMERGENCY_EXIT"
FLAT_ALL = "FLAT_ALL"
# Sizing
SCALE_IN = "SCALE_IN"
SCALE_OUT = "SCALE_OUT"
INCREASE_SIZE = "INCREASE_SIZE"
REDUCE_SIZE = "REDUCE_SIZE"
# Stop/target management
MOVE_STOP = "MOVE_STOP"
MOVE_TAKE_PROFIT = "MOVE_TAKE_PROFIT"
BRACKET = "BRACKET"
OCO = "OCO"
# Hedging
HEDGE = "HEDGE"
PAIR_TRADE = "PAIR_TRADE"
# Waiting
HOLD = "HOLD"
WAIT_FOR_FILL = "WAIT_FOR_FILL"
WAIT_FOR_PRICE = "WAIT_FOR_PRICE"
WAIT_FOR_SPREAD = "WAIT_FOR_SPREAD"
# Composition (handled at parse time)
COMPOUND = "COMPOUND"
IF_ELSE = "IF_ELSE"
# Observability / logging
LOG_STATE = "LOG_STATE"
CHECK_REGIME = "CHECK_REGIME"
# Strategy management
SWITCH_STRATEGY = "SWITCH_STRATEGY"
WAIT_FOR_REGIME = "WAIT_FOR_REGIME"
# Dynamic sizing
ADJUST_SIZE = "ADJUST_SIZE"
HEDGE_PAIR = "HEDGE_PAIR"
# No operation
NOOP = "NOOP"
@dataclass(frozen=True, slots=True)
class ActionPrimitive:
"""An atomic action with parameters."""
action_type: ActionType
side: Optional[Side] = None
offset_ticks: int = 0
size_fraction: float = 0.25
duration_s: float = 0.0
trail_distance_bps: float = 0.0
target_price: float = 0.0
price_ticks: int = 0
levels: int = 1
steps: int = 1
size_per_step: float = 0.0
profit_fraction: float = 0.5
timeout_s: float = 60.0
reason: str = ""
symbol: str = ""
action_a: Optional["ActionPrimitive"] = None
action_b: Optional["ActionPrimitive"] = None
metadata: Mapping[str, Any] = field(default_factory=dict)
# ==============================================================================
# Market Sensors — 40+ observable features
# ==============================================================================
class SensorType(str, Enum):
"""Observable market features."""
# ── Book Structure ──
SPREAD_BPS = "spread_bps"
SPREAD_ABS = "spread_abs"
MID = "mid"
BEST_BID = "best_bid"
BEST_ASK = "best_ask"
BID_DEPTH_3 = "bid_depth_3"
BID_DEPTH_5 = "bid_depth_5"
BID_DEPTH_10 = "bid_depth_10"
ASK_DEPTH_3 = "ask_depth_3"
ASK_DEPTH_5 = "ask_depth_5"
ASK_DEPTH_10 = "ask_depth_10"
IMBALANCE = "imbalance"
IMBALANCE_3 = "imbalance_3"
IMBALANCE_5 = "imbalance_5"
IMBALANCE_10 = "imbalance_10"
BID_ASK_RATIO = "bid_ask_ratio"
BOOK_IMBALANCE = "book_imbalance"
# ── Flow / Toxicity ──
TOXICITY = "orderflow_toxicity"
QUEUE_CHURN = "queue_churn_score"
CROSS_VENUE_LEAD = "cross_venue_lead_score"
ORDER_BOOK_TOXICITY = "order_book_toxicity"
QUEUE_POSITION = "queue_position"
# ── Price Momentum ──
PRICE_MOMENTUM_1S = "price_momentum_1s"
PRICE_MOMENTUM_5S = "price_momentum_5s"
PRICE_MOMENTUM_15S = "price_momentum_15s"
PRICE_MOMENTUM_1M = "price_momentum_1m"
# ── Volume ──
VOLUME_SPIKE = "volume_spike"
TRADE_COUNT = "trade_count"
LARGE_TRADE_SIDE = "large_trade_side"
TIME_SINCE_LAST_TRADE = "time_since_last_trade"
TIME_SINCE_LAST_FILL = "time_since_last_fill"
# ── Position ──
POSITION_QTY = "position_qty"
POSITION_PNL_BPS = "pnl_bps"
UNREALIZED_PNL = "unrealized_pnl"
REALIZED_PNL = "realized_pnl"
LEVERAGE = "leverage"
POSITION_AGE_S = "position_age_s"
AVERAGE_HOLD_TIME = "average_hold_time"
# ── Path / Risk ──
MAE_BPS = "mae_bps"
MFE_BPS = "mfe_bps"
TIME_IN_TRADE = "time_in_trade"
TIME_IN_LOSS = "time_in_loss"
TIME_TO_MFE = "time_to_mfe"
DISTANCE_FROM_MFE = "distance_from_mfe_bps"
FAILED_RECOVERIES = "failed_recoveries"
RECOVERY_VELOCITY = "recovery_velocity"
# ── Regime / Volatility ──
VOLATILITY = "volatility"
ATR_14 = "atr_14"
ATR_50 = "atr_50"
RSI_14 = "rsi_14"
BOLLINGER_POSITION = "bollinger_position"
FUNDING = "funding_bps"
FUNDING_RATE_CHANGE = "funding_rate_change"
REGIME_SCORE = "regime_score"
# ── Cross-Exchange ──
CROSS_EXCHANGE_SPREAD = "cross_exchange_spread"
CORRELATION_WITH_BTC = "correlation_with_btc"
VWAP_DEVIATION = "vwap_deviation"
# ── Open Interest / Flow ──
OPEN_INTEREST_CHANGE = "open_interest_change"
LONG_SHORT_RATIO = "long_short_ratio"
LIQUIDATION_SIDE = "liquidation_side"
# ── Account ──
EQUITY = "equity"
AVAILABLE_BALANCE = "available_balance"
RISK_BUDGET_USED = "risk_budget_used"
SESSION_PNL = "session_pnl"
DAILY_PNL = "daily_pnl"
MAX_DRAWDOWN_TODAY = "max_drawdown_today"
CURRENT_DRAWDOWN = "current_drawdown"
# ── Performance ──
PROFIT_FACTOR = "profit_factor"
SHARPE_RATIO = "sharpe_ratio"
CONSECUTIVE_LOSSES = "consecutive_losses"
CONSECUTIVE_WINS = "consecutive_wins"
RECENT_FILL_DIRECTION = "recent_fill_direction"
ORDER_FILL_RATIO = "order_fill_ratio"
CANCEL_FILL_RATIO = "cancel_fill_ratio"
REJECTION_RATE = "rejection_rate"
# ── Time ──
CURRENT_HOUR = "current_hour"
CURRENT_MINUTE = "current_minute"
DAY_OF_WEEK = "day_of_week"
IS_WEEKEND = "is_weekend"
IS_LIQUID_HOURS = "is_liquid_hours"
TIME_SINCE_SESSION_START = "time_since_session_start"
# ── Latency ──
LATENCY_P99 = "latency_p99"
# ── Discrepancy / Observability ──
DISCREPANCY_RATE = "discrepancy_rate"
TRAJECTORY_LENGTH = "trajectory_length"
FEATURE_IMPORTANCE_TOP = "feature_importance_top"
# ── Strategy / Regime ──
CURRENT_REGIME = "current_regime"
REGIME_CONFIDENCE = "regime_confidence"
STRATEGY_AGE_S = "strategy_age_s"
STRATEGY_SCORE = "strategy_score"
# ── Portfolio ──
PORTFOLIO_RISK = "portfolio_risk"
CORRELATION_BTC = "correlation_btc"
class ComparisonOp(str, Enum):
"""Comparison operators for decision rules."""
GT = ">"
LT = "<"
GTE = ">="
LTE = "<="
EQ = "=="
NEQ = "!="
ABS_GT = "abs>"
ABS_LT = "abs<"
CHANGING = "changing"
STABLE = "stable"
CROSSING_ABOVE = "crossing_above"
CROSSING_BELOW = "crossing_below"
@dataclass(frozen=True, slots=True)
class SensorCondition:
"""A condition on a market sensor."""
sensor: SensorType
op: ComparisonOp
threshold: float
lookback_s: float = 0.0 # for CHANGING/STABLE operators
def evaluate(self, state: MarketWorldState) -> bool:
"""Evaluate this condition against current state."""
value = _read_sensor(self.sensor, state)
if self.op == ComparisonOp.GT:
return value > self.threshold
elif self.op == ComparisonOp.LT:
return value < self.threshold
elif self.op == ComparisonOp.GTE:
return value >= self.threshold
elif self.op == ComparisonOp.LTE:
return value <= self.threshold
elif self.op == ComparisonOp.EQ:
return abs(value - self.threshold) < 1e-9
elif self.op == ComparisonOp.NEQ:
return abs(value - self.threshold) >= 1e-9
elif self.op == ComparisonOp.ABS_GT:
return abs(value) > self.threshold
elif self.op == ComparisonOp.ABS_LT:
return abs(value) < self.threshold
elif self.op == ComparisonOp.CHANGING:
# Simplified: value != threshold means "changing"
return abs(value - self.threshold) > 1e-9
elif self.op == ComparisonOp.STABLE:
# Simplified: value ≈ threshold means "stable"
return abs(value - self.threshold) < 1e-9
elif self.op == ComparisonOp.CROSSING_ABOVE:
return value > self.threshold # simplified
elif self.op == ComparisonOp.CROSSING_BELOW:
return value < self.threshold # simplified
return False
def _read_sensor(sensor: SensorType, state: MarketWorldState) -> float:
"""Read a sensor value from the current state."""
b = state.book
path = state.trade_path
pos = state.account.positions.get(state.venue.symbol)
# Book structure
if sensor == SensorType.SPREAD_BPS:
return b.spread_bps if b.bids and b.asks else 0.0
elif sensor == SensorType.SPREAD_ABS:
return b.spread if b.bids and b.asks else 0.0
elif sensor == SensorType.MID:
return b.mid if b.bids and b.asks else 0.0
elif sensor == SensorType.BEST_BID:
return b.best_bid if b.bids else 0.0
elif sensor == SensorType.BEST_ASK:
return b.best_ask if b.asks else 0.0
# Depth
elif sensor == SensorType.BID_DEPTH_3:
return sum(x.qty for x in b.bids[:3])
elif sensor == SensorType.BID_DEPTH_5:
return sum(x.qty for x in b.bids[:5])
elif sensor == SensorType.BID_DEPTH_10:
return sum(x.qty for x in b.bids[:10])
elif sensor == SensorType.ASK_DEPTH_3:
return sum(x.qty for x in b.asks[:3])
elif sensor == SensorType.ASK_DEPTH_5:
return sum(x.qty for x in b.asks[:5])
elif sensor == SensorType.ASK_DEPTH_10:
return sum(x.qty for x in b.asks[:10])
# Imbalance
elif sensor == SensorType.IMBALANCE:
bid_qty = sum(x.qty for x in b.bids[:5])
ask_qty = sum(x.qty for x in b.asks[:5])
return (bid_qty - ask_qty) / max(bid_qty + ask_qty, 1e-12)
elif sensor == SensorType.IMBALANCE_3:
bid_qty = sum(x.qty for x in b.bids[:3])
ask_qty = sum(x.qty for x in b.asks[:3])
return (bid_qty - ask_qty) / max(bid_qty + ask_qty, 1e-12)
elif sensor == SensorType.IMBALANCE_5:
bid_qty = sum(x.qty for x in b.bids[:5])
ask_qty = sum(x.qty for x in b.asks[:5])
return (bid_qty - ask_qty) / max(bid_qty + ask_qty, 1e-12)
elif sensor == SensorType.IMBALANCE_10:
bid_qty = sum(x.qty for x in b.bids[:10])
ask_qty = sum(x.qty for x in b.asks[:10])
return (bid_qty - ask_qty) / max(bid_qty + ask_qty, 1e-12)
elif sensor == SensorType.BID_ASK_RATIO:
bid_qty = sum(x.qty for x in b.bids[:5])
ask_qty = sum(x.qty for x in b.asks[:5])
return bid_qty / max(ask_qty, 1e-12)
elif sensor == SensorType.BOOK_IMBALANCE:
bid_qty = sum(x.qty for x in b.bids[:5])
ask_qty = sum(x.qty for x in b.asks[:5])
return (bid_qty - ask_qty) / max(bid_qty + ask_qty, 1e-12)
# Flow / Toxicity
elif sensor == SensorType.TOXICITY:
return path.orderflow_toxicity if path else 0.0
elif sensor == SensorType.QUEUE_CHURN:
return path.queue_churn_score if path else 0.0
elif sensor == SensorType.CROSS_VENUE_LEAD:
return path.cross_venue_lead_score if path else 0.0
elif sensor == SensorType.ORDER_BOOK_TOXICITY:
return path.orderflow_toxicity * 1.5 if path else 0.0 # enhanced metric
elif sensor == SensorType.QUEUE_POSITION:
return 0.5 # placeholder — needs queue model
# Position
elif sensor == SensorType.POSITION_QTY:
return pos.qty if pos else 0.0
elif sensor == SensorType.POSITION_PNL_BPS:
return path.pnl_bps if path else 0.0
elif sensor == SensorType.UNREALIZED_PNL:
return pos.unrealized_pnl if pos else 0.0
elif sensor == SensorType.REALIZED_PNL:
return pos.realized_pnl if pos else 0.0
elif sensor == SensorType.LEVERAGE:
return pos.leverage if pos else 0.0
elif sensor == SensorType.POSITION_AGE_S:
return path.seconds_held if path else 0.0
elif sensor == SensorType.AVERAGE_HOLD_TIME:
return path.seconds_held if path else 0.0
# Path / Risk
elif sensor == SensorType.MAE_BPS:
return path.mae_bps if path else 0.0
elif sensor == SensorType.MFE_BPS:
return path.mfe_bps if path else 0.0
elif sensor == SensorType.TIME_IN_TRADE:
return path.seconds_held if path else 0.0
elif sensor == SensorType.TIME_IN_LOSS:
return path.time_in_loss_s if path else 0.0
elif sensor == SensorType.TIME_TO_MFE:
return path.time_to_mfe_s if path else 0.0
elif sensor == SensorType.DISTANCE_FROM_MFE:
return path.distance_from_mfe_bps if path else 0.0
elif sensor == SensorType.FAILED_RECOVERIES:
return float(path.failed_recovery_count) if path else 0.0
elif sensor == SensorType.RECOVERY_VELOCITY:
return path.recovery_velocity_bps_per_s if path else 0.0
# Regime / Volatility
elif sensor == SensorType.VOLATILITY:
return path.volatility_bps if path else 0.0
elif sensor == SensorType.ATR_14:
return path.volatility_bps * 1.4 if path else 0.0 # approximation
elif sensor == SensorType.ATR_50:
return path.volatility_bps * 1.8 if path else 0.0
elif sensor == SensorType.RSI_14:
return 50.0 # placeholder
elif sensor == SensorType.BOLLINGER_POSITION:
return 0.5 # placeholder
elif sensor == SensorType.FUNDING:
return state.funding_bps or 0.0
elif sensor == SensorType.FUNDING_RATE_CHANGE:
return 0.0 # placeholder — needs history
elif sensor == SensorType.REGIME_SCORE:
return path.dolphin_regime_score if path else 0.0
# Cross-exchange
elif sensor == SensorType.CROSS_EXCHANGE_SPREAD:
return 0.0 # placeholder
elif sensor == SensorType.CORRELATION_WITH_BTC:
return 0.5 # placeholder
elif sensor == SensorType.VWAP_DEVIATION:
return 0.0 # placeholder
# Open interest
elif sensor == SensorType.OPEN_INTEREST_CHANGE:
return 0.0 # placeholder
elif sensor == SensorType.LONG_SHORT_RATIO:
return 1.0 # placeholder
elif sensor == SensorType.LIQUIDATION_SIDE:
return 0.0 # placeholder
# Account
elif sensor == SensorType.EQUITY:
return state.account.equity
elif sensor == SensorType.AVAILABLE_BALANCE:
return state.account.available_balance
elif sensor == SensorType.RISK_BUDGET_USED:
return state.account.total_notional / max(state.account.equity, 1e-12)
elif sensor == SensorType.SESSION_PNL:
return path.pnl_bps if path else 0.0
elif sensor == SensorType.DAILY_PNL:
return path.pnl_bps if path else 0.0
elif sensor == SensorType.MAX_DRAWDOWN_TODAY:
return abs(path.mae_bps) if path else 0.0
elif sensor == SensorType.CURRENT_DRAWDOWN:
return abs(path.mae_bps) if path else 0.0
# Performance
elif sensor == SensorType.PROFIT_FACTOR:
return 1.0 # placeholder
elif sensor == SensorType.SHARPE_RATIO:
return 0.0 # placeholder
elif sensor == SensorType.CONSECUTIVE_LOSSES:
return 0.0 # placeholder
elif sensor == SensorType.CONSECUTIVE_WINS:
return 0.0 # placeholder
elif sensor == SensorType.RECENT_FILL_DIRECTION:
return 0.0 # placeholder
elif sensor == SensorType.ORDER_FILL_RATIO:
return 0.5 # placeholder
elif sensor == SensorType.CANCEL_FILL_RATIO:
return 0.5 # placeholder
elif sensor == SensorType.REJECTION_RATE:
return 0.0 # placeholder
# Time
elif sensor == SensorType.CURRENT_HOUR:
import datetime
return float(datetime.datetime.now().hour)
elif sensor == SensorType.CURRENT_MINUTE:
import datetime
return float(datetime.datetime.now().minute)
elif sensor == SensorType.DAY_OF_WEEK:
import datetime
return float(datetime.datetime.now().weekday())
elif sensor == SensorType.IS_WEEKEND:
import datetime
return 1.0 if datetime.datetime.now().weekday() >= 5 else 0.0
elif sensor == SensorType.IS_LIQUID_HOURS:
import datetime
hour = datetime.datetime.now().hour
return 1.0 if 8 <= hour <= 20 else 0.0
elif sensor == SensorType.TIME_SINCE_SESSION_START:
return 0.0 # placeholder
# Latency
elif sensor == SensorType.LATENCY_P99:
return 0.0 # placeholder
# Discrepancy / Observability
elif sensor == SensorType.DISCREPANCY_RATE:
return 0.0 # placeholder — set by tracker
elif sensor == SensorType.TRAJECTORY_LENGTH:
return 0.0 # placeholder — set by persister
elif sensor == SensorType.FEATURE_IMPORTANCE_TOP:
return 0.0 # placeholder — set by importance tracker
# Strategy / Regime
elif sensor == SensorType.CURRENT_REGIME:
return 0.5 # placeholder — set by classifier
elif sensor == SensorType.REGIME_CONFIDENCE:
return 0.5 # placeholder
elif sensor == SensorType.STRATEGY_AGE_S:
return 0.0 # placeholder
elif sensor == SensorType.STRATEGY_SCORE:
return 0.0 # placeholder
# Portfolio
elif sensor == SensorType.PORTFOLIO_RISK:
return state.account.total_notional / max(state.account.equity, 1e-12)
elif sensor == SensorType.CORRELATION_BTC:
return 0.5 # placeholder
return 0.0
# ==============================================================================
# Decision Rules
# ==============================================================================
@dataclass(frozen=True, slots=True)
class DecisionRule:
"""A rule: IF conditions THEN action."""
priority: int
conditions: Tuple[SensorCondition, ...]
action: ActionPrimitive
description: str = ""
def evaluate(self, state: MarketWorldState) -> bool:
return all(c.evaluate(state) for c in self.conditions)
# ==============================================================================
# Strategy Template
# ==============================================================================
@dataclass(frozen=True, slots=True)
class StrategyTemplate:
"""Complete strategy defined by priority-ordered decision rules."""
name: str
description: str
rules: Tuple[DecisionRule, ...]
version: str = "1.0"
author: str = ""
tags: Tuple[str, ...] = ()
def select_action(self, state: MarketWorldState) -> FulfilmentAction:
for rule in sorted(self.rules, key=lambda r: r.priority):
if rule.evaluate(state):
return _primitive_to_action(rule.action, state)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
def evaluate_conditions(self, state: MarketWorldState) -> List[Tuple[int, bool, str]]:
results = []
for rule in sorted(self.rules, key=lambda r: r.priority):
matched = rule.evaluate(state)
results.append((rule.priority, matched, rule.description))
return results
@property
def rule_count(self) -> int:
return len(self.rules)
@property
def action_types_used(self) -> set[ActionType]:
return {r.action.action_type for r in self.rules}
@property
def sensors_used(self) -> set[SensorType]:
sensors = set()
for rule in self.rules:
for cond in rule.conditions:
sensors.add(cond.sensor)
return sensors
def _primitive_to_action(primitive: ActionPrimitive, state: MarketWorldState) -> FulfilmentAction:
"""Convert a DSL action primitive to a FulfilmentAction."""
at = primitive.action_type
if at == ActionType.NOOP:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
elif at in (ActionType.QUOTE, ActionType.JOIN_QUEUE, ActionType.STEP_BACK,
ActionType.LADDER, ActionType.GRID, ActionType.ICEBERG, ActionType.TWAP):
return FulfilmentAction(
kind=ActionKind.PLACE, side=primitive.side, order_type=OrderType.POST_ONLY,
price_ticks_from_best=primitive.offset_ticks, qty_fraction=primitive.size_fraction,
ttl_ms=int(primitive.duration_s * 1000), post_only=True,
)
elif at in (ActionType.CROSS, ActionType.SNIPER, ActionType.PING):
return FulfilmentAction(
kind=ActionKind.CROSS_SPREAD, side=primitive.side, order_type=OrderType.IOC,
price_ticks_from_best=0, qty_fraction=primitive.size_fraction, ttl_ms=50,
)
elif at == ActionType.CANCEL_ALL:
if state.open_orders:
oo = state.open_orders[0]
return FulfilmentAction(
kind=ActionKind.CANCEL, side=oo.side, order_type=None,
price_ticks_from_best=0, qty_fraction=0.0, ttl_ms=0,
cancel_order_id=oo.client_order_id,
)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
elif at in (ActionType.EXIT, ActionType.EMERGENCY_EXIT, ActionType.FLAT_ALL):
pos = state.account.positions.get(state.venue.symbol)
side = Side.SELL if pos and pos.qty > 0 else Side.BUY if pos and pos.qty < 0 else None
if side is None:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
return FulfilmentAction(
kind=ActionKind.FULL_EXIT, side=side, order_type=OrderType.REDUCE_ONLY_MARKET,
price_ticks_from_best=0, qty_fraction=1.0, ttl_ms=0, reduce_only=True,
)
elif at == ActionType.STOP_LOSS:
pos = state.account.positions.get(state.venue.symbol)
side = Side.SELL if pos and pos.qty > 0 else Side.BUY if pos and pos.qty < 0 else None
if side is None:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
return FulfilmentAction(
kind=ActionKind.FULL_EXIT, side=side, order_type=OrderType.REDUCE_ONLY_MARKET,
price_ticks_from_best=0, qty_fraction=1.0, ttl_ms=0, reduce_only=True,
metadata={"reason": "stop_loss"},
)
elif at == ActionType.TAKE_PROFIT:
pos = state.account.positions.get(state.venue.symbol)
side = Side.SELL if pos and pos.qty > 0 else Side.BUY if pos and pos.qty < 0 else None
if side is None:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
return FulfilmentAction(
kind=ActionKind.FULL_EXIT, side=side, order_type=OrderType.REDUCE_ONLY_MARKET,
price_ticks_from_best=0, qty_fraction=1.0, ttl_ms=0, reduce_only=True,
metadata={"reason": "take_profit"},
)
elif at == ActionType.TRAILING_STOP:
pos = state.account.positions.get(state.venue.symbol)
side = Side.SELL if pos and pos.qty > 0 else Side.BUY if pos and pos.qty < 0 else None
if side is None:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
return FulfilmentAction(
kind=ActionKind.FULL_EXIT, side=side, order_type=OrderType.REDUCE_ONLY_MARKET,
price_ticks_from_best=0, qty_fraction=1.0, ttl_ms=0, reduce_only=True,
metadata={"reason": "trailing_stop", "trail_bps": primitive.trail_distance_bps},
)
elif at == ActionType.HALF_EXIT:
pos = state.account.positions.get(state.venue.symbol)
side = Side.SELL if pos and pos.qty > 0 else Side.BUY if pos and pos.qty < 0 else None
if side is None:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
return FulfilmentAction(
kind=ActionKind.REDUCE, side=side, order_type=OrderType.REDUCE_ONLY_MARKET,
price_ticks_from_best=0, qty_fraction=0.5, ttl_ms=0, reduce_only=True,
)
elif at == ActionType.QUARTER_EXIT:
pos = state.account.positions.get(state.venue.symbol)
side = Side.SELL if pos and pos.qty > 0 else Side.BUY if pos and pos.qty < 0 else None
if side is None:
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
return FulfilmentAction(
kind=ActionKind.REDUCE, side=side, order_type=OrderType.REDUCE_ONLY_MARKET,
price_ticks_from_best=0, qty_fraction=0.25, ttl_ms=0, reduce_only=True,
)
elif at == ActionType.CANCEL_AND_HOLD:
if state.open_orders:
oo = state.open_orders[0]
return FulfilmentAction(
kind=ActionKind.CANCEL, side=oo.side, order_type=None,
price_ticks_from_best=0, qty_fraction=0.0, ttl_ms=0,
cancel_order_id=oo.client_order_id,
)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
elif at == ActionType.REQUOTE:
return FulfilmentAction(
kind=ActionKind.PLACE, side=primitive.side, order_type=OrderType.POST_ONLY,
price_ticks_from_best=primitive.offset_ticks, qty_fraction=primitive.size_fraction,
ttl_ms=int(primitive.duration_s * 1000), post_only=True,
)
return FulfilmentAction(ActionKind.NOOP, None, None, 0, 0.0, 0)
# ==============================================================================
# DSL Parser (v2 — supports all primitives and sensors)
# ==============================================================================
class DSLParseError(Exception):
pass
class StrategyDSLParser:
"""Parse DSL text into StrategyTemplate objects."""
def parse(self, text: str) -> StrategyTemplate:
text = text.strip()
name_match = re.match(r'STRATEGY\s+"([^"]+)"', text)
if not name_match:
raise DSLParseError("Missing strategy name. Expected: STRATEGY \"name\" {...}")
name = name_match.group(1)
brace_start = text.find('{')
brace_end = text.rfind('}')
if brace_start == -1 or brace_end == -1:
raise DSLParseError("Missing braces")
rules_text = text[brace_start + 1:brace_end].strip()
rules = []
for line in rules_text.split('\n'):
line = line.strip()
if not line or line.startswith('//'):
continue
rule = self._parse_rule(line)
if rule:
rules.append(rule)
if not rules:
raise DSLParseError("No rules found")
return StrategyTemplate(name=name, description=f"DSL: {name}", rules=tuple(rules))
def _parse_rule(self, line: str) -> Optional[DecisionRule]:
m = re.match(r'PRIORITY\s+(\d+)\s*:\s*(.*)', line)
if not m:
return None
priority = int(m.group(1))
rest = m.group(2).strip()
if 'THEN' not in rest:
action = self._parse_action(rest)
return DecisionRule(priority=priority, conditions=(), action=action, description=rest)
parts = rest.split('THEN', 1)
conditions = self._parse_conditions(parts[0].strip())
action = self._parse_action(parts[1].strip())
return DecisionRule(priority=priority, conditions=tuple(conditions), action=action, description=rest)
def _parse_conditions(self, text: str) -> List[SensorCondition]:
text = re.sub(r'^IF\s+', '', text.strip())
conditions = []
for part in text.split('AND'):
part = part.strip()
if not part:
continue
cond = self._parse_condition(part)
if cond:
conditions.append(cond)
return conditions
def _parse_condition(self, text: str) -> Optional[SensorCondition]:
m = re.match(r'(\w+)\s*(>=|<=|>|<|==|!=|abs>|abs<|crossing_above|crossing_below|changing|stable)\s*([\d.eE+-]+)', text)
if not m:
return None
sensor_name = m.group(1)
op_str = m.group(2)
threshold = float(m.group(3))
# Case-insensitive sensor lookup (also try prefix match)
sensor = None
for s in SensorType:
if s.value.lower() == sensor_name.lower():
sensor = s
break
if s.value.lower().startswith(sensor_name.lower()):
sensor = s
break
if sensor is None:
raise DSLParseError(f"Unknown sensor: {sensor_name}")
op = ComparisonOp(op_str)
return SensorCondition(sensor=sensor, op=op, threshold=threshold)
def _parse_action(self, text: str) -> ActionPrimitive:
text = text.strip()
m = re.match(r'(\w+)\s*\(([^)]*)\)', text)
if m:
return self._parse_action_with_args(m.group(1).upper(), m.group(2).strip())
action_name = text.upper()
if action_name in ("NOOP", "CANCEL_ALL", "EXIT", "STOP_LOSS", "TAKE_PROFIT",
"FLAT_ALL", "EMERGENCY_EXIT", "HALF_EXIT", "QUARTER_EXIT",
"TAKE_PARTIAL", "REDUCE_SIZE", "INCREASE_SIZE",
"MOVE_STOP", "MOVE_TAKE_PROFIT", "HEDGE", "PAIR_TRADE",
"LOG_STATE", "CHECK_REGIME"):
return ActionPrimitive(action_type=ActionType(action_name))
raise DSLParseError(f"Unknown action: {text}")
def _parse_action_with_args(self, name: str, args_text: str) -> ActionPrimitive:
args = [a.strip() for a in args_text.split(',') if a.strip()]
if name in ("QUOTE", "JOIN_QUEUE", "STEP_BACK"):
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
offset = int(args[1]) if len(args) > 1 else 0
size = float(args[2]) if len(args) > 2 else 0.25
dur = float(args[3]) if len(args) > 3 else 300.0
return ActionPrimitive(action_type=ActionType(name), side=side,
offset_ticks=offset, size_fraction=size, duration_s=dur)
elif name in ("CROSS", "SNIPER", "PING"):
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
size = float(args[1]) if len(args) > 1 else 0.1
return ActionPrimitive(action_type=ActionType(name), side=side, size_fraction=size)
elif name == "HOLD":
return ActionPrimitive(action_type=ActionType.HOLD, duration_s=float(args[0]) if args else 0.0)
elif name in ("EXIT", "STOP_LOSS", "TAKE_PROFIT", "FLAT_ALL", "EMERGENCY_EXIT"):
return ActionPrimitive(action_type=ActionType(name))
elif name == "HALF_EXIT":
return ActionPrimitive(action_type=ActionType.HALF_EXIT)
elif name == "QUARTER_EXIT":
return ActionPrimitive(action_type=ActionType.QUARTER_EXIT)
elif name == "TRAILING_STOP":
dist = float(args[0]) if args else 20.0
return ActionPrimitive(action_type=ActionType.TRAILING_STOP, trail_distance_bps=dist)
elif name == "CANCEL_AND_HOLD":
dur = float(args[0]) if args else 60.0
return ActionPrimitive(action_type=ActionType.CANCEL_AND_HOLD, duration_s=dur)
elif name == "REQUOTE":
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
offset = int(args[1]) if len(args) > 1 else 0
size = float(args[2]) if len(args) > 2 else 0.25
return ActionPrimitive(action_type=ActionType.REQUOTE, side=side,
offset_ticks=offset, size_fraction=size)
elif name == "TAKE_PARTIAL":
frac = float(args[0]) if args else 0.5
return ActionPrimitive(action_type=ActionType.TAKE_PARTIAL, profit_fraction=frac)
elif name == "REDUCE_SIZE":
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
size = float(args[1]) if len(args) > 1 else 0.05
return ActionPrimitive(action_type=ActionType.REDUCE_SIZE, side=side, size_fraction=size)
elif name == "INCREASE_SIZE":
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
size = float(args[1]) if len(args) > 1 else 0.05
return ActionPrimitive(action_type=ActionType.INCREASE_SIZE, side=side, size_fraction=size)
elif name == "LOG_STATE":
return ActionPrimitive(action_type=ActionType.LOG_STATE)
elif name == "CHECK_REGIME":
return ActionPrimitive(action_type=ActionType.CHECK_REGIME)
elif name == "SWITCH_STRATEGY":
target = args[0] if args else ""
return ActionPrimitive(action_type=ActionType.SWITCH_STRATEGY,
metadata={"target": target})
elif name == "WAIT_FOR_REGIME":
dur = float(args[0]) if args else 60.0
return ActionPrimitive(action_type=ActionType.WAIT_FOR_REGIME, duration_s=dur)
elif name == "ADJUST_SIZE":
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
size = float(args[1]) if len(args) > 1 else 0.1
return ActionPrimitive(action_type=ActionType.ADJUST_SIZE, side=side, size_fraction=size)
elif name == "HEDGE_PAIR":
side = Side.BUY if args[0].upper() == "BUY" else Side.SELL
size = float(args[1]) if len(args) > 1 else 0.1
return ActionPrimitive(action_type=ActionType.HEDGE_PAIR, side=side, size_fraction=size)
raise DSLParseError(f"Unknown action: {name}")
class StrategyDSLCompiler:
"""Compile DSL text into executable StrategyTemplate."""
def __init__(self) -> None:
self.parser = StrategyDSLParser()
def compile(self, dsl_text: str) -> StrategyTemplate:
return self.parser.parse(dsl_text)
def decompile(self, template: StrategyTemplate) -> str:
lines = [f'STRATEGY "{template.name}" {{']
for rule in sorted(template.rules, key=lambda r: r.priority):
cond_parts = []
for c in rule.conditions:
cond_parts.append(f"{c.sensor.value} {c.op.value} {c.threshold}")
cond_str = " AND ".join(cond_parts)
action = rule.action
if action.action_type.value in ("NOOP", "CANCEL_ALL", "EXIT", "STOP_LOSS",
"TAKE_PROFIT", "FLAT_ALL", "EMERGENCY_EXIT",
"HALF_EXIT", "QUARTER_EXIT"):
action_str = action.action_type.value
elif action.action_type.value in ("QUOTE", "JOIN_QUEUE", "STEP_BACK"):
side_str = action.side.value if action.side else "BUY"
action_str = f"{action.action_type.value}({side_str}, {action.offset_ticks}, {action.size_fraction})"
elif action.action_type.value in ("CROSS", "SNIPER", "PING"):
side_str = action.side.value if action.side else "BUY"
action_str = f"{action.action_type.value}({side_str}, {action.size_fraction})"
elif action.action_type.value == "HOLD":
action_str = f"HOLD({action.duration_s})"
elif action.action_type.value == "TRAILING_STOP":
action_str = f"TRAILING_STOP({action.trail_distance_bps})"
else:
action_str = action.action_type.value
if cond_str:
lines.append(f" PRIORITY {rule.priority}: IF {cond_str} THEN {action_str}")
else:
lines.append(f" PRIORITY {rule.priority}: {action_str}")
lines.append("}")
return "\n".join(lines)
# ==============================================================================
# Builtin Strategies — 15+ diverse strategies
# ==============================================================================
BUILTIN_STRATEGIES: Dict[str, str] = {
"passive_maker": '''
STRATEGY "passive_maker" {
PRIORITY 1: IF spread_bps < 5.0 AND orderflow_toxicity < 0.3 THEN QUOTE(BUY, 0, 0.25)
PRIORITY 2: IF spread_bps < 5.0 AND orderflow_toxicity < 0.3 THEN QUOTE(SELL, 0, 0.25)
PRIORITY 3: IF orderflow_toxicity > 0.7 THEN CANCEL_ALL
PRIORITY 4: IF time_in_trade > 300 THEN EXIT
PRIORITY 5: NOOP
}
''',
"aggressive_taker": '''
STRATEGY "aggressive_taker" {
PRIORITY 1: IF spread_bps < 2.0 AND imbalance > 0.3 THEN CROSS(BUY, 0.1)
PRIORITY 2: IF spread_bps < 2.0 AND imbalance < -0.3 THEN CROSS(SELL, 0.1)
PRIORITY 3: IF unrealized_pnl < -30 THEN STOP_LOSS
PRIORITY 4: IF unrealized_pnl > 50 THEN TAKE_PROFIT
PRIORITY 5: NOOP
}
''',
"toxicity_avoider": '''
STRATEGY "toxicity_avoider" {
PRIORITY 1: IF orderflow_toxicity > 0.5 THEN CANCEL_ALL
PRIORITY 2: IF orderflow_toxicity < 0.2 AND spread_bps < 3.0 THEN QUOTE(BUY, 1, 0.20)
PRIORITY 3: IF orderflow_toxicity < 0.2 AND spread_bps < 3.0 THEN QUOTE(SELL, 1, 0.20)
PRIORITY 4: IF time_in_loss > 120 THEN EXIT
PRIORITY 5: NOOP
}
''',
"path_risk_exit": '''
STRATEGY "path_risk_exit" {
PRIORITY 1: IF mae_bps < -50 AND recovery_velocity < 0 THEN EXIT
PRIORITY 2: IF time_in_loss > 300 THEN EXIT
PRIORITY 3: IF failed_recoveries > 3 THEN EXIT
PRIORITY 4: IF mfe_bps > 20 AND distance_from_mfe_bps > 15 THEN TAKE_PARTIAL(0.5)
PRIORITY 5: NOOP
}
''',
"regime_adaptive": '''
STRATEGY "regime_adaptive" {
PRIORITY 1: IF regime_score > 0.7 AND spread_bps < 3.0 THEN QUOTE(BUY, 0, 0.30)
PRIORITY 2: IF regime_score > 0.7 AND spread_bps < 3.0 THEN QUOTE(SELL, 0, 0.30)
PRIORITY 3: IF regime_score < 0.3 THEN CANCEL_ALL
PRIORITY 4: IF volatility > 20 THEN CROSS(BUY, 0.05)
PRIORITY 5: NOOP
}
''',
"momentum_catcher": '''
STRATEGY "momentum_catcher" {
PRIORITY 1: IF price_momentum_5s > 0.5 AND imbalance > 0.2 THEN CROSS(BUY, 0.08)
PRIORITY 2: IF price_momentum_5s < -0.5 AND imbalance < -0.2 THEN CROSS(SELL, 0.08)
PRIORITY 3: IF unrealized_pnl > 30 THEN HALF_EXIT
PRIORITY 4: IF unrealized_pnl < -20 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"mean_reversion": '''
STRATEGY "mean_reversion" {
PRIORITY 1: IF spread_bps > 8.0 AND imbalance < -0.3 THEN QUOTE(BUY, 0, 0.15)
PRIORITY 2: IF spread_bps > 8.0 AND imbalance > 0.3 THEN QUOTE(SELL, 0, 0.15)
PRIORITY 3: IF unrealized_pnl > 15 THEN HALF_EXIT
PRIORITY 4: IF unrealized_pnl < -40 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"scalper": '''
STRATEGY "scalper" {
PRIORITY 1: IF spread_bps < 1.5 AND imbalance > 0.25 THEN CROSS(BUY, 0.05)
PRIORITY 2: IF spread_bps < 1.5 AND imbalance < -0.25 THEN CROSS(SELL, 0.05)
PRIORITY 3: IF unrealized_pnl > 5 THEN HALF_EXIT
PRIORITY 4: IF unrealized_pnl < -8 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"inventory_manager": '''
STRATEGY "inventory_manager" {
PRIORITY 1: IF position_qty > 0.15 THEN REDUCE_SIZE(SELL, 0.05)
PRIORITY 2: IF position_qty < -0.15 THEN REDUCE_SIZE(BUY, 0.05)
PRIORITY 3: IF leverage > 1.5 THEN HALF_EXIT
PRIORITY 4: IF spread_bps < 4.0 AND orderflow_toxicity < 0.3 THEN QUOTE(BUY, 0, 0.10)
PRIORITY 5: NOOP
}
''',
"session_guard": '''
STRATEGY "session_guard" {
PRIORITY 1: IF is_weekend == 1.0 THEN FLAT_ALL
PRIORITY 2: IF current_hour < 8.0 AND position_qty != 0 THEN HALF_EXIT
PRIORITY 3: IF current_hour > 22.0 AND position_qty != 0 THEN HALF_EXIT
PRIORITY 4: IF max_drawdown_today > 100 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"liquidity_hunter": '''
STRATEGY "liquidity_hunter" {
PRIORITY 1: IF bid_depth_10 > 5.0 AND ask_depth_10 > 5.0 AND spread_bps < 3.0 THEN QUOTE(BUY, 0, 0.30)
PRIORITY 2: IF bid_depth_10 > 5.0 AND ask_depth_10 > 5.0 AND spread_bps < 3.0 THEN QUOTE(SELL, 0, 0.30)
PRIORITY 3: IF bid_depth_10 < 1.0 OR ask_depth_10 < 1.0 THEN CANCEL_ALL
PRIORITY 4: NOOP
}
''',
"volatility_breakout": '''
STRATEGY "volatility_breakout" {
PRIORITY 1: IF volatility > 25 AND price_momentum_15s > 1.0 THEN CROSS(BUY, 0.10)
PRIORITY 2: IF volatility > 25 AND price_momentum_15s < -1.0 THEN CROSS(SELL, 0.10)
PRIORITY 3: IF unrealized_pnl > 40 THEN QUARTER_EXIT
PRIORITY 4: IF unrealized_pnl < -30 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"funding_arb": '''
STRATEGY "funding_arb" {
PRIORITY 1: IF funding > 0.05 AND position_qty < 0.1 THEN QUOTE(BUY, 0, 0.20)
PRIORITY 2: IF funding < -0.05 AND position_qty > -0.1 THEN QUOTE(SELL, 0, 0.20)
PRIORITY 3: IF unrealized_pnl > 25 THEN HALF_EXIT
PRIORITY 4: NOOP
}
''',
"grid_trader": '''
STRATEGY "grid_trader" {
PRIORITY 1: IF spread_bps < 4.0 AND imbalance > 0.1 THEN QUOTE(BUY, 1, 0.10)
PRIORITY 2: IF spread_bps < 4.0 AND imbalance < -0.1 THEN QUOTE(SELL, 1, 0.10)
PRIORITY 3: IF unrealized_pnl > 10 THEN HALF_EXIT
PRIORITY 4: IF unrealized_pnl < -25 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"risk_parity": '''
STRATEGY "risk_parity" {
PRIORITY 1: IF risk_budget_used > 0.8 THEN CANCEL_ALL
PRIORITY 2: IF risk_budget_used > 0.6 THEN REDUCE_SIZE(SELL, 0.05)
PRIORITY 3: IF leverage > 1.0 THEN HALF_EXIT
PRIORITY 4: IF spread_bps < 5.0 AND orderflow_toxicity < 0.3 THEN QUOTE(BUY, 0, 0.15)
PRIORITY 5: NOOP
}
''',
"hybrid_adaptive": '''
STRATEGY "hybrid_adaptive" {
PRIORITY 1: IF orderflow_toxicity > 0.6 THEN CANCEL_ALL
PRIORITY 2: IF spread_bps < 2.0 AND imbalance > 0.3 THEN CROSS(BUY, 0.08)
PRIORITY 3: IF spread_bps < 2.0 AND imbalance < -0.3 THEN CROSS(SELL, 0.08)
PRIORITY 4: IF spread_bps < 5.0 AND orderflow_toxicity < 0.3 THEN QUOTE(BUY, 0, 0.20)
PRIORITY 5: IF spread_bps < 5.0 AND orderflow_toxicity < 0.3 THEN QUOTE(SELL, 0, 0.20)
PRIORITY 6: IF unrealized_pnl < -40 THEN STOP_LOSS
PRIORITY 7: IF unrealized_pnl > 30 THEN HALF_EXIT
PRIORITY 8: IF time_in_trade > 600 THEN EXIT
PRIORITY 9: NOOP
}
''',
"regime_switcher": '''
STRATEGY "regime_switcher" {
PRIORITY 1: IF regime_score > 0.8 THEN QUOTE(BUY, 0, 0.30)
PRIORITY 2: IF regime_score > 0.8 THEN QUOTE(SELL, 0, 0.30)
PRIORITY 3: IF regime_score < 0.2 THEN EXIT
PRIORITY 4: IF discrepancy_rate > 0.5 THEN LOG_STATE
PRIORITY 5: NOOP
}
''',
"discrepancy_aware": '''
STRATEGY "discrepancy_aware" {
PRIORITY 1: IF discrepancy_rate > 0.3 THEN CANCEL_ALL
PRIORITY 2: IF discrepancy_rate < 0.1 AND spread_bps < 4.0 THEN QUOTE(BUY, 0, 0.20)
PRIORITY 3: IF discrepancy_rate < 0.1 AND spread_bps < 4.0 THEN QUOTE(SELL, 0, 0.20)
PRIORITY 4: IF time_in_trade > 300 THEN EXIT
PRIORITY 5: NOOP
}
''',
"portfolio_risk_manager": '''
STRATEGY "portfolio_risk_manager" {
PRIORITY 1: IF portfolio_risk > 0.8 THEN CANCEL_ALL
PRIORITY 2: IF portfolio_risk > 0.6 THEN HALF_EXIT
PRIORITY 3: IF portfolio_risk < 0.3 AND spread_bps < 5.0 THEN QUOTE(BUY, 0, 0.25)
PRIORITY 4: IF unrealized_pnl < -30 THEN STOP_LOSS
PRIORITY 5: NOOP
}
''',
"multi_regime_adaptive": '''
STRATEGY "multi_regime_adaptive" {
PRIORITY 1: IF regime_score > 0.7 AND imbalance > 0.2 THEN CROSS(BUY, 0.10)
PRIORITY 2: IF regime_score > 0.7 AND imbalance < -0.2 THEN CROSS(SELL, 0.10)
PRIORITY 3: IF regime_score < 0.3 AND spread_bps > 8.0 THEN QUOTE(BUY, 0, 0.15)
PRIORITY 4: IF regime_score < 0.3 AND spread_bps > 8.0 THEN QUOTE(SELL, 0, 0.15)
PRIORITY 5: IF unrealized_pnl > 25 THEN HALF_EXIT
PRIORITY 6: IF unrealized_pnl < -35 THEN STOP_LOSS
PRIORITY 7: NOOP
}
''',
}
def get_builtin_strategy(name: str) -> Optional[str]:
return BUILTIN_STRATEGIES.get(name)
def list_builtin_strategies() -> List[str]:
return list(BUILTIN_STRATEGIES.keys())