docs(portfolios): design spec cartella portfolios (brainstorming)
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>
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user