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:
@@ -430,22 +430,36 @@ class StrategyWorker:
|
||||
# EXIT-16 close-confirm (2026-06-04): TP intrabar al livello come il
|
||||
# backtest; lo SL scatta SOLO se il close sfonda sl ∓ buf*ATR14 — i
|
||||
# wick che bucano lo stop e rientrano (l'overshoot che la fade fada)
|
||||
# non stoppano piu'. Uscita stop al CLOSE (prezzo corrente), non al
|
||||
# livello. PORT06: OOS Sharpe 8.82->10.06 (exit-lab, 34 agenti).
|
||||
buf = self.sl_confirm_atr * float(_atr(df, 14)[-1])
|
||||
# non stoppano piu'. PORT06: OOS Sharpe 8.82->10.06 (exit-lab, 34 agenti).
|
||||
#
|
||||
# FIX 2026-06-05: il confirm va valutato sul close di barra COMPLETATA,
|
||||
# come nel backtest (fade_base: c[j] di bar chiusi) — NON sul prezzo
|
||||
# della barra in formazione, che reintroduce la wick-sensitivity che
|
||||
# EXIT-16 elimina (audit live: 2 stop su 3 del 2026-06-05 erano scattati
|
||||
# su dip intrabar che il backtest avrebbe ignorato in quel momento).
|
||||
# L'ultima riga del df e' la candela in corso se non e' ancora trascorsa
|
||||
# la sua durata; il fill resta al prezzo corrente (lag di poll, stress
|
||||
# lag_close_exit superato in exit-lab). Il buf usa l'ATR della stessa
|
||||
# barra completata.
|
||||
ts_arr = df["timestamp"].values.astype("int64")
|
||||
bar_ms = int(np.median(np.diff(ts_arr[-50:]))) if len(ts_arr) > 1 else 0
|
||||
now_ms = int(time.time() * 1000)
|
||||
k = -1 if now_ms >= ts_arr[-1] + bar_ms else -2
|
||||
confirm_close = float(c[k])
|
||||
buf = self.sl_confirm_atr * float(_atr(df, 14)[k])
|
||||
if not np.isfinite(buf):
|
||||
buf = 0.0
|
||||
if self.direction == 1:
|
||||
if bar_high >= self.tp:
|
||||
self._close_position(self.tp, "take_profit")
|
||||
elif current_price < self.sl - buf:
|
||||
elif confirm_close < self.sl - buf:
|
||||
self._close_position(current_price, "stop_loss")
|
||||
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||
self._close_position(current_price, "time_limit")
|
||||
else:
|
||||
if bar_low <= self.tp:
|
||||
self._close_position(self.tp, "take_profit")
|
||||
elif current_price > self.sl + buf:
|
||||
elif confirm_close > self.sl + buf:
|
||||
self._close_position(current_price, "stop_loss")
|
||||
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||
self._close_position(current_price, "time_limit")
|
||||
|
||||
@@ -17,6 +17,8 @@ NOTIFY_EVENTS = {
|
||||
# esecuzione REALE (shadow su Deribit testnet)
|
||||
"REAL_EXEC_LIVE", # primo ordine reale verificato di un worker (conferma "e' vivo")
|
||||
"REAL_OPEN_FAIL", # un'apertura reale NON si e' verificata (problema da guardare)
|
||||
"STALE_FEED", # feed flat/fermo da >= N barre 1h (worker ciechi: il prossimo
|
||||
# prezzo reale puo' gappare, come ETH 2026-06-05 1655->1600)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -133,6 +133,41 @@ def _spec_assets_tf(spec: SleeveSpec):
|
||||
return [spec.asset], spec.tf
|
||||
|
||||
|
||||
_STALE_BARS = 2 # barre 1h COMPLETE consecutive flat (O=H=L=C) -> feed fermo
|
||||
|
||||
|
||||
def _check_stale_feed(asset: str, df: pd.DataFrame, alerted: set[str]):
|
||||
"""Osservabilita' (2026-06-05): alert Telegram quando il feed e' flat/fermo da
|
||||
>= _STALE_BARS barre 1h complete (i worker sono ciechi: il prossimo prezzo reale
|
||||
puo' gappare attraverso TP/SL, come ETH flat 13:00-14:50 -> gap 1655->1600) e al
|
||||
risveglio (con il gap % del primo prezzo reale). Una notifica per episodio.
|
||||
NB: SOLO osservabilita' — saltare gli ingressi post-flat PEGGIORA l'edge
|
||||
(testato: la candela-gap e' l'overshoot che la fade fada con profitto)."""
|
||||
import time as _t
|
||||
from src.live.telegram_notifier import notify_event
|
||||
if len(df) < _STALE_BARS + 2:
|
||||
return
|
||||
o, h, l, c = (df[k].values for k in ("open", "high", "low", "close"))
|
||||
# ultima barra COMPLETA: la riga -1 e' la candela in corso finche' non e' trascorsa
|
||||
k = -1 if int(_t.time() * 1000) >= int(df["timestamp"].iloc[-1]) + 3_600_000 else -2
|
||||
i = len(c) + k
|
||||
if i < 1:
|
||||
return
|
||||
flat = (o == h) & (h == l) & (l == c)
|
||||
n_flat = 0
|
||||
while i - n_flat >= 0 and flat[i - n_flat]:
|
||||
n_flat += 1
|
||||
if n_flat >= _STALE_BARS and asset not in alerted:
|
||||
alerted.add(asset)
|
||||
notify_event("STALE_FEED", {"asset": asset, "flat_bars_1h": n_flat,
|
||||
"ultimo_prezzo": float(c[i])})
|
||||
elif n_flat == 0 and asset in alerted:
|
||||
alerted.discard(asset)
|
||||
gap = (c[i] / c[i - 1] - 1) * 100 if c[i - 1] else 0.0
|
||||
notify_event("STALE_FEED", {"asset": asset, "status": "RIPRESO",
|
||||
"gap_pct": round(gap, 2), "prezzo": float(c[i])})
|
||||
|
||||
|
||||
def run(config_path: str = "portfolios.yml"):
|
||||
"""Loop live a portafoglio (tutti i tipi di sleeve). Data layer Cerbero v2 con resample;
|
||||
ribilancio a cambio giornata UTC."""
|
||||
@@ -207,6 +242,7 @@ def run(config_path: str = "portfolios.yml"):
|
||||
|
||||
inst_map = dict(INSTRUMENT_MAP)
|
||||
last_day = ""
|
||||
stale_alerted: set[str] = set() # asset con alert STALE_FEED attivo (dedup per episodio)
|
||||
while True:
|
||||
try:
|
||||
# fetch 1h per asset al lookback massimo richiesto
|
||||
@@ -225,6 +261,7 @@ def run(config_path: str = "portfolios.yml"):
|
||||
df = pd.DataFrame(candles)
|
||||
df["timestamp"] = df["timestamp"].astype("int64")
|
||||
raw1h[asset] = df.sort_values("timestamp").reset_index(drop=True)
|
||||
_check_stale_feed(asset, raw1h[asset], stale_alerted)
|
||||
|
||||
# tick di ogni worker col suo timeframe (resample dal 1h)
|
||||
for s in live_specs:
|
||||
|
||||
Reference in New Issue
Block a user