612f2bfced
Codice della tornata v1.1.27/28 (gia' in produzione, mai committato): - reconcile_account: estensione ordini RESTING (FILLED_UNBOOKED/MISSING/STALE, caso MR02_BTC: TP fillato di notte scoperto ore dopo) + expected_resting in books - strategy_worker: orphan_legs su REAL_CLOSE_PARTIAL anche single-leg, persistito - execution: circuit-breaker su venue-lock admin (stop ordini dopo errori ripetuti) - runner/hourly_report: alert FEED_BOOK_GAP + timestamp closed trades - cerbero_client: get_open_orders (merge all + trigger_all) Test: 12 nuovi, suite completa 126 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
"""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 == []
|