602c46e5bf
Portafogli come oggetti di prima classe (pool condiviso, backtest+live unificati, ribilancio periodico, 4 schemi pesi, data layer Cerbero v2). Default PORT06 (master+shape, cap pairs 33%, leva 2x). Include analisi accorpamento sleeve (cluster per asset/regime, pairs=47% rischio) e fuori-scope (ledger unico, Hyperliquid alt, cointegration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
234 lines
12 KiB
Markdown
234 lines
12 KiB
Markdown
# 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.
|