# 2026-06-04 — Shadow execution: divergenza sim/reale misurata + TODO limit reduce-only

## Deploy v1.0.6

Fix float deployata (`74670da` + release): `_quantize_step` con `Decimal` in
`src/live/execution.py` — `72*0.001 = 0.07200000000000001` causava `Invalid params`
su Deribit (1 `REAL_OPEN_FAIL` su MR07 il 2026-06-03). Resume pulito al restart.

## Misura della divergenza sim vs reale (primi 7 close shadow)

**Bilancio: PnL sim +11.85 USD vs reale +0.62 USD → delta −11.22 USD.**
Le fee NON sono il problema ($0.63 totali = 0.05%/lato, esattamente l'assunto del
backtest). Il problema è tutto nel **timing delle uscite**.

- **Aperture allineate**: slippage da −8 a +27 bps, mediana ~±7 bps. OK.
- **Chiusure take-profit: +235 bps di slippage medio** (MR07 ETH short: +257/+267/+283/+333 bps).
  Causa strutturale, non liquidità: il sim esce *al livello TP* intrabar (come il
  backtest), il reale chiude **a mercato solo al poll del tick**, quando il prezzo è
  già rientrato (TP toccato a ~1790, fill reale a ~1840). Sim +3 USD a trade, reale ~0.
- **Stop-loss: stessa meccanica ma a favore** (MR02 SL: −0.68 reale vs −2.87 sim).
  Le fade però hanno più TP che SL → netto negativo.
- **2 `REAL_CLOSE` non verificate** (order_id null, fallback su stima sim). Venue flat
  (`get_positions()=[]`), nessuna posizione orfana — ma sorvegliare i prossimi close.

**Implicazione onesta: l'edge di MR07 non sopravvive all'esecuzione market-on-poll.**
Il backtest assume il fill al livello TP; il reale incassa il prezzo post-rimbalzo.

## TODO (richiesto, da implementare)

Nello shadow-execution (`src/live/execution.py` + `StrategyWorker`): all'apertura
della posizione reale piazzare su Deribit un **LIMIT reduce-only appoggiato al
livello TP** (e idealmente stop-market per lo SL), invece di chiudere a mercato al
poll — così il fill reale replica l'assunzione intrabar del backtest.

Dettagli da considerare:
- cancellare/aggiornare l'ordine resting se il sim esce per `max_bars` o al restart (resume);
- i 3 fade BTC condividono lo strumento (posizioni nettate per conto): il limit
  reduce-only deve coprire la SOLA quota del worker, come già fa `close_amount`;
- verificare il fill del resting order via `get_trade_history` (order_id), come per i market.

## IMPLEMENTATO (sera): LIMIT reduce-only al TP

**Flusso nuovo.** A ogni `REAL_OPEN` verificato il worker piazza subito un
**limit reduce-only al livello TP** (lato opposto, stesso `amount` della SOLA
quota del worker — strumento condiviso fra i 3 fade per coin) → evento
`REAL_TP_RESTING` (o `REAL_TP_FAIL` → si resta sul vecchio market-on-poll).
Alla chiusura sim, qualunque sia la ragione, `_real_close` ora:
1. **cancella** il resting (innocuo se già fillato — e dopo il cancel nessun
   fill può più arrivare, niente race);
2. **riconcilia i fill** (anche parziali) dal `get_trade_history` per order_id
   (stessa fonte autorevole dei market), con retry se il cancel risulta
   "già fillato" ma la history è in ritardo;
3. chiude a **market reduce-only solo la quota residua** (exit non-TP:
   stop_loss/time_limit, o TP non raggiunto sullo strumento d'esecuzione);
4. ledger reale su **prezzo d'uscita combinato** (media pesata fill-TP +
   market) e fee sommate. `REAL_CLOSE` logga `tp_order_id`,
   `tp_filled_amount`, `market_amount`.

**Dettagli del TODO coperti:**
- `max_bars`/`stop_loss` → il cancel è il passo 1 di ogni chiusura;
- restart → `real_tp_order_id` è **persistito** in `status.json`: al resume il
  resting resta in book e la prima chiusura lo riconcilia normalmente;
- quota del worker → il limit usa lo stesso `amount` dell'apertura (mai
  `close_position`); il residuo è ri-quantizzato allo step (Decimal) dentro
  `close_amount` (niente artefatti float tipo `0.072-0.024`);
- prezzi limit quantizzati al **tick** dello strumento (`quantize_price`,
  BTC_USDC 0.5 / ETH_USDC 0.05 — Deribit rifiuta prezzi off-tick).

**SL: niente stop-market, decisione deliberata.** (a) Su Deribit un trigger
order, quando scatta, genera l'ordine eseguito con un **nuovo order_id** → il
fill non sarebbe verificabile via `get_trade_history` per order_id (il
requisito di verifica); (b) la misura di stamattina mostra che sul SL il
market-on-poll lavora **a favore** (il rimbalzo riduce la perdita). Lo SL resta
quindi market-on-poll.

**Bonus fee.** Quando il TP filla da resting il fill è **maker** (~0% su
Deribit) invece di taker 0.05% — il reale farà meglio dell'assunto 0.10% RT
sulle uscite TP.

**Verifica.** `live_shadow_smoke.py` esteso ai due percorsi e passato su
testnet: (A) resting in book → exit `time_limit` → cancel + market fallback,
conto flat; (B) TP già crossato → fill immediato del limit, chiusura
riconciliata dalla history **senza ordine market** (`market_amount=0`),
prezzo persino migliore del TP (+48 bps, taker sul cross). 54 test pytest OK.

**Caveat residuo onesto.** Se il processo muore DOPO il fill del resting e
PRIMA della chiusura sim, il worker al resume riconcilia al prossimo exit sim
(corretto); ma nell'intervallo la quota reale è già chiusa mentre il sim la
crede aperta — finestra breve e solo contabile. Inoltre il TP del sim è sul
feed (`BTC-PERPETUAL` inverse) mentre il limit vive sul lineare USDC: la base
fra i due può far fillare/non fillare il limit a cavallo del livello — il
fallback market copre il caso.
