"""Orfani single-leg (2026-06-12): un close fallito/cappato registra la quota residua in orphan_legs (parita' coi pairs) cosi' il reconciler la conta come drift SPIEGATO — prima il REAL_CLOSE_PARTIAL di MR07 (0.102 ETH nel lock testnet) lasciava drift non spiegato. Nessuna rete: executor finto.""" import json from types import SimpleNamespace from src.live.execution import Fill from src.live.strategy_worker import StrategyWorker from src.strategies.base import Signal from src.live import books class FailingCloseExec: """Apre ok, chiude con fill ZERO (venue locked / netting negato).""" verify_polls = 1 verify_sleep = 0.0 disaster_sl_pct = None def open(self, instrument, side, notional, label=None): amt = round(notional / 100.0, 6) return Fill(instrument, side, notional, amt, 100.0, 0.0, 0.05, "oid-open", "filled", True, filled_amount=amt) def place_tp_limit(self, *a, **k): return Fill("x", "sell", 0.0, 0.0, None, 0.0, 0.0, None, None, False) def cancel_order(self, oid): return {"state": "cancelled"} def resting_fills(self, instrument, oid): return 0.0, None, 0.0 def close_amount(self, instrument, side, amount, label=None): return Fill(instrument, "sell", 0.0, amount, None, 0.0, 0.0, None, "error", False, notes="place_order error: locked_by_admin", filled_amount=0.0) def _worker(tmp_path): return StrategyWorker( strategy=SimpleNamespace(name="MR07_return_reversal", fee_rt=0.001), asset="ETH", tf="1h", capital=100.0, position_size=0.5, leverage=2.0, data_dir=tmp_path, executor=FailingCloseExec(), exec_instrument="ETH_USDC-PERPETUAL") def test_failed_close_registers_orphan(tmp_path, monkeypatch): monkeypatch.setattr("src.live.strategy_worker.notify_event", lambda *a, **k: None) w = _worker(tmp_path) w._open_position(Signal(idx=0, direction=1, entry_price=100.0, metadata={"max_bars": 12}), 100.0) amt = w.real_amount assert w.real_in_position and amt > 0 w._close_position(105.0, "time_limit") assert not w.real_in_position assert len(w.orphan_legs) == 1 o = w.orphan_legs[0] assert o["instrument"] == "ETH_USDC-PERPETUAL" assert o["entry_side"] == "buy" and abs(o["amount"] - amt) < 1e-9 # persistito nello status (resume-safe) e visto da books.real_books st = json.loads((tmp_path / w.worker_id / "status.json").read_text()) assert st["orphan_legs"] == w.orphan_legs monkeypatch.setattr(books, "PAPER", tmp_path) _, orphans = books.real_books() assert abs(orphans["ETH_USDC-PERPETUAL"] - amt) < 1e-9 # buy = firmato + def test_clean_close_no_orphan(tmp_path, monkeypatch): monkeypatch.setattr("src.live.strategy_worker.notify_event", lambda *a, **k: None) w = _worker(tmp_path) w.executor.close_amount = lambda instrument, side, amount, label=None: Fill( instrument, "sell", 0.0, amount, 105.0, 0.0, 0.05, "oid-close", "filled", True, filled_amount=amount) w._open_position(Signal(idx=0, direction=1, entry_price=100.0, metadata={"max_bars": 12}), 100.0) w._close_position(105.0, "time_limit") assert w.orphan_legs == []