From 5abe0cc6e080139c056d39a19128598ef90c37f2 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 08:17:39 +0200 Subject: [PATCH] =?UTF-8?q?exec(unified):=20sync=20real=20KernelIntent=20t?= =?UTF-8?q?ranslator=20=C2=A77=20=E2=80=94=20verified=20side/size/cancel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit intent_translator.py: the injected to_kernel_intent KernelExecPort needs, wiring the pure engine to the live DITAv2 kernel. Side/size/cancel mappings each verified against vendored source (bingx_venue:627, rust_backend:448/897); EXIT inversion mutation-verified RED. Tested against REAL dita_v2 (importorskip). Lazy dita_v2 import keeps the rest of the package clean. 143 tests green. Co-Authored-By: Claude Opus 4.8 --- prod/exec_unified/intent_translator.py | 104 +++++++++++++++++ prod/exec_unified/test_intent_translator.py | 119 ++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 prod/exec_unified/intent_translator.py create mode 100644 prod/exec_unified/test_intent_translator.py diff --git a/prod/exec_unified/intent_translator.py b/prod/exec_unified/intent_translator.py new file mode 100644 index 0000000..706c799 --- /dev/null +++ b/prod/exec_unified/intent_translator.py @@ -0,0 +1,104 @@ +"""IntentSpec → dita_v2 KernelIntent — the ONE module allowed to import dita_v2 (spec §7). + +``kernel_port.KernelExecPort`` takes an INJECTED ``to_kernel_intent`` so the port (and the +whole pure engine) never imports the vendored kernel and stays local-testable against a fake. +This module supplies the real translator. The dita_v2 import is LAZY (inside the factory) so +merely importing this module never drags in dita_v2 — you pay the dependency only when you +actually wire a live kernel. + +THREE mappings, each VERIFIED against the vendored kernel source (not assumed — a wrong branch +here fires real wrong-direction / wrong-size orders): + + * SIDE (bingx_venue.py:627-628 `_legacy_intent`): ``KernelIntent.side`` is the POSITION side + (TradeSide.LONG/SHORT); the venue BUY/SELL is derived DOWNSTREAM from (side, action). Our + engine speaks ORDER side (Side.BUY/SELL), so the translator inverts on the reducing leg: + ENTER BUY→LONG SELL→SHORT (opening: order side == position direction) + EXIT BUY→SHORT SELL→LONG (reduceOnly: order side is opposite the position) + Truth table is exhaustively + mutation-tested. Getting one cell wrong = catastrophic. + + * SIZE / LEVERAGE (rust_backend.py:448, mock_venue.py:60/88/168/198): the submitted order + quantity is ``float(intent.target_size)`` VERBATIM — there is NO ``target_size × leverage`` + in the quantity path (leverage rides separately for margin/accounting). So target_size = + spec.size and leverage = 1.0 is correct AND leverage-agnostic (§2.3). The "×leverage poison" + in [[ditav2_kernel_audit]] was in the capital/PnL path, not the order-quantity path. + + * CANCEL side (rust_backend.py:897-909): a CANCEL resolves its target via the slot's + active_entry/exit order by slot_id — ``intent.side`` is NOT consulted. So CANCEL.side is + informational-only; we set it by the reducing-leg convention and it changes nothing. + +Pure translation. The datetime source is INJECTED (no ambient clock — §NFR). +""" +from __future__ import annotations + +from datetime import datetime +from typing import Any, Callable + +from .contract import Side +from .kernel_port import IntentSpec + +# One-way flatten: single slot, unit leverage (size is the final order quantity). +_SLOT_ID = 0 +_UNIT_LEVERAGE = 1.0 + + +def make_kernel_intent_translator( + clock: Callable[[], datetime], + *, + slot_id: int = _SLOT_ID, + leverage: float = _UNIT_LEVERAGE, +) -> Callable[[IntentSpec], Any]: + """Build a ``to_kernel_intent(spec) -> KernelIntent`` bound to an injected datetime clock. + + dita_v2 is imported HERE, lazily, so the module stays importable without it. Tries the + bare package name first, then the in-repo path, so it works whether ``clean_arch`` or the + repo root is the import anchor.""" + try: + from dita_v2.contracts import KernelCommandType, KernelIntent, TradeSide + except ImportError: # pragma: no cover - path depends on how the kernel is vendored + from prod.clean_arch.dita_v2.contracts import ( # type: ignore + KernelCommandType, KernelIntent, TradeSide, + ) + + _ACTION = { + "ENTER": KernelCommandType.ENTER, + "EXIT": KernelCommandType.EXIT, + "CANCEL": KernelCommandType.CANCEL, + } + + def _position_side(order_side: Side, action_kind: str): + """Order side (BUY/SELL) + action → POSITION side (LONG/SHORT). Verified truth table. + + ENTER is the only OPENING action: there the order side IS the position direction. Every + other action (EXIT, CANCEL) is on the reducing leg, where the order side is the OPPOSITE + of the position it closes. So: is_long = BUY on open, SELL on reduce.""" + opening = action_kind == "ENTER" + is_buy = order_side is Side.BUY + is_long = is_buy if opening else not is_buy + return TradeSide.LONG if is_long else TradeSide.SHORT + + def to_kernel_intent(spec: IntentSpec): + action = _ACTION.get(spec.action_kind) + if action is None: + raise ValueError(f"unknown action_kind {spec.action_kind!r} (want ENTER|EXIT|CANCEL)") + return KernelIntent( + timestamp=clock(), + intent_id=spec.request_id, + trade_id=spec.base_request_id, + slot_id=slot_id, + asset=spec.asset, + side=_position_side(spec.side, spec.action_kind), + action=action, + reference_price=float(spec.limit_price), + target_size=float(spec.size), # VERBATIM order quantity — never ×leverage + leverage=leverage, + reason=spec.reason, + order_type=spec.order_type, # "LIMIT" | "MARKET" (matches KernelIntent default) + limit_price=float(spec.limit_price), # 0 for MARKET (spec builds it that way) + metadata={ + "tif": spec.tif, # "PostOnly" | "GTC" — venue TIF rides metadata + "reduce_only": spec.reduce_only, + "exec_unified": True, # provenance: this intent came from the unified layer + }, + ) + + return to_kernel_intent diff --git a/prod/exec_unified/test_intent_translator.py b/prod/exec_unified/test_intent_translator.py new file mode 100644 index 0000000..7e58e3d --- /dev/null +++ b/prod/exec_unified/test_intent_translator.py @@ -0,0 +1,119 @@ +"""IntentSpec → real dita_v2 KernelIntent translation (spec §7). Runs against the ACTUAL +vendored kernel contracts (not fakes) so the field mapping is verified where it matters. + +The side truth table is the load-bearing test: a single wrong cell = a real wrong-direction +order. Mutation: drop the `not is_buy` inversion in _position_side -> the EXIT rows go RED. + +dita_v2 lives under prod/clean_arch; add it to the path if the repo root isn't the anchor. +If it truly can't be imported (a box without the kernel), skip loudly rather than fake it. + +Run: /home/dolphin/siloqy_env/bin/python3 -m pytest prod/exec_unified/test_intent_translator.py -q +""" +from __future__ import annotations + +import os +import sys +from datetime import datetime +from decimal import Decimal + +import pytest + +# Anchor dita_v2: try the repo's clean_arch dir (works both at /mnt and /root/dev copies). +for _cand in ("/mnt/dolphinng5_predict/prod/clean_arch", + os.path.join(os.path.dirname(__file__), "..", "clean_arch")): + if os.path.isdir(_cand) and _cand not in sys.path: + sys.path.insert(0, _cand) + +dita = pytest.importorskip("dita_v2.contracts", reason="vendored dita_v2 not importable on this box") +TradeSide = dita.TradeSide +KernelCommandType = dita.KernelCommandType + +from prod.exec_unified.contract import Side +from prod.exec_unified.intent_translator import make_kernel_intent_translator +from prod.exec_unified.kernel_port import IntentSpec + +FIXED_TS = datetime(2026, 7, 15, 12, 0, 0) + + +def _xlate(): + return make_kernel_intent_translator(lambda: FIXED_TS) + + +def _spec(**kw): + base = dict(request_id="req-0", base_request_id="pos-1", asset="BTCUSDT", side=Side.BUY, + action_kind="ENTER", order_type="LIMIT", limit_price=Decimal("100.5"), + size=Decimal("0.001"), tif="PostOnly", reduce_only=False, reason="test") + base.update(kw) + return IntentSpec(**base) + + +# ── THE side truth table (verified vs bingx_venue._legacy_intent) ───────────── + +@pytest.mark.parametrize("action,order_side,expect_pos", [ + ("ENTER", Side.BUY, TradeSide.LONG), # open long = buy + ("ENTER", Side.SELL, TradeSide.SHORT), # open short = sell + ("EXIT", Side.SELL, TradeSide.LONG), # close long = sell (INVERTED) + ("EXIT", Side.BUY, TradeSide.SHORT), # close short = buy (INVERTED) +]) +def test_position_side_truth_table(action, order_side, expect_pos): + reduce = action != "ENTER" + ki = _xlate()(_spec(action_kind=action, side=order_side, reduce_only=reduce, + order_type="MARKET" if reduce else "LIMIT", + limit_price=Decimal("0") if reduce else Decimal("100.5"))) + assert ki.side is expect_pos + # Mutation: `is_long = is_buy` (drop the inversion) -> the two EXIT rows fail. + + +# ── size / leverage: quantity is verbatim, never levered ───────────────────── + +def test_target_size_is_verbatim_and_leverage_is_unit(): + ki = _xlate()(_spec(size=Decimal("0.0731"))) + assert ki.target_size == 0.0731 # the exact order quantity — no ×leverage + assert ki.leverage == 1.0 # unit: exec_unified is leverage-agnostic (§2.3) + + +def test_custom_leverage_never_scales_size(): + # even with a non-unit leverage the quantity is untouched (leverage is accounting-only). + ki = make_kernel_intent_translator(lambda: FIXED_TS, leverage=5.0)(_spec(size=Decimal("2"))) + assert ki.target_size == 2.0 + assert ki.leverage == 5.0 + + +# ── action + order_type + price passthrough ────────────────────────────────── + +def test_action_mapping(): + assert _xlate()(_spec(action_kind="ENTER")).action is KernelCommandType.ENTER + assert _xlate()(_spec(action_kind="EXIT", reduce_only=True)).action is KernelCommandType.EXIT + assert _xlate()(_spec(action_kind="CANCEL")).action is KernelCommandType.CANCEL + + +def test_unknown_action_raises(): + with pytest.raises(ValueError): + _xlate()(_spec(action_kind="FROBNICATE")) + + +def test_maker_limit_carries_price_and_tif_metadata(): + ki = _xlate()(_spec(order_type="LIMIT", limit_price=Decimal("64547.5"), tif="PostOnly")) + assert ki.order_type == "LIMIT" + assert ki.limit_price == 64547.5 and ki.reference_price == 64547.5 + assert ki.metadata["tif"] == "PostOnly" + assert ki.metadata["reduce_only"] is False + assert ki.metadata["exec_unified"] is True + + +def test_market_carries_zero_price(): + ki = _xlate()(_spec(action_kind="EXIT", side=Side.SELL, reduce_only=True, + order_type="MARKET", limit_price=Decimal("0"))) + assert ki.order_type == "MARKET" + assert ki.limit_price == 0.0 + + +# ── identity + clock ───────────────────────────────────────────────────────── + +def test_ids_and_slot_and_injected_clock(): + ki = _xlate()(_spec(request_id="req-3", base_request_id="pos-9")) + assert ki.intent_id == "req-3" # per-attempt id + assert ki.trade_id == "pos-9" # position identity + assert ki.slot_id == 0 # single-slot flatten + assert ki.timestamp == FIXED_TS # injected, deterministic — no ambient now() + assert ki.asset == "BTCUSDT" # undashed at kernel layer (dialect dashes at venue)