""" 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" CHASE = "CHASE" # Chase: follow target price with cancel-retry loop # 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.LIMIT, 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): # CROSS: aggressive fill. Use MARKET for immediate execution. # SNIPER: precision entry, use LIMIT + IOC for partial fill control. # PING: small aggressive test, use LIMIT + IOC. if at == ActionType.CROSS: order_type = OrderType.MARKET else: order_type = OrderType.LIMIT return FulfilmentAction( kind=ActionKind.CROSS_SPREAD, side=primitive.side, order_type=order_type, price_ticks_from_best=0, qty_fraction=primitive.size_fraction, ttl_ms=50, time_in_force="IOC" if order_type == OrderType.LIMIT else "GTC", ) 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) # EMERGENCY_EXIT uses MARKET for immediate fill; EXIT uses STOP_MARKET order_type = OrderType.MARKET if at == ActionType.EMERGENCY_EXIT else OrderType.STOP_MARKET return FulfilmentAction( kind=ActionKind.FULL_EXIT, side=side, order_type=order_type, 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.STOP_MARKET, price_ticks_from_best=0, qty_fraction=1.0, ttl_ms=0, reduce_only=True, metadata={"reason": "stop_loss", "order_type_combo": "STOP_MARKET"}, ) 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.TRIGGER_MARKET, price_ticks_from_best=0, qty_fraction=1.0, ttl_ms=0, reduce_only=True, metadata={"reason": "take_profit", "order_type_combo": "TRIGGER_MARKET"}, ) 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.TRAILING_STOP, 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, "order_type_combo": "TRAILING_STOP"}, ) 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.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.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: # Requote: cancel existing order and place a new one at target offset. # The CANCEL_REPLACE action handles both cancel + place in a single step. # This is distinct from QUOTE (place new) and CHASE (place + wait + auto-cancel). if state.open_orders: oo = state.open_orders[0] return FulfilmentAction( kind=ActionKind.CANCEL_REPLACE, side=primitive.side, order_type=OrderType.LIMIT, price_ticks_from_best=primitive.offset_ticks, qty_fraction=primitive.size_fraction, ttl_ms=0, # Replaced order has no TTL (stays until next cancel/requote) post_only=True, cancel_order_id=oo.client_order_id, metadata={"requote": True, "requote_offset": primitive.offset_ticks}, ) # No existing order — fall back to PLACE return FulfilmentAction( kind=ActionKind.PLACE, side=primitive.side, order_type=OrderType.LIMIT, price_ticks_from_best=primitive.offset_ticks, qty_fraction=primitive.size_fraction, ttl_ms=0, post_only=True, metadata={"requote": True, "requote_offset": primitive.offset_ticks, "fallback": True}, ) elif at == ActionType.CHASE: # Chase: place at target with short TTL. CWM auto-cancels when TTL expires. # Next step the planner re-places at new offset (cancel-retry across CWM steps). # Key: uses primitive.duration_s as the wait_to_retry_ms (how long before cancel). ttl = int(primitive.duration_s * 1000) if primitive.duration_s > 0 else 100 return FulfilmentAction( kind=ActionKind.PLACE, side=primitive.side, order_type=OrderType.LIMIT, price_ticks_from_best=primitive.offset_ticks, qty_fraction=primitive.size_fraction, ttl_ms=ttl, post_only=True, metadata={"chase": True, "chase_offset": primitive.offset_ticks, "chase_ttl_ms": ttl, "chase_target": "follow_price"}, ) 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())