1159 lines
43 KiB
Python
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())
|