exec(unified): L2 foundation — contract §2 + whether-maker Router §6, pure, 26 tests
First code of the Unified Execution Layer (SPEC_UNIFIED_EXEC_LAYER_20260714.md). Pure policy, stdlib+Decimal only, zero I/O, zero venue knowledge, zero importers elsewhere — adopting it breaks nothing (freeze-safe; operator unparked the build 2026-07-14). - contract.py: ExecutionRequest input surface (§2.1) + V-TYPES (Side, UrgencyClass, ProtectiveSpec, ExecutionAdvice), frozen, validated-at-construction, illegal states unrepresentable. 'an asset, a size, and a prayer'. - router.py: decide(request) -> RoutingDecision — total pure map of the §6 urgency ladder. Encodes A1 adjudication verdict in the architecture: Router=whether-maker, SmartPlacer=where-in-book via the wants_placement/pre_submit seam. Complementary. - _constants.py: policy magnitudes with provenance; PROVISIONAL ones flagged for L8/L10 calibration (never vibes, never a hardcoded cadence). - test_exec_unified.py: 26 behaviour + mutation-litmus tests. Verified RED under CATASTROPHIC->MAKER and ACQUIRE cross_on_expiry->True mutations. Not yet wired: PINK drive-loop port (§7), venue dialect (§11), telemetry (§12). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
31
prod/exec_unified/__init__.py
Normal file
31
prod/exec_unified/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""Unified Execution Layer — the "T" of DITAv2 (spec SPEC_UNIFIED_EXEC_LAYER_20260714.md).
|
||||||
|
|
||||||
|
Standalone, pure-policy foundation. Contract (§2) + whether-maker Router (§6). Zero I/O,
|
||||||
|
zero venue knowledge, zero importers elsewhere yet — adopting it breaks nothing.
|
||||||
|
|
||||||
|
Not yet wired: the PINK drive-loop port (§7), the venue dialect (§11), telemetry (§12).
|
||||||
|
Those bolt on above/below this pure core. Kill switch discipline (UV_SMART_EXEC=0, §2.3
|
||||||
|
promise #5) lives at the wiring layer, not here.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .contract import (
|
||||||
|
ExecutionAdvice,
|
||||||
|
ExecutionRequest,
|
||||||
|
ProtectiveSpec,
|
||||||
|
Side,
|
||||||
|
UrgencyClass,
|
||||||
|
)
|
||||||
|
from .router import ExecutionMethod, RoutingDecision, TtlDiscipline, decide
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ExecutionAdvice",
|
||||||
|
"ExecutionMethod",
|
||||||
|
"ExecutionRequest",
|
||||||
|
"ProtectiveSpec",
|
||||||
|
"RoutingDecision",
|
||||||
|
"Side",
|
||||||
|
"TtlDiscipline",
|
||||||
|
"UrgencyClass",
|
||||||
|
"decide",
|
||||||
|
]
|
||||||
30
prod/exec_unified/_constants.py
Normal file
30
prod/exec_unified/_constants.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""Policy constants — from data, never vibes (spec §6 closing note).
|
||||||
|
|
||||||
|
Every constant cites its provenance. Where a number is not yet backed by an L8/L10
|
||||||
|
characterization row it is marked PROVISIONAL and must be calibrated before the smart
|
||||||
|
path arms with real capital — a provisional constant is a known-unknown, not a vibe.
|
||||||
|
|
||||||
|
Cadence is NEVER hardcoded here (no literal 1 s): the drive loop takes its clock
|
||||||
|
injected (spec §7, C1). These are policy magnitudes, not periods.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# PROTECT: "maker at touch, one reprice, ≤2 s total, then cross" (spec §6).
|
||||||
|
PROTECT_REPRICE_LIMIT: int = 1
|
||||||
|
PROTECT_MAX_MS: int = 2_000
|
||||||
|
|
||||||
|
# HARVEST: "maker at target ± offset, chase ≤ N; give up when regression eats X%
|
||||||
|
# of unrealized → cross" (spec §6). N and X are PROVISIONAL — no characterization
|
||||||
|
# row fixes them yet (§17 open question: chase bounds from bookDepth replay).
|
||||||
|
HARVEST_MAX_CHASES: int = 3 # PROVISIONAL — calibrate vs L8 fill_sim
|
||||||
|
HARVEST_GIVEUP_REGRESSION_FRAC: Decimal = Decimal("0.25") # PROVISIONAL — 25% of unrealized
|
||||||
|
|
||||||
|
# ACQUIRE: "patient maker inside spread; abandon, never chase" (spec §6, §4-1:
|
||||||
|
# a missed entry is free, a chased entry is not). No chase budget by construction.
|
||||||
|
ACQUIRE_MAX_CHASES: int = 0
|
||||||
|
|
||||||
|
# Dead-man's stop default multiple of software SL distance (spec §10, §17 open
|
||||||
|
# question: 2× vs ATR-scaled). PROVISIONAL default.
|
||||||
|
PROTECTIVE_STOP_MULT_DEFAULT: Decimal = Decimal("2.0")
|
||||||
138
prod/exec_unified/contract.py
Normal file
138
prod/exec_unified/contract.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
"""The Unified Execution Layer contract — "an asset, a size, and a prayer".
|
||||||
|
|
||||||
|
This is the WHOLE input surface (spec §2.1). Nothing about the caller's world may
|
||||||
|
leak in: not leverage math, not postures, not capital, not why-now. Every brain the
|
||||||
|
factory produces (BLUE, UV/BLUE-PRIME, VIOLET, VIBRASS children, MALKHUT) executes
|
||||||
|
through the same body by speaking only this vocabulary.
|
||||||
|
|
||||||
|
Pure stdlib + Decimal. Zero I/O, zero venue knowledge. V-TYPES discipline: frozen,
|
||||||
|
validated at construction, illegal states unrepresentable.
|
||||||
|
|
||||||
|
Provenance: prod/docs/SPEC_UNIFIED_EXEC_LAYER_20260714.md §2, §6.
|
||||||
|
Home-to-be: prod/clean_arch/dita_v2/exec/ (vendored). Lives standalone until the
|
||||||
|
vendor flow relocates it — do not import from the vendored dita_v2 tree yet.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Side(Enum):
|
||||||
|
BUY = "BUY"
|
||||||
|
SELL = "SELL"
|
||||||
|
|
||||||
|
|
||||||
|
class UrgencyClass(Enum):
|
||||||
|
"""The caller's declaration of *why* — the only "why" this layer ever learns (§6).
|
||||||
|
|
||||||
|
Ordered most-urgent → least. The ordinal is meaningful: dispatch priority and the
|
||||||
|
"never delay a more-urgent order" invariant read it (spec §9 priority ladder).
|
||||||
|
"""
|
||||||
|
|
||||||
|
CATASTROPHIC = 0 # SL / kill / liquidation — taker MARKET now, no cleverness ever
|
||||||
|
PROTECT = 1 # TP_FLOOR give-back, ADVSL retract — maker@touch, 1 reprice, then cross
|
||||||
|
HARVEST = 2 # FIXED_TP — maker at target ± offset, chase ≤N, give up on regression
|
||||||
|
ROTATE = 3 # MAX_HOLD / admin — patient maker, TTL from deadline_ms, cross at TTL
|
||||||
|
ACQUIRE = 4 # ENTER — patient maker inside spread, abandon (never chase)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_exit(self) -> bool:
|
||||||
|
"""Exit-side urgencies decrease a position; entries (ACQUIRE) increase it.
|
||||||
|
|
||||||
|
Exits may only ever tighten toward urgency, never reprice away (spec §4-2).
|
||||||
|
"""
|
||||||
|
return self in (UrgencyClass.CATASTROPHIC, UrgencyClass.PROTECT,
|
||||||
|
UrgencyClass.HARVEST, UrgencyClass.ROTATE)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProtectiveSpec:
|
||||||
|
"""Attach-a-dead-man's-stop geometry (spec §10). Mark-based, rides the entry order.
|
||||||
|
|
||||||
|
Either an explicit ``stop_price`` OR a ``stop_mult`` multiple of the caller's
|
||||||
|
software SL distance — never both, never neither. The venue-attached STOP_MARKET
|
||||||
|
fires only when the client is dead or blind (HZ silent-death lineage): parity-
|
||||||
|
invisible, because it only acts once the software layer has already failed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stop_price: Decimal | None = None
|
||||||
|
stop_mult: Decimal | None = None # × software-SL distance (e.g. 2.0)
|
||||||
|
working_type: str = "MARK_PRICE" # venue geometry truth = mark, never last
|
||||||
|
reduce_only: bool = True
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
has_px = self.stop_price is not None
|
||||||
|
has_mult = self.stop_mult is not None
|
||||||
|
if has_px == has_mult:
|
||||||
|
raise ValueError("ProtectiveSpec needs exactly one of stop_price / stop_mult")
|
||||||
|
if has_px and self.stop_price <= 0:
|
||||||
|
raise ValueError(f"stop_price must be positive, got {self.stop_price}")
|
||||||
|
if has_mult and self.stop_mult <= 0:
|
||||||
|
raise ValueError(f"stop_mult must be positive, got {self.stop_mult}")
|
||||||
|
if self.working_type != "MARK_PRICE":
|
||||||
|
# Geometry truth is the mark (spec §9 three-price-truths). Guardrail, not law.
|
||||||
|
raise ValueError("protective working_type must be MARK_PRICE")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExecutionAdvice:
|
||||||
|
"""MALKHUT/other advisor hints — an advisor, never an authority (spec §5, §11-L11).
|
||||||
|
|
||||||
|
The layer may take these hints or ignore them; CATASTROPHIC ignores them *by law*.
|
||||||
|
Advice can never change WHAT was asked (side/size/asset/reduce_only are sacred).
|
||||||
|
"""
|
||||||
|
|
||||||
|
source: str = "unknown" # who advised (provenance, non-authoritative)
|
||||||
|
prefer_post_only: bool | None = None # hint: rest as maker if possible
|
||||||
|
qty_fraction: Decimal | None = None # hint: work this fraction per slice (slicer above)
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.qty_fraction is not None and not (Decimal(0) < self.qty_fraction <= Decimal(1)):
|
||||||
|
raise ValueError(f"qty_fraction must be in (0,1], got {self.qty_fraction}")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExecutionRequest:
|
||||||
|
"""The whole surface. Nothing else may leak in (spec §2.1).
|
||||||
|
|
||||||
|
``guideline_price`` is a decision-time *reference*, NOT a limit — it resolves the
|
||||||
|
ADVSL stale-price concern: scan-clock exits pass the price that triggered them; the
|
||||||
|
drive loop executes against the live book, never against this frozen number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
request_id: str # caller-minted; idempotency key end-to-end
|
||||||
|
asset: str # canonical undashed ("BTCUSDT"); dialect dashes it
|
||||||
|
side: Side
|
||||||
|
size: Decimal # base quantity; sizing is the caller's solved problem
|
||||||
|
urgency: UrgencyClass # assigned by CALLER, NEVER inferred here
|
||||||
|
reduce_only: bool = False # position-decreasing intent (exits set True)
|
||||||
|
guideline_price: Decimal | None = None # reference, not a limit
|
||||||
|
protective: ProtectiveSpec | None = None
|
||||||
|
advice: ExecutionAdvice | None = None
|
||||||
|
deadline_ms: int | None = None # caller's patience budget (ROTATE/ACQUIRE)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.request_id:
|
||||||
|
raise ValueError("request_id is mandatory (idempotency key)")
|
||||||
|
if self.asset != self.asset.upper() or "-" in self.asset:
|
||||||
|
raise ValueError(f"asset must be canonical undashed upper, got {self.asset!r}")
|
||||||
|
if self.size <= 0:
|
||||||
|
raise ValueError(f"size must be positive, got {self.size}")
|
||||||
|
if self.guideline_price is not None and self.guideline_price <= 0:
|
||||||
|
raise ValueError(f"guideline_price must be positive, got {self.guideline_price}")
|
||||||
|
if self.deadline_ms is not None and self.deadline_ms <= 0:
|
||||||
|
raise ValueError(f"deadline_ms must be positive, got {self.deadline_ms}")
|
||||||
|
# Contract invariant: an entry (increases position) can't be reduce_only.
|
||||||
|
if self.reduce_only and self.urgency is UrgencyClass.ACQUIRE:
|
||||||
|
raise ValueError("ACQUIRE (entry) cannot be reduce_only")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_order_id_seed(self) -> str:
|
||||||
|
"""Deterministic seed for the venue clientOrderId (promise #4: never lose an order).
|
||||||
|
|
||||||
|
Prefix discipline lives in the dialect layer (u-/m-); this is the stable core.
|
||||||
|
"""
|
||||||
|
return self.request_id
|
||||||
136
prod/exec_unified/router.py
Normal file
136
prod/exec_unified/router.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""The whether-maker policy machine (spec §6 urgency ladder).
|
||||||
|
|
||||||
|
Adjudication verdict (spec §17-A1, ratified here in code): the Router and SmartPlacer
|
||||||
|
are **complementary, not competitors**. The Router decides *whether* to rest as maker
|
||||||
|
and how hard to work the order (reprice budget, TTL discipline, give-up rule). The
|
||||||
|
SmartPlacer decides *where in the book* the maker quote sits — it plugs into the
|
||||||
|
``pre_submit`` seam (``wants_placement`` below) and is free to return a replacement
|
||||||
|
plan. Neither subsumes the other; this module is the Router half and knows nothing
|
||||||
|
about order books, offsets, or venues.
|
||||||
|
|
||||||
|
PURE: no I/O, no clock, no venue, no ambient state. ``decide()`` is a total function of
|
||||||
|
the request alone. Everything time/price-dependent is expressed as a *rule the drive
|
||||||
|
loop enforces* (§7), not as a value read here — that is what keeps this layer
|
||||||
|
parity-invisible and venue-side-portable (C1).
|
||||||
|
|
||||||
|
Provenance: prod/docs/SPEC_UNIFIED_EXEC_LAYER_20260714.md §4, §6, §17-A1.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from decimal import Decimal
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from . import _constants as K
|
||||||
|
from .contract import ExecutionRequest, UrgencyClass
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionMethod(Enum):
|
||||||
|
MAKER = "MAKER" # rest post-only (GTX); the only certified fill-improvement path (§4-16)
|
||||||
|
TAKER = "TAKER" # cross the spread now (MARKET / marketable)
|
||||||
|
|
||||||
|
|
||||||
|
class TtlDiscipline(Enum):
|
||||||
|
IMMEDIATE = "immediate" # act now, no resting (CATASTROPHIC)
|
||||||
|
BOUNDED_MS = "bounded_ms" # rest up to a fixed wall-clock bound, then cross (PROTECT)
|
||||||
|
DEADLINE = "deadline" # rest until caller's deadline_ms, then cross (ROTATE)
|
||||||
|
UNBOUNDED = "unbounded" # rest patiently; do NOT cross on expiry (ACQUIRE abandons)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RoutingDecision:
|
||||||
|
"""What the Router decided — the plan the drive loop executes (§7).
|
||||||
|
|
||||||
|
Carries *rules*, not live values: ``max_ms`` is a bound the loop measures against
|
||||||
|
its injected clock; the Router itself never reads a clock.
|
||||||
|
"""
|
||||||
|
|
||||||
|
method: ExecutionMethod
|
||||||
|
max_reprices: int # chase budget; 0 = place-once (no chase)
|
||||||
|
ttl: TtlDiscipline
|
||||||
|
cross_on_expiry: bool # on TTL/reprice exhaustion: cross (True) or abandon (False)
|
||||||
|
wants_placement: bool # invite SmartPlacer to set the offset (pre_submit seam)
|
||||||
|
ignore_advice: bool # advice is inadmissible (CATASTROPHIC only, by law)
|
||||||
|
max_ms: int | None = None # wall-clock bound for BOUNDED_MS; else None
|
||||||
|
giveup_regression_frac: Decimal | None = None # HARVEST: cross if regression eats this of unrealized
|
||||||
|
rationale: str = ""
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.max_reprices < 0:
|
||||||
|
raise ValueError("max_reprices cannot be negative")
|
||||||
|
if (self.ttl is TtlDiscipline.BOUNDED_MS) != (self.max_ms is not None):
|
||||||
|
raise ValueError("max_ms must be set iff ttl is BOUNDED_MS")
|
||||||
|
|
||||||
|
|
||||||
|
def decide(request: ExecutionRequest) -> RoutingDecision:
|
||||||
|
"""Map urgency → routing policy. Total, pure, deterministic (spec §6).
|
||||||
|
|
||||||
|
The five promises this enforces (spec §2.3): CATASTROPHIC is never delayed; WHAT is
|
||||||
|
never changed (this function reads urgency, never mutates side/size/asset); advice is
|
||||||
|
ignored for CATASTROPHIC by law.
|
||||||
|
"""
|
||||||
|
u = request.urgency
|
||||||
|
|
||||||
|
if u is UrgencyClass.CATASTROPHIC:
|
||||||
|
# Taker MARKET immediately. No cleverness. Ever. Advice ignored by law (§6).
|
||||||
|
return RoutingDecision(
|
||||||
|
method=ExecutionMethod.TAKER,
|
||||||
|
max_reprices=0,
|
||||||
|
ttl=TtlDiscipline.IMMEDIATE,
|
||||||
|
cross_on_expiry=True,
|
||||||
|
wants_placement=False,
|
||||||
|
ignore_advice=True,
|
||||||
|
rationale="CATASTROPHIC: cross now, no resting, advice inadmissible",
|
||||||
|
)
|
||||||
|
|
||||||
|
if u is UrgencyClass.PROTECT:
|
||||||
|
# Maker at touch, one reprice, ≤2 s total, then cross (§6).
|
||||||
|
return RoutingDecision(
|
||||||
|
method=ExecutionMethod.MAKER,
|
||||||
|
max_reprices=K.PROTECT_REPRICE_LIMIT,
|
||||||
|
ttl=TtlDiscipline.BOUNDED_MS,
|
||||||
|
max_ms=K.PROTECT_MAX_MS,
|
||||||
|
cross_on_expiry=True,
|
||||||
|
wants_placement=False, # at touch, not an offset model
|
||||||
|
ignore_advice=False,
|
||||||
|
rationale="PROTECT: maker@touch, 1 reprice, cross at 2s",
|
||||||
|
)
|
||||||
|
|
||||||
|
if u is UrgencyClass.HARVEST:
|
||||||
|
# Maker at target ± SmartPlacer offset; chase ≤ N; give up on regression → cross (§6).
|
||||||
|
return RoutingDecision(
|
||||||
|
method=ExecutionMethod.MAKER,
|
||||||
|
max_reprices=K.HARVEST_MAX_CHASES,
|
||||||
|
ttl=TtlDiscipline.UNBOUNDED, # bounded by chases + regression, not wall-clock
|
||||||
|
cross_on_expiry=True, # give-up crosses (harvest still wants the fill)
|
||||||
|
wants_placement=True,
|
||||||
|
ignore_advice=False,
|
||||||
|
giveup_regression_frac=K.HARVEST_GIVEUP_REGRESSION_FRAC,
|
||||||
|
rationale="HARVEST: maker@target±offset, chase<=N, cross on regression give-up",
|
||||||
|
)
|
||||||
|
|
||||||
|
if u is UrgencyClass.ROTATE:
|
||||||
|
# Patient maker; TTL from caller's deadline_ms; cross at TTL (§6).
|
||||||
|
return RoutingDecision(
|
||||||
|
method=ExecutionMethod.MAKER,
|
||||||
|
max_reprices=K.HARVEST_MAX_CHASES, # patient chasing within the deadline
|
||||||
|
ttl=TtlDiscipline.DEADLINE,
|
||||||
|
cross_on_expiry=True,
|
||||||
|
wants_placement=True,
|
||||||
|
ignore_advice=False,
|
||||||
|
rationale="ROTATE: patient maker, cross at caller deadline",
|
||||||
|
)
|
||||||
|
|
||||||
|
if u is UrgencyClass.ACQUIRE:
|
||||||
|
# Patient maker inside spread; abandon, never chase (§4-1: a missed entry is free).
|
||||||
|
return RoutingDecision(
|
||||||
|
method=ExecutionMethod.MAKER,
|
||||||
|
max_reprices=K.ACQUIRE_MAX_CHASES, # == 0: no chase
|
||||||
|
ttl=TtlDiscipline.UNBOUNDED,
|
||||||
|
cross_on_expiry=False, # ABANDON on expiry — do NOT cross
|
||||||
|
wants_placement=True,
|
||||||
|
ignore_advice=False,
|
||||||
|
rationale="ACQUIRE: patient maker inside spread, abandon (never chase, never cross)",
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError(f"unhandled urgency {u!r}") # unreachable given the enum is closed
|
||||||
200
prod/exec_unified/test_exec_unified.py
Normal file
200
prod/exec_unified/test_exec_unified.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
"""Behaviour + mutation-litmus tests for the Unified Execution Layer core.
|
||||||
|
|
||||||
|
Testing doctrine (prod/docs/TESTING_DOCTRINE.md): these assert the VALUE/SHAPE/INVARIANT,
|
||||||
|
not "it ran". Each router test is written so that breaking the implementation goes RED —
|
||||||
|
the mutation litmus is spelled out per test. Run:
|
||||||
|
|
||||||
|
/home/dolphin/siloqy_env/bin/python3 -m pytest prod/exec_unified/test_exec_unified.py -q
|
||||||
|
|
||||||
|
Mutation examples that MUST turn something red:
|
||||||
|
* CATASTROPHIC method MAKER→... -> test_catastrophic_crosses_now
|
||||||
|
* ACQUIRE cross_on_expiry False→True -> test_acquire_abandons_never_crosses
|
||||||
|
* PROTECT max_reprices 1→2 -> test_protect_one_reprice_then_cross
|
||||||
|
* drop a urgency branch in decide() -> test_decide_total_over_all_urgencies
|
||||||
|
* ignore_advice True on a non-CATASTROPHIC -> test_only_catastrophic_ignores_advice
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from prod.exec_unified import (
|
||||||
|
ExecutionAdvice,
|
||||||
|
ExecutionMethod,
|
||||||
|
ExecutionRequest,
|
||||||
|
ProtectiveSpec,
|
||||||
|
RoutingDecision,
|
||||||
|
Side,
|
||||||
|
TtlDiscipline,
|
||||||
|
UrgencyClass,
|
||||||
|
decide,
|
||||||
|
)
|
||||||
|
from prod.exec_unified import _constants as K
|
||||||
|
|
||||||
|
|
||||||
|
def _req(urgency: UrgencyClass, **kw) -> ExecutionRequest:
|
||||||
|
base = dict(
|
||||||
|
request_id="r-1",
|
||||||
|
asset="BTCUSDT",
|
||||||
|
side=Side.BUY,
|
||||||
|
size=Decimal("0.001"),
|
||||||
|
urgency=urgency,
|
||||||
|
)
|
||||||
|
# ACQUIRE is the only non-reduce_only default that matters; keep entries clean.
|
||||||
|
if urgency is not UrgencyClass.ACQUIRE:
|
||||||
|
base["reduce_only"] = True
|
||||||
|
base.update(kw)
|
||||||
|
return ExecutionRequest(**base)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- router: the ladder
|
||||||
|
|
||||||
|
def test_catastrophic_crosses_now():
|
||||||
|
d = decide(_req(UrgencyClass.CATASTROPHIC))
|
||||||
|
assert d.method is ExecutionMethod.TAKER # never maker — this is the SL/kill path
|
||||||
|
assert d.max_reprices == 0 # no chase, no resting
|
||||||
|
assert d.ttl is TtlDiscipline.IMMEDIATE
|
||||||
|
assert d.ignore_advice is True # advice inadmissible by law
|
||||||
|
assert d.wants_placement is False # no book games on a catastrophe
|
||||||
|
|
||||||
|
|
||||||
|
def test_protect_one_reprice_then_cross():
|
||||||
|
d = decide(_req(UrgencyClass.PROTECT))
|
||||||
|
assert d.method is ExecutionMethod.MAKER
|
||||||
|
assert d.max_reprices == 1 # EXACTLY one (spec §6)
|
||||||
|
assert d.ttl is TtlDiscipline.BOUNDED_MS
|
||||||
|
assert d.max_ms == K.PROTECT_MAX_MS == 2_000 # ≤2 s total
|
||||||
|
assert d.cross_on_expiry is True # then cross — a protect must complete
|
||||||
|
|
||||||
|
|
||||||
|
def test_harvest_chases_then_gives_up_by_crossing():
|
||||||
|
d = decide(_req(UrgencyClass.HARVEST))
|
||||||
|
assert d.method is ExecutionMethod.MAKER
|
||||||
|
assert d.max_reprices == K.HARVEST_MAX_CHASES
|
||||||
|
assert d.wants_placement is True # SmartPlacer sets the target±offset
|
||||||
|
assert d.giveup_regression_frac == K.HARVEST_GIVEUP_REGRESSION_FRAC
|
||||||
|
assert d.cross_on_expiry is True # give-up still wants the harvest
|
||||||
|
|
||||||
|
|
||||||
|
def test_rotate_uses_caller_deadline():
|
||||||
|
d = decide(_req(UrgencyClass.ROTATE, deadline_ms=60_000))
|
||||||
|
assert d.method is ExecutionMethod.MAKER
|
||||||
|
assert d.ttl is TtlDiscipline.DEADLINE
|
||||||
|
assert d.cross_on_expiry is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_acquire_abandons_never_crosses():
|
||||||
|
# THE distinctive invariant: a missed entry is free, a chased/crossed entry is not (§4-1).
|
||||||
|
d = decide(_req(UrgencyClass.ACQUIRE))
|
||||||
|
assert d.method is ExecutionMethod.MAKER
|
||||||
|
assert d.max_reprices == 0 # never chase
|
||||||
|
assert d.cross_on_expiry is False # never cross — abandon
|
||||||
|
assert d.ttl is TtlDiscipline.UNBOUNDED
|
||||||
|
|
||||||
|
|
||||||
|
def test_only_catastrophic_ignores_advice():
|
||||||
|
for u in UrgencyClass:
|
||||||
|
d = decide(_req(u, deadline_ms=1000))
|
||||||
|
assert d.ignore_advice is (u is UrgencyClass.CATASTROPHIC), u
|
||||||
|
|
||||||
|
|
||||||
|
def test_decide_total_over_all_urgencies():
|
||||||
|
# No urgency may fall through unhandled (drop a branch -> this goes red).
|
||||||
|
for u in UrgencyClass:
|
||||||
|
d = decide(_req(u, deadline_ms=1000))
|
||||||
|
assert isinstance(d, RoutingDecision)
|
||||||
|
assert d.rationale # every path explains itself
|
||||||
|
|
||||||
|
|
||||||
|
def test_decide_is_pure_and_deterministic():
|
||||||
|
r = _req(UrgencyClass.HARVEST)
|
||||||
|
a, b = decide(r), decide(r)
|
||||||
|
assert a == b # same input -> same decision
|
||||||
|
# WHAT is never changed: decide reads urgency, never mutates the request (frozen).
|
||||||
|
assert r.side is Side.BUY and r.size == Decimal("0.001") and r.asset == "BTCUSDT"
|
||||||
|
|
||||||
|
|
||||||
|
def test_maker_urgencies_never_taker():
|
||||||
|
for u in (UrgencyClass.PROTECT, UrgencyClass.HARVEST,
|
||||||
|
UrgencyClass.ROTATE, UrgencyClass.ACQUIRE):
|
||||||
|
assert decide(_req(u, deadline_ms=1000)).method is ExecutionMethod.MAKER, u
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- RoutingDecision self-guard
|
||||||
|
|
||||||
|
def test_bounded_ms_requires_max_ms():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
RoutingDecision(
|
||||||
|
method=ExecutionMethod.MAKER, max_reprices=1,
|
||||||
|
ttl=TtlDiscipline.BOUNDED_MS, cross_on_expiry=True,
|
||||||
|
wants_placement=False, ignore_advice=False, max_ms=None, # contradiction
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_bounded_rejects_max_ms():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
RoutingDecision(
|
||||||
|
method=ExecutionMethod.MAKER, max_reprices=1,
|
||||||
|
ttl=TtlDiscipline.UNBOUNDED, cross_on_expiry=False,
|
||||||
|
wants_placement=True, ignore_advice=False, max_ms=1000, # contradiction
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------- contract validation
|
||||||
|
|
||||||
|
def test_urgency_exit_classification():
|
||||||
|
assert UrgencyClass.ACQUIRE.is_exit is False # entry
|
||||||
|
for u in (UrgencyClass.CATASTROPHIC, UrgencyClass.PROTECT,
|
||||||
|
UrgencyClass.HARVEST, UrgencyClass.ROTATE):
|
||||||
|
assert u.is_exit is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad", [
|
||||||
|
dict(request_id=""), # missing idempotency key
|
||||||
|
dict(asset="BTC-USDT"), # dashed (dialect's job, not caller's)
|
||||||
|
dict(asset="btcusdt"), # lowercase
|
||||||
|
dict(size=Decimal("0")), # non-positive size
|
||||||
|
dict(size=Decimal("-1")),
|
||||||
|
dict(guideline_price=Decimal("0")), # non-positive reference
|
||||||
|
dict(deadline_ms=0), # non-positive patience
|
||||||
|
])
|
||||||
|
def test_execution_request_rejects_bad_input(bad):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_req(UrgencyClass.HARVEST, **bad)
|
||||||
|
|
||||||
|
|
||||||
|
def test_acquire_cannot_be_reduce_only():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ExecutionRequest(
|
||||||
|
request_id="r", asset="BTCUSDT", side=Side.BUY,
|
||||||
|
size=Decimal("1"), urgency=UrgencyClass.ACQUIRE, reduce_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_protective_spec_needs_exactly_one_anchor():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ProtectiveSpec() # neither
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ProtectiveSpec(stop_price=Decimal("100"), stop_mult=Decimal("2")) # both
|
||||||
|
ok = ProtectiveSpec(stop_mult=Decimal("2"))
|
||||||
|
assert ok.working_type == "MARK_PRICE" and ok.reduce_only is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_protective_spec_rejects_non_mark_working_type():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ProtectiveSpec(stop_mult=Decimal("2"), working_type="LAST_PRICE")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("frac", [Decimal("0"), Decimal("-0.1"), Decimal("1.5")])
|
||||||
|
def test_advice_qty_fraction_bounds(frac):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ExecutionAdvice(qty_fraction=frac)
|
||||||
|
|
||||||
|
|
||||||
|
def test_advice_is_ignorable_metadata_only():
|
||||||
|
# Advice cannot change WHAT: it carries no side/size/asset. Structural proof.
|
||||||
|
adv = ExecutionAdvice(source="MALKHUT", prefer_post_only=True, note="rest deeper")
|
||||||
|
fields = adv.__dataclass_fields__
|
||||||
|
for forbidden in ("side", "size", "asset", "reduce_only"):
|
||||||
|
assert forbidden not in fields
|
||||||
Reference in New Issue
Block a user