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:
Codex
2026-07-15 08:05:26 +02:00
parent 21419b03d0
commit ec95bd6663
9 changed files with 968 additions and 5 deletions

View File

@@ -0,0 +1,106 @@
"""BingX venue dialect — the ONLY place venue-specific idioms live (spec §11, §4-14/15/17).
The layer above speaks a normalized order language (Side, ExecutionMethod, Decimal size);
this module translates it into a BingX REST order payload at the boundary. Everything that is
"BingX law" is encoded here and nowhere else, so a second venue is a second dialect, not a
rewrite.
Encoded findings (each a headstone):
* H4 / BI-1: clientOrderId unique per attempt, sanitized to the venue charset, length-capped
deterministically (BingX caps it) — a retry must not collide, FIX OrigClOrdID discipline.
* L7 (mm_ correction): POST_ONLY/IOC/FOK are BingX ``timeInForce`` values, NOT ``type`` —
the map that flattened them into ``type`` caused venue rejects. type = LIMIT|MARKET only.
* §4-15: quantize price to tick (conservatively by side, maker-safe) AND size to step
(floor — trade slightly less, never overshoot) BEFORE submit.
* §4-17: one-way flattening needs ``positionSide=BOTH``; MARKET carries no price/TIF; exits
are ``reduceOnly``.
Pure stdlib + Decimal. No I/O, no dita_v2.
"""
from __future__ import annotations
import hashlib
import re
from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal
from .contract import Side
from .router import ExecutionMethod
# BingX clientOrderId: conservative cap + charset. BingX accepts alphanumerics plus a few
# separators; keep it tight and deterministic so a retry id never collides.
CLIENT_ORDER_ID_MAX = 40
_CID_ALLOWED = re.compile(r"[^A-Za-z0-9_-]")
# Quote currencies, longest-first, for splitting the dash into a BingX wire symbol.
_QUOTES = ("USDT", "USDC", "BUSD", "USD")
def dash_symbol(asset: str) -> str:
"""Canonical undashed ("BTCUSDT") → BingX wire symbol ("BTC-USDT"). Idempotent."""
a = asset.upper()
if "-" in a:
return a
for q in _QUOTES:
if a.endswith(q) and len(a) > len(q):
return f"{a[:-len(q)]}-{q}"
return a
def client_order_id(request_id: str, attempt: int = 0, *, prefix: str = "u-") -> str:
"""Mint a venue-legal clientOrderId, unique per attempt (audit H4).
``<prefix><request_id>-<attempt>`` sanitized to [A-Za-z0-9_-] and capped at
CLIENT_ORDER_ID_MAX. If it would overflow, truncate the core and append a deterministic
8-hex digest of the FULL core so uniqueness (and per-attempt distinctness) survives.
"""
if attempt < 0:
raise ValueError(f"attempt must be >= 0, got {attempt}")
core = f"{request_id}-{attempt}"
cid = _CID_ALLOWED.sub("", f"{prefix}{core}")
if len(cid) <= CLIENT_ORDER_ID_MAX:
return cid
digest = hashlib.sha1(core.encode()).hexdigest()[:8]
keep = CLIENT_ORDER_ID_MAX - 9 # room for "-" + 8 hex
return f"{cid[:keep]}-{digest}"
def quantize_price(price: Decimal, tick: Decimal, side: Side) -> Decimal:
"""Quantize a maker price to tick CONSERVATIVELY by side (never cross the touch): BUY floors
(never up into the ask), SELL ceils (never down into the bid). §4-15 + placer's fill-rate fix."""
if tick <= 0:
raise ValueError(f"tick must be > 0, got {tick}")
rounding = ROUND_FLOOR if side is Side.BUY else ROUND_CEILING
return (price / tick).quantize(Decimal("1"), rounding=rounding) * tick
def quantize_size(size: Decimal, step: Decimal) -> Decimal:
"""Floor size to the venue step — trade slightly less, NEVER overshoot (§4-15; L8: partials
cluster on low-price/high-qty symbols, XRP 31.446→31 etc.)."""
if step <= 0:
raise ValueError(f"step must be > 0, got {step}")
return (size / step).quantize(Decimal("1"), rounding=ROUND_FLOOR) * step
def build_order_payload(*, asset: str, side: Side, method: ExecutionMethod,
limit_price: Decimal, size: Decimal, reduce_only: bool,
client_order_id: str, tick: Decimal, step: Decimal,
position_side: str = "BOTH") -> dict:
"""Normalized order → BingX REST payload. The single venue-idiom boundary."""
qty = quantize_size(size, step)
if qty <= 0:
raise ValueError(f"quantized size is non-positive (size={size}, step={step}) — "
"below the venue minimum; caller must not submit dust")
payload = {
"symbol": dash_symbol(asset),
"side": side.value, # BUY | SELL (order direction)
"positionSide": position_side, # BOTH for one-way flatten (§4-17)
"quantity": str(qty),
"clientOrderID": client_order_id,
"reduceOnly": "true" if reduce_only else "false",
}
if method is ExecutionMethod.MAKER:
payload["type"] = "LIMIT"
payload["price"] = str(quantize_price(limit_price, tick, side))
payload["timeInForce"] = "PostOnly" # L7: PostOnly is timeInForce, NOT type
else:
payload["type"] = "MARKET" # no price, no timeInForce on a market cross
return payload