diff --git a/docs/superpowers/specs/2026-05-29-portfolios-design.md b/docs/superpowers/specs/2026-05-29-portfolios-design.md new file mode 100644 index 0000000..033f65c --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-portfolios-design.md @@ -0,0 +1,233 @@ +# Design — Cartella `portfolios/`: portafogli come oggetti di prima classe + +**Data:** 2026-05-29 +**Stato:** approvato in brainstorming, pronto per il piano di implementazione +**Branch:** `shape_patterns` (o branch dedicato `portfolios`) + +## 1. Obiettivo e contesto + +Oggi le strategie del progetto vivono come *sleeve* indipendenti: ogni worker del paper +trader (`StrategyWorker`, `PairsWorker`) gestisce un conto autonomo da €1000, con capitale +e stato propri in `data/paper_trades/{worker_id}/`. I "portafogli" `PORT01-03` esistenti +sono soltanto script di **report offline**: normalizzano le equity storiche dei singoli +sleeve e ne calcolano metriche equipesate. Non esiste un livello che gestisca davvero un +capitale condiviso, i pesi, il ribilanciamento e il PnL aggregato in tempo reale. + +Questo design introduce una cartella `portfolios/` in cui il **portafoglio è un oggetto di +prima classe** che gestirà il trading e lo stato PnL. Un portafoglio possiede un capitale +totale, lo alloca ai propri sleeve secondo uno schema di pesi, dimensiona le posizioni, +ribilancia periodicamente e mantiene il ledger aggregato. La stessa definizione serve sia +al backtest sia al live, garantendo coerenza fra ciò che si misura e ciò che si tradia. + +L'obiettivo strategico resta invariato: partire da €1000 e arrivare verso €50/giorno con un +paniere diversificato delle famiglie validate (fade, honest, pairs, TSMOM, shape-ML). + +## 2. Decisioni di brainstorming + +1. **Modello di capitale: pool condiviso.** Il portafoglio possiede il capitale totale, lo + alloca ai sleeve secondo i pesi, ridimensiona le posizioni e tiene lo stato/PnL + aggregato. I worker diventano esecutori. +2. **Scope: backtest + live unificati.** Un'unica classe `Portfolio` come fonte di verità, + capace sia di backtest/report storico sia di gestione live. +3. **Ribilanciamento periodico.** Il capitale viene riallocato ai pesi target a cadenza + fissa (giornaliera di default, configurabile), coerente con tutte le metriche misurate + finora. +4. **Schemi di peso supportati (tutti):** `equal` (default), `cap` (tetto per + famiglia/cluster, es. pairs 33% — configurazione sobria raccomandata), `inverse_vol`, + `cluster_rp` (equal fra cluster naturali poi inverse-vol dentro), `manual`. +5. **Scope live v1: tutti gli sleeve** — fade, honest, pairs (2 gambe) e shape-ML (SH01 via + worker con retraining periodico, sfruttando il `MLWorkerWrapper` esistente). +6. **Data layer Cerbero v2.** Il runner live adotta gli endpoint unificati v2: `get_historical` + unificato, `get_instruments` (naming robusto, niente `INSTRUMENT_MAP` hardcoded), + `get_ticker_batch` (fetch multi-gamba efficiente). Venue di trading = Deribit come ora. + +### Analisi di accorpamento (a supporto delle decisioni) + +`scripts/analysis/sleeve_clustering.py` ha mostrato che: +- i **cluster naturali** delle 17 sleeve non coincidono con le famiglie ma con + asset/regime: BTC-reversion, ETH-reversion, trend (TR01+TSM01), shape (SH_BTC+SH_ETH), + rotation (ROT02); +- la **ridondanza è lieve** (correlazione massima 0.43 MR01_BTC↔DIP01_BTC, 0.37 TR01↔TSM01): + nessuno sleeve è davvero fondibile, ognuno aggiunge diversificazione; +- a equal-weight i **pairs pesano il 47% del rischio** → giustifica lo schema `cap`; +- in OOS calmo equal-weight batte inverse-vol e risk-parity (i pairs ad alto rischio/ritorno + corrono liberi), ma è un risultato di regime → il cap resta la scelta prudente. + +Il campo `cluster` di `SleeveSpec` codifica questi gruppi naturali per gli schemi `cap` e +`cluster_rp`. + +## 3. Architettura e layout + +Si rispecchia la struttura delle strategie (`src/strategies/` base + `scripts/strategies/` +concrete): + +``` +src/portfolio/ + __init__.py + base.py # Portfolio (definizione + .backtest()), SleeveSpec, PortfolioResult + sleeves.py # costruzione UNIFICATA delle equity-per-sleeve (backtest); + # centralizza la logica oggi in combine_portfolio + report_families + weighting.py # schemi pesi: equal, cap, inverse_vol, cluster_rp, manual + ledger.py # PortfolioLedger: capitale, allocazioni, equity, PnL, peak/DD, persistenza + runner.py # PortfolioRunner (live): pool capital, sizing, ribilancio, aggregazione + +scripts/portfolios/ + PORT01_honest.py PORT02_fade.py PORT03_master.py + PORT04_master_pairs.py PORT05_master_esteso.py PORT06_master_shape.py + # definizioni concrete (lista SleeveSpec + schema pesi); run() = report backtest + +portfolios.yml # config LIVE: portafoglio attivo, capitale, schema pesi, cap, cadenza, leva +``` + +**Integrazione col codice esistente:** +- Il backtest riusa i builder di equity-per-sleeve (`build_all_sleeves`, `pairs_sim`, + `shape_daily_equity`), centralizzati in `src/portfolio/sleeves.py`; `combine_portfolio.py` + e `report_families.py` diventano consumer sottili (niente duplicazione). +- Il live riusa da `multi_runner`: il fetch candele, `build_workers`, + `build_pairs_workers`, `MLWorkerWrapper`. `multi_runner` resta entrypoint legacy + single-sleeve finché `PortfolioRunner` non lo sostituisce. +- I vecchi `PORT01-03` di `scripts/strategies/` vengono migrati in `scripts/portfolios/` + come definizioni della nuova classe. + +## 4. Definizione del portafoglio (schema) + +```python +@dataclass +class SleeveSpec: + kind: str # "single" | "pairs" | "ml" + name: str # "MR01_bollinger_fade" | "PR01_pairs_reversion" | "SH01_shape_ml" + asset: str | None = None # single/ml + a: str | None = None # pairs: gamba long + b: str | None = None # pairs: gamba short + tf: str = "1h" + params: dict = field(default_factory=dict) + cluster: str = "" # BTC-rev | ETH-rev | trend | shape | rotation + +@dataclass +class Portfolio: + code: str # "PORT06" + label: str # "Master + shape" + sleeves: list[SleeveSpec] + weighting: str = "equal" # equal | cap | inverse_vol | cluster_rp | manual + weights: dict | None = None # solo manual (sleeve-id -> peso) + caps: dict | None = None # solo cap: chiave = FAMIGLIA (derivata da kind/name: + # PAIRS/FADE/HONEST/SHAPE/TSM), es. {"PAIRS": 0.33}. + # cluster_rp usa invece il campo `cluster` degli sleeve. + total_capital: float = 1000.0 + leverage: float = 3.0 # nota: 2x raccomandata per il live reale + rebalance: str = "1D" + vol_lookback: int = 90 # giorni per inverse_vol / cluster_rp + + def backtest(self, ...) -> PortfolioResult: ... + def weight_vector(self, sleeve_returns) -> dict[str, float]: ... +``` + +Gli schemi di peso (in `weighting.py`) restituiscono un dict `sleeve-id -> peso` che somma a +1. `equal/cap/manual` sono statici; `inverse_vol/cluster_rp` si ricalcolano a ogni ribilancio +sulla finestra trailing `vol_lookback`, identicamente in backtest e live. + +## 5. Faccia backtest + +`Portfolio.backtest()` riusa la macchina che ha prodotto tutte le metriche viste finora, +centralizzata in `src/portfolio/sleeves.py`: + +``` +build_sleeve_equity(spec) -> pd.Series # equity daily normalizzata su IDX comune + kind="single" -> fade/honest daily equity builders + kind="pairs" -> pairs_sim -> daily + kind="ml" -> shape_daily_equity +``` + +Poi: `weight_vector()` → pesi → `port_returns()` con ribilancio giornaliero → `metrics()` +FULL/OOS + `yearly_returns()`. Restituisce un `PortfolioResult` con ret/CAGR/DD/Sharpe +(FULL e OOS), tabella per-anno e contributo al rischio per sleeve e per cluster. Lo `run()` +di ogni `scripts/portfolios/PORTxx.py` stampa questo report. + +## 6. Faccia live (`PortfolioRunner`) + +Loop a poll: + +1. **Data layer v2.** All'avvio `get_instruments` risolve i nomi reali di ogni asset/coppia + (fallback a una mappa statica se l'endpoint non risponde). Per tick: `get_historical` + unificato per le candele + `get_ticker_batch` per i prezzi correnti di tutte le gambe in + un'unica chiamata. +2. **Costruzione sleeve→worker.** Riusa `build_workers` / `build_pairs_workers` / + `MLWorkerWrapper` (SH01). I worker sono esecutori, non possiedono più €1000 fissi. +3. **Capitale pool + sizing.** Il `PortfolioLedger` tiene `total_capital`. A ogni worker + viene assegnato `alloc_i = peso_i × total_capital`; il worker dimensiona il notional come + `alloc_i × position_size × leverage` (si riusa il campo `capital` del worker come base di + allocazione). +4. **Ribilancio (cadenza `rebalance`, default giornaliera).** `total_capital = Σ equity_sleeve` + (capitale + PnL realizzato); ricalcolo dei pesi (vol-based sulla finestra trailing o + statici); riallineo `alloc_i`. +5. **Aggregazione.** Dopo ogni tick il ledger aggiorna equity totale, peak, max_dd, PnL + aggregato e per-sleeve/cluster. + +### Approssimazione dichiarata (limite noto) + +Il ribilancio cambia la base di sizing delle posizioni **future**; le posizioni già aperte +restano sul notional con cui sono nate (nessun travaso forzato a metà trade). Per il paper +trading questo è fedele al backtest daily-rebalanced entro lo scarto dovuto al turnover +infragiornaliero. È un compromesso accettato per non introdurre la contabilità a ledger +unico (approccio C scartato in brainstorming), rimandata a quando si passerà a capitale +reale su un singolo conto-margine. + +## 7. Persistenza e stato PnL + +Stato del portafoglio separato dai singoli worker, in `data/portfolios/{code}/`: + +``` +data/portfolios/PORT06/ + status.json # resume: total_capital, equity, peak, max_dd, pesi correnti, + # alloc+capitale+PnL per sleeve, ultimo ribilancio, ts + equity.jsonl # append-only: una riga per tick/giorno (ts, equity, dd, pnl_day) -> curva live + events.jsonl # append-only: ribilanci (pesi prima/dopo), milestone, errori +``` + +- I worker continuano a scrivere il proprio `trades.jsonl`/`status.json` in + `data/paper_trades/{worker_id}/` (storico per-sleeve intatto). Il portafoglio aggrega + sopra, non duplica i trade. +- **Resume:** al restart il runner ricarica lo `status.json` del portafoglio e gli stati + dei worker → riprende capitale, pesi e posizioni senza perdere storico. +- **Indicatori target:** il ledger espone `pnl_total`, `pnl_today`, `€/day` medio e DD + corrente. +- **Notifiche Telegram:** riepilogo a livello portafoglio (equity, PnL giorno, DD, ribilanci) + oltre alle notifiche per-trade dei worker. + +## 8. Portafogli forniti e default + +| Codice | Label | Sleeve | Pesi | +|--------|-------|--------|------| +| PORT01 | Honest | DIP01·TR01·ROT02 | equal | +| PORT02 | Fade master | MR01/02/07 × BTC/ETH (6) | equal | +| PORT03 | Master | fade+honest (9) | equal / manual 50-50 | +| PORT04 | Master + pairs | 9 + 5 pairs | equal · cap pairs 0.33 | +| PORT05 | Master esteso | 9 + pairs + TSM01 | equal · cap pairs | +| **PORT06** | **Master + shape** *(default)* | 9 + pairs + TSM01 + SH01 (BTC/ETH) | **cap pairs 0.33** | + +**Default raccomandato:** PORT06 con `weighting="cap"` (pairs ~33%), `leverage=2` (sobrio), +`rebalance="1D"`. È la combinazione col miglior profilo OOS dell'analisi (Sharpe più alto, +DD più basso) e contiene tutte le famiglie validate. `portfolios.yml` seleziona il +portafoglio attivo e i suoi override. + +## 9. Test + +- **Unit** — `weighting.py` (somma pesi = 1, cap rispettato e ridistribuito, + inverse-vol/cluster corretti); `ledger.py` (capitale/PnL/DD, resume da status.json). +- **Parità backtest↔report** — `Portfolio.backtest()` di PORT03/04/05/06 riproduce + *esattamente* i numeri di `report_families.py` (regressione, stessa fonte). +- **Parità live↔backtest** — replay del `PortfolioRunner` su dati storici con ribilancio + giornaliero ≈ `Portfolio.backtest()` entro tolleranza (lo scarto è il turnover + infragiornaliero dichiarato), sullo stesso schema della validazione dei pairs. +- **Smoke live** — un tick reale end-to-end via Cerbero v2 (get_instruments + + get_historical + ticker_batch), nessun ordine reale, verifica ledger/persistenza/resume. + +## 10. Fuori scope (note per il futuro) + +- **Ledger unico / conto-margine reale** (approccio C): rinviato al passaggio a capitale + reale. +- **Hyperliquid come venue per gli alt** dei pairs (perp lineari nativi, evita i trap di + naming Deribit) — opzione abilitata dal data layer v2, non in v1. +- **Validazione pairs live via `get_cointegration_pairs`** e feature da macro/sentiment + (funding, liquidation, OI) per strategie future. +- **`run_backtest` server-side** di Cerbero come check incrociato.