feat(reconcile): guard STALE_REAL_POSITION — rileva posizioni reali non gestite

Caso MR02_BTC 1h (2026-06-13): ritirato dallo swap a 15m mentre short reale ->
short nudo (TP perso nel netting), reconciler cieco perche' lo status fermo
contava ancora come libro. compute_stale_real_positions: worker con
real_in_position e status.json fermo >15min = non gestito -> alert
STALE_REAL_POSITION. Discriminante = staleness (venue-agnostico: cattura
ritirati-da-swap/crashati). Gira dal cron host :40 (no rebuild). 3 test nuovi,
suite 132 passed. Orfano chiuso a mano (testnet, +0.85 netto).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-13 09:43:47 +00:00
parent 643ff7f943
commit 85043bf2d3
3 changed files with 127 additions and 3 deletions
+37
View File
@@ -94,3 +94,40 @@ def test_resting_stale_order_from_flat_worker(tmp_path, monkeypatch):
rows = ra.compute_resting_drift(cl)
assert len(rows) == 1
assert rows[0]["status"] == "STALE" and rows[0]["order_id"] == "OLD-7"
def _age(root: Path, wid: str, minutes: float):
"""Invecchia lo status.json di un worker di `minutes` minuti (mtime)."""
import os
p = root / wid / "status.json"
t = p.stat().st_mtime - minutes * 60
os.utime(p, (t, t))
def test_stale_real_position_flagged(tmp_path, monkeypatch):
# worker con posizione reale ma status.json vecchio = non gestito (caso
# MR02_BTC 1h ritirato dallo swap a 15m mentre short reale)
ra = _setup(tmp_path, monkeypatch, {
"MR02_donchian_fade__BTC__1h": {"real_in_position": True, "real_amount": 0.0028,
"real_side": "sell"}})
_age(ra.PAPER, "MR02_donchian_fade__BTC__1h", 800)
stale = ra.compute_stale_real_positions(max_age_min=15)
assert len(stale) == 1
assert stale[0]["worker"] == "MR02_donchian_fade__BTC__1h"
assert stale[0]["real_amount"] == 0.0028
def test_fresh_real_position_not_flagged(tmp_path, monkeypatch):
# worker vivo (status.json appena scritto) NON e' stantio anche se in posizione
ra = _setup(tmp_path, monkeypatch, {
"SH01_shape_ml__ETH__1h": {"real_in_position": True, "real_amount": 0.053,
"real_side": "sell"}})
assert ra.compute_stale_real_positions(max_age_min=15) == []
def test_flat_stale_worker_not_flagged(tmp_path, monkeypatch):
# worker vecchio MA flat: niente posizione reale -> non e' un orfano
ra = _setup(tmp_path, monkeypatch, {
"MR01_bollinger_fade__BTC__1h": {"real_in_position": False, "real_amount": 0.0}})
_age(ra.PAPER, "MR01_bollinger_fade__BTC__1h", 800)
assert ra.compute_stale_real_positions(max_age_min=15) == []