feat(live): reconcile resting + orphan single-leg + circuit-breaker venue-lock + FEED_BOOK_GAP
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>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"""Reconcile degli ordini RESTING (2026-06-12): TP/DSL attesi dai libri vs ordini
|
||||
in book + fill non bookati (caso MR02_BTC: TP resting fillato di notte e disaster-SL
|
||||
sparito, scoperti solo al close sim ore dopo). Nessuna rete: client stubbato,
|
||||
PAPER puntato su tmp_path."""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from src.live import books
|
||||
|
||||
|
||||
class _Client:
|
||||
def __init__(self, open_orders=None, trades=None):
|
||||
self._oo = open_orders or []
|
||||
self._tr = trades or []
|
||||
|
||||
def get_open_orders(self, currency="USDC", type="all"):
|
||||
# merge 'all'+'trigger_all' nel chiamante: qui si ritorna tutto due volte
|
||||
return self._oo
|
||||
|
||||
def get_trade_history(self, limit=100, instrument_name=None):
|
||||
return [t for t in self._tr
|
||||
if not instrument_name or t["instrument"] == instrument_name]
|
||||
|
||||
|
||||
def _mk_worker(root: Path, wid: str, st: dict):
|
||||
d = root / wid
|
||||
d.mkdir(parents=True)
|
||||
(d / "status.json").write_text(json.dumps(st))
|
||||
|
||||
|
||||
def _setup(tmp_path, monkeypatch, statuses: dict):
|
||||
root = tmp_path / "portfolio_paper"
|
||||
for wid, st in statuses.items():
|
||||
_mk_worker(root, wid, st)
|
||||
monkeypatch.setattr(books, "PAPER", root)
|
||||
import importlib
|
||||
ra = importlib.import_module("scripts.analysis.reconcile_account")
|
||||
monkeypatch.setattr(ra, "PAPER", root)
|
||||
return ra
|
||||
|
||||
|
||||
IN_POS = {"real_in_position": True, "real_tp_order_id": "TP-1",
|
||||
"real_dsl_order_id": "DSL-1"}
|
||||
|
||||
|
||||
def test_expected_resting_reads_single_leg(tmp_path, monkeypatch):
|
||||
_setup(tmp_path, monkeypatch, {
|
||||
"MR01_bollinger_fade__ETH__1h": IN_POS,
|
||||
# pairs e worker flat: NESSUN resting atteso
|
||||
"PR01_pairs_reversion__ETH_BTC__1h": {"real_in_position": True,
|
||||
"real_amount_a": 0.1},
|
||||
"MR02_donchian_fade__BTC__1h": {"real_in_position": False,
|
||||
"real_tp_order_id": "TP-9"},
|
||||
})
|
||||
exp = books.expected_resting()
|
||||
assert {(e["order_id"], e["kind"]) for e in exp} == {("TP-1", "tp"), ("DSL-1", "dsl")}
|
||||
assert all(e["instrument"] == "ETH_USDC-PERPETUAL" for e in exp)
|
||||
|
||||
|
||||
def test_resting_ok_when_on_book(tmp_path, monkeypatch):
|
||||
ra = _setup(tmp_path, monkeypatch, {"MR01_bollinger_fade__ETH__1h": IN_POS})
|
||||
cl = _Client(open_orders=[{"order_id": "TP-1", "label": "MR01_bollinger_fade__ETH__1h"},
|
||||
{"order_id": "DSL-1", "label": "MR01_bollinger_fade__ETH__1h"}])
|
||||
rows = ra.compute_resting_drift(cl)
|
||||
assert [r["status"] for r in rows] == ["OK", "OK"]
|
||||
|
||||
|
||||
def test_resting_filled_unbooked_vs_missing(tmp_path, monkeypatch):
|
||||
# TP sparito dal book MA con fill nel trade history -> FILLED_UNBOOKED (caso MR02);
|
||||
# DSL sparito senza fill (trigger genera order_id nuovo) -> MISSING
|
||||
ra = _setup(tmp_path, monkeypatch, {"MR01_bollinger_fade__ETH__1h": IN_POS})
|
||||
cl = _Client(open_orders=[],
|
||||
trades=[{"instrument": "ETH_USDC-PERPETUAL", "order_id": "TP-1",
|
||||
"amount": 0.103, "price": 1640.6}])
|
||||
by_kind = {r["kind"]: r for r in ra.compute_resting_drift(cl)}
|
||||
assert by_kind["tp"]["status"] == "FILLED_UNBOOKED"
|
||||
assert abs(by_kind["tp"]["filled"] - 0.103) < 1e-9
|
||||
assert by_kind["dsl"]["status"] == "MISSING"
|
||||
|
||||
|
||||
def test_resting_stale_order_from_flat_worker(tmp_path, monkeypatch):
|
||||
# ordine in book con label di un NOSTRO worker flat -> STALE (fillerebbe a sorpresa);
|
||||
# ordini di altri bot (label sconosciuta) ignorati
|
||||
ra = _setup(tmp_path, monkeypatch, {
|
||||
"MR01_bollinger_fade__ETH__1h": {"real_in_position": False}})
|
||||
cl = _Client(open_orders=[
|
||||
{"order_id": "OLD-7", "label": "MR01_bollinger_fade__ETH__1h",
|
||||
"instrument": "ETH_USDC-PERPETUAL", "order_type": "limit"},
|
||||
{"order_id": "X-1", "label": "altro_bot", "instrument": "ETH_USDC-PERPETUAL"},
|
||||
])
|
||||
rows = ra.compute_resting_drift(cl)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["status"] == "STALE" and rows[0]["order_id"] == "OLD-7"
|
||||
Reference in New Issue
Block a user