feat(exec): gate TP_PHANTOM — il tocco TP va confermato dal book reale, non dal feed
Il feed testnet stampa wick che 'toccano' il TP intrabar senza che il prezzo abbia mai scambiato al livello: il sim bookava +4% fantasma a bars_held=0 e _real_close chiudeva A MERCATO una posizione col resting TP a zero fill (-fee/spread a giro, 14 giri l'11-06). Ora StrategyWorker._tp_phantom sopprime l'exit take_profit quando: tocco sim + resting LIMIT zero-fill + prezzo corrente che non ha raggiunto il livello (il limit sul book reale e' l'oracolo del tocco). Zero parametri (verita' d'esecuzione, non filtro di strategia); SL close-confirm e max_bars restano attivi nello stesso tick; fill parziale/prezzo oltre il livello/worker non eseguito/errore rete -> comportamento storico. Log TP_PHANTOM dedup per barra + alert Telegram una tantum. 5 test nuovi (104 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,8 @@ class StrategyWorker:
|
||||
self.real_dsl_order_id = "" # STOP_MARKET disaster bracket on-book (persistito)
|
||||
self.real_trades = 0
|
||||
self.real_first_notified = False # alert Telegram "esecuzione viva" una tantum
|
||||
self._tp_phantom_ts = 0 # dedup log TP_PHANTOM per barra (non persistito)
|
||||
self._tp_phantom_notified = False # alert Telegram una tantum per processo
|
||||
|
||||
self.worker_id = f"{strategy.name}__{asset}__{tf}"
|
||||
self.work_dir = data_dir / self.worker_id
|
||||
@@ -448,6 +450,41 @@ class StrategyWorker:
|
||||
# applied: fill reali presenti (parts) o chiusura comunque verificata
|
||||
return real_pnl, bool(parts) or verified
|
||||
|
||||
def _tp_phantom(self, current_price: float, current_ts: int) -> bool:
|
||||
"""TP "toccato" solo nel feed? Il LIMIT resting sul book REALE e' l'oracolo:
|
||||
se il prezzo avesse davvero scambiato al livello si sarebbe fillato (almeno
|
||||
in parte). Tocco sim + resting a zero fill + prezzo corrente che NON ha
|
||||
raggiunto il TP = spike-print del feed (testnet, 2026-06-11: 14 giri fantasma
|
||||
su ETH, sim +4% l'uno, reale −fee/spread l'uno) → exit SOPPRESSA, la posizione
|
||||
continua il suo ciclo normale (SL close-confirm e max_bars restano attivi).
|
||||
Zero parametri: e' un check di verita' d'esecuzione, non un filtro di
|
||||
strategia; sui worker senza esecuzione reale ritorna False (parita' storica).
|
||||
Fail-open: se la query fill fallisce (rete) si chiude come prima."""
|
||||
if not (self.execution_enabled and self.real_in_position
|
||||
and self.real_tp_order_id and self.tp):
|
||||
return False
|
||||
# prezzo gia' oltre il livello → tocco genuino anche senza fill (gap fra poll)
|
||||
if (self.direction == 1 and current_price >= self.tp) or \
|
||||
(self.direction == -1 and current_price <= self.tp):
|
||||
return False
|
||||
try:
|
||||
tp_amt, _, _ = self.executor.resting_fills(self.exec_instrument,
|
||||
self.real_tp_order_id)
|
||||
except Exception:
|
||||
return False
|
||||
if tp_amt > 0:
|
||||
return False
|
||||
if current_ts != self._tp_phantom_ts:
|
||||
self._tp_phantom_ts = current_ts
|
||||
data = {"tp": self.tp, "price": current_price, "direction": self.direction,
|
||||
"tp_order_id": self.real_tp_order_id,
|
||||
"note": "wick solo nel feed (resting zero-fill, prezzo lontano dal livello) -> exit soppressa"}
|
||||
self._log("TP_PHANTOM", data)
|
||||
if not self._tp_phantom_notified:
|
||||
self._tp_phantom_notified = True
|
||||
self._notify("TP_PHANTOM", data)
|
||||
return True
|
||||
|
||||
def _close_position(self, current_price: float, reason: str):
|
||||
if not self.in_position:
|
||||
return
|
||||
@@ -549,14 +586,14 @@ class StrategyWorker:
|
||||
if not np.isfinite(buf):
|
||||
buf = 0.0
|
||||
if self.direction == 1:
|
||||
if bar_high >= self.tp:
|
||||
if bar_high >= self.tp and not self._tp_phantom(current_price, current_ts):
|
||||
self._close_position(self.tp, "take_profit")
|
||||
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:
|
||||
if bar_low <= self.tp and not self._tp_phantom(current_price, current_ts):
|
||||
self._close_position(self.tp, "take_profit")
|
||||
elif confirm_close > self.sl + buf:
|
||||
self._close_position(current_price, "stop_loss")
|
||||
@@ -568,14 +605,14 @@ class StrategyWorker:
|
||||
if self.direction == 1:
|
||||
if bar_low <= self.sl:
|
||||
self._close_position(self.sl, "stop_loss")
|
||||
elif bar_high >= self.tp:
|
||||
elif bar_high >= self.tp and not self._tp_phantom(current_price, current_ts):
|
||||
self._close_position(self.tp, "take_profit")
|
||||
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||
self._close_position(current_price, "time_limit")
|
||||
else:
|
||||
if bar_high >= self.sl:
|
||||
self._close_position(self.sl, "stop_loss")
|
||||
elif bar_low <= self.tp:
|
||||
elif bar_low <= self.tp and not self._tp_phantom(current_price, current_ts):
|
||||
self._close_position(self.tp, "take_profit")
|
||||
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||
self._close_position(current_price, "time_limit")
|
||||
|
||||
Reference in New Issue
Block a user