Files
PythagorasGoal/tests/portfolio/test_inverted_tp.py
T
Adriano Dal Pastro b576ee66ac 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>
2026-06-16 14:16:13 +00:00

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