fix(live): win netto-fee + filtro TP edge-minimo sulle fade
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>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 2026-06-01 — "Win" che perdono: metrica netto-fee + filtro TP edge-minimo
|
||||
|
||||
> Partito da un'osservazione dell'utente sui trade live PORT06: **ci sono close con
|
||||
> `win=True` ma `pnl` negativo**. Due problemi distinti, entrambi risolti.
|
||||
|
||||
## Problema 1 — la metrica `win` mentiva (lordo invece di netto)
|
||||
|
||||
In `strategy_worker.py::_close_position`:
|
||||
|
||||
```python
|
||||
trade_return = price_change * direction # LORDO, prima delle fee
|
||||
net = trade_return * leverage - fee_rt * leverage
|
||||
pnl = capital * position_size * net # corretto (netto)
|
||||
is_win = trade_return > 0 # BUG: usa il LORDO
|
||||
```
|
||||
|
||||
`is_win` scattava appena il prezzo si muoveva di un soffio a favore, **prima delle
|
||||
fee**. Capitale e PnL erano giusti (netti); solo la metrica `win`/`accuracy` era
|
||||
gonfiata.
|
||||
|
||||
**Quantificazione (51 close live):** 39 win dichiarate (76,5%) → **13 falsi win**
|
||||
(`win=True` ma `pnl≤0`) → accuratezza **netta reale 52,9%**. PnL realizzato +€0,77
|
||||
(resta positivo: lo trascinano i pairs).
|
||||
|
||||
**Fix:** `is_win = net > 0`. + `tests/portfolio/test_win_net_of_fees.py` (mossa
|
||||
sotto-fee = non win; oltre-fee = win; perdita = non win).
|
||||
|
||||
**Riconciliazione contatori persistiti:** i `total_wins` su disco erano gonfiati dal
|
||||
vecchio conteggio lordo. Ricalcolati come `net_return>0` dai `trades.jsonl`:
|
||||
**MR01_BTC 7→1, DIP01_BTC 7→1** (gli unici toccati; tutti gli altri già coerenti).
|
||||
Capitale invariato.
|
||||
|
||||
## Problema 2 — i 13 falsi win erano tutti MR01_BTC / DIP01_BTC in take_profit
|
||||
|
||||
Causa: in `MR01_bollinger_fade` e `DIP01_dip_buy` il **TP è la media** (`tp = ma[i]`)
|
||||
e l'entry è a `close[i]` appena fuori banda. Nel regime BTC **piatto** (inchiodato
|
||||
~73.700 per ore, vol bassissima) la media è a pochi dollari dall'entry → il TP cade
|
||||
**dentro** il costo round-trip (0,10%): colpire il TP = perdita netta garantita.
|
||||
|
||||
**Meccanismo del fix (importante).** "Spostare il TP più in là" NON garantisce di non
|
||||
perdere: il prezzo rientra solo fino alla media, non oltre → si finirebbe su SL/time-
|
||||
limit, perdendo di più. La mossa provabilmente non-perdente è un **filtro di edge
|
||||
minimo**: se `|tp − entry|/entry ≤ min_tp_frac` non si apre la trade. Break-even
|
||||
esatto = `fee_rt` (= 0,10%, indipendente dalla leva, perché
|
||||
`ret = mossa·lev − fee_rt·lev > 0 ⇔ mossa > fee_rt`).
|
||||
|
||||
**Implementazione:** parametro `min_tp_frac` (default 0.0 = off) in **tutte le 4 fade**
|
||||
(MR01 banda, MR02 midpoint canale, MR07 ATR-scaled) e DIP01; salta i segnali sotto
|
||||
soglia. Cablato negli sleeve live a **0.0015 (1,5× fee)** in `_defs.py` (`MIN_TP_FRAC`).
|
||||
|
||||
**Validazione backtest (BTC+ETH 1h, config sleeve, min_tp_frac ∈ {0,.001,.0015,.002}):**
|
||||
neutro su tutte e 4 le fade.
|
||||
- MR01: 0 trade rimossi (BTC +8028€, ETH +10395€) — metriche identiche.
|
||||
- DIP01 BTC: −1 trade a 0.002, **migliora** (+7492→+7522€, DD 26,3→25,9%).
|
||||
- MR02 BTC: −1 trade a 0.0015 (pnl invariato +12198€), ETH 0 rimossi.
|
||||
- MR07 BTC/ETH: 0 rimossi (TP ATR-scaled sempre ben oltre le fee nello storico).
|
||||
|
||||
Conclusione: i micro-scalp sotto-fee **non esistono nel campione storico** — sono un
|
||||
artefatto del regime attuale. Il filtro è **puro upside**: neutro sul backtest validato,
|
||||
protettivo dal vivo. (Le 12 trade live incriminate, tutte MR01/DIP01 BTC, avevano gap
|
||||
~0,026%, ben sotto 0,15% → tutte bloccate.)
|
||||
|
||||
+ `tests/portfolio/test_min_tp_frac.py` (monotonia + ogni superstite ha gap > soglia
|
||||
+ default-off invariato).
|
||||
|
||||
## Nota deploy — il codice è COTTO nell'immagine, non montato
|
||||
|
||||
Scoperta durante il deploy: `docker-compose.yml` monta solo `./data` e
|
||||
`./portfolios.yml`; il sorgente (`src/`, `scripts/`) è `COPY` nel Dockerfile. Quindi
|
||||
**`docker compose restart` NON ricarica le modifiche al codice** — serve
|
||||
`docker compose up -d --build`. Conseguenza retroattiva: anche il fix SH01
|
||||
horizon-exit di stamattina è andato live solo con questo rebuild. Da ricordare per ogni
|
||||
futura modifica ai worker. Il volume `./data` persiste → i 14 worker fanno RESUME
|
||||
puliti dopo il rebuild (capitale e posizioni intatti).
|
||||
|
||||
## Stato finale
|
||||
|
||||
- `is_win = net > 0` live; contatori riconciliati (MR01/DIP01 BTC 1/9).
|
||||
- Filtro `min_tp_frac=0.0015` live su tutti i fade + DIP01 (attivo solo MR01/DIP01).
|
||||
- Fix SH01 horizon-exit ora **effettivamente** live (rebuild).
|
||||
- Suite: 49 passed. Container ricostruito, healthy, 14 sleeve in RESUME.
|
||||
|
||||
## Lezione
|
||||
|
||||
1. Una metrica di "win" deve essere **netto fee**, altrimenti l'accuracy è teatro.
|
||||
2. Quando il TP è dentro il costo di transazione, la trade è persa in partenza: meglio
|
||||
**non prenderla** che ritoccare il TP.
|
||||
3. Per i worker live in Docker: **rebuild**, non restart. Il restart ricarica solo lo
|
||||
stato dal volume, non il codice.
|
||||
Reference in New Issue
Block a user