ab4f706057
Due correzioni emerse da close live con win=True ma pnl<0. 1) Metrica win lorda -> netta. _close_position contava is_win=trade_return>0 (lordo), gonfiando l'accuracy: un take-profit colpito con mossa < fee RT risultava "win" pur perdendo. 51 close live: 39 win (76,5%) -> 13 falsi win -> accuracy netta reale 52,9%. Fix: is_win = net > 0. Capitale/PnL erano già corretti (netti). Contatori persistiti riconciliati a parte (MR01/DIP01 BTC 7->1). 2) Filtro edge-minimo min_tp_frac. I 13 falsi win erano tutti MR01/DIP01 BTC in regime piatto: TP (la media) entro il costo round-trip -> perdenti garantiti. Aggiunto param min_tp_frac (default 0.0=off) a tutte e 4 le fade (MR01 banda, MR02 midpoint, MR07 ATR, DIP01): salta i segnali col TP entro la soglia. Non si "allarga" il TP (rischierebbe di perdere di piu'): si evita la trade. Cablato live a 0.0015 (1,5x fee) in _defs.py. Validazione backtest BTC+ETH 1h: neutro su tutte le fade (0-1 trade rimossi, pnl invariato o +leggero su DIP01). I micro-scalp sotto-fee non esistono nello storico -> artefatto del regime attuale. Filtro puro-upside. Test: test_win_net_of_fees.py, test_min_tp_frac.py (monotonia + gap > soglia + default-off invariato). Suite: 50 passed. NB deploy: il sorgente e' COPY nell'immagine, non montato -> serve `docker compose up -d --build`, non un semplice restart (vale anche per il fix SH01 horizon-exit, andato live solo con questo rebuild). Volume ./data persiste, 14 worker in RESUME puliti. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
54 lines
1.9 KiB
Python
54 lines
1.9 KiB
Python
"""Fix metrica `win`: una trade è win solo se il PnL è positivo NETTO delle fee, non sul
|
|
lordo. Prima del fix `is_win = trade_return > 0` (lordo): un take-profit colpito con un
|
|
movimento più piccolo del round-trip fee veniva contato come win pur perdendo denaro,
|
|
gonfiando l'accuracy (es. MR01/DIP01 BTC in regime piatto: TP entro le fee)."""
|
|
import pandas as pd
|
|
from src.live.strategy_worker import StrategyWorker
|
|
from src.live.strategy_loader import load_strategy
|
|
|
|
|
|
def _worker(tmp, fee_rt=0.001):
|
|
w = StrategyWorker(strategy=load_strategy("MR01_bollinger_fade"), asset="BTC", tf="1h",
|
|
capital=1000.0, data_dir=tmp)
|
|
w._notify = lambda *a, **k: None
|
|
w.fee_rt = fee_rt
|
|
w.in_position = True
|
|
w.direction = 1
|
|
w.entry_price = 100.0
|
|
return w
|
|
|
|
|
|
def _last_close(w):
|
|
import json
|
|
rows = [json.loads(l) for l in w.trades_path.read_text().strip().splitlines()]
|
|
return [r for r in rows if r.get("event") == "CLOSE"][-1]
|
|
|
|
|
|
def test_tiny_favorable_move_is_loss_after_fees(tmp_path):
|
|
"""Mossa lorda +0,05% < fee RT 0,10%: prezzo salito, ma netto negativo -> NON è win."""
|
|
w = _worker(tmp_path, fee_rt=0.001)
|
|
w._close_position(100.05, "take_profit") # +0.05% lordo, sotto le fee
|
|
c = _last_close(w)
|
|
assert c["net_return"] < 0
|
|
assert c["win"] is False # prima del fix era True
|
|
assert w.total_wins == 0
|
|
|
|
|
|
def test_move_beyond_fees_is_win(tmp_path):
|
|
"""Mossa lorda +0,30% > fee 0,10%: netto positivo -> win."""
|
|
w = _worker(tmp_path, fee_rt=0.001)
|
|
w._close_position(100.30, "take_profit")
|
|
c = _last_close(w)
|
|
assert c["net_return"] > 0
|
|
assert c["win"] is True
|
|
assert w.total_wins == 1
|
|
|
|
|
|
def test_loss_is_not_win(tmp_path):
|
|
"""Prezzo sceso su long: perdita netta -> non win."""
|
|
w = _worker(tmp_path, fee_rt=0.001)
|
|
w._close_position(99.0, "stop_loss")
|
|
c = _last_close(w)
|
|
assert c["win"] is False
|
|
assert w.total_wins == 0
|