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>
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