"""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=True, # at touch 2014 SmartPlacer places at best_bid/best_ask 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