exec(unified): sync local build — kernel_port + dialect §11 + friction §12 + size threading
Lands the /root/dev local increments (share was ENOSPC; now writable) into the canonical repo:
- kernel_port.py: real ExecPort over the DITAv2 kernel (duck-typed; injected to_kernel_intent).
- dialect.py §11: BingX boundary — clientOrderId(H4)/dash/quantize/payload (PostOnly=timeInForce).
- friction.py §12: effective-bps + naive-baseline savings; side-lane journal that can't raise
(b46ebd2); BingX commission-sign flip. DDL ships with code (register in applier verify-set).
- drive_loop/executor/working: Decimal size threaded through plan/working types (exit size cap).
All pure stdlib+Decimal, mutation-litmus RED on the two load-bearing asserts. 132 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
212
prod/exec_unified/friction.py
Normal file
212
prod/exec_unified/friction.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Friction telemetry — "before-and-after or it didn't happen" (spec §12).
|
||||
|
||||
This is the instrument that turns "smart exec is better" from a claim into a number. It
|
||||
computes, per resolved order, the effective friction in basis points (adverse slippage vs
|
||||
the caller's guideline price + realized fee) and the savings vs the naive-MARKET baseline a
|
||||
T0 flight would have paid. Aggregated per urgency class, that IS the §15 rung-by-rung delta
|
||||
and simultaneously MALKHUT's reward signal (§5-3) — one instrument, two consumers.
|
||||
|
||||
TWO LAWS this module exists to honor, both learned the hard way:
|
||||
|
||||
* §13 / §4-5 — **telemetry is a lossless SIDE-LANE and is NEVER allowed on the exec path.**
|
||||
(b46ebd2: a telemetry write on the hot path is a latent stall/raise in the order loop.)
|
||||
So ``FrictionJournal.record`` catches everything and CANNOT raise. A dead sink loses a
|
||||
telemetry row; it never touches an order. The pure bps math *can* raise (bad reference
|
||||
price is a programming error) — but only ever inside record()'s guard, off the exec path.
|
||||
|
||||
* BingX commission SIGN — a NEGATIVE commission is a COST/debit (opposite Binance);
|
||||
a positive one is a rebate. We paid for this: omp read −2.00 bps as a "rebate" when it
|
||||
was a +2.00 bps cost. ``fee_cost_from_commission`` encodes the flip so no caller repeats
|
||||
it. See [[bingx_maker_fee_and_commission_sign]].
|
||||
|
||||
Pure stdlib + Decimal. No I/O of its own; the CH sink is injected.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
from typing import Callable
|
||||
|
||||
from .contract import Side, UrgencyClass
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Measured venue rates (VST, 2026-07-14) — [[bingx_maker_fee_and_commission_sign]].
|
||||
# Maker = 2.00 bps (refutes the 1 bp savings-table assumption); taker = 5.016 bps over 1,455 fills.
|
||||
MAKER_FEE_BPS = Decimal("2.00")
|
||||
TAKER_FEE_BPS = Decimal("5.016")
|
||||
_BPS = Decimal("10000")
|
||||
|
||||
|
||||
def fee_cost_from_commission(commission: Decimal) -> Decimal:
|
||||
"""Venue commission → signed COST in quote currency (positive = we paid).
|
||||
|
||||
BingX reports commission NEGATIVE for a debit (a cost) and positive for a rebate — the
|
||||
opposite of Binance. Cost is therefore ``-commission``: a −0.013 commission is a +0.013
|
||||
cost; a +0.005 rebate is a −0.005 cost (we earned it). The one-line flip that omp's Q1
|
||||
got backwards. THE mutation test: change this to ``return commission`` and the sign test
|
||||
goes RED.
|
||||
"""
|
||||
return -commission
|
||||
|
||||
|
||||
def adverse_slippage_bps(guideline_px: Decimal, fill_px: Decimal, side: Side) -> Decimal:
|
||||
"""Signed slippage of the fill vs the caller's guideline, in bps. POSITIVE = adverse
|
||||
(we paid up on a BUY / sold down on a SELL); NEGATIVE = price improvement.
|
||||
|
||||
The side flip is the whole point: a higher fill helps a seller but hurts a buyer, so the
|
||||
same raw delta is friction for one side and improvement for the other."""
|
||||
if guideline_px <= 0:
|
||||
raise ValueError(f"guideline_px must be > 0 to reference slippage, got {guideline_px}")
|
||||
side_sign = Decimal(1) if side is Side.BUY else Decimal(-1)
|
||||
return side_sign * (fill_px - guideline_px) / guideline_px * _BPS
|
||||
|
||||
|
||||
def fee_bps(fee_cost: Decimal, notional: Decimal) -> Decimal:
|
||||
"""Realized fee as bps of notional. ``fee_cost`` is the SIGNED cost (see
|
||||
fee_cost_from_commission) so a rebate lowers friction. POSITIVE = cost."""
|
||||
if notional <= 0:
|
||||
raise ValueError(f"notional must be > 0 to reference fee bps, got {notional}")
|
||||
return fee_cost / notional * _BPS
|
||||
|
||||
|
||||
def effective_friction_bps(guideline_px: Decimal, fill_px: Decimal, side: Side,
|
||||
fee_cost: Decimal, size: Decimal) -> Decimal:
|
||||
"""Total realized friction = adverse slippage + fee, in bps. The single number §12 asks
|
||||
per order. POSITIVE = the order cost us that many bps vs a free fill at guideline."""
|
||||
notional = fill_px * size
|
||||
return adverse_slippage_bps(guideline_px, fill_px, side) + fee_bps(fee_cost, notional)
|
||||
|
||||
|
||||
def naive_baseline_bps(guideline_px: Decimal, touch_px: Decimal, side: Side,
|
||||
taker_fee_bps: Decimal = TAKER_FEE_BPS) -> Decimal:
|
||||
"""What a T0 naive-MARKET cross at the touch would have cost, in bps: it crosses the
|
||||
spread (guideline→touch, adverse by construction) and pays the taker fee. This is the
|
||||
per-order baseline the smart fill is measured against (§12; the 2026-07-10 audit is the
|
||||
frozen AGGREGATE baseline, this is its per-order model)."""
|
||||
return adverse_slippage_bps(guideline_px, touch_px, side) + taker_fee_bps
|
||||
|
||||
|
||||
def savings_vs_naive_bps(effective_bps: Decimal, baseline_bps: Decimal) -> Decimal:
|
||||
"""Smart-exec savings = baseline − effective. POSITIVE = we beat the naive MARKET; the
|
||||
number that makes "$91–231K" a measurement instead of a hope (§4-20)."""
|
||||
return baseline_bps - effective_bps
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrictionRecord:
|
||||
"""One resolved order's §12 telemetry row. Every field the journal DDL carries; the
|
||||
derived bps are computed lazily so a malformed row can never poison construction."""
|
||||
|
||||
request_id: str
|
||||
asset: str
|
||||
side: Side
|
||||
urgency: UrgencyClass
|
||||
maker: bool # True = filled as maker (post-only rested), False = taker cross
|
||||
guideline_px: Decimal # the caller's reference price at request time
|
||||
placed_px: Decimal # where we actually rested/sent (0 for pure MARKET)
|
||||
fill_px: Decimal # realized average fill
|
||||
touch_px: Decimal # the touch a naive MARKET would have crossed (baseline ref)
|
||||
size: Decimal
|
||||
fee_cost: Decimal # SIGNED quote-currency cost (positive = paid; see fee_cost_from_commission)
|
||||
chases: int = 0 # reprice attempts spent
|
||||
queue_age_s: Decimal = Decimal(0)
|
||||
advisor_id: str = "" # MALKHUT advice provenance ("" = none)
|
||||
triage: str = "" # terminal disposition (filled/crossed/abandoned/…)
|
||||
|
||||
@property
|
||||
def notional(self) -> Decimal:
|
||||
return self.fill_px * self.size
|
||||
|
||||
def effective_bps(self) -> Decimal:
|
||||
return effective_friction_bps(self.guideline_px, self.fill_px, self.side,
|
||||
self.fee_cost, self.size)
|
||||
|
||||
def baseline_bps(self) -> Decimal:
|
||||
return naive_baseline_bps(self.guideline_px, self.touch_px, self.side)
|
||||
|
||||
def savings_bps(self) -> Decimal:
|
||||
return savings_vs_naive_bps(self.effective_bps(), self.baseline_bps())
|
||||
|
||||
|
||||
FrictionSink = Callable[[FrictionRecord], None]
|
||||
|
||||
|
||||
class FrictionJournal:
|
||||
"""Side-lane friction telemetry (§4-5). ``record`` is fire-and-forget and CANNOT raise —
|
||||
a failing sink loses the row and logs, never disturbs the order loop (§13, b46ebd2).
|
||||
|
||||
The sink is injected: in prod it writes the §12 CH rows (``exec_smart_journal``); in
|
||||
tests it's a list. The journal keeps an in-memory copy only when ``retain`` so aggregate
|
||||
queries work without a database."""
|
||||
|
||||
def __init__(self, sink: FrictionSink | None = None, *, retain: bool = True,
|
||||
logger: logging.Logger = LOGGER) -> None:
|
||||
self._sink = sink
|
||||
self._retain = retain
|
||||
self.log = logger
|
||||
self._rows: list[FrictionRecord] = []
|
||||
|
||||
def record(self, rec: FrictionRecord) -> None:
|
||||
"""Emit one row. Swallows ALL exceptions — telemetry never breaks execution."""
|
||||
if self._retain:
|
||||
self._rows.append(rec)
|
||||
if self._sink is None:
|
||||
return
|
||||
try:
|
||||
self._sink(rec)
|
||||
except Exception as exc: # noqa: BLE001 — side-lane law: never propagate
|
||||
self.log.warning("friction.record: sink failed, row dropped: %r", exc)
|
||||
|
||||
def rows(self) -> list[FrictionRecord]:
|
||||
return list(self._rows)
|
||||
|
||||
def friction_by_urgency(self) -> dict[UrgencyClass, Decimal]:
|
||||
"""Mean effective friction bps per urgency class — the §15 rung delta. Rows whose bps
|
||||
can't be computed (bad reference px) are skipped, not fatal (side-lane)."""
|
||||
buckets: dict[UrgencyClass, list[Decimal]] = {}
|
||||
for r in self._rows:
|
||||
try:
|
||||
buckets.setdefault(r.urgency, []).append(r.effective_bps())
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
return {u: sum(v) / len(v) for u, v in buckets.items() if v}
|
||||
|
||||
def savings_by_urgency(self) -> dict[UrgencyClass, Decimal]:
|
||||
"""Mean savings vs naive-MARKET per urgency — the before/after claim, per class."""
|
||||
buckets: dict[UrgencyClass, list[Decimal]] = {}
|
||||
for r in self._rows:
|
||||
try:
|
||||
buckets.setdefault(r.urgency, []).append(r.savings_bps())
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
return {u: sum(v) / len(v) for u, v in buckets.items() if v}
|
||||
|
||||
|
||||
# ── DDL — ships WITH the code (§12: the 404-storm lesson — tables that don't exist journal
|
||||
# nothing). When this package syncs to /mnt, register EXEC_SMART_JOURNAL_DDL in the applier
|
||||
# verify-set (prod/clickhouse/uv/apply_uv_ddl.py) so a missing table fails LOUD, not silent.
|
||||
EXEC_SMART_JOURNAL_DDL = """\
|
||||
CREATE TABLE IF NOT EXISTS {db}.exec_smart_journal (
|
||||
ts DateTime64(3) DEFAULT now64(3),
|
||||
request_id String,
|
||||
asset String,
|
||||
side Enum8('BUY' = 1, 'SELL' = 2),
|
||||
urgency LowCardinality(String),
|
||||
maker UInt8,
|
||||
guideline_px Decimal(38, 12),
|
||||
placed_px Decimal(38, 12),
|
||||
fill_px Decimal(38, 12),
|
||||
touch_px Decimal(38, 12),
|
||||
size Decimal(38, 12),
|
||||
fee_cost Decimal(38, 12),
|
||||
effective_bps Decimal(18, 6),
|
||||
baseline_bps Decimal(18, 6),
|
||||
savings_bps Decimal(18, 6),
|
||||
chases UInt16,
|
||||
queue_age_s Decimal(18, 6),
|
||||
advisor_id String,
|
||||
triage LowCardinality(String)
|
||||
) ENGINE = MergeTree ORDER BY (asset, ts)
|
||||
"""
|
||||
Reference in New Issue
Block a user