# 2026-06-08 — Executor a 2 gambe per i pairs (PairsExecutionClient) Implementato il pezzo di esecuzione reale più impegnativo rimasto: l'esecuzione shadow a 2 gambe per la famiglia PAIRS (29% del portafoglio, finora solo simulata). ## Cosa - **`execution.PairsExecutionClient`**: compone l'`ExecutionClient` single-leg. - `open_pair(inst_a, inst_b, direction, notional)`: piazza le 2 gambe (long A/short B o viceversa) a market, verifica per-gamba sul trade; **LEG-RISK**: se UNA sola gamba filla → UNWIND (richiude la fillata reduce-only) per non restare direzionali → verified=False. - `close_pair(...)`: chiude entrambe reduce-only (solo `close_amount` della quota, MAI `close_position`), riconcilia fee/prezzi. - `register_contract`: fetch dinamico spec USDC da Deribit per strumenti non hardcodati. - Strumenti = lineari USDC (payoff lineare = matematica del backtest a 2 gambe). Aggiunti spec LTC/ADA/SOL_USDC (step 0.1/0.2/0.1). - **`PairsWorker` shadow**: ledger reale parallelo (`real_capital`, `real_dir`, `real_amount_a/b`, `real_entry_a/b`, `real_notional_a/b`, fee), persistito e resume-safe; `_real_open_pair`/`_real_close_pair` agganciati a `_open`/`_close`; PnL reale per gamba (dir A=+d, dir B=−d). Doppio arrotondamento → piccolo sbilanciamento notional, riportato. - **runner**: `pairs_executor` (PairsExecutionClient su stesso ExecutionClient dei fade), `_pairs_exec_for`, gate su `execution.pairs_enabled`. Config `portfolios.yml`: instruments estesi (LTC/ADA/SOL) + `pairs_enabled: false` (capability pronta, SPENTA). ## Validazione - Test (executor finto): 92/92 — open/close a 2 gambe, **leg-risk unwind**, ledger reale persiste e resume. - **Smoke testnet end-to-end** (`live_pairs_smoke.py`, €0): open 2 gambe verificate (long ETH 0.011 @1666 / short BTC 0.0003 @63263, fee $0.019), close 2 gambe reduce-only, riconciliazione PnL reale −0.039 vs sim −0.036 (coerente), **conto flat dopo**. ## Incidente operativo (testnet, €0) — da ricordare Durante la prima esecuzione, lo smoke è crashato (bug del test: prezzi sim=0 → divisione per zero nel ramo sim) lasciando una posizione aperta. Per pulirla ho usato a mano `ExecutionClient.close()` (= `close_position`) che **flatta l'intero strumento** → ho chiuso anche le 3 posizioni reali SHADOW dei fade del runner (BTC/ETH_USDC condivisi). Impatto: testnet, shadow (non guida le decisioni), €0; 3 worker fade con `real_in_position=True` su conto flat → si auto-riconciliano al prossimo close sim (close_amount su flat → verified=False, reset; "shadow pulito dalla prossima apertura"). I TP resting erano già stati cancellati da close_position (cancel → order_not_found). **Lezione (CLAUDE.md la documentava già)**: MAI `close_position` su strumenti condivisi. Fix permanente: lo smoke ora (a) usa solo `close_amount` della quota, (b) ABORTA se ci sono posizioni di produzione aperte sugli strumenti, (c) usa prezzi sim reali. ## Stato: PRONTO ma SPENTO (`pairs_enabled: false`) L'executor è validato (test + smoke). NON attivato in produzione: accenderlo richiede una finestra a conto flat e un periodo di osservazione, dato il conto condiviso coi fade. Quando si accende: i 5 pairs eseguiranno reale a 2 gambe accanto al sim.