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:
@@ -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) == []
|
||||
|
||||
Reference in New Issue
Block a user