diff --git a/docs/diary/2026-06-08-real-only-portfolio.md b/docs/diary/2026-06-08-real-only-portfolio.md new file mode 100644 index 0000000..da11461 --- /dev/null +++ b/docs/diary/2026-06-08-real-only-portfolio.md @@ -0,0 +1,42 @@ +# 2026-06-08 — Portafoglio live REALE-only: i €2000 ai soli sleeve eseguiti + +Decisione utente: il portafoglio live deve mostrare il **risultato reale puro**. I €2000 +si dividono SOLO tra i 14 sleeve che eseguono davvero (6 fade + DIP01 + 5 pairs + SH01); +i 3 multi-asset (TR01/ROT02/TSM01), non eseguibili in reale (bloccati dal capitale), +escono dal pool e girano **solo per statistica**. + +## Implementazione (runner-level, non rompe le definizioni) + +- `portfolios.yml`: `paper_sleeves: [TR01, ROT02, TSM01]`. +- `runner`: separa `live_specs` (pool/pesi/ledger) da `paper_specs`. I paper: + - capitale NOZIONALE fisso = `total_capital / N_sleeve_totali` (la fetta equal che + avrebbero avuto), NON dal pool; + - girano in `data/portfolio_paper_stats/` (binario separato); + - ticcati per statistica ma MAI in `ledger.update_equity` → non toccano l'equity reale. +- `hourly_report`: la sezione multi-asset ora legge `portfolio_paper_stats/` ed è + etichettata "PAPER — solo statistica, FUORI dal conto reale". + +## Pesi del portafoglio reale-only (14 sleeve, cap) + +sum=1.0; non-cappati 0.0873 (erano 0.0647 su 17), PAIRS 0.33 (cap), SHAPE 0.0588 (cap, +SH ciascuno 0.0294). Il cap SHAPE resta valido e protettivo anche su 14. + +## Il costo, misurato e accettato + +| | FULL Sh | FULL DD | OOS Sh | OOS DD | +|---|---|---|---|---| +| PORT06 completo (17, validato) | 6.43 | 3.96% | 8.58 | 1.36% | +| **REALE-only (14, live ora)** | 6.49 | **5.34%** | 8.54 | **1.70%** | + +Perdendo i 3 diversificatori PAPER (corr 0.07), il DD del portafoglio reale sale +~3.96→5.34% FULL e 1.36→1.70% OOS, Sharpe sostanzialmente invariato. È il **prezzo di +vedere il risultato reale puro** invece di una curva mista reale+paper: scelta +consapevole dell'utente. I PAPER restano misurati per ri-integrarli quando saranno +eseguibili (SH01 è già stato integrato oggi; i multi-asset attendono capitale ~€20k). + +## Note di transizione + +- Il ledger `PORT06` (code invariato) ora rappresenta i 14 reali: la curva equity ha + una discontinuità di composizione da questo deploy (atteso). +- I 3 paper ripartono puliti in `portfolio_paper_stats/` con capitale nozionale uniforme + (statistica comparabile); i vecchi status in `portfolio_paper/` restano come storico. diff --git a/portfolios.yml b/portfolios.yml index 6da237c..2ce7fa0 100644 --- a/portfolios.yml +++ b/portfolios.yml @@ -11,6 +11,13 @@ overrides: leverage: 2 # sobrio per il live reale rebalance: 1D poll_seconds: 60 + # SLEEVE PAPER (2026-06-08): fuori dal pool/pesi/ledger — i €2000 si dividono SOLO + # tra i 14 sleeve REALI (fade+DIP01+pairs+SH01). I multi-asset (no esecuzione reale, + # bloccati dal capitale) girano in data/portfolio_paper_stats/ con capitale nozionale + # fisso, SOLO per statistica in vista di future implementazioni reali. NB: il portafoglio + # live diverge ora dal PORT06 canonico (17 sleeve) -> DD reale ~5.35% vs 3.96% validato: + # il prezzo di vedere il risultato reale puro (scelta utente). + paper_sleeves: [TR01, ROT02, TSM01] # Frazione di capitale-sleeve per posizione (canonico backtest = 0.15). # 0.5 con leva 2x = 100% della fetta impegnata quando in posizione (max impiego # dei 2K senza debito di margine). NB: il DD scala ~lineare (~×3.3 vs validato). diff --git a/scripts/portfolios/hourly_report.py b/scripts/portfolios/hourly_report.py index eb31ebf..194dbe9 100644 --- a/scripts/portfolios/hourly_report.py +++ b/scripts/portfolios/hourly_report.py @@ -22,6 +22,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] PAPER = ROOT / "data" / "portfolio_paper" +PAPER_STATS = ROOT / "data" / "portfolio_paper_stats" # sleeve PAPER fuori dal conto reale PORT_STATUS = ROOT / "data" / "portfolios" / "PORT06" / "status.json" @@ -130,13 +131,13 @@ def collect(): def multi_asset_section() -> str: - """Worker multi-asset (TR01/ROT02/TSM01): book corrente + eta' dell'ultimo flip - + freschezza dello status. Prima erano INVISIBILI nel report (collect() filtra - su event/in_position che non emettevano): impossibile distinguere 'flat in - chop / risk-off by-design' da 'wiring rotto' (improvement-sweep 2026-06-06).""" + """Sleeve PAPER (TR01/ROT02/TSM01): SOLO statistica, FUORI dal conto reale dei + €2000 (2026-06-08). Vivono in data/portfolio_paper_stats/ con capitale nozionale + fisso, raccolti per eventuali future implementazioni reali. Book corrente + eta' + ultimo flip + freschezza.""" now = datetime.now(timezone.utc) rows = [] - for sp in sorted(glob.glob(str(PAPER / "*" / "status.json"))): + for sp in sorted(glob.glob(str(PAPER_STATS / "*" / "status.json"))): d = Path(sp).parent st = json.loads(Path(sp).read_text()) book = st.get("positions") if "positions" in st else st.get("weights") @@ -162,7 +163,7 @@ def multi_asset_section() -> str: rows.append(f"{code:<7}{hb:<26}{flip:>8} {fresh}") if not rows: return "" - return ("📈 MULTI-ASSET (book | ultimo flip | status)\n
"
+ return ("📈 PAPER — solo statistica, FUORI dal conto reale (book | ultimo flip | status)\n"
+ "\n".join(rows) + "")
diff --git a/src/portfolio/runner.py b/src/portfolio/runner.py
index cb3b271..0d8ded9 100644
--- a/src/portfolio/runner.py
+++ b/src/portfolio/runner.py
@@ -232,12 +232,25 @@ def run(config_path: str = "portfolios.yml"):
def _supported(s):
return s.kind in ("pairs",) + _MULTI_KINDS or s.name in _STRAT_MODULE
- live_specs = [s for s in p.sleeves if _supported(s)]
+ supported = [s for s in p.sleeves if _supported(s)]
skipped = [s.sid for s in p.sleeves if not _supported(s)]
if skipped:
print(f"[runner] sleeve saltati nel live (worker non disponibili): {skipped}")
+
+ # SLEEVE "PAPER" (solo statistica, 2026-06-08): NON entrano nel pool/pesi/ledger del
+ # portafoglio — i €total_capital si dividono SOLO tra gli sleeve reali. I paper girano
+ # con un capitale nozionale fisso (la fetta equal che avrebbero avuto) per raccogliere
+ # statistica in vista di future implementazioni reali. Default: TR01/ROT02/TSM01
+ # (multi-asset, esecuzione reale bloccata dal capitale).
+ paper_codes = {str(c).upper() for c in (_ov.get("paper_sleeves") or [])}
+ live_specs = [s for s in supported if s.name.upper() not in paper_codes]
+ paper_specs = [s for s in supported if s.name.upper() in paper_codes]
+ if paper_specs:
+ print(f"[runner] sleeve PAPER (solo statistica, fuori dal pool): "
+ f"{[s.sid for s in paper_specs]}")
live_ids = [s.sid for s in live_specs]
clusters = {s.sid: (s.cluster or s.sid) for s in live_specs}
+ paper_notional = p.total_capital / max(len(supported), 1) # fetta equal nozionale
ledger = PortfolioLedger(p.code, total_capital=p.total_capital)
client = CerberoClient()
@@ -294,6 +307,14 @@ def run(config_path: str = "portfolios.yml"):
if ps_family:
print(f"[runner] position_size globale={position_size} override famiglia={ps_family}")
+ # worker PAPER (solo statistica): capitale nozionale fisso, NESSUNA esecuzione reale,
+ # NON nel ledger del portafoglio. Salvano in data/portfolio_paper_stats/.
+ paper_dir = DATA_DIR.parent / "portfolio_paper_stats"
+ paper_workers = {s.sid: build_worker_for(s, paper_notional, p.leverage,
+ data_dir=paper_dir,
+ position_size=pos_for_spec(s.sid, position_size, ps_family))
+ for s in paper_specs}
+
# bootstrap storia full per gli sleeve ML (SH01): parquet locale + feed live.
# L'edge SH01 richiede train expanding full-history (sh01_trainwindow_validate);
# il path live fitta solo l'ultimo blocco (last_block_only nei params SHAPE).
@@ -374,11 +395,10 @@ def run(config_path: str = "portfolios.yml"):
_check_stale_feed(asset, raw1h[asset], stale_alerted)
# tick di ogni worker col suo timeframe (resample dal 1h)
- for s in live_specs:
- w = workers[s.sid]
+ def _tick(s, w):
assets, tf = _spec_assets_tf(s)
if any(a not in raw1h for a in assets):
- continue
+ return
res = {a: _resample(raw1h[a], tf) for a in assets}
if s.kind == "pairs":
w.tick(res[s.a], res[s.b])
@@ -393,6 +413,12 @@ def run(config_path: str = "portfolios.yml"):
# single (fade/dip): StrategyWorker su feed live.
w.tick(res[s.asset])
+ for s in live_specs:
+ _tick(s, workers[s.sid])
+ # PAPER: ticcati per statistica, MAI nel ledger del portafoglio
+ for s in paper_specs:
+ _tick(s, paper_workers[s.sid])
+
ledger.update_equity({sid: _worker_equity(wk) for sid, wk in workers.items()})
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")