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