b576ee66ac
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>
77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
"""INVERTED_TP_SKIP (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) scatterebbe a bars_held=0 in PERDITA, con churn di fee e TP
|
|
reduce-only respinti. Verita' d'esecuzione, zero parametri: se il TP e' gia' sfondato
|
|
all'ingresso il segnale e' malformato -> NON si apre. TP dal lato giusto (o assente)
|
|
-> comportamento storico."""
|
|
from src.live.strategy_worker import StrategyWorker
|
|
from src.live.strategy_loader import load_strategy
|
|
from src.strategies.base import Signal
|
|
|
|
|
|
def _worker(tmp):
|
|
w = StrategyWorker(strategy=load_strategy("MR02_donchian_fade"), asset="BTC", tf="15m",
|
|
capital=1000.0, data_dir=tmp)
|
|
w._notify = lambda *a, **k: None
|
|
return w
|
|
|
|
|
|
def test_long_inverted_tp_skipped(tmp_path):
|
|
# long con tp SOTTO l'entry (caso reale 16-06: entry 66780, tp 66189) -> no entry
|
|
w = _worker(tmp_path)
|
|
sig = Signal(idx=119, direction=1, entry_price=66780.0,
|
|
metadata={"tp": 66189.25, "sl": 64218.21, "max_bars": 24})
|
|
w._open_position(sig, 66780.0, current_ts=1)
|
|
assert not w.in_position
|
|
assert w.tp == 0.0
|
|
|
|
|
|
def test_short_inverted_tp_skipped(tmp_path):
|
|
# short con tp SOPRA l'entry -> il TP intrabar `bar_low<=tp` scatterebbe subito -> no entry
|
|
w = _worker(tmp_path)
|
|
sig = Signal(idx=119, direction=-1, entry_price=100.0,
|
|
metadata={"tp": 105.0, "sl": 110.0, "max_bars": 24})
|
|
w._open_position(sig, 100.0, current_ts=1)
|
|
assert not w.in_position
|
|
|
|
|
|
def test_long_valid_tp_opens(tmp_path):
|
|
# controllo positivo: long con tp SOPRA l'entry -> apre normalmente
|
|
w = _worker(tmp_path)
|
|
sig = Signal(idx=119, direction=1, entry_price=100.0,
|
|
metadata={"tp": 103.0, "sl": 98.0, "max_bars": 24})
|
|
w._open_position(sig, 100.0, current_ts=1)
|
|
assert w.in_position
|
|
assert w.direction == 1
|
|
assert w.tp == 103.0
|
|
|
|
|
|
def test_short_valid_tp_opens(tmp_path):
|
|
# controllo positivo: short con tp SOTTO l'entry -> apre normalmente
|
|
w = _worker(tmp_path)
|
|
sig = Signal(idx=119, direction=-1, entry_price=100.0,
|
|
metadata={"tp": 97.0, "sl": 102.0, "max_bars": 24})
|
|
w._open_position(sig, 100.0, current_ts=1)
|
|
assert w.in_position
|
|
assert w.direction == -1
|
|
|
|
|
|
def test_no_tp_opens(tmp_path):
|
|
# segnale senza tp (strategie a orizzonte puro, es. SH01) -> il guard non si applica
|
|
w = _worker(tmp_path)
|
|
sig = Signal(idx=119, direction=1, entry_price=100.0, metadata={"max_bars": 12})
|
|
w._open_position(sig, 100.0, current_ts=1)
|
|
assert w.in_position
|
|
|
|
|
|
def test_inverted_skip_deduped_per_bar(tmp_path):
|
|
# stesso bar_ts -> un solo log INVERTED_TP_SKIP; non blocca un bar successivo valido
|
|
w = _worker(tmp_path)
|
|
sig = Signal(idx=119, direction=1, entry_price=66780.0,
|
|
metadata={"tp": 66189.25, "max_bars": 24})
|
|
w._open_position(sig, 66780.0, current_ts=1)
|
|
w._open_position(sig, 66780.0, current_ts=1)
|
|
assert not w.in_position
|
|
assert w._inverted_tp_ts == 1
|