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>
12 KiB
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
- 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.
- Scope: backtest + live unificati. Un'unica classe
Portfoliocome fonte di verità, capace sia di backtest/report storico sia di gestione live. - Ribilanciamento periodico. Il capitale viene riallocato ai pesi target a cadenza fissa (giornaliera di default, configurabile), coerente con tutte le metriche misurate finora.
- 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. - Scope live v1: tutti gli sleeve — fade, honest, pairs (2 gambe) e shape-ML (SH01 via
worker con retraining periodico, sfruttando il
MLWorkerWrapperesistente). - Data layer Cerbero v2. Il runner live adotta gli endpoint unificati v2:
get_historicalunificato,get_instruments(naming robusto, nienteINSTRUMENT_MAPhardcoded),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 insrc/portfolio/sleeves.py;combine_portfolio.pyereport_families.pydiventano consumer sottili (niente duplicazione). - Il live riusa da
multi_runner: il fetch candele,build_workers,build_pairs_workers,MLWorkerWrapper.multi_runnerresta entrypoint legacy single-sleeve finchéPortfolioRunnernon lo sostituisce. - I vecchi
PORT01-03discripts/strategies/vengono migrati inscripts/portfolios/come definizioni della nuova classe.
4. Definizione del portafoglio (schema)
@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
equal/cap/manualsono statici;inverse_vol/cluster_rpsi ricalcolano a ogni ribilancio sulla finestra trailingvol_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:
- Data layer v2. All'avvio
get_instrumentsrisolve i nomi reali di ogni asset/coppia (fallback a una mappa statica se l'endpoint non risponde). Per tick:get_historicalunificato per le candele +get_ticker_batchper i prezzi correnti di tutte le gambe in un'unica chiamata. - Costruzione sleeve→worker. Riusa
build_workers/build_pairs_workers/MLWorkerWrapper(SH01). I worker sono esecutori, non possiedono più €1000 fissi. - Capitale pool + sizing. Il
PortfolioLedgertienetotal_capital. A ogni worker viene assegnatoalloc_i = peso_i × total_capital; il worker dimensiona il notional comealloc_i × position_size × leverage(si riusa il campocapitaldel worker come base di allocazione). - Ribilancio (cadenza
rebalance, default giornaliera).total_capital = Σ equity_sleeve(capitale + PnL realizzato); ricalcolo dei pesi (vol-based sulla finestra trailing o statici); riallineoalloc_i. - 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.jsonindata/paper_trades/{worker_id}/(storico per-sleeve intatto). Il portafoglio aggrega sopra, non duplica i trade. - Resume: al restart il runner ricarica lo
status.jsondel portafoglio e gli stati dei worker → riprende capitale, pesi e posizioni senza perdere storico. - Indicatori target: il ledger espone
pnl_total,pnl_today,€/daymedio 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 direport_families.py(regressione, stessa fonte). - Parità live↔backtest — replay del
PortfolioRunnersu 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_pairse feature da macro/sentiment (funding, liquidation, OI) per strategie future. run_backtestserver-side di Cerbero come check incrociato.