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)