# 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.
