feat(worker): guard TP-invertito — sopprime l'entry quando il TP e' gia' sfondato (wick-print)

Un wick transitorio fa calcolare un tp dal lato sbagliato dell'entry (donchian: segnale
su barra wickata, entry al prezzo recuperato oltre il proprio tp) -> l'exit intrabar
scatta a bars_held=0 in perdita (16-06: 8 giri MR02_BTC 15m, sim -17.9 / reale -2.3).
TP_PHANTOM non lo prende (niente resting oracle, prezzo oltre il livello). Gate
zero-parametri in StrategyWorker._open_position, solo path live. Test + diario + CLAUDE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-16 14:16:13 +00:00
parent ef797f1ff0
commit b576ee66ac
4 changed files with 187 additions and 4 deletions
+32 -4
View File
@@ -80,6 +80,8 @@ class StrategyWorker:
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._tp_phantom_cache = (0, 0.0) # (bar_ts, monotonic): TTL del verdetto phantom
self._inverted_tp_ts = 0 # dedup log INVERTED_TP_SKIP per barra
self._inverted_tp_notified = False # alert Telegram una tantum per processo
self.worker_id = f"{strategy.name}__{asset}__{tf}"
self.work_dir = data_dir / self.worker_id
@@ -236,7 +238,34 @@ class StrategyWorker:
enriched = {"worker": self.worker_id, **(data or {})}
notify_event(event, enriched)
def _open_position(self, signal: Signal, current_price: float):
def _open_position(self, signal: Signal, current_price: float, current_ts: int = 0):
meta = signal.metadata or {}
tp = float(meta.get("tp", 0.0) or 0.0)
# GUARD TP-invertito (2026-06-16): un wick transitorio puo' far calcolare alla
# strategia un tp dal lato SBAGLIATO dell'entry (es. donchian: segnale su barra
# wickata, entry al prezzo recuperato gia' oltre il proprio tp) -> l'exit intrabar
# `bar_high>=tp` (long) / `bar_low<=tp` (short) scatta a bars_held=0 in PERDITA,
# con churn di fee e TP reduce-only respinti (16-06: 8 giri MR02_BTC 15m, sim
# -17.9 / reale -2.3). Verita' d'esecuzione, zero parametri: se il TP e' gia'
# sfondato all'ingresso il segnale e' malformato -> NON apriamo. (cerotto testnet:
# il fix vero e' mainnet, dove l'arbitraggio elimina i wick-print.)
if tp and ((signal.direction == 1 and tp <= current_price) or
(signal.direction == -1 and tp >= current_price)):
data = {
"direction": "long" if signal.direction == 1 else "short",
"price": round(current_price, 2),
"tp": round(tp, 2),
"note": "TP gia' sfondato all'ingresso (wick-print) -> entry soppressa",
}
if current_ts != self._inverted_tp_ts:
self._inverted_tp_ts = current_ts
self._log("INVERTED_TP_SKIP", data)
if not self._inverted_tp_notified:
self._inverted_tp_notified = True
self._notify("INVERTED_TP_SKIP", data)
return
notional = self.capital * self.position_size * self.leverage
size = notional / current_price if current_price > 0 else 0
@@ -246,8 +275,7 @@ class StrategyWorker:
self.entry_time = datetime.now(timezone.utc).isoformat()
self.bars_held = 0
meta = signal.metadata or {}
self.tp = float(meta.get("tp", 0.0) or 0.0)
self.tp = tp
self.sl = float(meta.get("sl", 0.0) or 0.0)
self.max_bars = int(meta.get("max_bars", 0) or 0)
@@ -729,7 +757,7 @@ class StrategyWorker:
last_idx = len(df) - 1
if last_signal.idx >= last_idx - 1:
self._open_position(last_signal, current_price)
self._open_position(last_signal, current_price, current_ts)
self.last_bar_ts = current_ts
self._save_state()