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:
106
prod/exec_unified/dialect.py
Normal file
106
prod/exec_unified/dialect.py
Normal 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
|
||||
Reference in New Issue
Block a user