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,28 @@
|
||||
"""PORT01 — Honest (default). Report backtest del portafoglio."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
CODE = "PORT01"
|
||||
|
||||
|
||||
def run():
|
||||
p = PORTFOLIOS[CODE]
|
||||
r = p.backtest()
|
||||
print("=" * 80)
|
||||
print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
|
||||
print("=" * 80)
|
||||
print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% "
|
||||
f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}")
|
||||
print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}")
|
||||
print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
|
||||
print(" rischio % per sleeve:", {k: round(v, 1) for k, v in
|
||||
sorted(r.risk.items(), key=lambda x: -x[1])})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""PORT02 — Fade master (default). Report backtest del portafoglio."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
CODE = "PORT02"
|
||||
|
||||
|
||||
def run():
|
||||
p = PORTFOLIOS[CODE]
|
||||
r = p.backtest()
|
||||
print("=" * 80)
|
||||
print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
|
||||
print("=" * 80)
|
||||
print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% "
|
||||
f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}")
|
||||
print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}")
|
||||
print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
|
||||
print(" rischio % per sleeve:", {k: round(v, 1) for k, v in
|
||||
sorted(r.risk.items(), key=lambda x: -x[1])})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""PORT03 — Master (default). Report backtest del portafoglio."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
CODE = "PORT03"
|
||||
|
||||
|
||||
def run():
|
||||
p = PORTFOLIOS[CODE]
|
||||
r = p.backtest()
|
||||
print("=" * 80)
|
||||
print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
|
||||
print("=" * 80)
|
||||
print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% "
|
||||
f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}")
|
||||
print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}")
|
||||
print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
|
||||
print(" rischio % per sleeve:", {k: round(v, 1) for k, v in
|
||||
sorted(r.risk.items(), key=lambda x: -x[1])})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""PORT04 — Master + pairs (default). Report backtest del portafoglio."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
CODE = "PORT04"
|
||||
|
||||
|
||||
def run():
|
||||
p = PORTFOLIOS[CODE]
|
||||
r = p.backtest()
|
||||
print("=" * 80)
|
||||
print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
|
||||
print("=" * 80)
|
||||
print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% "
|
||||
f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}")
|
||||
print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}")
|
||||
print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
|
||||
print(" rischio % per sleeve:", {k: round(v, 1) for k, v in
|
||||
sorted(r.risk.items(), key=lambda x: -x[1])})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""PORT05 — Master esteso (default). Report backtest del portafoglio."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
CODE = "PORT05"
|
||||
|
||||
|
||||
def run():
|
||||
p = PORTFOLIOS[CODE]
|
||||
r = p.backtest()
|
||||
print("=" * 80)
|
||||
print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
|
||||
print("=" * 80)
|
||||
print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% "
|
||||
f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}")
|
||||
print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}")
|
||||
print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
|
||||
print(" rischio % per sleeve:", {k: round(v, 1) for k, v in
|
||||
sorted(r.risk.items(), key=lambda x: -x[1])})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,28 @@
|
||||
"""PORT06 — Master + shape (default). Report backtest del portafoglio."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
CODE = "PORT06"
|
||||
|
||||
|
||||
def run():
|
||||
p = PORTFOLIOS[CODE]
|
||||
r = p.backtest()
|
||||
print("=" * 80)
|
||||
print(f" {p.code} — {p.label} | pesi={p.weighting} caps={p.caps} leva={p.leverage}x")
|
||||
print("=" * 80)
|
||||
print(f" FULL ret {r.full['ret']:+.0f}% CAGR {r.full['cagr']:.0f}% "
|
||||
f"DD {r.full['dd']:.1f}% Sharpe {r.full['sharpe']:.2f}")
|
||||
print(f" OOS ret {r.oos['ret']:+.0f}% DD {r.oos['dd']:.1f}% Sharpe {r.oos['sharpe']:.2f}")
|
||||
print(" per anno:", {y: round(v) for y, v in sorted(r.yearly.items())})
|
||||
print(" rischio % per sleeve:", {k: round(v, 1) for k, v in
|
||||
sorted(r.risk.items(), key=lambda x: -x[1])})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Definizioni canoniche dei portafogli (tutti i tipi visti finora)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.portfolio.base import Portfolio, SleeveSpec # noqa: E402
|
||||
|
||||
# Universo live tradabile (8 asset con feed Cerbero v2 + parquet). ROT02/TSM01 ci ruotano sopra.
|
||||
UNIVERSE8 = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]
|
||||
|
||||
# Edge minimo (solo live): salta i fade/dip il cui TP cade entro 1.5x il costo round-trip
|
||||
# (perdenti garantiti in regime piatto). Neutro sul backtest storico (0 trade rimossi su
|
||||
# MR01, +leggero su DIP01), protettivo dal vivo. Solo MR01/DIP01 leggono il param;
|
||||
# MR02/MR07 lo ignorano (**params). Vedi docs/diary/2026-06-01-tp-min-edge.md.
|
||||
MIN_TP_FRAC = 0.0015
|
||||
|
||||
# Filtro TREND (live, swap dal loss-guard Hurst il 2026-06-07): salta le fade quando il prezzo
|
||||
# e' esteso oltre trend_max*ATR(14) dalla EMA(ema_long) — non si fada un trend/crollo esteso.
|
||||
# E' la config che la ricerca EXIT-16 (2026-06-04) ha effettivamente promosso (entries
|
||||
# trend-filtrate, niente hurst). Gate PORT06 sul path live (trendmax_port06_impact, 2026-06-07):
|
||||
# TREND-ONLY domina la config hurst-only su TUTTE le metriche (FULL Sharpe 7.23->7.89,
|
||||
# OOS Sharpe 9.35->9.91, OOS DD 1.68->1.20), plateau robusto su trend_max 2.5/3.0/3.5.
|
||||
# Il loss-guard Hurst (validato 2026-06-02 sull'engine PRE-EXIT-16 con SL intrabar) e'
|
||||
# RIDONDANTE-DANNOSO dopo EXIT-16: attacca lo stesso regime (trending) saltando ingressi che
|
||||
# con EXIT-16 sono tornati vincenti; la combinazione hurst+trend over-filtra (meta' dei trade,
|
||||
# FULL Sharpe 7.11). hurst_skip_mask resta disponibile in fade_base (param hurst_max).
|
||||
# Vedi docs/diary/2026-06-07-trendmax-gate.md.
|
||||
TREND_MAX = 3.0
|
||||
EMA_LONG = 200
|
||||
|
||||
# EXIT-16 close-confirm SL (live, 2026-06-04): lo SL intrabar fisso veniva triggerato dai
|
||||
# wick transitori (falsi negativi: l'overshoot che buca lo stop rientra — e' il movimento
|
||||
# che la fade fada). Lo stop scatta solo se il CLOSE sfonda sl ∓ 0.5*ATR14. PORT06:
|
||||
# FULL Sharpe 6.47->7.84 DD 4.10->2.60, OOS 8.82->10.06 DD 1.30->1.15 (gate del loss-guard
|
||||
# superato con margine; 34 agenti, 3 verifiche avversariali + tail-risk: coda ~= base,
|
||||
# esce nei crash veri tipo FTX). Vedi docs/diary/2026-06-04-exit-lab.md.
|
||||
SL_CONFIRM_ATR = 0.5
|
||||
|
||||
|
||||
# Override stop-largo su MR02/ETH (2026-06-09). Lo sleeve a DD piu' alto e peggior performer reale
|
||||
# (whipsaw: fada un downtrend in long, poi shorta il rimbalzo). Ricerca 127 strategie + verifica
|
||||
# avversariale + gate PORT06: nessun SOSTITUTO batte il live EXIT-16; la miglioria e' ALLARGARE lo
|
||||
# stop (sl_atr 2.0->3.0) mantenendo il close-confirm. Cattura il beneficio del no-SL (gli stop di
|
||||
# meta'-discesa sono falsi negativi) SENZA la coda da sl=0 (worst-trade -50% vs -65% del no-SL; la
|
||||
# rete di catastrofe resta -> regola "mai sl=0" rispettata). Gate PORT06 (swap del solo MR02_ETH):
|
||||
# FULL Sh 6.73->6.77 DD 3.67->3.30, OOS Sh 8.80->8.82 DD 1.23 (domina il live, FULL DD -0.37pp).
|
||||
# Scoped a MR02/ETH: gli altri 5 fade (logica diversa) NON sono stati ri-gateati -> invariati.
|
||||
# Diario docs/diary/2026-06-08-mr02eth-replace-search.md (addendum 2026-06-09).
|
||||
SL_ATR_WIDE_MR02ETH = 3.0
|
||||
|
||||
|
||||
def _fade_params(c: str, a: str) -> dict:
|
||||
p = {"min_tp_frac": MIN_TP_FRAC, "trend_max": TREND_MAX,
|
||||
"ema_long": EMA_LONG, "sl_confirm_atr": SL_CONFIRM_ATR}
|
||||
if (c, a) == ("MR02", "ETH"):
|
||||
p["sl_atr"] = SL_ATR_WIDE_MR02ETH
|
||||
return p
|
||||
|
||||
|
||||
# SWAP timeframe 1h -> 15m (2026-06-12, scelta utente "swap secco"). Gate
|
||||
# fade15m_port06_gate.py: parametri 1h trasferiti a 15m NON ri-tunati (pre-registrati,
|
||||
# anti-overfit), corr daily 15m-1h media 0.26 (NON e' lo stesso edge piu' veloce),
|
||||
# SWAP promosso — FULL CAGR 74->101% DD 3.46->2.47%, OOS Sh 10.07->10.86 (OOS DD
|
||||
# 1.48->2.09 accettato); l'edge ETH sopravvive al flat-entry-skip (non e' stale-print
|
||||
# come pairs-alt). Stessi sid -> pesi/alloc invariati; il worker cambia cadenza
|
||||
# (max_bars=24 barre = 6h; EXIT-16 confirm sulla barra 15m completata). Il backtest
|
||||
# canonico segue (combine_portfolio.FADE_TF). MR02_BTC e' il piu' fee-sensitive
|
||||
# (fee2x OOS Sh 0.60): monitorare le fee reali nel report orario.
|
||||
# Diario docs/diary/2026-06-12-fade15m-gate.md.
|
||||
FADE = [SleeveSpec(kind="single", name=c, sid=f"{c}_{a}", asset=a, tf="15m",
|
||||
cluster=f"{a}-rev", params=_fade_params(c, a))
|
||||
for a in ("BTC", "ETH") for c in ("MR01", "MR02", "MR07")]
|
||||
HONEST = [
|
||||
# DIP01: single-asset 1h -> StrategyWorker (Strategy DIP01_dip_buy). TR01/ROT02: multi-asset.
|
||||
# EXIT-16 esteso a DIP01 (2026-06-07, sweep punto 9): gate dip01_exit16_impact.py con
|
||||
# engine GAP-AWARE — grid 36/36 BTC e 35/36 ETH (train E oos), plateau buffer piatto,
|
||||
# PORT06 FULL Sh 6.43->6.61 DD 3.96->3.58, OOS 8.58->8.77. Era l'unico sleeve BTC con
|
||||
# esecuzione reale ancora sul branch SL intrabar wick-sensitive.
|
||||
SleeveSpec(kind="single", name="DIP01", sid="DIP01_BTC", asset="BTC", cluster="BTC-rev",
|
||||
params={"min_tp_frac": MIN_TP_FRAC, "sl_confirm_atr": SL_CONFIRM_ATR}),
|
||||
SleeveSpec(kind="basket", name="TR01", sid="TR01_basket", cluster="trend",
|
||||
params={"universe": ["BNB", "BTC", "DOGE", "SOL", "XRP"], "tf": "4h"}),
|
||||
SleeveSpec(kind="rotation", name="ROT02", sid="ROT02_rot", cluster="rotation",
|
||||
params={"universe": UNIVERSE8, "tf": "1d", "top_k": 3, "gross": 0.45}),
|
||||
]
|
||||
PAIRS = [
|
||||
SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHBTC", a="ETH", b="BTC", cluster="ETH-rev"),
|
||||
# BLEND timeframe: ETH/BTC anche a 15m (flat-skip), accanto al 1h. Decorrelato 0.37 dal
|
||||
# 1h -> diversificatore intra-pairs. Worker validato (validate_worker_pairs 15m, replay
|
||||
# == pairs_sim_flat). Gate PORT06: docs/diary/2026-06-09-pairs15m-live-path.md.
|
||||
SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHBTC_15M", a="ETH", b="BTC", tf="15m",
|
||||
params={"n": 66, "z_in": 1.674, "z_exit": 1.0, "max_bars": 35, "flat_skip": True,
|
||||
"position_size": 0.10}, # meta' del family PAIRS (0.20): blend-tilt
|
||||
cluster="ETH-rev"),
|
||||
SleeveSpec(kind="pairs", name="PR01", sid="PR_LTCETH", a="LTC", b="ETH", cluster="ETH-rev"),
|
||||
SleeveSpec(kind="pairs", name="PR01", sid="PR_ADAETH", a="ADA", b="ETH", cluster="ETH-rev"),
|
||||
SleeveSpec(kind="pairs", name="PR01", sid="PR_BTCLTC", a="BTC", b="LTC", cluster="BTC-rev"),
|
||||
SleeveSpec(kind="pairs", name="PR01", sid="PR_ETHSOL", a="ETH", b="SOL", cluster="ETH-rev"),
|
||||
]
|
||||
TSM = [SleeveSpec(kind="tsmom", name="TSM01", sid="TSM01", cluster="trend",
|
||||
params={"universe": UNIVERSE8, "tf": "1d",
|
||||
"horizons": [63, 126, 252], "thr": 1.0, "gross": 0.30})]
|
||||
# SH01 live (punto-10, 2026-06-07): il runner passa la storia FULL (parquet+feed) e
|
||||
# last_block_only fitta/predice solo l'ultimo blocco del walk-forward (segnali ==
|
||||
# WF completo per costruzione). Il regime corto train-365g NON e' robusto (BTC
|
||||
# negativo a fee 2x, trade-rate 22% vs 10% validato): sh01_trainwindow_validate.py.
|
||||
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape",
|
||||
params={"last_block_only": True})
|
||||
for a in ("BTC", "ETH")]
|
||||
# XS01 — reversione CROSS-SECTIONAL (8 asset, market-neutral). Famiglia nuova, scorrelata
|
||||
# (~0) da pairs e fade. Gate PORT06: +XS01 -> OOS Sharpe 9.66->10.07, FULL DD 3.68->3.46.
|
||||
# 8 gambe -> niente esecuzione reale: gira PAPER (come TR01/ROT02/TSM01). Worker validato
|
||||
# (validate_xsec_worker: replay == backtest esatto). Diario 2026-06-09.
|
||||
# DISPERSION-GATE (2026-06-10): entry solo se std cross-section del momentum lb >= disp_min
|
||||
# (p50 TRAIN = 0.0313). Diagnostica monotona TRAIN+OOS, plateau p30-p70, ogni anno migliora
|
||||
# (standalone Sharpe 2.50->3.46, regge fee 2x), PORT06 OOS Sh 10.07->10.37 a DD pari. Solo
|
||||
# path LIVE (backtest canonico NON filtrato, come trend/hurst sulle fade) -> il live fara'
|
||||
# meglio del backtest. Diario 2026-06-10, gate scripts/analysis/xs01_dispersion_gate.py.
|
||||
# PHASE-TRANCHING (2026-06-11): tranches=3 sub-book sfasati di hold/3, capitale comune.
|
||||
# La fase del roll e' arbitraria e da sola muove Sharpe FULL daily 1.52-2.33 / DD 13.8-33%
|
||||
# (timing-luck): l'ensemble di fase la elimina senza parametri fittati. Gate
|
||||
# xs01_tranche_gate.py: plateau K=2 E K=3 promossi (PORT06 OOS Sh 10.07->10.15, DD
|
||||
# 1.48->1.38, FULL pari). Solo path live come disp_min (backtest canonico single-phase).
|
||||
XSEC = [SleeveSpec(kind="xsec", name="XS01", sid="XS01", cluster="xsec",
|
||||
params={"universe": ["BTC", "ETH", "LTC", "ADA", "SOL", "BNB", "XRP", "DOGE"],
|
||||
"tf": "1h", "lb": 48, "hold": 12, "disp_min": 0.0313,
|
||||
"tranches": 3})]
|
||||
|
||||
PORTFOLIOS = {
|
||||
"PORT01": Portfolio("PORT01", "Honest", HONEST, weighting="equal"),
|
||||
"PORT02": Portfolio("PORT02", "Fade master", FADE, weighting="equal"),
|
||||
"PORT03": Portfolio("PORT03", "Master", FADE + HONEST, weighting="equal"),
|
||||
"PORT04": Portfolio("PORT04", "Master + pairs", FADE + HONEST + PAIRS,
|
||||
weighting="cap", caps={"PAIRS": 0.33}),
|
||||
"PORT05": Portfolio("PORT05", "Master esteso", FADE + HONEST + PAIRS + TSM,
|
||||
weighting="cap", caps={"PAIRS": 0.33}),
|
||||
# SHAPE cappata a ~mezzo sleeve equal (2026-06-05): SH01 non ha SL per design e la
|
||||
# ricerca multi-agente (sh01_exit_lab, 11 famiglie di stop, 0 sopravvissute) dimostra
|
||||
# che NESSUNO stop taglia la coda ETH senza rompere l'edge -> si dimezza l'esposizione
|
||||
# (costo backtest ~0: FULL 6.47->6.43, OOS 8.82->8.58, FULL DD 4.10->3.96). Vedi
|
||||
# docs/diary/2026-06-05-sh01-sl-research.md.
|
||||
"PORT06": Portfolio("PORT06", "Master + shape", FADE + HONEST + PAIRS + TSM + SHAPE + XSEC,
|
||||
weighting="cap", caps={"PAIRS": 0.33, "SHAPE": 0.0588}, leverage=2.0),
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Confronto di tutti i portafogli PORT01-06 (backtest in un solo processo).
|
||||
|
||||
all_sleeve_equities() è cache-ata: la build (fade+honest+pairs+tsm+shape) avviene una
|
||||
volta sola, poi i 6 backtest la riusano. Stampa una tabella FULL/OOS e i pesi/rischio.
|
||||
Run: uv run python scripts/portfolios/compare_all.py
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.portfolios._defs import PORTFOLIOS # noqa: E402
|
||||
|
||||
|
||||
def run():
|
||||
print("=" * 104)
|
||||
print(" CONFRONTO PORTAFOGLI PORT01-06 | netto fee, finestra comune 2021-2026, OOS = ultimo 30%")
|
||||
print("=" * 104)
|
||||
print(f" {'code':<7s}{'label':<16s}{'n':>3s}{'pesi':>9s}"
|
||||
f"{'FULLret':>9s}{'CAGR':>6s}{'DD':>6s}{'Shrp':>6s} |"
|
||||
f"{'OOSret':>8s}{'oDD':>6s}{'oShrp':>7s}")
|
||||
print(" " + "-" * 100)
|
||||
rows = []
|
||||
for code in ("PORT01", "PORT02", "PORT03", "PORT04", "PORT05", "PORT06"):
|
||||
p = PORTFOLIOS[code]
|
||||
r = p.backtest()
|
||||
cap = f"{p.weighting}" + (f"{int(p.caps['PAIRS']*100)}" if p.caps else "")
|
||||
print(f" {p.code:<7s}{p.label:<16s}{len(p.sleeves):>3d}{cap:>9s}"
|
||||
f"{r.full['ret']:>+9.0f}{r.full['cagr']:>6.0f}{r.full['dd']:>6.1f}{r.full['sharpe']:>6.2f} |"
|
||||
f"{r.oos['ret']:>+8.0f}{r.oos['dd']:>6.1f}{r.oos['sharpe']:>7.2f}")
|
||||
rows.append((code, r))
|
||||
|
||||
# miglior per Sharpe OOS e per DD OOS
|
||||
best_sharpe = max(rows, key=lambda x: x[1].oos["sharpe"])
|
||||
best_dd = min(rows, key=lambda x: x[1].oos["dd"])
|
||||
print(" " + "-" * 100)
|
||||
print(f" miglior Sharpe OOS: {best_sharpe[0]} ({best_sharpe[1].oos['sharpe']:.2f}) "
|
||||
f"miglior DD OOS: {best_dd[0]} ({best_dd[1].oos['dd']:.1f}%)")
|
||||
print("\n PORT06 (default) — top contributi al rischio:")
|
||||
r6 = dict(rows)["PORT06"]
|
||||
top = sorted(r6.risk.items(), key=lambda x: -x[1])[:6]
|
||||
print(" " + " ".join(f"{k}={v:.0f}%" for k, v in top))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,267 @@
|
||||
"""Report orario PORT06 -> Telegram.
|
||||
|
||||
Legge lo stato persistito del paper trader a portafoglio (data/portfolio_paper/*/ +
|
||||
data/portfolios/PORT06/status.json) e invia su Telegram:
|
||||
1) trade CHIUSI: positivi/negativi (netto fee) con breakdown per motivo e PnL;
|
||||
2) trade IN CORSO (posizioni aperte);
|
||||
3) PnL realizzato totale + equity mark-to-market.
|
||||
|
||||
Eseguibile standalone (es. da cron orario):
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/portfolios/hourly_report.py
|
||||
|
||||
Carica .env da solo (cron non eredita l'env del container). Legge file world-readable
|
||||
scritti dal container; non tocca lo stato del trader.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import glob
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
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"
|
||||
|
||||
|
||||
def _load_env():
|
||||
"""Carica TELEGRAM_* da .env nell'os.environ (cron non li ha)."""
|
||||
import os
|
||||
envf = ROOT / ".env"
|
||||
if not envf.exists():
|
||||
return
|
||||
for line in envf.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
os.environ.setdefault(k.strip(), v.strip())
|
||||
|
||||
|
||||
def _short(wid: str) -> str:
|
||||
"""SH01_shape_ml__BTC__1h -> SH01/BTC ; PR01_..._ETH_SOL__1h -> PR01/ETH_SOL."""
|
||||
parts = wid.split("__")
|
||||
code = parts[0].split("_")[0]
|
||||
tag = parts[1] if len(parts) > 1 else ""
|
||||
return f"{code}/{tag}" if tag else code
|
||||
|
||||
|
||||
# --- monitor filtri fade: stop-rate per epoca di config ---
|
||||
# epoche: PRE (nessun filtro) -> HURST (loss-guard 0.55, v1.0.0) -> TREND (swap
|
||||
# hurst->trend_max=3.0, 2026-06-07: gate trendmax_port06_impact, hurst ridondante
|
||||
# post-EXIT-16). Confronta lo stop-rate live di ogni epoca col backtest.
|
||||
LOSSGUARD_SINCE = "2026-06-02T14:34:30"
|
||||
TRENDSWAP_SINCE = "2026-06-07T10:10:00"
|
||||
FADE_PREFIXES = ("MR01", "MR02", "MR07")
|
||||
LOSSGUARD_MIN_SAMPLE = 30
|
||||
|
||||
|
||||
def lossguard_section() -> str:
|
||||
pre = [0, 0] # [closes, stops] nessun filtro
|
||||
hurst = [0, 0] # epoca loss-guard Hurst
|
||||
trend = [0, 0] # epoca filtro trend (config attuale)
|
||||
for sp in glob.glob(str(PAPER / "*" / "status.json")):
|
||||
wid = Path(sp).parent.name
|
||||
if not wid.startswith(FADE_PREFIXES):
|
||||
continue
|
||||
tp = Path(sp).parent / "trades.jsonl"
|
||||
if not tp.exists():
|
||||
continue
|
||||
for line in tp.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
ev = json.loads(line)
|
||||
if ev.get("event") != "CLOSE":
|
||||
continue
|
||||
ts = ev.get("ts", "")
|
||||
b = trend if ts >= TRENDSWAP_SINCE else hurst if ts >= LOSSGUARD_SINCE else pre
|
||||
b[0] += 1
|
||||
if ev.get("reason") == "stop_loss":
|
||||
b[1] += 1
|
||||
|
||||
def rate(b):
|
||||
return b[1] / b[0] * 100 if b[0] else 0.0
|
||||
|
||||
L = ["🛡️ <b>Filtri fade — stop-rate per epoca</b>"]
|
||||
L.append(f" PRE {rate(pre):.0f}% (n={pre[0]}) → HURST {rate(hurst):.0f}% (n={hurst[0]})"
|
||||
f" → TREND {rate(trend):.0f}% (n={trend[0]})")
|
||||
if trend[0] >= LOSSGUARD_MIN_SAMPLE:
|
||||
delta = rate(pre) - rate(trend)
|
||||
L.append(f" VERDETTO epoca TREND (n≥{LOSSGUARD_MIN_SAMPLE}): {delta:+.0f}pp vs PRE → "
|
||||
f"{'✅ riduce gli stop' if delta > 0 else '⚠️ nessuna riduzione'}")
|
||||
else:
|
||||
L.append(f" campione TREND {trend[0]}/{LOSSGUARD_MIN_SAMPLE} → verdetto rimandato")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
# Epoca v1.1.26 (deploy 2026-06-11 ~21:40 UTC): gate TP_PHANTOM attivo. I close
|
||||
# precedenti includono il churn da TP fantasma dell'11-06 17:32-17:58 (~24 giri,
|
||||
# win-rate inquinato) -> le accuratezze "pulite" si leggono da qui in poi.
|
||||
EPOCH_V1126 = "2026-06-11T21:40:00"
|
||||
|
||||
|
||||
def collect():
|
||||
closed = [] # (sleeve, reason, net_return, pnl, win, ts)
|
||||
open_pos = [] # dict per posizione aperta
|
||||
realized = 0.0
|
||||
for sp in sorted(glob.glob(str(PAPER / "*" / "status.json"))):
|
||||
d = Path(sp).parent
|
||||
wid = d.name
|
||||
st = json.loads(Path(sp).read_text())
|
||||
tp = d / "trades.jsonl"
|
||||
if tp.exists():
|
||||
for line in tp.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
ev = json.loads(line)
|
||||
if ev.get("event") != "CLOSE":
|
||||
continue
|
||||
nr = ev.get("net_return", 0.0)
|
||||
pnl = ev.get("pnl", 0.0)
|
||||
realized += pnl
|
||||
# win = flag del worker (col real-truth segue il PnL REALE; net_return
|
||||
# resta il sim diagnostico: sui TP fantasma da spike testnet diceva
|
||||
# 26/0 mentre il reale era 11/15). Fallback nr>0 per eventi storici.
|
||||
closed.append((_short(wid), ev.get("reason", "?"), nr, pnl,
|
||||
bool(ev.get("win", nr > 0)), ev.get("ts", "")))
|
||||
if "positions" in st or "weights" in st:
|
||||
continue # multi-asset (TR01/ROT02/TSM01): sezione dedicata
|
||||
if st.get("in_position"):
|
||||
open_pos.append({
|
||||
"sleeve": _short(wid),
|
||||
"dir": st.get("direction", 0),
|
||||
"entry": st.get("entry_price") or st.get("entry_a"),
|
||||
"entry_b": st.get("entry_b"),
|
||||
"bars": st.get("bars_held", 0),
|
||||
"cap": st.get("capital", 0.0),
|
||||
})
|
||||
return closed, open_pos, realized
|
||||
|
||||
|
||||
def multi_asset_section() -> str:
|
||||
"""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_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")
|
||||
if book is None and "books" in st:
|
||||
# XS01 tranched (2026-06-11): aggrega i sub-book in un book medio per-asset
|
||||
k = max(1, len(st["books"]))
|
||||
book = {}
|
||||
for b in st["books"]:
|
||||
for a, v in (b.get("weights") or {}).items():
|
||||
book[a] = book.get(a, 0.0) + v / k
|
||||
if book is None:
|
||||
continue # single-leg/pairs: gia' coperti da collect()
|
||||
# abs(): il book XS01 e' long/short market-neutral — col filtro v>0 le
|
||||
# gambe SHORT sparivano e un book net-short appariva "flat" nel report
|
||||
held = {a: v for a, v in book.items() if abs(v) > 1e-4}
|
||||
flip = "mai"
|
||||
tp = d / "trades.jsonl"
|
||||
if tp.exists():
|
||||
lines = [ln for ln in tp.read_text().splitlines() if ln.strip()]
|
||||
if lines:
|
||||
ts = json.loads(lines[-1]).get("ts", "")
|
||||
if ts:
|
||||
days = (now - datetime.fromisoformat(ts)).days
|
||||
flip = f"{days}g fa"
|
||||
fresh = "?"
|
||||
lu = st.get("ts") or st.get("last_update")
|
||||
if lu:
|
||||
h = (now - datetime.fromisoformat(lu)).total_seconds() / 3600
|
||||
fresh = "OK" if h < 2 else f"STALE {h:.0f}h"
|
||||
code = d.name.split("__")[0].split("_")[0] # TR01_basket__... -> TR01
|
||||
hb = ",".join(f"{a}:{v:.2f}" for a, v in sorted(held.items())) if held else "flat"
|
||||
rows.append(f"{code:<7}{hb:<26}{flip:>8} {fresh}")
|
||||
if not rows:
|
||||
return ""
|
||||
return ("📈 <b>PAPER — solo statistica, FUORI dal conto reale</b> (book | ultimo flip | status)\n<pre>"
|
||||
+ "\n".join(rows) + "</pre>")
|
||||
|
||||
|
||||
def build_report() -> str:
|
||||
closed, open_pos, realized = collect()
|
||||
pos = sum(1 for c in closed if c[4])
|
||||
neg = len(closed) - pos
|
||||
|
||||
# breakdown per motivo
|
||||
by_reason = defaultdict(lambda: [0, 0, 0.0]) # reason -> [win, loss, pnl]
|
||||
for _, reason, _, pnl, win, _ in closed:
|
||||
r = by_reason[reason]
|
||||
r[0 if win else 1] += 1
|
||||
r[2] += pnl
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
try:
|
||||
ver = (ROOT / "VERSION").read_text().strip()
|
||||
except Exception:
|
||||
ver = "?"
|
||||
eq = dd = cap = None
|
||||
if PORT_STATUS.exists():
|
||||
ps = json.loads(PORT_STATUS.read_text())
|
||||
eq, cap, dd = ps.get("equity"), ps.get("total_capital"), ps.get("max_dd")
|
||||
|
||||
L = [f"📊 <b>PORT06 — Report orario</b> <code>v{ver}</code>", now]
|
||||
if eq is not None:
|
||||
L.append(f"Equity €{eq:.2f} | Cap €{cap:.2f} | maxDD {dd:.3f}%")
|
||||
|
||||
# 1) CHIUSI — totale storico + epoca corrente (post gate TP_PHANTOM): i
|
||||
# numeri pre-v1.1.26 includono il churn fantasma e non misurano la strategia
|
||||
cur = [c for c in closed if c[5] >= EPOCH_V1126]
|
||||
cpos = sum(1 for c in cur if c[4])
|
||||
L.append(f"\n✅ <b>CHIUSI</b>: {pos} positivi / {neg} negativi (netto fee)")
|
||||
L.append(f" epoca v1.1.26+ (TP_PHANTOM attivo): {cpos}/{len(cur) - cpos}")
|
||||
rows = [f"{'motivo':<12}{'✅':>3}{'❌':>4}{'PnL€':>9}"]
|
||||
for reason, (w, l, pnl) in sorted(by_reason.items(), key=lambda x: x[1][2]):
|
||||
rows.append(f"{reason:<12}{w:>3}{l:>4}{pnl:>+9.2f}")
|
||||
L.append("<pre>" + "\n".join(rows) + "</pre>")
|
||||
|
||||
# 2) IN CORSO
|
||||
L.append(f"🟢 <b>IN CORSO</b>: {len(open_pos)} posizioni")
|
||||
if open_pos:
|
||||
rows = [f"{'sleeve':<14}{'d':<2}{'barre':>6} {'entry'}"]
|
||||
for p in sorted(open_pos, key=lambda x: x["sleeve"]):
|
||||
d = "L" if p["dir"] == 1 else "S" if p["dir"] == -1 else "-"
|
||||
entry = p["entry"]
|
||||
es = f"{entry:.6g}" if isinstance(entry, (int, float)) else str(entry)
|
||||
if p["entry_b"]:
|
||||
es = f"{entry:.6g}/{p['entry_b']:.6g}" # coppia: 2 gambe
|
||||
rows.append(f"{p['sleeve']:<14}{d:<2}{p['bars']:>6} {es}")
|
||||
L.append("<pre>" + "\n".join(rows) + "</pre>")
|
||||
|
||||
# 2a) worker multi-asset (TR01/ROT02/TSM01)
|
||||
mas = multi_asset_section()
|
||||
if mas:
|
||||
L.append(mas)
|
||||
|
||||
# 2b) monitor loss-guard
|
||||
L.append(lossguard_section())
|
||||
|
||||
# 3) TOTALE
|
||||
L.append(f"💰 <b>PnL realizzato totale: €{realized:+.2f}</b>")
|
||||
if eq is not None:
|
||||
unreal = eq - cap
|
||||
L.append(f" equity mark-to-market: €{eq:.2f} (non realizz. €{unreal:+.2f})")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def main():
|
||||
_load_env()
|
||||
import sys
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from src.live.telegram_notifier import send_telegram
|
||||
report = build_report()
|
||||
print(report)
|
||||
ok = send_telegram(report)
|
||||
print("\n[telegram]", "inviato" if ok else "NON inviato (token/chat mancanti o errore rete)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user