Files
PythagorasGoal/Old/docs/superpowers/specs/2026-05-29-portfolios-design.md
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:20:59 +00:00

12 KiB
Raw Permalink 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.