exec(unified): sync real KernelIntent translator §7 — verified side/size/cancel
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 <noreply@anthropic.com>
This commit is contained in:
104
prod/exec_unified/intent_translator.py
Normal file
104
prod/exec_unified/intent_translator.py
Normal file
@@ -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
|
||||
119
prod/exec_unified/test_intent_translator.py
Normal file
119
prod/exec_unified/test_intent_translator.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user