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>
213 lines
9.8 KiB
Python
213 lines
9.8 KiB
Python
"""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)
|
||
"""
|