fix(live): EXIT-16 confirm-SL valutato sul close di barra COMPLETATA + alert STALE_FEED

Il worker valutava il confirm sul prezzo della candela IN FORMAZIONE ad ogni poll,
reintroducendo la wick-sensitivity che EXIT-16 elimina (audit crash ETH 2026-06-05:
2 stop su 3 erano wick-stop che il backtest validato non avrebbe preso in quel
momento). Ora il confirm usa SOLO il close dell'ultima barra completata (riga -1 =
candela in corso finche' now < ts[-1]+bar_ms), buf dall'ATR della stessa barra,
fill al prezzo corrente (~ stress lag_close_exit OK in exit-lab), TP intrabar
invariato. Concausa feed-gap: non mitigabile lato exit (fill reali ~ sim);
entry-guard post-flat TESTATA e BOCCIATA (skippare i segnali dopo barre flat
peggiora tutti gli sleeve ETH: la candela-gap e' l'overshoot che la fade fada).
Aggiunto alert Telegram STALE_FEED (>=2 barre 1h flat -> notifica + gap % al
risveglio, dedup per episodio, solo osservabilita').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-05 18:32:14 +00:00
parent ff2ff6de7d
commit 2da3457ca7
7 changed files with 243 additions and 5 deletions
+49
View File
@@ -72,3 +72,52 @@ def test_small_breach_within_buffer_holds(tmp_path):
df = _df(last_high=100.0, last_low=94.0, last_close=97.95) # TR=6 -> ATR~0.43 -> buf~0.21
w.tick(df)
assert w.in_position
# ---------------------------------------------------------------- FIX 2026-06-05
# Il confirm va valutato sul close di barra COMPLETATA (come fade_base.backtest),
# non sul prezzo della barra in formazione: il dip intrabar che rientra NON deve
# stoppare (audit live 2026-06-05: 2 stop su 3 erano wick-stop da barra in corso).
def _df_forming(prev_close, forming_low, forming_close, n=120, price=100.0):
"""Ultima riga = candela IN CORSO (timestamp = ora corrente flooral'ora):
il worker deve valutare il confirm sulla riga -2 (completata)."""
c = [price] * n
h = [price] * n
l = [price] * n
c[-2] = prev_close
l[-1] = forming_low
c[-1] = forming_close
h[-1] = max(price, forming_close)
# ts in MILLISECONDI espliciti (Timestamp.now() ha risoluzione us: l'astype
# darebbe unita' sbagliate). Ultima barra = ora corrente floor -> in corso.
end_ms = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000)
ts = [end_ms - (n - 1 - i) * 3_600_000 for i in range(n)]
return pd.DataFrame({"timestamp": ts, "open": [price] * n, "high": h, "low": l,
"close": c, "volume": 1.0})
def test_forming_bar_dip_does_not_stop(tmp_path):
# barra in corso sprofonda sotto sl-buf (close corrente 96.5) ma la barra
# COMPLETATA (-2) ha chiuso a 100 -> NIENTE stop (prima del fix stoppava)
w = _long_worker(tmp_path)
w.tick(_df_forming(prev_close=100.0, forming_low=96.0, forming_close=96.5))
assert w.in_position
def test_completed_close_breach_stops_even_if_recovered(tmp_path):
# la barra COMPLETATA ha chiuso sotto sl-buf; la barra in corso e' rimbalzata
# sopra -> stop comunque (fill al prezzo corrente, come lag_close_exit)
w = _long_worker(tmp_path)
w.tick(_df_forming(prev_close=96.0, forming_low=98.5, forming_close=99.0))
assert not w.in_position
def test_tp_intrabar_on_forming_bar_unchanged(tmp_path):
# TP toccato dalla barra in corso -> esce al TP (intrabar invariato)
w = _long_worker(tmp_path)
df = _df_forming(prev_close=100.0, forming_low=99.5, forming_close=100.0)
df.loc[df.index[-1], "high"] = 102.5
w.tick(df)
assert not w.in_position
assert w.capital > 1000.0