Snapshot PINK DITAv2 system + Sprint 0 flaw-fix verification
First commit of the previously-untracked PINK-on-DITAv2 migration system (execution moves to the Rust kernel; policy stays on legacy DITA, so Alpha Engine algorithmic integrity is preserved). BLUE is untouched. Sprint 0 (safety snapshot + flaw-fix verification, MARKET single-leg scope): - Verified Rust FSM fixes (flaws 2,4,10,11,13) by source read of lib.rs. - Hardened 5 vacuous/guarded assertions in test_flaws.py so each flaw test genuinely exercises its fix. Most important: Flaw 5 now asserts capital moves by EXACTLY realized PnL (was entering/exiting at the same price). - Offline suites: 533 passed, 0 failed (35 flaws + 402 kernel/accounting/ bridge + 96 runtime/persistence/multi-exit/restart/seams). - GATE PASS: MARKET-path-critical flaws 1,2,5 confirmed fixed + green. - Added SPRINT0_FLAW_VERIFICATION.md report and _rust_kernel/.gitignore (excludes Rust target/ build artifacts). LIMIT/partial-fill remain explicitly out of scope (MARKET-only bring-up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
394
prod/tests/test_multi_exit_retraction_integration.py
Normal file
394
prod/tests/test_multi_exit_retraction_integration.py
Normal file
@@ -0,0 +1,394 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_MOD_PATH = Path("/mnt/dolphinng5_predict/prod/nautilus_event_trader.py")
|
||||
_SPEC = importlib.util.spec_from_file_location("nautilus_event_trader_mod", _MOD_PATH)
|
||||
assert _SPEC and _SPEC.loader
|
||||
mod = importlib.util.module_from_spec(_SPEC)
|
||||
_SPEC.loader.exec_module(mod) # type: ignore[arg-type]
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Pos:
|
||||
trade_id: str
|
||||
asset: str
|
||||
entry_price: float
|
||||
notional: float
|
||||
current_price: float = 0.0
|
||||
pnl_pct: float = 0.0
|
||||
|
||||
|
||||
class _ExitMgr:
|
||||
def __init__(self) -> None:
|
||||
self._positions: dict[str, dict] = {}
|
||||
|
||||
|
||||
class _Eng:
|
||||
def __init__(self, pos: _Pos | None, capital: float = 25_000.0) -> None:
|
||||
self.position = pos
|
||||
self.capital = capital
|
||||
self.exit_manager = _ExitMgr()
|
||||
if pos is not None:
|
||||
self.exit_manager._positions[pos.trade_id] = {"dummy": True}
|
||||
|
||||
|
||||
class _Map:
|
||||
def __init__(self, initial: dict | None = None) -> None:
|
||||
self._d = dict(initial or {})
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def blocking(self):
|
||||
return self
|
||||
|
||||
def get(self, key):
|
||||
with self._lock:
|
||||
return self._d.get(key)
|
||||
|
||||
def put(self, key, val):
|
||||
with self._lock:
|
||||
self._d[key] = val
|
||||
class _F:
|
||||
def add_done_callback(self, _cb):
|
||||
return None
|
||||
return _F()
|
||||
|
||||
|
||||
def _mk_trader() -> mod.DolphinLiveTrader:
|
||||
t = object.__new__(mod.DolphinLiveTrader)
|
||||
tmpdir = Path(tempfile.mkdtemp(prefix="dolphin_retract_test_"))
|
||||
mod.CAPITAL_DISK_CHECKPOINT = tmpdir / "capital_checkpoint.json"
|
||||
mod.CAPITAL_CORRECTIVE_REPLAY = tmpdir / "capital_replay.json"
|
||||
mod.CAPITAL_UPDATE_LEDGER = tmpdir / "capital_update_ledger.json"
|
||||
t.eng_lock = threading.Lock()
|
||||
t.state_map = _Map({})
|
||||
t.pnl_map = _Map({})
|
||||
t.control_map = _Map({"blue_runtime_commands": "[]"})
|
||||
t._processed_retract_commands = mod.deque(maxlen=5000)
|
||||
t._processed_retract_set = set()
|
||||
t._pending_entries = {}
|
||||
t.current_day = "2026-05-12"
|
||||
t.bar_idx = 100
|
||||
return t
|
||||
|
||||
|
||||
def _seed_chain(t: mod.DolphinLiveTrader, trade_id: str) -> None:
|
||||
pending = t._pending_entries[trade_id]
|
||||
pending.update(t._chain_state_for_pending(
|
||||
trade_id,
|
||||
pending,
|
||||
chain_mode="LIVE",
|
||||
chain_head_leg_id=f"{trade_id}:open",
|
||||
chain_prev_leg_id="",
|
||||
chain_seq=0,
|
||||
))
|
||||
|
||||
|
||||
def _retract_cmd(t: mod.DolphinLiveTrader, trade_id: str, *, command_id: str, fraction: float, reason: str) -> dict:
|
||||
pending = t._pending_entries[trade_id]
|
||||
return {
|
||||
"command_id": command_id,
|
||||
"trade_id": trade_id,
|
||||
"action": "RETRACT",
|
||||
"fraction": fraction,
|
||||
"reason": reason,
|
||||
"source": "tui_hotkey",
|
||||
"chain_root_trade_id": pending["chain_root_trade_id"],
|
||||
"chain_head_leg_id": pending["chain_head_leg_id"],
|
||||
"chain_prev_leg_id": pending["chain_prev_leg_id"],
|
||||
"chain_seq": pending["chain_seq"],
|
||||
"chain_token": pending["chain_token"],
|
||||
}
|
||||
|
||||
|
||||
def test_runtime_command_partial_exit_updates_position_and_capital(monkeypatch):
|
||||
rows = []
|
||||
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123456789)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-1", "STXUSDT", 1.0, 1000.0, current_price=0.95)
|
||||
t.eng = _Eng(pos, capital=25000.0)
|
||||
t._pending_entries["T-1"] = {
|
||||
"asset": "STXUSDT",
|
||||
"side": "SHORT",
|
||||
"entry_price": 1.0,
|
||||
"entry_bar": 90,
|
||||
"entry_date": "2026-05-12",
|
||||
"notional": 1000.0,
|
||||
"notional_entry": 1000.0,
|
||||
"retraction_legs": 0,
|
||||
"realized_pnl_legs_total": 0.0,
|
||||
}
|
||||
_seed_chain(t, "T-1")
|
||||
cmd = _retract_cmd(t, "T-1", command_id="c-1", fraction=0.5, reason="HOTKEY_RETRACT")
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
forced = t._process_runtime_commands({"STXUSDT": 0.95})
|
||||
|
||||
assert forced is None
|
||||
assert t.eng.position is not None
|
||||
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
|
||||
assert t._pending_entries["T-1"]["retraction_legs"] == 1
|
||||
assert pytest.approx(t._pending_entries["T-1"]["realized_pnl_legs_total"], abs=1e-9) == 25.0
|
||||
assert pytest.approx(t.eng.capital, abs=1e-9) == 25025.0
|
||||
assert any(tbl == "trade_exit_legs" for tbl, _ in rows)
|
||||
recon_rows = [r for tbl, r in rows if tbl == "trade_reconstruction"]
|
||||
assert recon_rows
|
||||
assert any(json.loads(r["payload_json"]).get("chain", {}).get("chain_token") for r in recon_rows)
|
||||
assert any(tbl == "hotkey_audit" and r["result"] == "PARTIAL_OK" for tbl, r in rows)
|
||||
|
||||
stale_cmd = dict(cmd)
|
||||
stale_cmd["command_id"] = "c-1-stale"
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([stale_cmd]))
|
||||
t._process_runtime_commands({"STXUSDT": 0.94})
|
||||
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
|
||||
assert any(tbl == "hotkey_audit" and "CHAIN_MISMATCH" in r["result"] for tbl, r in rows)
|
||||
|
||||
|
||||
def test_runtime_command_full_close_returns_forced_exit(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-2", "FETUSDT", 2.0, 200.0, current_price=1.9)
|
||||
t.eng = _Eng(pos, capital=1000.0)
|
||||
t._pending_entries["T-2"] = {
|
||||
"asset": "FETUSDT",
|
||||
"side": "SHORT",
|
||||
"entry_price": 2.0,
|
||||
"entry_bar": 95,
|
||||
"entry_date": "2026-05-12",
|
||||
"notional": 200.0,
|
||||
"notional_entry": 200.0,
|
||||
"retraction_legs": 0,
|
||||
"realized_pnl_legs_total": 0.0,
|
||||
}
|
||||
_seed_chain(t, "T-2")
|
||||
cmd = _retract_cmd(t, "T-2", command_id="c-2", fraction=1.0, reason="HOTKEY_RETRACT")
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
forced = t._process_runtime_commands({"FETUSDT": 1.9})
|
||||
|
||||
assert forced is not None
|
||||
assert forced["trade_id"] == "T-2"
|
||||
assert forced["reason"] == "HOTKEY_RETRACT"
|
||||
assert forced["capital_already_realized"] is True
|
||||
assert forced["economic_pnl"] == pytest.approx(forced["net_pnl"], abs=1e-12)
|
||||
assert forced["economic_pnl_pct"] == pytest.approx(forced["pnl_pct"], abs=1e-12)
|
||||
assert t.eng.position is None
|
||||
assert "T-2" not in t.eng.exit_manager._positions
|
||||
|
||||
|
||||
def test_full_retract_close_path_does_not_double_apply_capital(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
t.state_map = _Map({})
|
||||
t.pnl_map = _Map({})
|
||||
pos = _Pos("T-2B", "FETUSDT", 2.0, 200.0, current_price=1.9)
|
||||
t.eng = _Eng(pos, capital=1000.0)
|
||||
t._pending_entries["T-2B"] = {
|
||||
"asset": "FETUSDT",
|
||||
"side": "SHORT",
|
||||
"entry_price": 2.0,
|
||||
"entry_bar": 95,
|
||||
"entry_date": "2026-05-12",
|
||||
"notional": 200.0,
|
||||
"notional_entry": 200.0,
|
||||
"quantity": 100.0,
|
||||
"retraction_legs": 0,
|
||||
"realized_pnl_legs_total": 0.0,
|
||||
}
|
||||
_seed_chain(t, "T-2B")
|
||||
cmd = _retract_cmd(t, "T-2B", command_id="c-2b", fraction=1.0, reason="HOTKEY_RETRACT")
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
forced = t._process_runtime_commands({"FETUSDT": 1.9})
|
||||
assert forced is not None
|
||||
|
||||
# First accounting application happened in retract leg.
|
||||
assert t.eng.capital == pytest.approx(1010.0, abs=1e-9)
|
||||
pending = t._pending_entries["T-2B"]
|
||||
realized_pnl, realized_source = t._resolved_realized_trade_pnl(pending, forced, exit_price=1.9)
|
||||
assert realized_source == "net_pnl"
|
||||
assert realized_pnl == pytest.approx(10.0, abs=1e-9)
|
||||
|
||||
# Close-path accounting must be suppressed because leg accounting already realized pnl.
|
||||
cap_delta, cap_source = t._resolved_capital_apply_pnl(forced, realized_pnl)
|
||||
assert cap_source == "already_realized"
|
||||
assert cap_delta == pytest.approx(0.0, abs=1e-12)
|
||||
cap_before, cap_after = t._apply_trade_capital_update(
|
||||
cap_delta,
|
||||
reason="HOTKEY_RETRACT",
|
||||
source="trade_close",
|
||||
trade_id="T-2B",
|
||||
asset="FETUSDT",
|
||||
)
|
||||
assert cap_before == pytest.approx(1010.0, abs=1e-9)
|
||||
assert cap_after == pytest.approx(1010.0, abs=1e-9)
|
||||
assert t.eng.capital == pytest.approx(1010.0, abs=1e-9)
|
||||
|
||||
|
||||
def test_idempotent_replay_is_noop(monkeypatch):
|
||||
rows = []
|
||||
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-3", "DASHUSDT", 10.0, 1000.0, current_price=9.5)
|
||||
t.eng = _Eng(pos, capital=5000.0)
|
||||
t._pending_entries["T-3"] = {
|
||||
"asset": "DASHUSDT",
|
||||
"side": "SHORT",
|
||||
"entry_price": 10.0,
|
||||
"entry_bar": 90,
|
||||
"entry_date": "2026-05-12",
|
||||
"notional": 1000.0,
|
||||
"notional_entry": 1000.0,
|
||||
"retraction_legs": 0,
|
||||
"realized_pnl_legs_total": 0.0,
|
||||
}
|
||||
_seed_chain(t, "T-3")
|
||||
cmd = _retract_cmd(t, "T-3", command_id="dup", fraction=0.5, reason="HOTKEY_RETRACT")
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd, cmd]))
|
||||
t._process_runtime_commands({"DASHUSDT": 9.5})
|
||||
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 500.0
|
||||
replays = [r for tbl, r in rows if tbl == "hotkey_audit" and r.get("result") == "IDEMPOTENT_REPLAY"]
|
||||
assert replays
|
||||
|
||||
|
||||
def test_idempotent_replay_does_not_change_capital(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-3B", "DASHUSDT", 10.0, 1000.0, current_price=9.5)
|
||||
t.eng = _Eng(pos, capital=5000.0)
|
||||
t._pending_entries["T-3B"] = {
|
||||
"asset": "DASHUSDT",
|
||||
"side": "SHORT",
|
||||
"entry_price": 10.0,
|
||||
"entry_bar": 90,
|
||||
"entry_date": "2026-05-12",
|
||||
"notional": 1000.0,
|
||||
"notional_entry": 1000.0,
|
||||
"retraction_legs": 0,
|
||||
"realized_pnl_legs_total": 0.0,
|
||||
}
|
||||
_seed_chain(t, "T-3B")
|
||||
cmd = _retract_cmd(t, "T-3B", command_id="dup-2", fraction=0.5, reason="HOTKEY_RETRACT")
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd, cmd]))
|
||||
t._process_runtime_commands({"DASHUSDT": 9.5})
|
||||
cap_after_first = t.eng.capital
|
||||
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
t._process_runtime_commands({"DASHUSDT": 9.5})
|
||||
assert t.eng.capital == pytest.approx(cap_after_first, abs=1e-9)
|
||||
|
||||
|
||||
def test_trade_id_mismatch_is_rejected_and_position_unchanged(monkeypatch):
|
||||
rows = []
|
||||
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-4", "STXUSDT", 1.0, 1000.0, current_price=1.01)
|
||||
t.eng = _Eng(pos, capital=1000.0)
|
||||
t._pending_entries["T-4"] = {"asset": "STXUSDT", "side": "SHORT", "entry_price": 1.0, "entry_bar": 80, "entry_date": "2026-05-12"}
|
||||
_seed_chain(t, "T-4")
|
||||
cmd = {"command_id": "bad", "trade_id": "OTHER", "action": "RETRACT", "fraction": 0.5, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": "OTHER", "chain_head_leg_id": "OTHER:open", "chain_prev_leg_id": "", "chain_seq": 0, "chain_token": "stale"}
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
|
||||
t._process_runtime_commands({"STXUSDT": 1.01})
|
||||
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 1000.0
|
||||
assert any(tbl == "hotkey_audit" and "TRADE_MISMATCH" in r["result"] for tbl, r in rows)
|
||||
|
||||
|
||||
def test_command_queue_drained_atomically(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-5", "LINKUSDT", 10.0, 1000.0, current_price=9.8)
|
||||
t.eng = _Eng(pos, capital=500.0)
|
||||
t._pending_entries["T-5"] = {"asset": "LINKUSDT", "side": "SHORT", "entry_price": 10.0, "entry_bar": 88, "entry_date": "2026-05-12"}
|
||||
_seed_chain(t, "T-5")
|
||||
cmds = [
|
||||
_retract_cmd(t, "T-5", command_id="a", fraction=0.25, reason="HOTKEY_RETRACT"),
|
||||
_retract_cmd(t, "T-5", command_id="b", fraction=0.25, reason="HOTKEY_RETRACT"),
|
||||
]
|
||||
t.control_map.put("blue_runtime_commands", json.dumps(cmds))
|
||||
t._process_runtime_commands({"LINKUSDT": 9.8})
|
||||
assert t.control_map.get("blue_runtime_commands") == "[]"
|
||||
|
||||
|
||||
def test_bad_fraction_rejected(monkeypatch):
|
||||
rows = []
|
||||
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-6", "SOLUSDT", 100.0, 1000.0, current_price=95.0)
|
||||
t.eng = _Eng(pos, capital=1000.0)
|
||||
t._pending_entries["T-6"] = {"asset": "SOLUSDT", "side": "SHORT", "entry_price": 100.0, "entry_bar": 80, "entry_date": "2026-05-12"}
|
||||
_seed_chain(t, "T-6")
|
||||
cmd = {"command_id": "badfrac", "trade_id": "T-6", "action": "RETRACT", "fraction": 0.0, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": t._pending_entries["T-6"]["chain_root_trade_id"], "chain_head_leg_id": t._pending_entries["T-6"]["chain_head_leg_id"], "chain_prev_leg_id": t._pending_entries["T-6"]["chain_prev_leg_id"], "chain_seq": t._pending_entries["T-6"]["chain_seq"], "chain_token": t._pending_entries["T-6"]["chain_token"]}
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
t._process_runtime_commands({"SOLUSDT": 95.0})
|
||||
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 1000.0
|
||||
assert any(tbl == "hotkey_audit" and r["result"] == "BAD_FRACTION" for tbl, r in rows)
|
||||
|
||||
|
||||
def test_retract_with_missing_price_falls_back_to_entry_and_keeps_capital(monkeypatch):
|
||||
monkeypatch.setattr(mod, "ch_put", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("T-6B", "SOLUSDT", 100.0, 1000.0, current_price=0.0)
|
||||
t.eng = _Eng(pos, capital=1000.0)
|
||||
t._pending_entries["T-6B"] = {
|
||||
"asset": "SOLUSDT",
|
||||
"side": "SHORT",
|
||||
"entry_price": 100.0,
|
||||
"entry_bar": 80,
|
||||
"entry_date": "2026-05-12",
|
||||
"notional": 1000.0,
|
||||
"notional_entry": 1000.0,
|
||||
"retraction_legs": 0,
|
||||
"realized_pnl_legs_total": 0.0,
|
||||
}
|
||||
_seed_chain(t, "T-6B")
|
||||
cmd = _retract_cmd(t, "T-6B", command_id="c-6b", fraction=0.5, reason="HOTKEY_RETRACT")
|
||||
t.control_map.put("blue_runtime_commands", json.dumps([cmd]))
|
||||
t._process_runtime_commands({})
|
||||
assert t.eng.capital == pytest.approx(1000.0, abs=1e-9)
|
||||
|
||||
|
||||
def test_multi_slot_future_safety_non_target_commands_do_not_mutate_open_slot(monkeypatch):
|
||||
"""
|
||||
Future-proof guard: if multiple slot commands exist, only matching trade_id may mutate current open position.
|
||||
"""
|
||||
rows = []
|
||||
monkeypatch.setattr(mod, "ch_put", lambda table, row: rows.append((table, row)))
|
||||
monkeypatch.setattr(mod, "_ch_ts_us", lambda: 123)
|
||||
|
||||
t = _mk_trader()
|
||||
pos = _Pos("ACTIVE", "ATOMUSDT", 5.0, 500.0, current_price=4.9)
|
||||
t.eng = _Eng(pos, capital=1000.0)
|
||||
t._pending_entries["ACTIVE"] = {"asset": "ATOMUSDT", "side": "SHORT", "entry_price": 5.0, "entry_bar": 99, "entry_date": "2026-05-12"}
|
||||
_seed_chain(t, "ACTIVE")
|
||||
cmds = [
|
||||
{"command_id": "x1", "trade_id": "INACTIVE", "action": "RETRACT", "fraction": 1.0, "reason": "HOTKEY_RETRACT", "chain_root_trade_id": "INACTIVE", "chain_head_leg_id": "INACTIVE:open", "chain_prev_leg_id": "", "chain_seq": 0, "chain_token": "stale"},
|
||||
_retract_cmd(t, "ACTIVE", command_id="x2", fraction=0.5, reason="HOTKEY_RETRACT"),
|
||||
]
|
||||
t.control_map.put("blue_runtime_commands", json.dumps(cmds))
|
||||
t._process_runtime_commands({"ATOMUSDT": 4.9})
|
||||
assert pytest.approx(t.eng.position.notional, abs=1e-9) == 250.0
|
||||
assert any(tbl == "hotkey_audit" and "TRADE_MISMATCH" in r["result"] for tbl, r in rows)
|
||||
assert any(tbl == "hotkey_audit" and r["result"] == "PARTIAL_OK" for tbl, r in rows)
|
||||
Reference in New Issue
Block a user