Files
PythagorasGoal/docs/superpowers/specs/2026-05-29-portfolios-design.md
T
Adriano 602c46e5bf 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>
2026-05-29 15:24:17 +02:00

12 KiB
Raw Blame History

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)

@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

  • Unitweighting.py (somma pesi = 1, cap rispettato e ridistribuito, inverse-vol/cluster corretti); ledger.py (capitale/PnL/DD, resume da status.json).
  • Parità backtest↔reportPortfolio.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.