"""DITAv2 Execution Router — the execution-style layer (SOR seed). Decides HOW an intent reaches the venue (taker MARKET vs post-only maker LIMIT, quote price, TTL, miss policy) — never WHETHER (that is the alpha layer's job and is not touched here). This module is the abstraction the S3 "Smart Order Router" TODO in ``adapters/bingx_direct.py`` calls for: ``submit`` paths stay thin; policy lives here; future improvements (OBF depth gating, price-impact models, TWAP/iceberg) plug in via hooks. Design rules (DO NOT WEAKEN): 1. Exits are NEVER skipped. A maker exit that misses its TTL is always escalated to MARKET. Only entries may be skipped. 2. One working order per slot. While an entry (or exit) quote is working, duplicate ENTER (or same-urgency EXIT) intents are suppressed — this is the double-entry guard. An *urgent* exit always preempts a working maker exit (cancel + MARKET). 3. Bounded retries. ``entry_retries`` re-quotes maximum, then the configured exhaust action (skip|market). No unbounded loops. 4. Pure policy. This module does no I/O, no asyncio, no venue calls — the runtime drives cancels/submits. That is what makes it testable "to heavens and high back". 5. Default config == legacy behavior (pure taker). With ``DOLPHIN_PINK_EXEC_STYLE`` unset every plan is MARKET and the registry stays empty. Hook points (each receives ``(plan_or_event, ctx)`` and may return a replacement ``ExecutionPlan`` from ``pre_submit``; exceptions are isolated and logged, never propagated to the trading path): - ``pre_plan`` — observe/adjust planning inputs - ``pre_submit`` — last-look mutation of the plan (e.g. depth gate) - ``on_working`` — a maker quote was registered as working - ``on_fill`` — a working quote filled (or immediate fill) - ``on_miss`` — a working entry expired and a miss action was taken - ``on_escalate`` — a working exit expired / was preempted to MARKET - ``on_cancel`` — a working quote was cancelled Configuration (env, parsed once by ``ExecConfig.from_env()``): DOLPHIN_PINK_EXEC_STYLE taker|maker_entry|maker_exit|maker_both [taker] DOLPHIN_PINK_MAKER_ENTRY_TTL_S float seconds quote lifetime [8.0] DOLPHIN_PINK_MAKER_EXIT_TTL_S float seconds quote lifetime [5.0] DOLPHIN_PINK_MAKER_ENTRY_MISS skip|retry|market [skip] DOLPHIN_PINK_MAKER_ENTRY_RETRIES int max re-quotes when MISS=retry [1] DOLPHIN_PINK_MAKER_RETRY_EXHAUST skip|market after retries spent [skip] DOLPHIN_PINK_MAKER_OFFSET_TICKS int quote distance from reference [1] DOLPHIN_PINK_MAKER_MAX_SPREAD_BPS float; spread wider than this → taker [5.0] DOLPHIN_PINK_POST_ONLY 0|1 send PostOnly TIF on maker quotes [1] DOLPHIN_PINK_TICK_SIZE_ per-symbol tick override (e.g. _BTCUSDT) Maker-eligible exit reasons: TAKE_PROFIT only. CATASTROPHIC_LOSS, MAX_HOLD, MEAN_REVERSION and anything unrecognised are urgent → MARKET. """ from __future__ import annotations import logging import os import time from dataclasses import dataclass, field, replace from typing import Any, Callable, Dict, List, Optional, Tuple LOGGER = logging.getLogger("dita_v2.exec_router") # Exit reasons that tolerate a resting reduce-only quote. Everything else # (stops, max-hold, mean-reversion flips, reconcile-driven closes) demands # immediacy and is executed as taker MARKET regardless of style. MAKER_EXIT_REASONS = frozenset({"TAKE_PROFIT"}) VALID_STYLES = ("taker", "maker_entry", "maker_exit", "maker_both") VALID_MISS = ("skip", "retry", "market") VALID_EXHAUST = ("skip", "market") HOOK_STAGES = ( "pre_plan", "pre_submit", "on_working", "on_fill", "on_miss", "on_escalate", "on_cancel", ) # Tick sizes from the BingX characterization sweep # (prod/docs/BingX_FILL_CHARACTERIZATION_AND_ADVANTAGES.md §Precision). DEFAULT_TICKS: Dict[str, float] = { "BTCUSDT": 0.1, "ETHUSDT": 0.01, "AAVEUSDT": 0.01, "SOLUSDT": 0.001, "XRPUSDT": 0.0001, "DOGEUSDT": 0.00001, "SHIBUSDT": 1e-9, "YFIUSDT": 0.01, "XAUTUSDT": 0.1, "ADAUSDT": 0.0001, "TRXUSDT": 0.00001, "ALGOUSDT": 0.0001, } _FALLBACK_TICK_FRACTION = 1e-5 # unknown symbol: ~0.1 bp of price def _env_float(name: str, default: float, lo: float, hi: float) -> float: raw = os.environ.get(name) if raw is None or not str(raw).strip(): return default try: val = float(str(raw).strip()) except Exception: LOGGER.warning("exec_router: bad %s=%r — using default %s", name, raw, default) return default if not (lo <= val <= hi): clamped = min(max(val, lo), hi) LOGGER.warning("exec_router: %s=%s outside [%s, %s] — clamped to %s", name, val, lo, hi, clamped) return clamped return val def _env_int(name: str, default: int, lo: int, hi: int) -> int: return int(_env_float(name, float(default), float(lo), float(hi))) def _env_choice(name: str, default: str, choices: Tuple[str, ...]) -> str: raw = str(os.environ.get(name, default) or default).strip().lower() if raw not in choices: LOGGER.warning("exec_router: %s=%r not in %s — using %r", name, raw, choices, default) return default return raw def _env_bool(name: str, default: bool) -> bool: raw = os.environ.get(name) if raw is None or not str(raw).strip(): return default return str(raw).strip().lower() in ("1", "true", "yes", "on") @dataclass(frozen=True) class ExecConfig: """Validated execution-policy configuration. Frozen: build once at boot.""" style: str = "taker" entry_ttl_s: float = 8.0 exit_ttl_s: float = 5.0 entry_miss: str = "skip" entry_retries: int = 1 retry_exhaust: str = "skip" offset_ticks: int = 1 max_spread_bps: float = 5.0 post_only: bool = True tick_overrides: Dict[str, float] = field(default_factory=dict) @property def maker_entry(self) -> bool: return self.style in ("maker_entry", "maker_both") @property def maker_exit(self) -> bool: return self.style in ("maker_exit", "maker_both") @classmethod def from_env(cls) -> "ExecConfig": ticks: Dict[str, float] = {} for key, raw in os.environ.items(): if key.startswith("DOLPHIN_PINK_TICK_SIZE_"): sym = key[len("DOLPHIN_PINK_TICK_SIZE_"):].upper() try: val = float(raw) if val > 0: ticks[sym] = val except Exception: LOGGER.warning("exec_router: bad tick override %s=%r", key, raw) return cls( style=_env_choice("DOLPHIN_PINK_EXEC_STYLE", "taker", VALID_STYLES), entry_ttl_s=_env_float("DOLPHIN_PINK_MAKER_ENTRY_TTL_S", 8.0, 0.5, 300.0), exit_ttl_s=_env_float("DOLPHIN_PINK_MAKER_EXIT_TTL_S", 5.0, 0.5, 300.0), entry_miss=_env_choice("DOLPHIN_PINK_MAKER_ENTRY_MISS", "skip", VALID_MISS), entry_retries=_env_int("DOLPHIN_PINK_MAKER_ENTRY_RETRIES", 1, 0, 10), retry_exhaust=_env_choice("DOLPHIN_PINK_MAKER_RETRY_EXHAUST", "skip", VALID_EXHAUST), offset_ticks=_env_int("DOLPHIN_PINK_MAKER_OFFSET_TICKS", 1, 0, 100), max_spread_bps=_env_float("DOLPHIN_PINK_MAKER_MAX_SPREAD_BPS", 5.0, 0.0, 1000.0), post_only=_env_bool("DOLPHIN_PINK_POST_ONLY", True), tick_overrides=ticks, ) @dataclass(frozen=True) class ExecutionPlan: """How one intent should be executed. Produced by the router, consumed by the runtime, forwarded to the venue via KernelIntent fields/metadata.""" order_type: str = "MARKET" # "MARKET" | "LIMIT" limit_price: float = 0.0 post_only: bool = False ttl_s: float = 0.0 # 0 = no TTL management (taker) is_maker: bool = False action: str = "ENTER" # "ENTER" | "EXIT" reason: str = "taker_default" # provenance for logs/persistence suppress: bool = False # True → do not submit (dup guard) metadata: Dict[str, Any] = field(default_factory=dict) def sane(self) -> bool: if self.order_type not in ("MARKET", "LIMIT"): return False if self.order_type == "LIMIT" and not (self.limit_price > 0.0): return False return True @dataclass class WorkingOrder: """Runtime-registered maker quote awaiting fill or TTL.""" trade_id: str asset: str side: str # "SHORT" | "LONG" (position side) action: str # "ENTER" | "EXIT" plan: ExecutionPlan submitted_at: float # monotonic clock deadline: float retries_left: int base_trade_id: str # original id before retry suffixes retry_n: int = 0 class MissAction: SKIP = "skip" RETRY = "retry" MARKET = "market" class ExecutionRouter: """Pure-policy execution router with a working-order registry. The runtime asks ``plan_entry``/``plan_exit`` before each kernel submission, registers maker quotes via ``register_working``, polls ``expired`` from its TTL loop, and reports outcomes back via ``note_fill``/``note_cancel``. All venue I/O stays in the runtime. """ def __init__(self, config: Optional[ExecConfig] = None, *, logger: Any = LOGGER, clock: Callable[[], float] = time.monotonic): self.config = config or ExecConfig() self.logger = logger self.clock = clock self._working: Dict[str, WorkingOrder] = {} # trade_id → WorkingOrder self._hooks: Dict[str, List[Callable]] = {s: [] for s in HOOK_STAGES} self.counters: Dict[str, int] = { "plans_entry": 0, "plans_exit": 0, "maker_entries": 0, "maker_exits": 0, "taker_entries": 0, "taker_exits": 0, "suppressed_dup_enter": 0, "suppressed_dup_exit": 0, "spread_gate_taker": 0, "entry_miss_skip": 0, "entry_miss_retry": 0, "entry_miss_market": 0, "exit_escalations": 0, "fills_working": 0, "cancels": 0, "hook_errors": 0, } # ── hooks ──────────────────────────────────────────────────────────────── def register_hook(self, stage: str, fn: Callable) -> Callable[[], None]: """Register ``fn`` at ``stage``; returns an unregister callable.""" if stage not in self._hooks: raise ValueError(f"unknown hook stage {stage!r}; valid: {HOOK_STAGES}") self._hooks[stage].append(fn) def _unregister() -> None: try: self._hooks[stage].remove(fn) except ValueError: pass return _unregister def _run_hooks(self, stage: str, payload: Any, ctx: Dict[str, Any]) -> Any: """Run hooks; a ``pre_submit`` hook may return a replacement plan. Hook exceptions are isolated — the trading path must never die in a plugin.""" out = payload for fn in list(self._hooks.get(stage, ())): try: ret = fn(out, dict(ctx)) if stage == "pre_submit" and isinstance(ret, ExecutionPlan): if ret.sane(): out = ret else: self.logger.warning( "exec_router: hook %r returned insane plan — ignored", fn) except Exception as exc: self.counters["hook_errors"] += 1 self.logger.warning("exec_router: hook %r failed at %s: %s", fn, stage, exc) return out # ── pricing ────────────────────────────────────────────────────────────── def tick_size(self, asset: str) -> float: sym = str(asset or "").upper() if sym in self.config.tick_overrides: return self.config.tick_overrides[sym] return DEFAULT_TICKS.get(sym, 0.0) def maker_price(self, *, asset: str, order_side: str, reference_price: float) -> float: """Quote price that rests on the book on our side of the touch. ``order_side`` is the ORDER side ("SELL"/"BUY"), not the position side. SELL rests at/above reference; BUY rests at/below. Post-only rejects any residual cross, so quoting at the touch is safe. """ ref = float(reference_price) if not (ref > 0.0): return 0.0 tick = self.tick_size(asset) if tick <= 0.0: tick = ref * _FALLBACK_TICK_FRACTION off = self.config.offset_ticks * tick if str(order_side).upper() == "SELL": return ref + off return max(tick, ref - off) @staticmethod def order_side(action: str, position_side: str) -> str: """Map (action, position side) → order side, mirroring the adapter.""" pos = str(position_side).upper() if str(action).upper() == "EXIT": return "SELL" if pos == "LONG" else "BUY" return "BUY" if pos == "LONG" else "SELL" # ── planning ───────────────────────────────────────────────────────────── def _spread_allows_maker(self, spread_bps: Optional[float]) -> bool: if spread_bps is None: return True # no OBF data — quote anyway; post-only caps the risk return float(spread_bps) <= self.config.max_spread_bps def plan_entry(self, *, trade_id: str, asset: str, position_side: str, reference_price: float, spread_bps: Optional[float] = None) -> ExecutionPlan: """Plan an ENTER execution. Never raises; falls back to MARKET.""" self.counters["plans_entry"] += 1 ctx = {"trade_id": trade_id, "asset": asset, "side": position_side, "reference_price": reference_price, "spread_bps": spread_bps, "action": "ENTER"} self._run_hooks("pre_plan", None, ctx) # Double-entry guard: a working entry means the slot is spoken for. for wo in self._working.values(): if wo.action == "ENTER": self.counters["suppressed_dup_enter"] += 1 return ExecutionPlan(action="ENTER", suppress=True, reason=f"working_entry_exists:{wo.trade_id}") plan = ExecutionPlan(action="ENTER", reason="taker_default") if self.config.maker_entry and reference_price > 0.0: if not self._spread_allows_maker(spread_bps): self.counters["spread_gate_taker"] += 1 plan = ExecutionPlan(action="ENTER", reason=f"spread_gate:{spread_bps}bps") else: side = self.order_side("ENTER", position_side) px = self.maker_price(asset=asset, order_side=side, reference_price=reference_price) if px > 0.0: plan = ExecutionPlan( order_type="LIMIT", limit_price=px, post_only=self.config.post_only, ttl_s=self.config.entry_ttl_s, is_maker=True, action="ENTER", reason="maker_entry", ) plan = self._run_hooks("pre_submit", plan, ctx) if plan.is_maker: self.counters["maker_entries"] += 1 elif not plan.suppress: self.counters["taker_entries"] += 1 return plan def plan_exit(self, *, trade_id: str, asset: str, position_side: str, reference_price: float, reason: str, spread_bps: Optional[float] = None) -> ExecutionPlan: """Plan an EXIT execution. RULE 1: exits are never skipped. A non-maker-eligible reason, a bad reference price, or a wide spread all degrade to MARKET — never to suppression, except the duplicate-guard case where a maker exit for the SAME trade is already working and the new reason is equally non-urgent (the resting quote IS the exit in flight). """ self.counters["plans_exit"] += 1 urgent = str(reason or "").upper() not in MAKER_EXIT_REASONS ctx = {"trade_id": trade_id, "asset": asset, "side": position_side, "reference_price": reference_price, "spread_bps": spread_bps, "action": "EXIT", "reason": reason, "urgent": urgent} self._run_hooks("pre_plan", None, ctx) wo = self._working.get(trade_id) if wo is not None and wo.action == "EXIT": if not urgent: # Same-trade maker exit already resting → nothing to add. self.counters["suppressed_dup_exit"] += 1 return ExecutionPlan(action="EXIT", suppress=True, reason="working_exit_exists") # Urgent reason preempts the resting quote: runtime must cancel # the working order, then submit this MARKET plan. self.counters["exit_escalations"] += 1 plan = ExecutionPlan(action="EXIT", reason=f"escalate:{reason}", metadata={"preempt_working": True}) return self._run_hooks("pre_submit", plan, ctx) plan = ExecutionPlan(action="EXIT", reason=f"taker_exit:{reason}") if (self.config.maker_exit and not urgent and reference_price > 0.0 and self._spread_allows_maker(spread_bps)): side = self.order_side("EXIT", position_side) px = self.maker_price(asset=asset, order_side=side, reference_price=reference_price) if px > 0.0: plan = ExecutionPlan( order_type="LIMIT", limit_price=px, post_only=self.config.post_only, ttl_s=self.config.exit_ttl_s, is_maker=True, action="EXIT", reason="maker_exit:TAKE_PROFIT", ) plan = self._run_hooks("pre_submit", plan, ctx) if plan.suppress: # RULE 1 enforcement against plugins: only the dup-guard branches # above may suppress an exit; a hook returning suppress is # overridden to MARKET so a position can never be stranded. plan = ExecutionPlan(action="EXIT", reason="hook_suppress_overridden_market") if not plan.sane(): # Hard floor: an exit must reach the venue. Insane plan → MARKET. plan = ExecutionPlan(action="EXIT", reason="sanity_fallback_market") if plan.is_maker: self.counters["maker_exits"] += 1 elif not plan.suppress: self.counters["taker_exits"] += 1 return plan # ── working-order registry ─────────────────────────────────────────────── def register_working(self, *, trade_id: str, asset: str, position_side: str, plan: ExecutionPlan, base_trade_id: Optional[str] = None, retry_n: int = 0) -> WorkingOrder: now = self.clock() wo = WorkingOrder( trade_id=trade_id, asset=asset, side=str(position_side).upper(), action=plan.action, plan=plan, submitted_at=now, deadline=now + max(0.5, plan.ttl_s), retries_left=self.config.entry_retries if plan.action == "ENTER" else 0, base_trade_id=base_trade_id or trade_id, retry_n=retry_n, ) if retry_n > 0: wo.retries_left = max(0, self.config.entry_retries - retry_n) self._working[trade_id] = wo self._run_hooks("on_working", wo, {"trade_id": trade_id}) return wo def working(self, trade_id: str) -> Optional[WorkingOrder]: return self._working.get(trade_id) def working_orders(self) -> List[WorkingOrder]: return list(self._working.values()) def has_working_entry(self) -> bool: return any(wo.action == "ENTER" for wo in self._working.values()) def expired(self, now: Optional[float] = None) -> List[WorkingOrder]: t = self.clock() if now is None else now return [wo for wo in self._working.values() if t >= wo.deadline] def note_fill(self, trade_id: str) -> None: wo = self._working.pop(trade_id, None) if wo is not None: self.counters["fills_working"] += 1 self._run_hooks("on_fill", wo, {"trade_id": trade_id}) def note_cancel(self, trade_id: str) -> None: wo = self._working.pop(trade_id, None) if wo is not None: self.counters["cancels"] += 1 self._run_hooks("on_cancel", wo, {"trade_id": trade_id}) def clear_working(self, trade_id: str) -> None: self._working.pop(trade_id, None) # ── miss / escalation policy ───────────────────────────────────────────── def entry_miss_action(self, wo: WorkingOrder) -> str: """Decide what to do with an expired working ENTRY (after the runtime has cancelled the quote). Returns a ``MissAction``. retry policy: up to ``entry_retries`` fresh quotes, then ``retry_exhaust`` (skip|market). ``entry_miss`` skip|market apply immediately with no re-quote. """ mode = self.config.entry_miss if mode == "skip": self.counters["entry_miss_skip"] += 1 action = MissAction.SKIP elif mode == "market": self.counters["entry_miss_market"] += 1 action = MissAction.MARKET else: # retry if wo.retries_left > 0: self.counters["entry_miss_retry"] += 1 action = MissAction.RETRY elif self.config.retry_exhaust == "market": self.counters["entry_miss_market"] += 1 action = MissAction.MARKET else: self.counters["entry_miss_skip"] += 1 action = MissAction.SKIP self._run_hooks("on_miss", wo, {"action": action}) return action def retry_plan(self, wo: WorkingOrder, *, reference_price: float) -> Tuple[str, ExecutionPlan]: """Fresh quote for a retried entry. Returns (new_trade_id, plan). New trade_id guarantees clientOrderId uniqueness on the venue and a clean kernel FSM lifecycle for the re-quote.""" n = wo.retry_n + 1 new_tid = f"{wo.base_trade_id}-r{n}" side = self.order_side("ENTER", wo.side) px = self.maker_price(asset=wo.asset, order_side=side, reference_price=reference_price) plan = ExecutionPlan( order_type="LIMIT", limit_price=px, post_only=self.config.post_only, ttl_s=self.config.entry_ttl_s, is_maker=True, action="ENTER", reason=f"maker_entry_retry_{n}", metadata={"retry_n": n, "base_trade_id": wo.base_trade_id}, ) if not plan.sane(): plan = ExecutionPlan(action="ENTER", reason="retry_price_insane_market", metadata={"retry_n": n, "base_trade_id": wo.base_trade_id}) return new_tid, plan def market_fallback_plan(self, wo: WorkingOrder) -> Tuple[str, ExecutionPlan]: """MARKET fallback after a missed/escalated quote. ENTER: fresh trade_id (``-m`` suffix) — the cancelled quote's lifecycle is closed; the fallback is a new order. EXIT: SAME trade_id — the exit must stay attached to the open position's lifecycle in the kernel FSM. """ if wo.action == "ENTER": new_tid = f"{wo.base_trade_id}-m" self._run_hooks("on_escalate", wo, {"to": "MARKET"}) return new_tid, ExecutionPlan( action="ENTER", reason="entry_miss_market_fallback", metadata={"base_trade_id": wo.base_trade_id}) self.counters["exit_escalations"] += 1 self._run_hooks("on_escalate", wo, {"to": "MARKET"}) return wo.trade_id, ExecutionPlan( action="EXIT", reason="exit_ttl_market_fallback", metadata={"base_trade_id": wo.base_trade_id}) # ── observability ──────────────────────────────────────────────────────── def snapshot(self) -> Dict[str, Any]: return { "style": self.config.style, "working": [ {"trade_id": w.trade_id, "action": w.action, "asset": w.asset, "age_s": round(self.clock() - w.submitted_at, 3), "retry_n": w.retry_n} for w in self._working.values() ], "counters": dict(self.counters), }