Files
siloqy/prod/clean_arch/dita_v2/exec_router.py
Codex 84e4a50e3f repo hygiene: track the PINK launcher import closure
67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:09:32 +02:00

564 lines
25 KiB
Python

"""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_<SYMBOL> 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),
}