12 Commits

Author SHA1 Message Date
Adriano 8292e5e6b8 docs: verify cerbero-bite MCP is MAINNET (real option chain) -> VRP source unlocked
- 3-level check: spot matches certified feed (<0.3%); environment_info=mainnet/testnet=false;
  chain-level decisive: ETH_USDC-26JUN26-1650-P identical bid/ask/IV/delta on ccxt mainnet
  vs Cerbero MCP (25.6/26.6/54.54%/-0.315)
- contamination was the OLD testnet token on get_historical, NOT Cerbero MCP itself
- Cerbero MCP gives real per-strike bid/ask/IV/greeks/OI + regime features ccxt lacks
  (dealer gamma, gamma-flip, OI delta, liquidation, funding) -> usable for VRP validation
- no deep chain history -> multi-year VRP still model-priced, but model-vs-real calibration
  now robust/repeatable. Token used only for verification, never logged/committed.
2026-06-19 22:11:14 +02:00
Adriano 922947d2aa research: verify options sleeve on REAL Deribit quotes (spread+skew haircut)
- options_real_quote_check.py: fetches real weekly BTC put chain, measures premium
  haircut (bid vs BS@DVOL-ATM), re-runs CSP sleeve with real haircut
- KEY FINDING (reverses a prior critique): backtest UNDER-prices the OTM put by using
  ATM DVOL; real skew (+28% gross) exceeds the ~4% bid/ask spread -> real bid premium
  = 1.29x modeled. Sleeve premium is conservative at current (calm) quotes.
- Real risk SHIFTS to the tail + roll-liquidity in stress (skew = market pricing fat
  tail), not premium magnitude. Breakpoint: sleeve dies below ~70% premium capture.
- updated eval diary with the verification
2026-06-19 21:48:12 +02:00
Adriano 69be9eb75f docs: critical eval of external crypto_backtest strategy (trend + options VRP)
- spot sleeve independently confirms our TP01 (12h vol-targeted trend); our multi-horizon
  blend is slightly better (Sharpe 1.32 vs their 0.77 window / 1.07 full)
- options sleeve (75% weight) = the most promising lead to break the ~1.3 Sharpe ceiling
  (adds VRP, a different return source) BUT priced on modeled IV (DVOL+BS, no real bid/ask,
  ATM vol on OTM puts ignoring skew, tail under-modeled, leverage no funding, window bias)
  -> headline blend Sharpe 1.21 must be discounted until validated on real option quotes
- next steps: real Deribit quotes/skew, crash-week stress, testnet paper trading
2026-06-19 21:43:08 +02:00
Adriano 58fc10de77 research tracks H+I: volume/vol/range + alt-momentum/reversal (both NEGATIVE for alpha)
- trackH volume_vol: no uncorrelated additive edge; profitable signals are trend-in-disguise
  (corr 0.6-0.75); MR/declining-volume fade dead even at fee 0; OBV-up filter is a defensive
  DD overlay only (13.3->10.1% DD but -CAGR), not new alpha
- trackI momentum/reversal: no formulation beats 1-3-6m sign-blend OOS on both assets;
  z-score continuous momentum = same edge (corr 0.96), lower DD 8.4% but lower CAGR;
  long-horizon reversal not bankable (negative/flat standalone). ~1.3 Sharpe ceiling holds.
- TP01 (12h sign-blend) remains the deployable winner
2026-06-19 21:22:49 +02:00
Adriano eac2aa1d00 audit+fix: anti-look-ahead audit, migrate deployable config to >=12h
- trackD_lookahead_audit.py: relabel test (left==right, no labeling leak) + execution-lag
  stress -> our trend pipeline is CLEAN (4h Sharpe 1.36 robust to +1 bar lag, label-invariant)
- ADOPT conservative conclusion: deploy at 12h (sub-12h: costs/overfit dominate, slight Sharpe
  bump unreliable). 12h: Sharpe 1.32, DD 13.3%, CAGR 16.2% ~ identical and robust
- trend_portfolio: DEPLOY_TF=12h, resample_tf(rule); paper trader + tests on 12h
- calendar research (NEGATIVE, both): trackF seasonality (spurious), trackG prior-levels
  (breakouts continue, fade dead; only long-drift survivor, redundant with TP01)
- gitignore data/paper_trend runtime state
2026-06-19 21:13:57 +02:00
Adriano 7b34e11476 docs: update CLAUDE.md with post-research state (TP01 winner + research outcome) 2026-06-19 20:36:01 +02:00
Adriano ae7f3d17f2 deploy: TP01 trend portfolio (PORT LF4h) module + paper trader
- src/strategies/trend_portfolio.py: canonical winner, causal/no-leakage,
  reproduces CAGR +16.5% Sharpe 1.36 maxDD 13.8%
- scripts/live/paper_trend.py: forward-only paper trader, persistent state, resume
- tests/test_trend_portfolio.py: 5 tests (causality, profitability, long-only, paper parity)
2026-06-19 20:35:28 +02:00
Adriano 3b6ff02197 fix: resolution-safe timestamp in trackD_timing resample (pandas 2.x datetime64[ms])
- combination study: PORT LF4h (BTC+ETH) Sharpe 1.32 DD 12.3% remains best
- RV ETH/BTC market-neutral sleeve is genuinely uncorrelated (~0.05) but too weak
  (Sharpe 0.27) to raise portfolio Sharpe; combining the two TF configs is redundant
  (same-asset cross-config corr 0.80)
2026-06-19 20:18:03 +02:00
Adriano 8dbdadd509 research: add per-year trade counts + turnover to trackD_timing 2026-06-19 20:09:32 +02:00
Adriano 33267584d9 research: Track D winner across timeframes (15m-1d) + per-year PnL/DD
- trackD_timing.py: same TSMOM 1-3-6m blend config sampled at 15m/1h/4h/1d
- robust plateau across all TFs; 4h marginally best (LF Sharpe 1.36, DD 13.8%)
- per-year PnL and per-year max drawdown tables
2026-06-19 19:39:02 +02:00
Adriano dc2b5697da research wave 1: 5 honest tracks on certified BTC/ETH + synthesis
- trackA trend, trackB ML, trackC mean-rev, trackD trend-portfolio, trackE xsec/ensemble
- VERDICT: Track D vol-targeted BTC+ETH trend portfolio is the one robust deployable
  earner (Sharpe 1.0-1.32, DD 13-19%, positive every year 2019-2026)
- mean-reversion confirmed dead on clean data; weak-but-real ML/trend residuals
- honest: EUR50/day on 2000 in 1-2y is not reachable (needs ~137k capital or ruinous DD)
2026-06-19 19:14:53 +02:00
Adriano 6b9c469832 research: rebuild certified BTC/ETH feed + honest backtest harness
- rebuilt BTC/ETH from Deribit mainnet (certified 1.7-1.9bps vs Coinbase)
- archived contaminated alt data to Old/data/raw
- add src/backtest/harness.py: leakage-free, fee-aware signal engine
  (entry at close[i], intrabar TP/SL, CAGR/Sharpe/DD/per-year/OOS)
2026-06-19 18:41:15 +02:00
70 changed files with 58 additions and 5554 deletions
-11
View File
@@ -1,11 +0,0 @@
Old/
data/
.venv/
.git/
logs/
__pycache__/
**/__pycache__/
*.pyc
.env
.env.mainnet
docs/
-7
View File
@@ -43,12 +43,5 @@ data/games/
# archived data (mirrors top-level data/ ignores, which are top-level-anchored)
Old/data/
Old/**/__pycache__/
# run logs (rigenerabili dagli script)
logs/
# cache della ricerca trackE (rigenerabile)
.cache_trackE_*.npy
# feed backup pre-rebuild (binari rigenerabili, NON in git) + stato paper trader (runtime)
data/_feed_backup/
data/paper_trend/
+21 -85
View File
@@ -21,65 +21,19 @@ Cosa è cambiato:
Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condiviso
`src/backtest/harness.py`). Sintesi in `docs/diary/2026-06-19-research-synthesis.md`.
- **TP01 Trend Portfolio — strategia DIFENSIVA robusta (non alpha)** —
`src/strategies/trend_portfolio.py`. TSMOM multi-orizzonte (1-3-6 mesi) vol-targeted, long-flat,
50/50 BTC+ETH. Config canonica **PORT LF1d** (**>=12h, 1d raccomandato**, vol-target 20%, leva cap 2x):
**FULL Sharpe ~1.30, maxDD ~14%; HOLD-OUT 2025-26 Sharpe ~0.31 / +3.5%** mentre il buy&hold 50/50
faceva 39%/DD60%. Verificata indipendentemente col gauntlet onesto (hold-out + cross-asset +
plateau + deflated-Sharpe 0.999): **regge**. **Valore = taglio del drawdown ~6× vs buy&hold**, NON
generazione di ritorno (CAGR ~16% vs ~48% del buy&hold sul toro).
⚠️ **LOOK-AHEAD (2026-06-19):** un ffill MIXED-TIMEFRAME su barre open-labeled gonfiava il 4h
(~1.60 → reale ~1.1). Il calcolo per-singolo-TF è leak-free, ma **NON scendere sotto le 12h**:
costi+overfitting dominano senza vantaggio (FULL Sh piatto ~1.3 da 12h a 4h; hold-out migliore a 1d).
Deploy/paper a **1d**. Diari `2026-06-19-tp01-verification.md` / `-tp01-lookahead-fix-lf.md`.
Paper trader: `scripts/live/paper_trend.py` (1d). Test: `tests/test_trend_portfolio.py`.
Ri-verifica: `scripts/analysis/{verify_tp01,stress_tp01,tp01_lowfreq}.py`.
- **XS01 Cross-Sectional Momentum (Hyperliquid) — DIVERSIFICATORE che migliora il portafoglio** —
`src/portfolio/sleeves.py:_xsec_returns`. Market-neutral su **19 alt liquidi major** Hyperliquid (1d,
dal 2024): ogni 10g long i 5 più forti / short i 5 più deboli, vol-target 20%. **Scorrelato a TP01
(~0.12).** Affinato (2026-06-19): **(a) blend di lookback [30,90]** (z-score cross-sectional mediato,
come il multi-orizzonte di TP01); **(b) gate di dispersione p30** (entra solo se la dispersione
cross-section del momentum supera il percentile espandente causale, altrimenti flat — XS è rumore in
regime compatto). Standalone FULL Sh **1.50** / HOLD 1.71 / DD 11%, plateau robusto (lookback, gate
p15-35). **Caveat:** storia ~2.5 anni; STAT-MODE (book a 19 gambe non eseguibile a 2k, serve ~20k) →
monitor forward. NB il gate concentra XS nei regimi dispersi (2025-26 = hold-out alta-dispersione).
Ricerca `scripts/portfolio/{xsec_research,xsec_blend,xsec_dispgate}.py`. Diari `2026-06-19-hyperliquid-xsec`
/ `-xsec-blend` / `-xsec-dispgate` / `-xsec-universe-expansion` / `-trend-multiasset`.
- **PORTAFOGLIO ATTIVO = TP01 (55%) + XS01 (25%) + VRP01 (20%)** (`src/portfolio/sleeves.active_sleeves`):
TP01+XS01 combinato **FULL Sharpe 1.55, HOLD-OUT 1.55, DD 4.4%**. Aggiunto **VRP01** (options
short-vol, sotto): TP01+VRP01 da solo fa FULL Sh 1.30→1.44 / HOLD 0.31→0.40 a peso 20% (3-way da
validare locale con dati HL). Report `scripts/portfolio/run_portfolio.py`. Sleeve a date d'inizio
diverse → outer-join con pesi rinormalizzati (TP01 da solo 2019-20, VRP dal 2021, blend pieno dal 2024).
- **VRP01 Options Short-Vol — DIVERSIFICATORE da FinanceOld/OptionsAgent** — `src/portfolio/sleeves._vrp_combo_returns`.
Put credit spread settimanale (vendi put -0.28, compra put -0.10) gated su IV-rank. Idee portate da
`../FinanceOld/OptionsAgent` (Bear Call Spread + gate d'ingresso). Migliora il lead VRP nudo
(options_vrp_lab): **(a) defined-risk** taglia la coda (worst-week -16.6%→-7.4%, DD 33%→14%);
**(b) gate IV-rank>0.30** = vendi vol solo ricca → ribalta HOLD-OUT da -0.25 a +0.28 (l'alpha è il
filtro di regime). Standalone **FULL Sh 1.10, HOLD 0.60, DD 12%**, positivo/piatto ogni anno (2022
crash incluso). Scorrelato a TP01 (~+0.01-0.07). **CAVEAT:** premio MODELLATO su DVOL ATM (skew non
esplicito), book a 1d, f di stress reale non catturato → LEAD robusto, non deploy pieno. Ricerca
`scripts/research/options_vrp_v2.py` (vs baseline `options_vrp_lab.py`). Test `tests/test_vrp_sleeve.py`.
Diario `2026-06-20-financeold-analysis-vrp-v2.md`.
- **Universo Hyperliquid: ESPANDERLO NON aiuta XS01** (provato): 52-asset / top-liquidità dinamico /
trend-multi-asset → tutti peggiori (small-cap/memecoin diluiscono il momentum relativo; il trend
multi-asset è ridondante con TP01, corr 0.74). I margini su XS sono nella STRUTTURA DEL SEGNALE
(blend + gate), non nel numero di asset. I **51** parquet certificati restano per ricerca futura.
⚠️ Il test "52-asset = negativo" era in parte inquinato dal backfill sintetico (AXS 83%, ALGO/SAND
37% di barre vol=0) poi rimosso — vedi correzione estrazione 2026-06-20 sotto; resta comunque vero
che il long-tail diluisce XS01, ma il numero netto post-fix è 51.
- **Lead OPZIONI VRP (income short-vol) — quantificato, NON deploy** — `scripts/research/options_vrp_*.py`.
Vendita put settimanali che incassa il volatility risk premium (IV>RV), scorrelato al trend (~0.07).
Premio prezzato BS su DVOL reale (`fetch_dvol.py`) + calibrato su quote REALI cerbero-bite mainnet
(`options_vrp_calibrate.py`): **f reale ≈ 1.0** (non 1.29) → Sharpe ~0.71, DD 33%, coda severa
(settimane 15..26% su LUNA/FTX). Diversificatore DEBOLE a premio reale, e short-vol da modello.
**Regola: niente short-vol da modello in deploy.** Rivalutare quando cerbero-bite cattura un crash
(per il f di stress reale). Diari `2026-06-19-options-vrp-lab` / `-eval-crypto-backtest-options`.
- **Edge deboli/scartati:** ML walk-forward BTC (Sh ~0.57), trend 1h L/S (~1.0), RV ETH/BTC (Sh 0.27,
regime-luck), calendar/seasonality (buy&hold travestito), volume/vol e momentum-reversal (negativi).
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero — la vecchia
libreria +201%/+1238% era contaminazione); trend 5m/15m (fee).
- **Soffitto strutturale BTC/ETH-direzionale ~1.3** superato SOLO espandendo a un meccanismo diverso:
cross-sectional su universo Hyperliquid certificato (XS01) → portafoglio Sharpe ~1.55.
- **VINCITRICE (l'unica robusta e profittevole): TP01 Trend Portfolio** —
`src/strategies/trend_portfolio.py`. TSMOM multi-orizzonte (1-3-6 mesi) vol-targeted,
50/50 BTC+ETH. Config canonica **PORT LF4h** (4h, long-flat, vol-target 20%, leva cap 2x):
**CAGR ~16.6%, Sharpe ~1.32-1.36, maxDD ~12-14%, positiva ogni anno 2019-2026**.
Robusta su tutti i TF (15m-1d), regge fee fino a 0.40% RT, su entrambi gli asset.
Paper trader: `scripts/live/paper_trend.py`. Test: `tests/test_trend_portfolio.py`.
- **Edge deboli ma reali** (NON standalone, NON migliorano il portafoglio): ML walk-forward
su BTC (Sharpe ~0.57), trend 1h long-short (Sharpe ~1.0), relative-value market-neutral
ETH/BTC (scorrelato ~0.05 ma Sharpe solo 0.27 → troppo debole per alzare lo Sharpe).
- **MORTO/confermato artefatto:** mean-reversion / fade (negativo anche a fee zero su dati
certi — la vecchia libreria +201%/+1238% era pura contaminazione); trend 5m/15m (fee).
- **Soffitto strutturale:** con i soli BTC/ETH lo Sharpe di portafoglio si ferma a **~1.3**.
Combinare TF o aggiungere la RV non aiuta (ridondanza/edge troppo debole).
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
@@ -105,17 +59,13 @@ netto fee, out-of-sample, robusto su griglia, e su dati certificati + liquidi +
src/data/downloader.py → load_data(asset, tf): legge i parquet certificati da data/raw/
src/strategies/base.py → Strategy (ABC), Signal, BacktestResult, YearlyStats
src/strategies/indicators.py → indicatori condivisi (ema, atr, keltner, ...)
src/strategies/trend_portfolio.py → TP01: strategia DIFENSIVA robusta (PORT LF1d, >=12h), causale
src/portfolio/ → PORTAFOGLIO DI STRATEGIE estensibile (Sleeve + StrategyPortfolio)
portfolio.py → combina N sleeve per peso su griglia giornaliera; metriche FULL/hold-out/anno
sleeves.py → REGISTRY sleeve attivi: TP01 (55%) + XS01 (25%) + VRP01 (20%). Aggiungere = una riga
src/strategies/trend_portfolio.py → TP01: strategia VINCENTE (PORT LF4h), causale, deployabile
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
src/backtest/engine.py → engine di backtesting riusabile
src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals no-leakage, OOS)
src/version.py → APP_VERSION (legge il file VERSION)
scripts/research/ → ricerca: track{A-I}_*.py + options_vrp_*.py + fetch_dvol.py
scripts/portfolio/ → run_portfolio.py (report) + xsec_*.py (ricerca/affinamento XS01)
scripts/live/paper_trend.py → paper trader forward-only di TP01 (1d) (no esecuzione reale)
scripts/research/ → ricerca post-reset: track{A-E}_*.py (trend/ML/MR/portfolio/xsec)
scripts/live/paper_trend.py → paper trader forward-only di TP01 (no esecuzione reale)
scripts/analysis/ → SOLO i tool dati certificati:
rebuild_history.py → (ri)costruisce lo storico da Deribit mainnet (base 5m + resample)
certify_feed.py → certifica il feed (integrità, coerenza resample, spike, cross-venue)
@@ -137,10 +87,7 @@ uv run python scripts/analysis/certify_feed.py # certifica i feed
uv run python scripts/analysis/certify_feed.py --local # solo check locali (veloce)
uv run python scripts/research/trackD_trendport.py # backtest strategia vincente (full report)
uv run python scripts/research/trackD_timing.py # vincitrice su 15m/1h/4h/1d + PnL/DD/trade per anno
uv run python scripts/analysis/fetch_hyperliquid.py # fetch+certify universo Hyperliquid (Cerbero mainnet) -> data/raw/hl_*
uv run python scripts/portfolio/xsec_research.py # ricerca cross-sectional su Hyperliquid (XS01)
uv run python scripts/portfolio/run_portfolio.py # report del PORTAFOGLIO attivo (TP01+XS01)
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only, 1d)
uv run python scripts/live/paper_trend.py # avanza il paper trader TP01 (forward-only)
uv run pytest # test
```
@@ -164,21 +111,10 @@ df = load_data("BTC", "1h") # OK. load_data("SOL", ...) -> FileNotFoundError (
### Universo ricercabile certificato
- **BTC / ETH**: puliti (2-6 bps vs Coinbase USD su tutta la storia), liquidi (~0% barre flat a 1h),
storia lunga (2018/2019→oggi) → **ogni timeframe (5m/15m/1h)**. È l'unico dato in `data/raw`.
- **Alt Deribit (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat, run ~3 giorni),
divergenti, o non certificabili. Archiviati in `Old/data/raw`.
- **Universo Hyperliquid (Cerbero MCP MAINNET): 19 alt liquidi a 1d, dal 2024** — BTC/ETH/SOL/BNB/XRP/
DOGE/AVAX/LINK/LTC/ADA/ARB/OP/SUI/APT/INJ/TIA/SEI/NEAR/AAVE. Certificati (`fetch_hyperliquid.py`):
flat 0%, cross-venue 4-9 bps vs Binance, >1% ≈0% → `data/raw/hl_*_1d.parquet`. **Caveat:** storia
nativa solo **~2.5 anni** (2024-2026; pre-2024 = backfill, vol 0). Abilita le strategie
CROSS-SECTIONAL (impossibili a 2 asset). NB: Cerbero col token TESTNET = farlocco; col token
**mainnet** (`.env.mainnet`) = reale, ma SEMPRE da certificare (cross-venue + liquidità).
⚠️ **CORREZIONE estrazione (2026-06-20):** il backfill NON è solo pre-2024 — cerbero MCP padda con
barre SINTETICHE (volume 0, prezzi copiati da Binance → matchano cross-venue e non sono flat) ogni
asset listato su HL **dopo** lo START. Il `flat`+cross-venue da soli non lo vedono: il rivelatore è
il **VOLUME**. `fetch_hyperliquid.py` ora (1) taglia il run iniziale a volume 0, (2) scarta chi resta
< 365g reali (es. **AXS 83% sintetico → fuori**), (3) gata i gap vol=0 interni. Universo certificato
= **51** (era 52). I **19 major di XS01 hanno 0 backfill → invariati** (strategia live non toccata).
Verificato direttamente su cerbero MCP. Diario `2026-06-20-cerbero-backfill-fix.md`.
- **Alt (SOL/XRP/ADA/LTC/DOGE/BNB): FUORI.** Illiquidi (LTC 5m 82% barre flat O=H=L=C, run fino a
~3 giorni), divergenti (LTC/DOGE >1% su 10-21% delle barre 2022-23), o non certificabili
(XRP delistato da Coinbase per causa SEC; BNB non listato + storia da 2024-10). Sono archiviati in
`Old/data/raw`. Riammetterne uno richiede prima una ricertificazione che dimostri liquidità + accordo.
## Metodologia obbligatoria per ogni nuova strategia
-11
View File
@@ -1,11 +0,0 @@
FROM python:3.11-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev
COPY src/ src/
COPY scripts/ scripts/
COPY VERSION ./
VOLUME /app/data
# Monitor PAPER del portafoglio attivo (TP01+XS01). Esecuzione REALE disabilitata.
CMD ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
-7
View File
@@ -1,7 +0,0 @@
{
"_nota": "Config esecuzione LIVE di TP01. execution_enabled=true + --execute -> ordini REALI. ARMATO 2026-06-20.",
"execution_enabled": true,
"max_notional_per_asset_usd": 300,
"min_order_usd": 5,
"disaster_sl_pct": 0.30
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-15
View File
@@ -1,15 +0,0 @@
# Solo MONITOR (dashboard paper) del portafoglio attivo. Niente runner/esecuzione reale
# (archiviati in Old/). v2.0.0+.
services:
dashboard:
build: .
container_name: pythagoras-dashboard
restart: unless-stopped
command: ["uv", "run", "python", "-m", "src.live.dashboard", "--port", "8787"]
ports:
- "8787:8787"
volumes:
- ./data:/app/data:ro
# token mainnet (sola lettura) per lo "Shadow live": conto/posizioni reali sulla dashboard.
# Montato a runtime (NON nell'immagine: .env.mainnet e' dockerignored). Solo letture, nessun ordine.
- ./.env.mainnet:/app/.env.mainnet:ro
-35
View File
@@ -1,35 +0,0 @@
# 2026-06-19 — Wave 1 "beat TP01" (26 agenti BTC/ETH): nessun 3º sleeve robusto
Goal "trova strategie che battano l'esistente e inseriscile": GIA' soddisfatto da XS01 (cross-
sectional Hyperliquid, integrato → portafoglio TP01 70% + XS01 30%, FULL Sh 1.41 / HOLD 1.15).
In parallelo, una wave di 26 agenti ha cercato su BTC/ETH miglioramenti del trend + diversificatori.
## Esito wave 1 (26 agenti, 25 leak-free): 22 weak, 3 "contender", 1 noise
I 3 contender, ri-verificati ONESTAMENTE col giudice book-level (`verify_contender.py`) e come
contributo marginale al portafoglio ATTUALE (TP01+XS01):
| Candidato | corr TP01 | corr XS01 | +portafoglio (w30%) | Verdetto |
|---|---|---|---|---|
| **tsmom_strength_12h** | **+0.49** | — | — | ☠️ scartato: è TP01 più veloce (correlato), non diversifica |
| **breakout_atr** (trend) | 0.04 | 0.04 | FULL +0.48 / **HOLD +0.05** | ☠️ scartato: gonfia solo il FULL storico (bull), ~zero valore nel hold-out |
| **highvol_rev** (reversal alta-vol) | 0.08 | 0.05 | FULL +0.20 / **HOLD +0.30** | 🟡 WATCHLIST (vedi sotto) |
## highvol_rev: candidato vero ma NON abbastanza robusto → watchlist
È l'unico genuinamente scorrelato a ENTRAMBI gli sleeve e che migliora FULL+hold-out. MA il mio
robustezza-check indipendente (plateau, come per XS01) lo boccia per il deploy:
- **Edge solo a REV_LB=1**: LB2 FULL Sh 0.33, LB3 ~0.05 → **picco a singola-barra, non plateau**.
- **FULL standalone mediocre** (0.74); la forza è nel hold-out (HOLD 0.97-1.39 vs FULL ~0.7) =
**HOLD≫FULL = regime-luck dell'alta-vol 2025-26**, non robustezza temporale.
- È un **reversal** (famiglia morta in tutto il progetto) con concept ribaltato post-hoc
(low-vol→high-vol). Regge fee fino ~0.3% ma con margine ridotto.
Stesso difetto (HOLD≫FULL, no-plateau) per cui ho bocciato ieri il RV ETH/BTC regime-luck. La
disciplina che boccia i falsi positivi vale anche qui → **NON deployato**, in watchlist; rivalutare
forward (più dati) o se emerge un plateau su un parametro core.
## Conclusione
Wave 1 NON aggiunge un 3º sleeve robusto. **Portafoglio invariato: TP01 (70%) + XS01 (30%).** Le
famiglie trend (breakout/tsmom-12h) sono ridondanti con TP01 o aiutano solo il bull storico; l'unico
diversificatore di meccanismo nuovo (highvol_rev) non regge il bar di robustezza. Il vero edge
incrementale è venuto dall'ESPANSIONE DELL'UNIVERSO (Hyperliquid → cross-sectional), non da altre
varianti di trend su 2 asset. Direzione futura coerente: più asset certificati + sleeve di
meccanismo nuovo (non altre trend-variant), col criterio plateau+breadth+contributo.
@@ -1,62 +0,0 @@
# 2026-06-19 — Ricerca frattale multi-agente (63 agenti) su BTC/ETH
Su richiesta: 50+ agenti in parallelo a cercare strategie NUOVE ispirate ai due documenti
frattali (`Libro_frattali` + `Pythagoras_Trading_Prediction`), timing/asset diversi, ognuna
validata sull'harness onesto. Eseguito come Workflow: **63 agenti, ~2h, 3.8M token.**
## Cosa è stato testato
16 concetti frattali estratti dai documenti (sotto la patina esoterica: coscienza, frequenze
Solfeggio, numeri sacri → idee testabili): alfabeto candle U/D/0 (3-6, LONG), Fourier/cicli,
ricorrenza di Poincaré (analoghi kNN), centro di inversione Evideon (mirror tempo+prezzo),
indicatore H-C (~588/25 ≈ 23.5 barre), numeri-universali come periodi, invarianza di forma,
entropia di Shannon ("coscienza") come gate, confluenza multi-TF, grammatica composizionale,
fase-ricorrenza. × BTC/ETH × {5m,15m,1h} = **52 ipotesi**.
Ogni segnale: scritto da un agente come funzione `signal()`, valutato da `eval_signal.py`
(stesso harness onesto) con **guard automatico anti-look-ahead** (ricalcolo su prefissi).
Superstiti → verifica avversariale con sblocco una-tantum del **hold-out 2025-26**.
## Esito
- **Verdetti**: 29 rumore, 12 "real" (netto-fee positivo ma non battono il buy&hold), 11 "edge"
(in-sample: battono B&H + null p<0.05 + leak=0).
- **Guard anti-look-ahead**: nessun leak passato (gli agenti hanno prodotto segnali causali; i
pochi tentativi con futuro sono stati auto-squalificati e corretti).
- **Hold-out (la prova del nove)**: dei **11 superstiti in-sample, 10 REFUTATI** — performance
catastrofica nel 2025-26 (hurst-DFA 0.49, hc-cycle 0.83, vol-accel 1.16, universal-periods
0.42…−1.04, spectral-entropy 0.38/+0.29, multitf 0.49, solfeggio-BTC 0.64). Stessa firma di
sempre: **regime-luck del toro 2018-2024, sparito out-of-sample.**
- **1 "confermato"** dalla verifica per-agente: `momentum_solfeggio_cycle` **ETH 1h** (holdout
Sharpe +1.19, ret +49% mentre il buy&hold ETH faceva 49%). Sembrava un trionfo.
## Ma il "vincitore" cade al test cross-asset (kill decisivo)
Il guard per-agente valuta un asset alla volta e non poteva vedere il quadro. Ho rieseguito **lo
stesso identico codice** sui due major:
| Signal (identico) | FULL Sharpe | HOLD-OUT Sharpe | HOLD-OUT ret |
|---|---|---|---|
| Solfeggio-cycle su **ETH** 1h | 1.54 | **+1.19** | +49% |
| Solfeggio-cycle su **BTC** 1h | 1.17 | **0.25** | 7.5% |
Un edge robusto non fallisce sull'altro major. La stessa logica (long-only ~20% esposta, filtro
SMA(588), timing su ciclo ~24) che ha "schivato" il crash ETH 2025 **perde su BTC nello stesso
hold-out**. È **fortuna di regime di un singolo asset**, non skill. Aggravanti: costanti
numerologiche ad-hoc (24/588/56, "odore" di overfit, già notato dal verificatore); e con 52 trial,
trovare 1 segnale che passa un singolo regime di hold-out è atteso per puro caso (1/11 ≈ chance).
## VERDETTO
**La ricerca frattale multi-agente (52 ipotesi, 63 agenti) NON ha trovato alcun edge robusto.**
I concetti frattali/esoterici si sono comportati esattamente come le famiglie convenzionali (Fasi
1-3): edge in-sample da regime-luck del toro, refutati dal hold-out; e l'unico che passava il
hold-out su un asset fallisce sull'altro. **Nessuna magia nei numeri Solfeggio/sacri.**
Il valore: il processo disciplinato (guard anti-look-ahead + hold-out bloccato + **test cross-asset**)
ha catturato un falso "trionfo" (+49% vs 49%!) che sul vecchio sistema contaminato sarebbe finito
dritto in produzione. È la quinta conferma indipendente che su BTC/ETH non c'è un edge facile.
## Stato della ricerca dopo tutte le fasi
Testato: mean-reversion, momentum/trend, vol, lead-lag, hurst, shape-ML, e 16 famiglie frattali ×
multi-TF/asset. **Niente di robusto, fee-surviving, OOS e cross-asset.** Le direzioni oneste
restano: (a) accettare il ceiling = long risk-managed (no alpha); (b) allargare l'universo dati
CERTIFICATO oltre BTC/ETH; (c) fonti di segnale ortogonali al prezzo (on-chain, basis multi-venue,
opzioni multi-regime) — tutte richiedono nuovi dati certificati. Artefatti: `eval_signal.py`,
workflow `fractal-strategy-search`, ~52 segnali in `/tmp/pyth_sig_*.py`.
-60
View File
@@ -1,60 +0,0 @@
# 2026-06-19 — Espansione universo (Hyperliquid via Cerbero mainnet) → XS01 batte il portafoglio
L'utente: "ci dovrebbe essere uno storico dati preso da cerbero". Aveva ragione, ed è la chiave per
superare il soffitto a 2 asset.
## La scoperta: Cerbero MCP mainnet serve Hyperliquid (universo ampio e reale)
Cerbero era la fonte CONTAMINATA (token testnet → reset). MA col token **mainnet** (`.env.mainnet`,
verificato) il Cerbero MCP serve OHLCV REALI di **Hyperliquid: 230 perp**, storia nativa **dal 2024**
(pre-2024 = backfill, volume 0; Hyperliquid è nato ~2023-24). Prezzi recenti plausibili.
## Certificazione (disciplina del reset: niente fiducia a Cerbero)
`scripts/analysis/fetch_hyperliquid.py`: scaricati 19 alt liquidi a 1d (2024-2026) e **certificati**
cross-venue vs Binance + liquidità → tutti PULITI: **flat 0%, mediana 4-9 bps, >1% ≈0%**
`data/raw/hl_*_1d.parquet` (namespace dedicato). Caveat onesto: **~2.5 anni** di storia nativa.
## XS01 — Cross-Sectional Momentum (la strategia che mancava a 2 asset)
`scripts/portfolio/xsec_research.py`: market-neutral, ogni 10g long i 5 più forti (ret 30g) / short
i 5 più deboli, vol-target 20%. Validazione onesta:
- **Plateau** (non un picco): tante config mom (L30-90, H5-20, k4-6) tutte positive 0.6-0.98.
- **Fee-robusto**: FULL Sh 0.79→0.68 da fee 0% a 0.3% RT.
- **Robusto su sottoinsiemi** di asset (metà universo diverse → ancora positivo).
- **Scorrelato a TP01 (~0.06)**, **positivo OGNI anno** (2024 +2%, 2025 +19%, 2026 +20%).
- **Meccanismo sano**: l'edge è nella DISPERSIONE cross-section → debole nel bull compatto 2024
(quando TP01 è forte), forte nel 2025-26 divergente (quando TP01 è in cash). **Complementare**.
Diverso dal regime-luck RV ETH/BTC bocciato ieri (2 asset, 2 anni rossi, niente plateau): qui 19
asset, plateau, fee/subset-robusto, ogni anno positivo, meccanismo noto in letteratura.
## Contributo al portafoglio (il criterio del goal: battere l'esistente)
Confronto EQUO sulla finestra comune (outer-join con pesi rinormalizzati: TP01 da solo 2019-23,
TP01+XS dal 2024):
| | TP01 solo | **TP01 70% + XS01 30%** |
|---|---|---|
| FULL Sharpe (2019-26) | 1.30 | **1.41** |
| **HOLD-OUT 2025-26 Sharpe** | 0.31 | **1.15** |
| HOLD-OUT ret / DD | +3.5% / 7.5% | **+15.1% / 5.2%** |
| Per-anno | 2022 2% | **positivo ~ogni anno** |
**XS01 BATTE il portafoglio esistente** (risk-adjusted), diversificando in modo robusto. Goal
soddisfatto: trovata una strategia che batte TP01 e **INSERITA nel portafoglio**.
## Integrazione
- `src/portfolio/portfolio.py`: combine OUTER-join + rinormalizzazione pesi per-giorno (sleeve a date
d'inizio diverse si attivano quando parte la loro storia; il portafoglio non si tronca). Test nuovo.
- `src/portfolio/sleeves.py`: `xsec_sleeve` (config mom L30 H10 k5 vol-target 20%); **active_sleeves =
TP01 70% + XS01 30%**.
- `fetch_hyperliquid.py`, `xsec_research.py`. 12 test passano.
## Caveat onesti (da non dimenticare)
- **Storia XS solo ~2.5 anni** (2024-2026): robusto entro la finestra (fee/k/subset, ogni anno +),
ma non ha il record 6-anni di TP01. Cross-sectional momentum è literature-robust → prior favorevole.
- **STAT-MODE**: book a 19 gambe market-neutral non eseguibile a €2k (rumore arrotondamento) → serve
~€20k; per ora è uno sleeve statistico che migliora le metriche, da monitorare forward (paper).
- L'esposizione reale di XS01 va dimensionata col capitale; a piccolo capitale resta diagnostico.
## Stato
Portafoglio attivo = **TP01 (70%) + XS01 (30%)**, FULL Sh 1.41 / HOLD 1.15. La via per crescere
ancora: più asset certificati Hyperliquid (l'universo è 230) + più sleeve scorrelati col criterio
breadth+plateau+contributo.
-81
View File
@@ -1,81 +0,0 @@
# 2026-06-19 — Options VRP sleeve: infrastruttura + prima validazione onesta
Impostata la ricerca dello sleeve income opzioni (vendita put settimanali, incassa il volatility
risk premium IV>RV). Lead identificato dalla valutazione di `crypto_backtest` come la via per
superare il soffitto Sharpe ~1.3 (fonte di rendimento DIVERSA, scorrelata al trend).
## Infrastruttura costruita
- `scripts/research/fetch_dvol.py`: storia DVOL (IV 30d Deribit) BTC/ETH **2021-03 → 2026-06**
(1914g) → `data/raw/dvol_*.parquet`. È l'input IV.
- `scripts/research/options_vrp_lab.py`: motore backtest CSP settimanale. Prezzo put BS su DVOL
reale + **calibrazione f** (skew/spread vs quote reali), strike a delta target, payoff sul path
realizzato dei prezzi certificati. Causale (decisione a sell-date, payoff a scadenza). Gauntlet:
VRP context, sweep f/delta, per-anno, worst-weeks (coda), correlazione + contributo vs TP01.
- `scripts/research/options_real_quote_check.py` (dal branch): verifica premio su quote reali.
## VRP reale (contesto)
BTC DVOL 61% vs RV 53% → **VRP +7.8 pt, positivo 78% del tempo**; ETH +3.7 pt, 67%. Il premio di
volatilità esiste ed è più ricco su BTC.
## Risultati (book 50/50 BTC+ETH, put settimanali delta -0.28)
**Tutto dipende dalla CALIBRAZIONE f del premio:**
| f | Sharpe | CAGR | maxDD | worst-week |
|---|---|---|---|---|
| 0.70 | 0.32 | 12% | 51% | 26% |
| 0.85 | 0.20 | +1% | 35% | 26% |
| **1.00 (conservativo, IV-ATM)** | **0.71** | +16% | 33% | 26% |
| 1.15 | 1.22 | +34% | 32% | 25% |
| **1.29 (reale calm, con skew)** | **1.70** | +52% | 31% | 25% |
- A f=1.0 (ignora il bonus skew): Sharpe **0.71** — SOTTO TP01. A f=1.29 (skew reale misurato in
regime calmo): **1.70**. La verità sta in mezzo E f varia col regime (skew più alto nello stress).
- **Delta**: più ATM = più premio + più rischio (0.15→Sh 0.25, 0.28→0.71, 0.40→0.95).
**La CODA è severa (è short-vol):** maxDD standalone **30-33%**, singole settimane **15..26%**
(2021-05 crash, 2022-05/06 LUNA, 2026-02/06). Per-anno (f=1.0): 2022 **9%**, 2026-YTD **14%** —
sanguina negli anni di crash. HOLD-OUT 2025-26: Sharpe **0.04** a f=1.0 (piatto), 0.94 a f=1.29.
**Diversificazione (reale):** corr settimanale a TP01 **+0.07** (scorrelato). Contributo (f=1.0):
TP01 70% + OPT 30% → Sharpe settimanale 0.71→**0.97**, DD basso (11%). Anche al premio conservativo
migliora il portafoglio per pura decorrelazione.
## Verdetto — LEAD reale, NON deploy-ready
- ✅ Il VRP è reale (IV>RV 78%), lo sleeve è **genuinamente scorrelato** al trend (+0.07) e
**migliora il portafoglio** anche a premio conservativo. È la fonte di rendimento DIVERSA che
cercavamo per superare il soffitto ~1.3.
- ⚠️ MA: (a) le metriche headline dipendono da una calibrazione **ottimistica** (f=1.29);
conservativo (f=1.0) → Sharpe 0.71 con **DD 33%**. (b) Premio **MODELLATO** (BS su DVOL), non un
backtest su catena reale; la verifica su quote reali è UN solo snapshot calmo. (c) Il **rischio di
coda** (roll/assignment/gap nello stress, skew che esplode) NON è pienamente catturato.
- Regola del progetto: **mai deployare uno short-vol prezzato da un modello.** → NON aggiunto al
portafoglio. Portafoglio attivo invariato: TP01 70% + XS01 30%.
## CALIBRAZIONE su quote REALI cerbero-bite (`options_vrp_calibrate.py`) — corregge l'ottimismo
cerbero-bite GIA' accumula la catena reale mainnet (option_chain_snapshots, BTC 224k / ETH 237k
righe, 2026-05→oggi). Usandola (non un nuovo snapshotter), misurato il fattore f reale su 223
snapshot/asset (put weekly ~delta -0.28, vendita al BID):
- **BTC: f mediano 1.03** (IQR 0.89-1.21), skew reale **+1.9 pt** (IV put 43.5% vs DVOL 41.6%).
- **ETH: f mediano 0.97** (IQR 0.88-1.11), skew **+1.5 pt**.
- **Il f reale e' ~1.0, NON 1.29.** Lo snapshot singolo del branch (skew +4.8 → f 1.29) era un
OUTLIER; sulla media lo skew e' modesto e il bid/ask lo compensa → premio reale ≈ modellato.
→ Il VRP sleeve sta sul punto **f≈1.0 dello sweep = Sharpe ~0.71** (caso CONSERVATIVO), DD 33%,
hold-out ~piatto (0.04). Non il 1.70 ottimistico. Resta un diversificatore modesto (corr +0.07,
migliora il portafoglio settimanale 0.71→0.97 a 30%), ma standalone SOTTO TP01 e con coda severa.
**CAVEAT:** la finestra di calibrazione reale e' ~10 giorni densi (06-09→06-19, cerbero-bite ruota
le scadenze → i weekly compaiono sparsi) e UN regime calmo. Il f di STRESS resta non misurato.
## Verdetto aggiornato
Al premio REALE (f≈1.0), il VRP sleeve e' un diversificatore DEBOLE (Sharpe ~0.71 < TP01, DD 33%,
hold-out piatto): la modesta decorrelazione NON giustifica il rischio di coda short-vol senza molto
piu' dato reale multi-regime. **Confermato NON-deploy.** Il valore vero arriva solo se cerbero-bite,
continuando ad accumulare, copre un CRASH: lì si misura il f reale di stress e si fa un backtest su
catena reale. Fino ad allora, lead quantificato ma in attesa. Portafoglio invariato TP01 70%+XS01 30%.
## Prossimi passi per graduare il lead a sleeve deployabile
1. **Accumulo forward di quote reali** (bid/ask + skew della put settimanale delta-0.28, ogni giorno,
su più regimi) → sostituire il premio modellato con quello reale e misurare f nello stress.
2. **Stress crash-week con spread reali** (rollabilità, assignment, gap inverso/coin-settled).
3. **Daily-MTM** dello short put per l'integrazione nel portafoglio giornaliero (ora è settimanale).
4. **Paper-trade su Deribit testnet** prima di qualsiasi capitale.
Solo dopo, se regge a premi reali multi-regime, aggiungerlo come 3º sleeve (scorrelato, income).
@@ -1,58 +0,0 @@
# 2026-06-19 — Ricerca v2.0.0: Fase 0 (harness) + Fase 1 (triage superstiti)
Primo log di ricerca post-reset. Universo certificato: BTC/ETH, 1h. Hold-out 2025+ BLOCCATO.
## Fase 0 — harness onesto (`scripts/analysis/research_lab.py`)
Banco di prova causale per costruzione (modello a SERIE DI POSIZIONE: `pos[i]` decisa entro
`close[i]`, guadagna `close[i]→close[i+1]`, fee sul turnover |Δpos|). Metriche
Sharpe/CAGR/DD/exposure/turnover, finestra OOS, **null model a rotazione circolare**
(p-value: il timing batte il caso?), baseline buy&hold, sweep fee.
**Self-test del banco (valida l'HARNESS, non una strategia):**
- buy&hold BTC: Sharpe 0.79 (sanity OK).
- CHEAT look-ahead (pos = segno del rendimento futuro): Sharpe **58**, p=0.005 → l'engine
VEDE un edge reale quando esiste.
- NOISE causale a basso turnover: Sharpe **0.14**, p=0.26 → l'engine NON inventa edge dal
nulla (niente leak, niente skill spuria).
Il banco è affidabile. → ogni numero qui sotto è netto fee e causale.
## Fase 1 — triage dei 2 superstiti (`scripts/analysis/phase1_survivors.py`)
Sul feed pulito solo SH01 (shape-ML) e frammenti HONEST mostravano segnale residuo. Delle
HONEST solo **DIP** è testabile su BTC/ETH (TR01/ROT02 richiedono alt esclusi). Re-implementati
come serie di posizione, passati ai gate onesti.
### DIP reversion (long-only) — ☠️ MORTO
Griglia 3×3 (n, k) **tutta negativa** su entrambi gli asset (nessun plateau). Config centrale
n50 k2.0: FULL Sharpe 0.17 (BTC) / 0.06 (ETH); a fee 0% appena +0.02/+0.09 (niente edge nemmeno
lordo). OOS-VAL marginale (+0.36/+0.16) ma **null p=0.84-0.89** (peggio del caso). Rumore.
### SH01 shape-ML (walk-forward LogReg) — ☠️ FEE-DEAD
Pattern coerente su BTC/ETH, long/short e long-only:
| Variante | Sh fee0% | Sh fee0.05% | Sh fee0.10% | trade/anno | null p |
|---|---|---|---|---|---|
| BTC L/S | +0.32 | 0.70 | 1.71 | 877 | 0.167 |
| BTC long-only | +0.73 | 0.06 | 0.84 | 555 | 0.072 |
| ETH L/S | +0.31 | 0.40 | 1.11 | 773 | 0.137 |
| ETH long-only | +0.46 | 0.04 | 0.53 | 485 | 0.142 |
C'è un **sussurro di segnale LORDO** (Sharpe 0.3-0.7 a fee zero) ma il turnover (485-877
trade/anno) lo divora: a fee reale tutte negative, e **nessuna batte il null** (p>0.05). Net-fee:
rumore.
## VERDETTO Fase 1
**Né DIP né shape-ML sopravvivono su BTC/ETH certificato net-fee.** Nessuno dà Sharpe netto >0,
nessuno batte il null (p<0.05), nessuno batte il **buy&hold** (Sharpe 0.79/0.84 — di fatto la
"strategia" più forte vista finora). Si conferma: i "superstiti" della vecchia libreria erano,
come il resto, non-edge. Chiusi.
## Lead onesto per la Fase 2
L'unico segnale non-nullo è il **gross shape-ML** (Sharpe 0.3-0.7 a fee zero), ucciso dal
turnover. Direzione: esprimere quel segnale a **turnover molto più basso** (orizzonte di holding
lungo, soglia forte, o come GATE di regime invece che flip per-barra) per vedere se il sussurro
lordo sopravvive alle fee. È un lead, NON un edge. Inoltre: la barra reale da battere è il
**buy&hold** (Sharpe ~0.8) — una strategia di timing deve fare meglio di "stai sempre long",
net-fee.
@@ -1,69 +0,0 @@
# 2026-06-19 — Ricerca v2.0.0: Fase 2 (famiglie) + analisi OPTIONS
Universo certificato BTC/ETH. Barra da battere = **buy&hold** (Sharpe 0.79 BTC / 0.84 ETH).
Tutto netto fee 0.10% RT, hold-out 2025+ BLOCCATO. Harness: `research_lab.py`.
## Fase 2 — esplorazione famiglie (`phase2_families.py`)
24 combinazioni famiglia×asset×TF, ognuna: scan griglia → config migliore → gate onesti
(FULL/OOS-VAL, vs buy&hold, null p-value a rotazione, sweep fee).
### Esiti per famiglia
- **REVERSAL (mean-reversion breve): ☠️ MORTA OVUNQUE.** FULL Sharpe da 1 a 5.6 (peggio a
15m: fee-death, 5.6 BTC / 4.6 ETH), gross ≈0, null p 0.45-0.82. **Smentisce definitivamente
la tesi storica del progetto ("l'edge è sempre mean-reversion")**: era artefatto del feed.
- **TSMOM / MA-cross / Donchian (trend, long-only): segnale REALE ma MODESTO.** Le versioni
long-only (basso turnover) battono o eguagliano il buy&hold:
- **MA-cross long-only**: ETH FULL **1.12** / OOS 0.89 / p **0.007**; BTC FULL **0.90** / OOS
1.99 / p **0.040**. Plateau sulla griglia (ETH 12/48 e 48/192 entrambi 1.1), coerente sui
DUE asset, basso turnover (53-106 trade/anno). **Unici 2 a passare: battono B&H + OOS>0 + p<0.05.**
- Donchian long-only: FULL 0.84-0.94, OOS ottimo (BTC 2.37) ma p 0.08-0.10 (pochi trade → null
rumoroso). TSMOM long-only: ETH 0.83 (≈B&H). Le L/S perdono (turnover + short su asset in trend).
- **VOL-TARGET overlay**: ≈ buy&hold (FULL 0.77-0.84), p alto → non distinguibile dal B&H, ma è
un riduttore di vol/DD (mantiene lo Sharpe scalando l'esposizione).
- **HURST-gate, LEAD-LAG BTC↔ETH**: niente. (Hurst-mom ETH p=0.043 ma sotto il B&H; lead-lag
fee-dead.)
### Verdetto Fase 2
L'unica cosa reale su BTC/ETH certificato è il **trend-following long-only** (MA-cross in testa):
un **long con gestione del rischio** che batte il buy&hold di poco (Sharpe ~0.9-1.1 vs 0.8)
evitando i drawdown peggiori. È un effetto noto in letteratura (time-series momentum), NON alpha
market-neutral. **Caveat multiple-testing**: 2 flag su ~24 test ≈ soglia del caso; ma la stessa
famiglia vince su ENTRAMBI gli asset con plateau → è un LEAD genuino, non confermato. La barra
vera resta il B&H, e l'OOS-VAL alto di BTC (1.99) puzza di "2024 anno di trend forte" → serve la
prova del hold-out 2025-26 + regimi bear + stress fee/slippage + deflated-Sharpe (Fase 3).
## Analisi OPTIONS (`options_analysis.py`)
Dati reali cerbero-bite mainnet, ma finestra **~2026-05-01→06-11 (~6 sett., REGIME UNICO calmo)**.
### Livelli misurati (reali)
- **VRP (IV RV) positivo il 100% del tempo**: BTC +10, ETH +14 punti di vol annua. Le opzioni
sono sistematicamente CARE in questa finestra → vendere vol/covered-call avrebbe incassato premio.
- **Skew put positivo**: BTC IV put-10%OTM 44% vs call 35% (skew +10 pt); ETH 54 vs 49 (+5). Il
crash è prezzato (assicurazione cara).
- **Costo put protettiva** (mensile, %-del-notional): ~10% OTM = **0.98% BTC / 1.36% ETH**; ATM
3.3%/5.0%; ~15% OTM 0.83%/0.71%. Liquidità: ATM spread ~3%, OTM 7-12%. Mensile ben popolato
(499-2043 strike), settimanale OTM sottile. Funding perp ≈ 0 (nessun carry).
### Verdetto OPTIONS
**Nessun edge su opzioni è validabile ora**: 6 settimane, regime unico calmo. Il segnale
VRP-positivo / sell-vol è ESATTAMENTE ciò che brilla in calma e salta in aria nei crash (è il
rischio che viene pagato) — non testabile senza un crash nel campione. Ruoli legittimi (entrambi
NON validabili ora, solo forward):
- **(a) Tail-cap / catastrofe**: put OTM standing su un book long (il candidato trend ha DD grossi).
Costa ~1-1.5%/mese a 10% OTM — gateabile coi premi reali misurati qui. Overlay per-trade 24h
INFATTIBILE (strike OTM corti inesistenti/illiquidi); standing settimanale/mensile FATTIBILE.
- **(b) Harvest del VRP** (covered call / put-spread): +10-14 pt ci sono ORA, ma è una scommessa
short-vol che richiede un crash nel campione per essere giudicata onestamente. Non l'abbiamo.
**Raccomandazione**: le opzioni NON sono un'avenue di ricerca a breve (manca storia multi-regime).
Mosse: (1) lasciare cerbero-bite ad accumulare (gratis, reale, costruisce in avanti il dataset
multi-regime); (2) rivalutare quando la finestra attraversa un crash/alta-vol; (3) intanto, l'unico
uso giustificato è come OVERLAY (tail-cap su una strategia spot), gateato sui premi reali qui sopra.
## Prossimo passo
Fase 3 sul solo candidato reale (trend-following long-only, MA-cross): sblocco UNA volta del
hold-out 2025-26, comportamento nei bear (2018/2022), stress fee×2 + slippage + lag, deflated-Sharpe
per il multiple-testing. Se regge → è la prima strategia onesta del progetto v2.0.0 (modesta:
migliora il buy&hold, non lo stravolge). Se non regge → anche il trend era sample-luck.
@@ -1,62 +0,0 @@
# 2026-06-19 — Ricerca v2.0.0: Fase 3, conferma avversariale del candidato trend
Candidato: **trend-following long-only (MA-cross)**, l'unico a passare i gate base in Fase 2.
Protocollo: selezione config solo pre-hold-out → sblocco una-tantum del hold-out 2025-26 →
breakdown bear → stress → deflated-Sharpe. Script `phase3_confirm.py`.
## Esito: ☠️ NON CONFERMATO — era regime-luck del mercato toro
### (1) Pre-hold-out (2018-2024): forte e robusto
Plateau pieno: BTC Sharpe 0.91-1.16, ETH 1.19-1.48 su tutte le config. **Deflated-Sharpe**
(N=60 trial): BTC DSR **0.990**, ETH **0.982** → l'effetto trend era REALE e robusto al
multiple-testing **sul 2018-2024**.
### (2) HOLD-OUT 2025-26 (sbloccato una volta) — FALLISCE
| | buy&hold | trend 24/96 | trend 96/288 (slow) |
|---|---|---|---|
| BTC Sharpe | 0.37 | **0.81** | 0.00 |
| BTC ret | 32.9% | 33.6% | 5.0% |
| ETH Sharpe | 0.32 | **0.95** | 0.01 |
| ETH ret | 49.3% | 52.0% | 11.3% |
Il 2025-26 è stato un periodo in DISCESA (buy&hold negativo). Il trend long-only — che "dovrebbe"
schivare i bear — si è fatto **frullare** (whipsaw): perde quanto o PIÙ del buy&hold, Sharpe negativo
su ogni config. Solo la MA lentissima (96/288) limita i danni a ~flat (5/11%), ma è cherry-pick
post-hoc e comunque NON positiva.
### (3) Per anno — il meccanismo
Il trend cattura ~70-80% degli anni TORO (2019-2024) e attutisce i bear IN-SAMPLE (2018 1% vs
39%; 2022 47% vs 65%). MA nel 2025 OUT-OF-SAMPLE ha fatto **peggio** del buy&hold (BTC 25% vs
7%; ETH 41% vs 11%): frullato in un mercato choppy/discendente. È il classico fallimento del
trend-following nei bear laterali. → l'edge 2018-24 era **beta del toro con risk-management**, non
alpha persistente.
### (4) Stress
FULL regge modestamente (Sharpe 0.65-0.91 anche a fee2x+lag), ma HOLD-OUT è negativo ovunque
(0.81 → 1.34) e peggiora sotto stress. Fragile.
### (5) Deflated-Sharpe
DSR>0.95 sul pre-hold-out → conferma che l'effetto era statisticamente reale **nel campione di
training**. Lezione chiave: **robustezza statistica in-sample ≠ persistenza out-of-sample.** Il
hold-out bloccato ha colto ciò che DSR da solo non poteva — il cambio di regime.
## VERDETTO FINALE (Fasi 0-3)
**Nessun edge validato, fee-surviving e out-of-sample esiste su BTC/ETH tra le famiglie testate.**
Il trend-following era il miglior candidato: reale 2018-24 (toro), ma **bocciato sul hold-out
2025-26** (whipsaw, sotto il buy&hold). La barra realistica resta il **buy&hold** (Sharpe ~0.8
sullo storico, ma 0.3/0.4 nel 2025-26: anche "stai long" è stato duro di recente).
Il processo disciplinato ha funzionato: **ha evitato di deployare un falso edge** (che, sul vecchio
sistema contaminato, sarebbe finito in produzione). Questo è il valore del reset.
## Implicazioni / direzioni
- **Non deployare** il trend come edge: è regime-dipendente, non batte il buy&hold OOS.
- Con **solo BTC/ETH prezzo**, il pozzo dei segnali è poco profondo: timing puro non ha edge robusto.
- Opzioni: nessun ruolo a breve (confermato). Tenere cerbero-bite ad accumulare per uno studio
multi-regime futuro.
- Scelte oneste per andare avanti: (a) accettare che il "ceiling" su BTC/ETH è un long risk-managed
(no alpha) e ottimizzare quello (vol-target per ridurre DD, non per battere il mercato); (b)
allargare l'universo dati CERTIFICATO (servono asset liquidi+puliti oltre BTC/ETH, che Deribit non
offre bene → valutare un secondo venue mainnet certificabile); (c) fonti di segnale ortogonali al
prezzo (on-chain, funding/basis multi-venue, opzioni multi-regime) — tutte richiedono nuovi dati
certificati prima di poterci credere.
@@ -1,44 +0,0 @@
# 2026-06-19 — Caccia al secondo sleeve: nessun diversificatore robusto (TP01-only resta)
Continuazione naturale del portafoglio: cercare un secondo sleeve SCORRELATO a TP01 (trend
long-flat, in cash gran parte del tempo). Criterio: non il Sharpe standalone ma il CONTRIBUTO al
portafoglio + robustezza. Tool: `scripts/portfolio/second_sleeve_hunt.py` (riusa le RV di trackE).
## Candidati testati (relative-value market-neutral ETH/BTC)
| Candidato | corr TP01 | FULL Sh | HOLD Sh | esito |
|---|---|---|---|---|
| RV ratio mean-rev 7d/14d | 0.09/0.05 | 1.36/1.03 | 0.62/0.76 | ☠️ morto (mean-rev dead, come sempre) |
| RV ratio_trend / xs_momentum 30d | +0.04 | **0.56** | **1.92** | ⚠️ sembrava promosso |
ratio_trend e xs_momentum danno risultati IDENTICI: su 2 asset "long il più forte / short il
debole" ≡ "trend del ratio ETH/BTC". È UN segnale (relative-momentum), non due.
## Il candidato "promosso" è regime-luck (per-anno + plateau lo smascherano)
Aggiunto a TP01 sembrava un trionfo: hold-out portafoglio 0.31 → 1.18 (w20%) / 1.51 (w30%),
corr +0.04. MA:
- **Hold-out (1.92) >> FULL (0.56)**: bandiera rossa (immagine speculare della trappola di Fase 3).
- **Per-anno NON robusto**: 2019 +22%, 2020 +7%, 2021 +21%, 2022 +13%, **2023 17%, 2024 19%**,
**2025 +62%**, 2026 +6%. Due anni consecutivi negativi; il "guadagno" è concentrato nel 2025
(ETH sottoperforma BTC in modo netto e sostenuto). FULL Sharpe mediocre 0.56, DD 41%.
- **Nessun plateau**: l'hold-out Sharpe oscilla 0.25→1.92 al variare di (N, hold) → picco
config+regime, non altopiano.
- Il beneficio FULL al portafoglio è solo **+0.09 Sharpe** (la legittima diversificazione di uno
sleeve scorrelato a Sharpe 0.56: √(1.30²+0.56²)≈1.42). Il resto del "miglioramento" è il 2025.
## Decisione: NON promosso — TP01-only resta il portafoglio deployato
La stessa disciplina che ha bocciato i falsi positivi in-sample (Fasi 1-3) e cross-asset (frattali)
deve bocciare questo falso positivo nel hold-out. Il relative-momentum BTC/ETH è un edge debole e
regime-dipendente (2 anni a 17/19%), il cui contributo robusto al portafoglio è marginale
(+0.09 FULL); il grosso del beneficio è la fortuna del 2025. Aggiungerlo significherebbe
scommettere sul ripetersi di quel regime.
**Lezione/criterio aggiornato per i futuri sleeve:** "migliora il hold-out" da solo NON basta (il
hold-out è UN regime). Un secondo sleeve va promosso solo se: causale, corr bassa, **positivo nella
maggioranza degli anni** (no 2 anni consecutivi rossi), **plateau** sui parametri, e migliora il
portafoglio su FULL E hold-out — non solo per via di un singolo anno fortunato.
## Stato
Portafoglio = **TP01-only** (difensivo, Sharpe FULL 1.30 / hold-out 0.31). `active_sleeves()`
invariato. `second_sleeve_hunt.py` resta come tool per valutare candidati futuri col criterio
corretto (contributo + breadth per-anno + plateau). Il relative-momentum BTC/ETH è in WATCHLIST,
non deployato.
@@ -1,31 +0,0 @@
# 2026-06-19 — Portafoglio di strategie estensibile (TP01 primo sleeve)
Creato un contenitore di portafoglio (`src/portfolio/`) con TP01 come unico sleeve attivo per ora,
progettato per aggiungerne altri (ognuno validato col gauntlet onesto).
## Design
- **Sleeve** = una strategia validata che produce una serie di rendimenti netti per-barra
(datetime-indexed, CAUSALE, netto fee). Opzionale `pos_fn` per le posizioni correnti (live).
- **StrategyPortfolio**: porta ogni sleeve su griglia GIORNALIERA comune (compounding intra-giorno
→ mixa TF diversi in modo coerente), combina per PESO rinormalizzato sui giorni comuni
(= equal-capital-by-weight ribilanciato di continuo). Metriche FULL + HOLD-OUT 2025-26 (bloccato)
+ per-anno + standalone per-sleeve, vs benchmark buy&hold 50/50.
- **Estensibilità**: aggiungere uno sleeve = una riga in `src/portfolio/sleeves.active_sleeves`
(dopo validazione: research_lab + hold-out + cross-asset + causality guard). Niente sleeve non validati.
## Stato attuale (1 sleeve = TP01, peso 100%)
`scripts/portfolio/run_portfolio.py`:
- **FULL** Sharpe 1.30 / ret +201% / DD 14.3% / ~€1.52/g su 2k (n=2655 giorni 2019-2026)
- **HOLD-OUT 2025-26** Sharpe 0.31 / +3.5% / DD 7.5% (buy&hold 50/50: Sharpe 0.32 / 39% / DD 59%)
- Per-anno positivo quasi ovunque (2022 2.1%, 2026-YTD 0.7%)
- Posizione corrente: **flat** (TP01 in cash nel regime attuale = difensivo)
## File
- `src/portfolio/{__init__,portfolio,sleeves}.py`, `scripts/portfolio/run_portfolio.py`,
`tests/test_portfolio.py` (6 test, passano). CLAUDE.md aggiornato.
## Prossimo
Il portafoglio è pronto per ospitare nuovi sleeve. Candidati naturali (da validare prima):
un secondo edge scorrelato a TP01 (TP01 è trend long-flat → serve qualcosa di diverso, es. una
strategia che lavori quando TP01 è flat). Finché non c'è un secondo edge che regge il gauntlet,
il portafoglio = TP01 difensivo. Quando arriverà, basta una riga in sleeves.py.
@@ -1,43 +0,0 @@
# 2026-06-19 — TP01: look-ahead ffill mixed-TF, ri-verifica e adozione bassa frequenza (>=12h)
Segnalazione utente/agente: un look-ahead **ffill MIXED-TIMEFRAME su barre open-labeled**
(`resample(label="left")`) gonfiava il 4h a Sharpe ~1.60; il risultato reale è ~1.1.
Conclusione: **NON scendere sotto le 12h** — costi e overfitting dominano.
## Cosa ho verificato (`scripts/analysis/tp01_lowfreq.py`)
Ricalcolo TP01 PULITO **per singolo TF** (barre discrete, posizione shiftata +1, NESSUN
ffill/combine mixed-TF), con un **guard di causalità esplicito** (ricalcolo `target_series` su
prefisso → `tgt[i]` invariato). Esito (fee 0.10% RT, hold-out 2025-26 bloccato):
| TF | leak | FULL Sh | FULL ret | HOLD Sh | HOLD ret | HOLD DD |
|---|---|---|---|---|---|---|
| 4h | **0** | 1.36 | +204% | 0.27 | +2.8% | 8.3% |
| 6h | **0** | 1.42 | +217% | 0.21 | +2.1% | 7.9% |
| 12h | **0** | 1.32 | +198% | 0.22 | +2.3% | 8.6% |
| **1d** | **0** | 1.30 | +201% | **0.31** | **+3.5%** | 7.5% |
| buy&hold 50/50 1d | — | 0.92 | +1671% | **0.32** | **39%** | 59% |
## Lettura
- **Il path single-TF che ho usato in verify/stress è LEAK-FREE** (guard=0 su ogni TF): il
gonfiaggio 1.60 stava nel path **mixed-TF ffill** (ensemble/combine, es. trackE), NON nel
portafoglio single-TF. Per questo il mio 4h era 1.36 (non 1.60).
- **La conclusione "≥12h" è comunque CORRETTA e la adotto**: il FULL Sharpe è PIATTO ~1.3 da 12h
a 4h → scendere sotto le 12h NON dà vantaggio reale, aggiunge solo costi/turnover e rischio
overfit/look-ahead (lo stress mostrava il margine hold-out del 4h fragile a lag/fee). **1d è il
migliore**: hold-out Sharpe 0.31 (il più alto), DD 7.5%, turnover/costi minimi, leak-free.
- Allinea anche col numero dell'agente: il "reale ~1.1" è del path mixed-TF corretto; il mio
single-TF pulito dà ~1.3 FULL. In ogni caso **edge difensivo modesto**, non alpha.
## Decisioni applicate
- **Canonica deploy → PORT LF1d** (era LF4h). `trend_portfolio.py`: docstring aggiornata + nota
look-ahead; aggiunti `resample_tf`/`resample_1d`, `resample_4h` marcato deprecato per il deploy.
- **Paper trader → 1d** (`paper_trend.py`: `resample_1d`, `build_bars`, etichette 1d; gira, 5 test ok).
- **CLAUDE.md**: TP01 ridescritta come DIFENSIVA, canonica ≥12h/1d, gotcha look-ahead documentato.
- **Gotcha riusabile:** mai ffill/combine MIXED-TIMEFRAME su timestamp open-labeled (`label="left"`):
la close del bar (nota solo a fine bar) verrebbe propagata indietro all'open-label → look-ahead.
Il calcolo per-singolo-TF a barre discrete (posizione +1) è sicuro; il guard prefix-recompute lo prova.
## Verdetto invariato
TP01 resta la prima strategia onesta del progetto: **difensiva** (taglia il DD ~6× vs buy&hold,
hold-out 2025-26 positivo su entrambi gli asset), modesta nel ritorno. Deploy a **1d**, forward-only
paper trader, prima di qualsiasi capitale reale.
@@ -1,84 +0,0 @@
# 2026-06-19 — Verifica TP01 (branch strategy-research-2026-06) col gauntlet onesto
Una ricerca PARALLELA (branch `strategy-research-2026-06`, AdrianoDev) dallo stesso baseline
v2.0.0 ha trovato TP01 come "unica vincitrice". La mia linea (Fasi 2-3) aveva bocciato il trend
sul hold-out 2025-26. Ho riprodotto TP01 VERBATIM (`scripts/analysis/verify_tp01.py`) e l'ho
passato al mio gauntlet. **TP01 REGGE — la mia conclusione precedente era incompleta.**
## TP01 = TSMOM 30/90/180g, **vol-target 20%**, leva cap 2x, **long-flat**, portafoglio 50/50 BTC+ETH (4h)
## Esiti del gauntlet
**(A) Multi-TF (4h cherry-picked?) — NO, plateau robusto:**
| TF | FULL Sharpe | HOLD-OUT Sharpe |
|---|---|---|
| 15m | 0.93 | 0.31 |
| 1h | 1.32 | +0.20 |
| **4h** | **1.36** | **+0.27** |
| 1d | 1.30 | +0.31 |
1h/4h/1d danno tutti FULL ~1.3 e hold-out positivo → non è un artefatto di un singolo TF (solo il 15m, fee-sensibile, fallisce).
**(C) HOLD-OUT 2025-26 (il test che ha ucciso il mio trend 1h) — TP01 PROTEGGE:**
| | Sharpe | ret | DD |
|---|---|---|---|
| **TP01 portfolio** | **+0.27** | **+2.8%** | **8.3%** |
| buy&hold 50/50 | 0.35 | **39.4%** | 59.8% |
**(D) Cross-asset nel hold-out — regge su ENTRAMBI** (BTC sleeve +2.9% Sh 0.24, ETH +2.4% Sh 0.24).
A differenza del "vincitore" frattale (+ETH/BTC), TP01 protegge coerentemente su BTC E ETH.
**(B) Per anno:** positiva quasi ogni anno 2019-2026 (eccezioni piccole: 2022 2.4%, 2026-YTD 0.9%),
DD annui 1-12%. Il claim "positiva ogni anno" è lievemente ottimistico ma sostanzialmente vero.
## Perché TP01 regge dove il MIO trend (Fase 3) è caduto
La differenza chiave è il **VOL-TARGETING** (che NON avevo combinato col trend): TP01 scala
l'esposizione ∝ 1/vol_realizzata → nel crollo 2025-26 la vol è esplosa e TP01 si è messo
quasi in cash, schivando il drawdown. Il mio MA-cross 1h aveva esposizione fissa ed è rimasto
long nel chop → frullato. Concorrono: TSMOM multi-orizzonte (più liscio del MA-cross), long-flat
(niente perdite short), diversificazione 50/50. **La mia "trend = regime-luck" era vera per il
trend NUDO; TP01 = trend + vol-target + portafoglio è un'altra cosa, e robusta.**
## Cosa È onestamente TP01 (no oversell)
- **Edge DIFENSIVO, non alpha**: FULL Sharpe 1.36 vs buy&hold 0.92 — MA CAGR +16.6% vs +48.1%.
Su tutto il toro il buy&hold ha reso ~8x di più. Il valore di TP01 è il **DD** (13.8% vs 77.5%
full; 8% vs 60% nel hold-out) e la **protezione dai crash**.
- Nel hold-out 2025-26 ha fatto solo +2.8% (Sharpe 0.27, basso): ha **protetto, non profittato**.
- Un solo regime di hold-out, ma il vol-targeting è meccanico (high vol → low expo) → generalizza
per costruzione meglio di un timing fittato.
- Config canonica (30/90/180, vol20%, lev2x) non iper-tunata; 4h non cherry-picked (plateau).
## VERDETTO
**TP01 è la PRIMA strategia onesta e robusta del progetto post-reset.** Supera il mio gauntlet
(hold-out positivo su entrambi gli asset, plateau multi-TF, causale, fee-aware). È modesta e
difensiva (Sharpe ~1.3, soffitto strutturale dichiarato corretto), ma è reale: migliora il
rischio/rendimento del buy&hold tagliando i drawdown e proteggendo nei crash. La ricerca parallela
ha fatto centro proprio sul pezzo che la mia linea non aveva combinato (vol-target sul trend).
**Raccomandazione:** integrare il branch su main (modulo `trend_portfolio.py` + paper trader),
trattare TP01 come baseline operativa difensiva. Aspettative oneste verso il target €50/g: a
Sharpe 1.3 / CAGR 16.6% servono molto capitale o leva (con più DD) — TP01 è un fondamento solido,
non una scorciatoia.
## STRESS-TEST (`scripts/analysis/stress_tp01.py`, integrato e rieseguito sul modulo vero)
| Dimensione | Esito |
|---|---|
| **Sweep fee** | FULL robusto fino a **0.40% RT** (Sh 1.44→1.36→1.28→1.13). HOLD-OUT SOTTILE: +2.8%/Sh0.27 a 0.10% → ~flat (Sh 0.03) a 0.40% |
| **Lag/slippage** | FULL robusto (1.29-1.43). HOLD-OUT si erode: lag1(4h)→Sh0.12, lag2→−0.02, lag1+fee0.20%→0.04 |
| **Plateau parametri** | OTTIMO — target_vol/leva/orizzonti/vol_win tutti reggono o migliorano (orizzonti 20/60/120 → Sh 1.61). **NON un picco cherry-picked** |
| **Deflated-Sharpe** | DSR **0.999** a N=10/40/100 trial → il Sharpe FULL non è artefatto di multiple-testing |
**Verdetto stress (onesto):**
- **Robustezza FULL-period: FORTE.** TP01 supera fee 0.40%, lag, ampio plateau di parametri, e
deflated-Sharpe. NON è overfit né cherry-picked — la proprietà robusta è il **taglio del
drawdown** (13.8% vs 77.5% full; 8% vs 60% hold-out), invariante a tutto lo stress.
- **Edge di RITORNO nel hold-out: REALE ma SOTTILE e sensibile alla frizione.** Nel 2025-26 ha
schivato il crash in modo affidabile (DD 8% vs 60%) ma ha **protetto più che profittato** (+2.8%,
Sh 0.27), e quel sottile positivo si assottiglia a zero sotto fee2x o lag 2 barre.
**Conclusione:** la proprietà **deployabile e robusta di TP01 è la PROTEZIONE del drawdown**, non
la generazione di alpha. È una strategia difensiva genuina (prima del progetto a superare gauntlet
+ stress), ma a basso ritorno: il valore è "Sharpe ~1.3 con DD ~6× più piccolo del buy&hold",
non "battere il mercato". Per il capitale reale: il vol-targeting + long-flat sono meccanici e
generalizzano; il rischio residuo è la frizione di esecuzione sul filo del sottile edge di ritorno
nei regimi avversi → da monitorare col paper trader forward-only prima di scalare.
-28
View File
@@ -1,28 +0,0 @@
# 2026-06-19 — Strato trend multi-asset sui 52 alt: RIDONDANTE col trend di TP01
Tentativo: aggiungere un terzo sleeve = TSMOM (stessa logica TP01 CANONICAL, long-flat vol-target)
applicato a OGNI alt dei 52 Hyperliquid certificati, equal-weight ragged. Idea: trend più
diversificato che diversifichi TP01 (BTC/ETH). `scripts/portfolio/trend_multiasset.py`.
## Esito: ridondante e peggiore
- **TREND-52 standalone**: FULL Sh 0.66, **HOLD-OUT 1.03** (negativo), anni+ 33%. Gli alt sono
stati long nel calo 2025-26 e hanno sanguinato — a differenza di TP01 (BTC/ETH) che il
vol-target+trend portò in cash. I trend degli alt sono più rumorosi/whippy.
- **corr a TP01 = +0.74** (stessa beta direzionale, come previsto) | corr a XS01 0.05.
- **Contributo al portafoglio (TP01 70 + XS 30):** +TREND-52 w20% → FULL 0.01, **HOLD 0.16**;
w30% → FULL 0.02, **HOLD 0.27**. PEGGIORA.
## Lezione
Broadenizzare il TREND su molti alt NON diversifica: è la **stessa direzionalità** (corr 0.74 con
TP01) su asset più rumorosi → aggiunge perdita/rumore, non edge. La dimensione trend è già catturata
in modo pulito da TP01 (BTC/ETH, vol-targeted). L'unica espansione che diversifica davvero resta
quella **market-neutral** (XS01 cross-sectional), perché è ortogonale alla beta direzionale.
## Conclusione (chiusura del filone "espansione universo")
Esplorate tutte le vie di espansione sui certificati Hyperliquid:
1. XS su 52-all → diluito (memecoin), negativo.
2. XS top-liquidità dinamico → peggiore del fisso-19 (liquidità ≠ qualità).
3. Trend multi-asset su 52 → ridondante (corr 0.74) + hold-out negativo.
Nessuna migliora il portafoglio. **Configurazione validata e invariata: TP01 70% + XS01 (19 major)
30% — FULL Sh 1.41 / HOLD 1.15.** I margini reali per crescere NON sono nell'universo crypto-
direzionale (saturo), ma in un MECCANISMO diverso (opzioni VRP, in attesa di dati di stress reali).
-35
View File
@@ -1,35 +0,0 @@
# 2026-06-19 — Affinamento XS01: blend di lookback [30,90]
Come TP01 fonde gli orizzonti 30/90/180, XS01 ora fonde piu' lookback del momentum cross-sectional
(z-score cross-sectional per lookback, mediato) invece del singolo L=30. `scripts/portfolio/xsec_blend.py`.
## Sweep lookback (19 major, 899g) — FULL/OOS/DD/anni+/corrTP
| lookbacks | FULL | OOS25 | DD% | anni+ | corrTP |
|---|---|---|---|---|---|
| [30] (prima) | 0.80 | 1.20 | 21 | 100% | 0.06 |
| [90] | 0.88 | 0.90 | 17 | 100% | 0.05 |
| **[30,90]** | **1.10** | **1.03** | **14** | **100%** | **0.12** |
| [20,40,90] | 0.51 | 0.67 | 25 | 100% | 0.12 |
| [30,60,120] | 0.68 | 0.74 | 16 | 100% | 0.13 |
**[30,90] e' il sweet spot**: fonde i DUE singoli robusti (30 e 90), FULL Sh 0.80→1.10, DD 21→14%,
corr a TP01 0.06→−0.12 (diversifica meglio), 100% anni+. Non e' un cell fortunato: e' la
combinazione dei due lookback gia' validati (anti-overfit, come il multi-orizzonte di TP01).
## Effetto sul portafoglio (TP01 70% + XS01 30%)
| | XS01 [30] | XS01 blend [30,90] |
|---|---|---|
| XS01 standalone FULL / DD | 0.80 / 21% | **1.10 / 14%** |
| Portafoglio FULL Sharpe | 1.41 | **1.48** |
| Portafoglio HOLD-OUT Sharpe | 1.15 | 1.06 |
| Portafoglio DD | 5.2% | **4.6%** |
| ~€/giorno (2k) | +1.65 | +1.78 |
Migliora FULL Sharpe + DD + robustezza (due orizzonti) al costo di un hold-out marginalmente piu'
basso (0.09, dentro il rumore di una singola finestra). Giudizio: il blend e' piu' robusto
(meno dipendente da un singolo lookback) e diversifica meglio -> PROMOSSO.
## Azione
`src/portfolio/sleeves.XS_CFG`: `L=30` -> `lookbacks=(30,90)`; engine `_xsec_returns` usa lo score
blended (media z-score cross-sectional per lookback). **Portafoglio attivo: TP01 70% + XS01 blend
30%, FULL Sh 1.48 / HOLD 1.06 / DD 4.6%.** 12 test ok. Sleeve sempre sui 19 major.
-44
View File
@@ -1,44 +0,0 @@
# 2026-06-19 — Affinamento XS01: gate di dispersione (p30)
Il momentum cross-sectional vive nella DISPERSIONE (winners/losers distanti). In regime compatto
(tutti gli asset insieme) e' rumore. Gate: entra solo se la dispersione cross-section del momentum
supera il percentile ESPANDENTE causale `disp_pct`; altrimenti flat. Sul blend [30,90] dei 19 major.
`scripts/portfolio/xsec_dispgate.py`. (È il concetto del vecchio XS01 pre-reset, disp_min=p50.)
## Sweep soglia (19 major, 899g) — XS01 standalone + contributo portafoglio
| soglia | XS FULL | XS OOS | PORT FULL | PORT HOLD | %flat |
|---|---|---|---|---|---|
| no gate | 1.10 | 1.03 | 1.50 | 1.06 | 0% |
| p15 | 1.32 | 1.39 | 1.64 | 1.36 | 28% |
| p20 | 1.46 | 1.63 | 1.72 | 1.52 | 31% |
| p25 | 1.46 | 1.63 | 1.72 | 1.52 | 31% |
| **p30** | **1.50** | **1.71** | **1.74** | **1.56** | 35% |
| p35 | 1.60 | 1.90 | 1.81 | 1.69 | 37% |
| p40-p50 | 1.0 | 0.8 | 1.36-1.38 | 0.77-0.93 | 42-49% |
**PLATEAU robusto p15-p35** (cinque punti, tutti molto > no-gate); il crollo a p40+ e' OVER-gating
(salta troppo). Scelto **p30** (centro sicuro del plateau, lontano dal cliff p40). Non un knife-edge.
## Effetto sul portafoglio (TP01 70% + XS01 30%)
| XS01 | PORT FULL | PORT HOLD | PORT DD |
|---|---|---|---|
| [30] originale | 1.41 | 1.15 | 5.2% |
| + blend [30,90] | 1.48 | 1.06 | 4.6% |
| + dispersion gate p30 | **1.55** | **1.55** | **4.4%** |
XS01 standalone: FULL 1.10→1.50, HOLD 1.03→1.71, DD 14%→10.8%, ~€/g 1.64→2.36. Il gate alza SIA
FULL SIA hold-out (a differenza del solo blend, che barattava un po' di hold-out).
## Meccanismo + caveat onesti
- **Causale**: soglia = percentile espandente della dispersione PASSATA; nessun look-ahead.
- **Perche' funziona**: tiene XS attivo nei regimi DISPERSI (2025-26, dove gli alt divergono) e flat
nei bull compatti (2024). L'hold-out 2025-26 e' ad alta dispersione -> il gate concentra
l'attivita' di XS proprio li' -> hold-out forte. E' il comportamento voluto, ma NB che il salto
del hold-out riflette anche che il 2025-26 e' stato un regime ad alta dispersione.
- Caveat XS01 invariati: storia ~2.5 anni; STAT-MODE (book 19 gambe non eseguibile a 2k).
## Azione
`src/portfolio/sleeves.XS_CFG`: aggiunto `disp_pct=30`; engine `_xsec_returns` gatea su dispersione.
**Portafoglio attivo: TP01 70% + XS01 (blend [30,90] + gate disp p30) 30% — FULL Sh 1.55 / HOLD 1.55
/ DD 4.4%.** 12 test ok. Affinamenti del SEGNALE (blend + gate) hanno funzionato dove l'espansione
universo no: i margini su XS sono nella struttura del segnale, non nel numero di asset.
@@ -1,62 +0,0 @@
# 2026-06-19 — Espandere l'universo XS01: PIÙ asset DILUISCONO (i 19 major sono il sweet spot)
Richiesta: aggiungere altri asset Hyperliquid certificati per rafforzare XS01 (cross-sectional
momentum). Fatto il lavoro, esito ONESTO: **non rafforza — diluisce.**
## Cosa ho fatto
- Esteso `fetch_hyperliquid.py` a ~54 candidati alt maggiori (mappa Binance auto SYM/USDT, k-prefissi
esclusi). **52 certificati** (cross-venue 4-11 bps vs Binance, flat 0%, storia 2024+): aggiunti
ATOM DYDX APE CRV LDO STX GMX SNX BCH COMP WLD UNI TRX FIL RUNE ENA ORDI JUP WIF PYTH FET AR ETC
ALGO GALA SAND AXS DOT BLUR JTO PENDLE ONDO TAO. Esclusi MKR (delistato HL 2025-09) e FXS
(migrazione Frax 2026-01) via nuovo gate "ultima barra recente".
## Il finding: il cross-section dei 52 è NEGATIVO; i 19 major sono positivi
Stessa finestra (2024-04 → 2026-06, 807g), mom L*/H10:
| Universo | k | FULL Sharpe (L30/L60/L90) |
|---|---|---|
| **52 asset** | 5 | 0.13 / 0.21 / 0.35 |
| **52 asset** | 8-12 | tutti negativi (k grande non aiuta) |
| **19 major** | 5 | +0.30 / +0.36 / **+0.67** (OOS 0.91) |
I ~33 small/new-cap aggiunti (WIF, JUP, ORDI, PYTH, TAO, GALA, AR, BLUR…) sono idiosincratici/
mean-reverting: il loro rumore **rovescia** il momentum relativo. Cross-sectional momentum su crypto
funziona fra i MAJOR liquidi, non sul long tail. Allargare l'universo NON è gratis.
## Azione
- **XS01 resta sui 19 major** (sweet spot già validato: plateau/fee/subset). Lo sleeve
`_xsec_returns` ora usa una **lista esplicita `XS_UNIVERSE` (19)**, non più glob-all (così
aggiungere parquet certificati non lo cambia/rompe — avevo inavvertitamente fatto vedere allo
sleeve 52 asset = negativo).
- I 52 parquet certificati restano su disco: dato valido per ricerca futura (uno strato diverso —
es. trend-following multi-asset, o un XS ristretto ai top-liquidità — potrebbe usarli), ma NON XS01.
- Portafoglio invariato e ripristinato: **TP01 70% + XS01 30%, FULL Sh 1.41 / HOLD 1.15.**
## Lezione
"Più asset = più robusto" è FALSO per il cross-sectional momentum: il long tail di alt piccoli
diluisce/inverte l'edge. La breadth utile è quella dei major liquidi (corr-strutturata), non il
numero grezzo.
## Tentativo 2: UNIVERSO TOP-LIQUIDITÀ DINAMICO (`xsec_dynuniverse.py`) — anch'esso PEGGIORE
Provato a selezionare a ogni ribilancio i top-N per dollar-volume 30g (causale) dai 52, poi XS
momentum fra quelli (adattivo, ragged-aware). Esito:
| Universo | FULL Sh | OOS25 | anni+ |
|---|---|---|---|
| top12 dinamico (L30H10k5) | 0.65 | 0.54 | 67% (2026 4%) |
| top15/20/25 dinamico | 0.14-0.38 | ≤0.30 | 33-67% |
| **fisso-19 major (L30H10k5)** | **0.80** | **1.20** | **100%** |
| fisso-19 major (L90H10k5) | 0.88 | 0.90 | 100% |
Contributo: TP01+DYN 70/30 = FULL 1.10 / HOLD 0.60 vs **TP01+XS19 = FULL 1.25 / HOLD 1.15**.
**Perché fallisce:** la classifica per dollar-volume ammette comunque i MEMECOIN ad alto volume
(WIF, ORDI, JUP, PEPE...) che hanno volumi enormi ma momentum erratico/mean-reverting →
diluiscono. **Liquidità ≠ qualità** nelle crypto. I 19 major *curati* (established, corr-strutturati,
non solo alto volume) restano il sweet spot.
## Conclusione
Né più nomi (52) né top-liquidità dinamico migliorano XS01. **XS01 resta sui 19 major curati**
(FULL 0.80 / OOS 1.20, 100% anni+). Portafoglio invariato: TP01 70% + XS01 30% (FULL 1.41/HOLD 1.15).
Per rafforzarlo davvero servirebbe una curatela di QUALITÀ (established majors), che è già ciò che i
19 sono. Coerente con la disciplina: nessuna espansione senza che migliori il gauntlet. I 52 parquet
certificati restano per ricerca futura (es. trend multi-asset, dove il long tail non diluisce).
@@ -1,86 +0,0 @@
# 2026-06-20 — Correzione estrazione cerbero MCP: il backfill sintetico (vol=0) ingannava la certificazione
## Contesto
Richiesta: "analizza cerbero MCP correggendo l'estrazione dati storici secondo le analisi fatte".
Le analisi del progetto avevano già fissato un principio — *"storia nativa Hyperliquid solo dal 2024,
pre-2024 = backfill, volume 0"* — e `fetch_hyperliquid.py` lo gestiva con un floor `START=2024-01-01`.
**Il floor non basta.**
## Il difetto
`fetch_hl` chiedeva a cerbero MCP `get_historical` dal 2024-01-01 e certificava ogni asset con tre
gate: **flat-bar** (O==H==L==C), **cross-venue** (mediana |close Binance| < 60 bps), **recency**.
Nessuno guardava il **volume**. Risultato: gli asset listati su HL *dopo* lo START passavano come
PULITO pur essendo in gran parte **backfill sintetico**.
Ispezione del volume sui parquet (leading run di barre a volume 0):
| asset | barre | leading vol=0 | primo trade reale | % sintetico |
|---|---|---|---|---|
| **AXS** | 902 | **748** | 2026-01-18 | **82.9%** |
| ALGO | 902 | 338 | 2024-12-04 | 37.5% |
| SAND | 902 | 338 | 2024-12-04 | 37.5% |
| AR | 902 | 58 | 2024-02-28 | 6.4% |
| ETC | 902 | 11 | 2024-01-12 | 1.2% |
| BTC/ETH + 19 major | 902 | 0 | 2024-01-01 | 0% |
AXS era **certificato PULITO** (flat 0%, cross-venue 9.5 bps) pur avendo solo ~5 mesi di trading reale.
## Verifica diretta su cerbero MCP (token mainnet)
Interrogato l'endpoint `cerbero-mcp.tielogic.xyz/mcp/tools/get_historical` (bot-tag
`pythagoras-mainnet`):
- **BTC**: 902 barre, leading vol=0 = 0, volume reale dal 2024-01-01 (V=699, 2437, 5306…). Nativo. ✓
- **AXS**: 902 barre, **748 leading vol=0**, primo vol>0 = 2026-01-18. Le barre a volume 0 hanno
prezzi (O/H/L/C) che **coincidono con Binance**:
| data | cerbero close | binance close | Δ |
|---|---|---|---|
| 2024-01-01 | 9.262 | 9.26 | 2.2 bps |
| 2024-01-02 | 8.949 | 8.94 | 10.1 bps |
| 2024-01-03 | 7.937 | 7.95 | 16.4 bps |
**Diagnosi provata:** cerbero MCP riempie il periodo pre-quotazione con barre **sintetiche — volume 0,
prezzi copiati da un venue di riferimento (Binance)**. Per questo i vecchi gate venivano ingannati:
- cross-venue passa → i prezzi *sono* Binance (Δ 116 bps);
- flat passa → le barre non sono flat (hanno movimento di prezzo);
- ma **volume 0** → su HL quelle candele **non erano negoziabili**. È esattamente il caso v2.0.0
(edge su un book che non c'era).
## Correzione (`scripts/analysis/fetch_hyperliquid.py`)
1. **Il VOLUME è il rivelatore del backfill**`trim_backfill()` taglia il run iniziale di barre a
volume 0; si tiene solo la **serie nativa**.
2. **Gate storia nativa** `MIN_NATIVE_DAYS=365`: dopo il taglio serve ≥ 1 anno di vita reale →
scarta chi è troppo corto (AXS, 154 barre reali → fuori).
3. **Gate vol=0 interno** `INTERIOR_VOL0_MAX=5%`: gap di liquidità oltre il taglio iniziale.
4. **cross-venue/flat ricalcolati SOLO sulle barre reali** (non più sui sintetici).
5. **I parquet degli asset scartati vengono rimossi** (disco == set certificato; niente file
contaminati a riposo).
## Risultato
- Universo certificato: **52 → 51** (AXS scartato).
- ALGO/SAND (338 barre), AR (58), ETC (11) ripuliti dal backfill → ora start reale corretto.
- **I 19 major di XS01 hanno 0 backfill → invariati**: la strategia live (`XS_UNIVERSE` esplicito) NON
è toccata. Verificato: portafoglio 3-way (TP01+XS01+VRP01) gira identico, FULL Sh 1.68 / HOLD 1.67.
- Re-fetch end-to-end su cerbero reale: 51 PULITO, sweep su tutti i file → 0 backfill residuo.
## Nota su una conclusione precedente
Il diario `2026-06-19-xsec-universe-expansion.md` concludeva "cross-section dei 52 = negativo". Quella
finestra includeva i sintetici (AXS 83%, ALGO/SAND 37% di barre vol=0 con ritorni non eseguibili): la
magnitudine del risultato era **in parte un artefatto**. La conclusione qualitativa (il long-tail
diluisce XS01; i 19 major sono il sweet spot) resta valida, ma il numero netto è 51 e il test andrebbe
ri-girato sui dati puliti se si volesse riusare quell'universo.
## Lezione
`flat` + cross-venue **non bastano** a certificare un feed che fa backfill copiando un altro venue: il
backfill è plausibile sui prezzi proprio perché è copiato. Il **volume** (=liquidità reale) è il gate
che mancava. Coerente con la regola di prim'ordine v2.0.0: certificare il dato — anche il *volume*,
non solo il prezzo — prima della strategia.
File: `scripts/analysis/fetch_hyperliquid.py`. Universo: `data/raw/hl_*_1d.parquet` (51, serie native).
@@ -1,93 +0,0 @@
# 2026-06-20 — Analisi strategie FinanceOld + VRP v2 (defined-risk + gate IV-rank)
## Contesto
Richiesta: analizzare le strategie in `../FinanceOld`, provare a migliorarle, testarle su dati storici.
Quattro progetti esaminati. Verdetto di **backtestabilità onesta** sui dati certificati (BTC/ETH
Deribit mainnet + DVOL):
| Progetto | Strategia | Backtestabile sui dati certi? |
|---|---|---|
| **FundingRateArbitrage** | Spread funding cross-exchange (perp-perp, spot-hedge) | ❌ Nessun dato funding storico nel repo (solo `exchange_settings.json`). Edge = differenza cross-venue, non ricostruibile. |
| **Polybot** | Latency-arb Polymarket (BS digital-option) + sure-bet delta-neutral | ❌ `dataVPS/collector.db` (645MB) ha solo **~3 giorni** di `poly_books`+`funding`, e la tabella `ticks` (prezzi perp = cuore dell'edge) è **corrotta** ("database disk image is malformed"). L'edge è la latenza: non riproducibile su barre OHLC comunque. |
| **OptionSpalping** (→Cerbero) | LLM autonomo su opzioni Deribit + perp Hyperliquid | ⚠️ È un agente LLM, non una regola meccanica. Il *concetto* (income short-vol su Deribit) è testabile. |
| **OptionsAgent** | **Bear Call Spread + Long VIX hedge** su IWM, con 5 gate d'ingresso | ✅ Il *concetto* (vendi premio rischio-definito, incassa VRP, gate su IV-rank/regime) mappa direttamente sul nostro `options_vrp_lab.py`. |
→ Scelta operatore: **focus VRP opzioni**. L'unico filone con dati veri + metodologia onesta.
## Baseline (options_vrp_lab.py, ora con fee)
Vendita put NUDA settimanale delta -0.28, premio BS su DVOL reale. f = premio_reale/modellato.
- `f=1.0` (conservativo): **FULL Sh 0.78, DD 33%, worst-week -16.6%, HOLD-OUT Sh -0.25** → muore OOS.
- Il rischio è la **CODA**: worst-week su LUNA (2022-06), crash 2021-05. Anno 2022 = -9%.
## VRP v2 — 3 idee di OptionsAgent portate nel framework
Nuovo script `scripts/research/options_vrp_v2.py`. Tutto **causale** (strike/premio/gate da dati
≤ sell-date; payoff a scadenza sui prezzi certificati). Fee opzioni Deribit modellate (12.5% del
premio netto per round-trip = cap del fee reale). Capitale = strike corto (cash-secured) per
entrambe le strutture → DD/worst comparabili.
1. **Rischio definito (PUT CREDIT SPREAD)** — vendi put -0.28, COMPRI put -0.10. Il long wing
**cappa la coda per costruzione**: worst-week -16.6% → **-7.4%**, DD 33% → 21%, Sh 0.78 → 0.99.
2. **Gate IV-RANK > 0.30** (cond. d'ingresso di OptionsAgent) — vendi vol solo quando ricca
(percentile espandente causale di DVOL). Trada il **58%** delle settimane → **Sh 1.35** e
ribalta **HOLD-OUT da -0.25 a +0.28**. È l'alpha vero: il filtro di regime, non la struttura.
3. **Crash-skip IV-rank > 0.90** (NO-GO, come "VIX>35" di OptionsAgent) — marginale da solo.
4. **Gate VRP>0** (DVOL>RV30 causale) — marginale (il VRP è >0 il 78% del tempo, poco selettivo).
### Risultati chiave (book 50/50 BTC+ETH, f=1.0 conservativo)
| Config | FULL Sh | DD | worst-wk | HOLD-OUT Sh | attivo |
|---|---|---|---|---|---|
| naked (baseline) | 0.78 | 33% | -16.6% | **-0.25** | 100% |
| spread | 0.99 | 21% | -7.4% | -0.26 | 100% |
| spread + ivr30 | **1.35** | 14% | -7.4% | **+0.28** | 58% |
| **COMBO** (spread+vrp+ivr30+crashskip) | 1.10 | 12% | -7.4% | **+0.60** | 41% |
COMBO f=1.0 per-anno: 2021 +26%, 2022 **-6%**, 2023 +2%, 2024 +18%, 2025 -0%, 2026 +5%
(il 2022, anno-crash che dimezzava il nudo, è quasi piatto: la coda è tagliata).
A `f=1.29` (skew reale misurato in regime calmo) la COMBO fa FULL Sh 1.87 / HOLD 1.45 / DD 9%.
### Contributo al portafoglio (COMBO f=1.0 vs TP01)
- Corr settimanale **+0.07** (scorrelato, come il VRP nudo).
- TP01 70% + OPT 30% → Sh **1.00** (TP01 solo 0.73), DD **7%**.
- TP01 50% + OPT 50% → Sh **1.19**, DD 7%.
## Conclusione onesta
Le idee di OptionsAgent **migliorano davvero** lo sleeve VRP, in modo OOS-robusto:
- la **struttura defined-risk** taglia la coda (worst -16.6%→-7.4%, DD -19pt) → meno dipendenza dal
f di stress, che era il rischio non catturato del lead nudo;
- il **gate IV-rank** è l'alpha: ribalta l'HOLD-OUT da negativo a positivo vendendo solo vol ricca.
Resta un **lead, non un deploy**: premio MODELLATO su DVOL ATM (skew non esplicito), book a 1d, e
serve la catena reale (cerbero-bite) per il f di stress in un crash. Ma è un miglioramento netto,
quantificato e onesto, del miglior lead income che avevamo. Prossimo passo: rivalutare il f di stress
quando cerbero-bite cattura un crash, e validare lo skew reale sul long wing (-0.10).
Script: `scripts/research/options_vrp_v2.py`. Baseline: `scripts/research/options_vrp_lab.py`.
## Integrazione come sleeve (VRP01)
La COMBO è stata integrata nel portafoglio come **VRP01** (`src/portfolio/sleeves._vrp_combo_returns`,
`vrp_sleeve()`). Implementazione self-contained in `src/` (niente import da `scripts/`): pricing BS +
strike-from-delta + gate causali inline, DVOL da `data/raw/dvol_*.parquet`.
**Settimanale → giornaliero (onesto):** il rendimento settimanale è piazzato sul **giorno di
scadenza**, 0.0 sugli altri giorni dello span. Questo PRESERVA lo Sharpe annualizzato (niente
smoothing che gonfierebbe il daily Sharpe) e tiene lo sleeve presente ogni giorno → peso costante
nell'outer-join del portafoglio. Verificato: lo sleeve daily replica i numeri settimanali
(FULL Sh 1.09, HOLD 0.60, DD 12%), corr daily vs TP01 = +0.01.
**Pesi (per evidenza, engine reale):** TP01+VRP01 monotòno fino al 40% VRP (FULL 1.30→1.55,
HOLD 0.31→0.52, DD fermo 14%). Essendo VRP un lead MODELLATO (non deploy pieno), non lo sovrappeso:
registry = **TP01 0.55 / XS01 0.25 / VRP01 0.20** (TP01 resta maggioranza, l'unico deployable pieno).
La validazione 3-way completa richiede i dati Hyperliquid (XS01, gitignored, token Cerbero) → gira
locale con `scripts/portfolio/run_portfolio.py`.
Test: `tests/test_vrp_sleeve.py` (5 pass: monotonìa BS, ordering strike, determinismo+griglia
giornaliera, gate riducono l'attività, coda tagliata <-15%).
-109
View File
@@ -1,109 +0,0 @@
"""EVALUATOR STANDARD per i segnali della ricerca multi-agente (Fase frattale, v2.0.0).
Ogni agente scrive SOLO una funzione `signal(df, asset, tf) -> np.ndarray` (posizione per barra
in [-1,1], decisa entro close[i]) in un file. Questo evaluator la valuta in modo UNIFORME e ONESTO
sull'harness research_lab, e — cruciale — esegue un GUARD ANTI-LOOK-AHEAD automatico: ricalcola il
segnale su prefissi del df e verifica che pos[i] non dipenda da barre future (leak>0 = sospetto).
uv run python scripts/analysis/eval_signal.py <signal_file.py> <BTC|ETH> <5m|15m|1h> [--holdout]
Stampa una riga "RESULT_JSON:{...}" con tutte le metriche (gli agenti riportano quei campi esatti).
"""
from __future__ import annotations
import sys
import json
import importlib.util
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, load_tf, ts, _net_series, VAL_START, HOLDOUT_START
def load_signal(path):
spec = importlib.util.spec_from_file_location("usig", path)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
if not hasattr(m, "signal"):
raise AttributeError("il file non definisce signal(df, asset, tf)")
return m.signal
def causality_guard(signal, df, asset, tf, k=12):
"""Ricalcola il segnale su prefissi df[:i+1] e confronta pos[i] col run completo.
Se differiscono -> il segnale usa dati FUTURI (look-ahead). Ritorna #violazioni (0 = pulito)."""
full = np.asarray(signal(df, asset, tf), float)
n = len(df)
if len(full) != n:
return -1
rng = np.random.default_rng(0)
idx = rng.integers(int(n * 0.6), n - 1, size=k)
bad = 0
for i in idx:
try:
p = np.asarray(signal(df.iloc[:i + 1].copy(), asset, tf), float)
except Exception:
bad += 1; continue
if len(p) != i + 1 or not np.isclose(np.nan_to_num(p[i]), np.nan_to_num(full[i]), atol=1e-6):
bad += 1
return bad
def main():
args = sys.argv[1:]
holdout = "--holdout" in args
args = [a for a in args if a != "--holdout"]
sigfile, asset, tf = args[0], args[1].upper(), args[2]
res = {"asset": asset, "tf": tf, "sigfile": sigfile}
try:
signal = load_signal(sigfile)
df = load_tf(asset, tf)
pos = np.asarray(signal(df, asset, tf), float)
res["n"] = int(len(df))
res["len_ok"] = bool(len(pos) == len(df))
if not res["len_ok"]:
res["error"] = f"len(pos)={len(pos)} != len(df)={len(df)}"
print("RESULT_JSON:" + json.dumps(res)); return
res["finite"] = bool(np.isfinite(np.nan_to_num(pos, nan=0.0)).all())
res["leak"] = int(causality_guard(signal, df, asset, tf))
full = backtest(df, pos, tf)
oos = backtest(df, pos, tf, lo=VAL_START, hi=HOLDOUT_START)
bh = buy_hold(df, tf)
_, p, _, _ = mc_pvalue(df, pos, tf, n=250)
res.update(
implemented=True,
full_sharpe=round(full.sharpe, 3), full_ret=round(full.ret, 3), full_dd=round(full.maxdd, 3),
oos_sharpe=round(oos.sharpe, 3), bh_sharpe=round(bh.sharpe, 3),
gross_sharpe=round(backtest(df, pos, tf, fee_rt=0.0).sharpe, 3),
fee02_sharpe=round(backtest(df, pos, tf, fee_rt=0.002).sharpe, 3),
turnover=round(full.ntrades, 1), exposure=round(full.exposure, 3),
null_p=round(p, 4),
beats_bh=bool(full.sharpe > bh.sharpe and oos.sharpe > 0),
)
# breadth per-anno (pre-hold-out): % anni positivi, anni rossi consecutivi
net, _, _, _ = _net_series(df, pos)
s = pd.Series(net, index=ts(df))
s = s[s.index < pd.Timestamp(HOLDOUT_START, tz="UTC")]
yr = {int(y): float((1 + g).prod() - 1) for y, g in s.groupby(s.index.year)}
vals = list(yr.values())
max_consec_red = 0; cur = 0
for v in vals:
cur = cur + 1 if v < 0 else 0
max_consec_red = max(max_consec_red, cur)
res["per_year_preho"] = {y: round(v, 3) for y, v in yr.items()}
res["pct_years_pos"] = round(sum(v > 0 for v in vals) / len(vals), 2) if vals else 0.0
res["max_consec_red_years"] = int(max_consec_red)
if holdout:
ho = backtest(df, pos, tf, lo=HOLDOUT_START)
res["holdout_sharpe"] = round(ho.sharpe, 3)
res["holdout_ret"] = round(ho.ret, 3)
res["holdout_dd"] = round(ho.maxdd, 3)
except Exception as e:
res["implemented"] = False
res["error"] = f"{type(e).__name__}: {str(e)[:200]}"
print("RESULT_JSON:" + json.dumps(res))
if __name__ == "__main__":
main()
-132
View File
@@ -1,132 +0,0 @@
"""FETCH + CERTIFY universo Hyperliquid (Cerbero MCP MAINNET) — espansione cross-sectional.
Hyperliquid (via cerbero-mcp mainnet) offre ~230 perp liquidi, ma storia nativa REALE solo dal
2024 (pre-2024 = backfill, volume 0). Qui scarico un set liquido a 1d (2024+), e CERTIFICO ogni
asset come BTC/ETH: cross-venue vs Binance (realismo) + flat-bar + VOLUME (liquidita'). Scrivo SOLO
i puliti in data/raw/hl_<sym>_1d.parquet (namespace dedicato, NON mischiato col Deribit BTC/ETH).
Disciplina: Cerbero ci ha gia' bruciato (testnet) -> niente fiducia, solo certificazione.
CORREZIONE estrazione (2026-06-20, "analisi fatte"): il floor START=2024-01-01 NON basta. Cerbero
restituisce BACKFILL SINTETICO (volume==0, ma prezzi copiati da un venue di riferimento -> matchano
Binance e NON sono flat) per il periodo PRIMA che l'asset quotasse davvero su Hyperliquid. Cosi'
asset listati a meta'/fine 2024+ passavano cross-venue+flat ed erano certificati PULITO pur essendo
in gran parte sintetici (es. AXS 83% backfill: trading reale solo da 2026-01; ALGO/SAND 37%). E' lo
stesso errore v2.0.0 (edge su un book che non c'era). Fix: (1) il VOLUME e' il rivelatore di backfill
-> si TAGLIA il run iniziale di barre a volume 0 e si tiene solo la serie NATIVA; (2) gate su storia
nativa minima (>= MIN_NATIVE_DAYS reali) -> scarta chi e' troppo corto dopo il taglio; (3) gate su
volume-0 INTERNO (gap di liquidita') oltre il taglio iniziale; (4) cross-venue/flat ricalcolati SOLO
sulle barre reali; (5) i parquet degli asset scartati vengono RIMOSSI (disco == set certificato).
uv run python scripts/analysis/fetch_hyperliquid.py
"""
from __future__ import annotations
import sys, time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd, requests, ccxt
RAW = PROJECT_ROOT / "data" / "raw"
START = "2024-01-01"; END = pd.Timestamp.now("UTC").strftime("%Y-%m-%d") # dinamico (refresh giornaliero)
MIN_NATIVE_DAYS = 365 # storia NATIVA reale minima (post-taglio backfill) per entrare nell'universo
INTERIOR_VOL0_MAX = 5.0 # % max di barre a volume 0 DOPO il taglio iniziale (gap di liquidita' interni)
# UNIVERSO ESTESO: alt liquidi noti su Hyperliquid (mappa Binance auto = SYM/USDT). Il gate di
# certificazione (cross-venue + liquidita' + flat) scarta i non-conformi. k-prefissi esclusi
# (scaling 1000x complica il cross-venue). MATIC morto escluso.
SYMS = ["BTC","ETH","SOL","BNB","XRP","DOGE","AVAX","LINK","LTC","ADA","ARB","OP","SUI","APT",
"INJ","TIA","SEI","NEAR","AAVE","ATOM","DYDX","APE","CRV","LDO","STX","GMX","SNX","BCH",
"COMP","MKR","WLD","UNI","TRX","FIL","RUNE","ENA","ORDI","JUP","WIF","PYTH","FET","AR",
"ETC","ALGO","GALA","SAND","AXS","DOT","FXS","BLUR","JTO","PENDLE","ONDO","TAO"]
BINANCE = {s: f"{s}/USDT" for s in SYMS}
def _h():
env={}
for ln in open(PROJECT_ROOT/".env.mainnet"):
ln=ln.strip()
if ln and not ln.startswith("#") and "=" in ln: k,v=ln.split("=",1); env[k]=v.strip()
return {"Authorization":f"Bearer {env['CERBERO_TOKEN']}","X-Bot-Tag":env.get('CERBERO_BOT_TAG','fetch'),"Content-Type":"application/json"}
def fetch_hl(sym, H, interval="1d"):
r=requests.post("https://cerbero-mcp.tielogic.xyz/mcp/tools/get_historical",
headers=H, json={"exchange":"hyperliquid","instrument":sym,"interval":interval,
"start_date":START,"end_date":END}, timeout=60)
c=r.json().get("candles",[])
if not c: return pd.DataFrame()
df=pd.DataFrame(c)[["timestamp","open","high","low","close","volume"]]
return df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
def binance_daily(sym_b, start_ms, end_ms):
ex=ccxt.binance({"enableRateLimit":True})
out={}; since=start_ms
while since<=end_ms:
try: r=ex.fetch_ohlcv(sym_b,"1d",since=since,limit=500)
except Exception: break
r=[x for x in r if x[0]>=since]
if not r: break
for x in r:
if start_ms<=x[0]<=end_ms and x[4]: out[int(x[0])]=float(x[4])
nxt=int(r[-1][0])+86400000
if nxt<=since: break
since=nxt
return pd.Series(out)
def trim_backfill(df):
"""Taglia il run INIZIALE di barre a volume 0 (= backfill sintetico pre-quotazione su HL).
Ritorna (serie_nativa, n_barre_tagliate). Il volume e' il rivelatore: il backfill copia i
prezzi da un venue di riferimento (non flat, matcha Binance) ma ha volume 0."""
vol = df["volume"].to_numpy()
lead = int(np.argmax(vol > 0)) if (vol > 0).any() else len(df)
return df.iloc[lead:].reset_index(drop=True), lead
def main():
H=_h()
print("="*100); print(" FETCH + CERTIFY Hyperliquid 1d (Cerbero mainnet) — cross-venue + flat + VOLUME (no backfill)"); print("="*100)
print(f" {'sym':<6}{'reali':>6}{'bfill':>6}{'start_reale':>13}{'flat%':>7}{'vol0%':>7}{'med_bps':>9}{'>1%':>7}{'verdetto':>14}")
certified=[]
for s in SYMS:
path = RAW/f"hl_{s.lower()}_1d.parquet"
raw=fetch_hl(s,H)
if raw.empty:
print(f" {s:<6} vuoto"); path.unlink(missing_ok=True); continue
# --- CORREZIONE: taglia il backfill sintetico (volume 0 iniziale), tieni la serie nativa ---
df, n_bfill = trim_backfill(raw)
if df.empty:
print(f" {s:<6} tutto backfill (vol0) -> scarta"); path.unlink(missing_ok=True); continue
ts=pd.to_datetime(df["timestamp"],unit="ms",utc=True)
flat=((df.open==df.high)&(df.high==df.low)&(df.low==df.close)).mean()*100
vol0=(df["volume"].to_numpy()==0).mean()*100 # gap di liquidita' INTERNI (post-taglio)
# cross-venue vs Binance USDT (daily close) — SOLO sulle barre reali
ref=binance_daily(BINANCE[s], int(df["timestamp"].iloc[0]), int(df["timestamp"].iloc[-1]))
a=df.set_index("timestamp")["close"]
m=pd.concat([a.rename("a"),ref.rename("b")],axis=1,join="inner").dropna()
if len(m)>5:
bps=(m["a"]-m["b"]).abs()/m["b"]*1e4
med=bps.median(); g1=(bps>100).mean()*100
else: med=g1=float("nan")
# gate "delistato/migrato": l'ultima barra dev'essere recente (entro ~21g da END),
# altrimenti l'asset tronca l'universo cross-sectional (es. MKR fermo a 2025-09, FXS 2026-01).
recent = (pd.Timestamp(END, tz="UTC") - ts.iloc[-1]) <= pd.Timedelta("21D")
# gate storia NATIVA: dopo il taglio dev'esserci abbastanza vita reale (es. AXS quotato 2026-01 -> scarta)
native_days = (ts.iloc[-1] - ts.iloc[0]).days
enough = native_days >= MIN_NATIVE_DAYS
clean = (not np.isnan(med)) and med<60 and g1<3 and flat<5 and vol0<INTERIOR_VOL0_MAX and recent and enough
if clean: v="PULITO"
elif not enough: v=f"corto<{MIN_NATIVE_DAYS}g"
else: v="scarta"
print(f" {s:<6}{len(df):>6}{n_bfill:>6}{str(ts.iloc[0].date()):>13}{flat:>6.1f}%{vol0:>6.1f}%{med:>9.1f}{g1:>6.1f}%{v:>14}")
if clean:
df.to_parquet(path, index=False); certified.append(s)
else:
path.unlink(missing_ok=True) # disco == set certificato (niente parquet contaminati a riposo)
print(f"\n CERTIFICATI ({len(certified)}): {certified}")
print(" Scritti in data/raw/hl_<sym>_1d.parquet (namespace dedicato, SERIE NATIVA senza backfill).")
if __name__=="__main__":
main()
-121
View File
@@ -1,121 +0,0 @@
"""ANALISI OPTIONS per BTC/ETH — onesta sui dati REALI disponibili (cerbero-bite mainnet).
Dati: Old/data/options (chain per-strike + dvol + market_snapshots). Finestra ~2026-05-01→06-11
(~6 settimane, REGIME UNICO calmo). NON si può validare OOS un edge su opzioni qui; si possono
MISURARE i livelli reali (VRP, premi put, skew, liquidità) e ragionare sull'USO delle opzioni
per il book BTC/ETH certificato. cerbero-bite è ancora vivo -> la fonte continua ad accumulare.
uv run python scripts/analysis/options_analysis.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
OPT = PROJECT_ROOT / "Old" / "data" / "options"
def load(name):
return pd.read_parquet(OPT / name)
def market_snapshots_analysis():
print("=" * 90)
print(" (1) MARKET SNAPSHOTS — VRP, DVOL, funding, dealer-gamma (livelli reali)")
print("=" * 90)
ms = load("market_snapshots.parquet")
t = pd.to_datetime(ms["timestamp"], utc=True, errors="coerce")
print(f" copertura: {t.min()} -> {t.max()} ({len(ms)} righe)")
for a in ("BTC", "ETH"):
d = ms[ms["asset"] == a].dropna(subset=["iv_minus_rv"])
if len(d) == 0:
print(f" {a}: nessun dato"); continue
vrp = d["iv_minus_rv"].astype(float)
dvol = d["dvol"].astype(float)
rv = d["realized_vol_30d"].astype(float)
fund = d["funding_perp_annualized"].astype(float) if "funding_perp_annualized" in d else pd.Series([np.nan])
gam = d["dealer_net_gamma"].astype(float) if "dealer_net_gamma" in d else pd.Series([np.nan])
print(f"\n {a} (n={len(d)})")
print(f" VRP (IV-RV): media {vrp.mean():+.1f} mediana {vrp.median():+.1f} "
f">0 nel {100*(vrp>0).mean():.0f}% del tempo [IV-RV in punti di vol annua]")
print(f" DVOL: media {dvol.mean():.1f} range [{dvol.min():.1f}, {dvol.max():.1f}]")
print(f" Realized30d: media {rv.mean():.1f}")
print(f" Funding perp: media {fund.mean():+.1f}% annuo")
if gam.notna().any():
print(f" Dealer net-γ: >0 nel {100*(gam>0).mean():.0f}% del tempo (>0 = dealer long gamma = mean-rev)")
def chain_analysis(asset):
print("\n" + "=" * 90)
print(f" (2) CHAIN {asset} — premi put protettivi, skew, liquidità (livelli reali)")
print("=" * 90)
ch = load(f"{asset.lower()}_chain.parquet")
for col in ("strike", "bid", "ask", "mid", "iv", "delta", "gamma"):
if col in ch:
ch[col] = pd.to_numeric(ch[col], errors="coerce")
ch["option_type"] = ch["option_type"].astype(str)
dv = load("dvol_history.parquet")
dv = dv[dv["asset"] == asset][["timestamp", "spot"]].copy()
dv["spot"] = pd.to_numeric(dv["spot"], errors="coerce")
# timestamp -> datetime UTC nativo (sono datetime64[tz], NON ms int: to_numeric li romperebbe)
ch["t"] = pd.to_datetime(ch["timestamp"], utc=True, errors="coerce")
dv["t"] = pd.to_datetime(dv["timestamp"], utc=True, errors="coerce")
ch = ch.dropna(subset=["t"]).sort_values("t").reset_index(drop=True)
dv = dv.dropna(subset=["t", "spot"]).sort_values("t").reset_index(drop=True)
# spot causale per timestamp della chain (merge_asof nearest, tolleranza 1h)
ch = pd.merge_asof(ch, dv[["t", "spot"]], on="t", direction="nearest",
tolerance=pd.Timedelta("1h"))
ch = ch.dropna(subset=["spot", "mid", "strike"])
# days-to-expiry
exp = pd.to_datetime(ch["expiry"], utc=True, errors="coerce")
ch["dte"] = (exp - ch["t"]).dt.total_seconds() / 86_400.0
ch = ch[(ch["dte"] > 0.5) & (ch["dte"] < 90)]
ch["money"] = ch["strike"] / ch["spot"]
ch["prem_pct"] = ch["mid"] * 100 # mid è in COIN (frazione del sottostante) -> %-del-notional
# NB: iv è GIÀ in percento (35.94 = 35.94%, coerente col DVOL ~40) -> non riscalare
ch["spread_pct"] = (ch["ask"] - ch["bid"]) / ch["mid"].replace(0, np.nan) * 100
puts = ch[ch["option_type"].str.lower().str.startswith("p")]
calls = ch[ch["option_type"].str.lower().str.startswith("c")]
def band(df, mlo, mhi, dlo, dhi):
s = df[(df["money"] >= mlo) & (df["money"] <= mhi) & (df["dte"] >= dlo) & (df["dte"] <= dhi)]
return s
print(" PUT protettive — premio reale (mid/spot) e liquidità per tenor/moneyness:")
print(f" {'tenor':<10s}{'moneyness':<14s}{'premio%':>9s}{'/mese%':>9s}{'spread%':>9s}{'n':>7s}{'strike?':>9s}")
for dlo, dhi, tn in [(5, 12, "settim."), (18, 45, "mensile")]:
for mlo, mhi, ml in [(0.97, 1.03, "ATM"), (0.88, 0.93, "~10% OTM"), (0.83, 0.88, "~15% OTM")]:
s = band(puts, mlo, mhi, dlo, dhi)
if len(s) == 0:
print(f" {tn:<10s}{ml:<14s}{'':>9s}{'':>9s}{'':>9s}{0:>7d}{'NO':>9s}")
continue
prem = s["prem_pct"].median()
permonth = prem * 30.0 / s["dte"].median()
print(f" {tn:<10s}{ml:<14s}{prem:>8.2f}%{permonth:>8.2f}%{s['spread_pct'].median():>8.1f}%"
f"{len(s):>7d}{'SI':>9s}")
# skew: IV put 10% OTM vs IV call 10% OTM (stesso tenor mensile)
pv = band(puts, 0.88, 0.93, 12, 50)["iv"].median()
cv = band(calls, 1.07, 1.12, 12, 50)["iv"].median()
atmv = band(ch, 0.98, 1.02, 12, 50)["iv"].median()
if pd.notna(pv) and pd.notna(cv):
print(f" SKEW: IV put 10%OTM {pv:.0f}% vs call 10%OTM {cv:.0f}% vs ATM {atmv:.0f}%"
f" -> skew put {pv-cv:+.0f} pt vol (>0 = put care = paura del crash prezzata)")
def main():
market_snapshots_analysis()
for a in ("BTC", "ETH"):
chain_analysis(a)
print("\n" + "=" * 90)
print(" NB: finestra ~6 settimane, REGIME UNICO calmo -> livelli REALI misurabili, ma NESSUN")
print(" edge su opzioni è validabile OOS qui. Vedi commento finale.")
print("=" * 90)
if __name__ == "__main__":
main()
-158
View File
@@ -1,158 +0,0 @@
"""FASE 1 — triage dei 2 superstiti su BTC/ETH, sull'harness onesto (research_lab).
Sul feed pulito solo SH01 (shape-ML) e frammenti HONEST mostravano segnale residuo. Delle
HONEST solo DIP (dip-reversion) è testabile su BTC/ETH (TR01/ROT02 richiedono alt esclusi).
Qui ri-implemento DIP e SH01-shape-ML come SERIE DI POSIZIONE e li passo ai gate onesti
(FULL/OOS-VAL, vs buy&hold, null p-value, sweep fee, griglia). Hold-out 2025+ resta BLOCCATO.
uv run python scripts/analysis/phase1_survivors.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
from src.data.downloader import load_data
from scripts.analysis.research_lab import (
backtest, buy_hold, mc_pvalue, report, VAL_START, HOLDOUT_START, FEE_RT,
)
# ----------------------------- DIP reversion (long-only) -----------------------------
def dip_signal(df, n=50, k=2.0, z_exit=0.0, max_bars=72):
"""Long-only: entra (pos=1) quando lo z-score causale del prezzo vs MA(n) <= -k (dip),
esce quando z>=z_exit o dopo max_bars. Decisione a close[i] (z[i] usa close[i]), guadagna
close[i]->close[i+1]. Niente fill su estremi di candela."""
c = df["close"].values.astype(float)
s = pd.Series(c)
ma = s.rolling(n).mean().values
sd = s.rolling(n).std().values
z = np.where(sd > 0, (c - ma) / sd, np.nan)
pos = np.zeros(len(c))
inpos = False
held = 0
for i in range(len(c)):
if not inpos:
if not np.isnan(z[i]) and z[i] <= -k:
inpos, held = True, 0
pos[i] = 1.0
else:
held += 1
if (not np.isnan(z[i]) and z[i] >= z_exit) or held >= max_bars:
inpos = False # esce al close[i]: pos[i]=0
else:
pos[i] = 1.0
return pos
# ----------------------------- SH01 shape-ML (walk-forward) -----------------------------
def _shape_features(df, W):
"""~12 feature di FORMA causali per barra, dalla finestra che termina a i (usa solo <=i)."""
o = df["open"].values.astype(float); h = df["high"].values.astype(float)
l = df["low"].values.astype(float); c = df["close"].values.astype(float)
s = pd.Series(c)
ret1 = s.pct_change()
rng = (h - l) / np.where(c > 0, c, np.nan)
body = (c - o) / np.where(h - l > 0, h - l, np.nan)
up_sh = (h - np.maximum(o, c)) / np.where(h - l > 0, h - l, np.nan)
dn_sh = (np.minimum(o, c) - l) / np.where(h - l > 0, h - l, np.nan)
# RSI(14)
d = s.diff()
gain = d.clip(lower=0).rolling(14).mean()
loss = (-d.clip(upper=0)).rolling(14).mean()
rsi = 100 - 100 / (1 + gain / loss.replace(0, np.nan))
hi_w = pd.Series(h).rolling(W).max(); lo_w = pd.Series(l).rolling(W).min()
feat = {
"mom_w": s / s.shift(W) - 1.0, # rendimento sulla finestra
"mom_half": s / s.shift(W // 2) - 1.0, # accelerazione
"vol_w": ret1.rolling(W).std(),
"rsi": rsi / 100.0,
"ma_dist": (c - s.rolling(W).mean()) / s.rolling(W).std(),
"pos_in_range": (c - lo_w) / (hi_w - lo_w).replace(0, np.nan), # dove sta il close nel range W
"range": pd.Series(rng).rolling(3).mean(),
"body": pd.Series(body).rolling(3).mean(),
"up_shadow": pd.Series(up_sh).rolling(3).mean(),
"dn_shadow": pd.Series(dn_sh).rolling(3).mean(),
"ret1": ret1,
"skew_w": ret1.rolling(W).skew(),
}
X = pd.DataFrame(feat).values
return X
def shape_ml_signal(df, W=24, H=12, th=0.55, refit=750, warmup=3000, long_short=True):
"""LogisticRegression walk-forward sulla forma. Label = segno del rendimento a H barre.
Al tempo di decisione i si allena SOLO su campioni j con esito già realizzato (j+H <= i):
strettamente causale, nessun leak. Rifit ogni `refit` barre (velocità). pos = +1 se
P(up)>th, -1 se P(up)<1-th (long_short), altrimenti 0."""
c = df["close"].values.astype(float)
n = len(c)
X = _shape_features(df, W)
fwd = np.full(n, np.nan)
fwd[:n - H] = c[H:] / c[:n - H] - 1.0
y = (fwd > 0).astype(float)
valid = ~np.isnan(X).any(axis=1)
pos = np.zeros(n)
model = scaler = None
start = max(warmup, W + H + 200)
for i in range(start, n):
if model is None or (i - start) % refit == 0:
# campioni di training: feature valide E label realizzata entro i (j+H <= i)
tr = np.where(valid & (np.arange(n) + H <= i) & (np.arange(n) >= W))[0]
tr = tr[tr < i - H]
if len(tr) >= 500 and len(np.unique(y[tr])) == 2:
scaler = StandardScaler().fit(X[tr])
model = LogisticRegression(max_iter=200, C=1.0).fit(scaler.transform(X[tr]), y[tr])
if model is not None and valid[i]:
p_up = float(model.predict_proba(scaler.transform(X[i:i + 1]))[0, 1])
pos[i] = 1.0 if p_up > th else (-1.0 if (long_short and p_up < 1 - th) else 0.0)
return pos
# ----------------------------------- run -----------------------------------
def main():
TF = "1h"
print("=" * 90)
print(f" FASE 1 — triage superstiti su BTC/ETH {TF} | netto fee 0.10% RT | hold-out {HOLDOUT_START}+ BLOCCATO")
print("=" * 90)
data = {a: load_data(a, TF) for a in ("BTC", "ETH")}
# ---------- DIP: griglia robustezza (plateau?) ----------
print("\n" + "#" * 90)
print(" DIP reversion (long-only) — griglia FULL Sharpe (plateau = robusto, picco = overfit)")
print("#" * 90)
GRID = [(n, k) for n in (30, 50, 100) for k in (1.5, 2.0, 2.5)]
for a in ("BTC", "ETH"):
df = data[a]
print(f"\n {a}: " + " ".join(
f"n{n}k{k}{backtest(df, dip_signal(df, n=n, k=k), TF).sharpe:>5.2f}" for n, k in GRID))
# report onesto sulla config centrale
for a in ("BTC", "ETH"):
report(f"DIP {a} (n50 k2.0)", data[a], dip_signal(data[a], n=50, k=2.0), TF)
# ---------- SH01 shape-ML: config record + paio di varianti ----------
print("\n" + "#" * 90)
print(" SH01 shape-ML (walk-forward LogReg) — long/short")
print("#" * 90)
for a in ("BTC", "ETH"):
df = data[a]
pos = shape_ml_signal(df, W=24, H=12, th=0.55, long_short=True)
report(f"SH-ML {a} (W24 H12 th.55 L/S)", df, pos, TF)
# variante long-only (meno fee)
pos_lo = shape_ml_signal(df, W=24, H=12, th=0.55, long_short=False)
report(f"SH-ML {a} (W24 H12 th.55 LONG-only)", df, pos_lo, TF)
print("\n" + "=" * 90)
print(" VERDETTO: un edge è REALE solo se FULL e OOS-VAL Sharpe > 0, regge il sweep fee,")
print(" e BATTE il null (p<0.05). Altrimenti = rumore, si chiude.")
print("=" * 90)
if __name__ == "__main__":
main()
-221
View File
@@ -1,221 +0,0 @@
"""FASE 2 — esplorazione larga per famiglie su BTC/ETH, harness onesto (research_lab).
Famiglie (serie di posizione, causali, netto fee, vs buy&hold + null p-value):
TSMOM (momentum) | REVERSAL | MA-cross | DONCHIAN breakout | VOL-TARGET overlay |
LEAD-LAG BTC<->ETH | HURST-gated momentum. Multi-TF dove sensato (1h + 15m).
La barra DA BATTERE è il buy&hold (Sharpe ~0.8 su BTC/ETH): una strategia di timing vale solo
se fa MEGLIO net-fee. Per ogni famiglia: scan griglia (FULL Sharpe), poi report onesto sulla
config migliore. Selezionare il best-di-griglia GONFIA -> i gate veri sono OOS-VAL + null p<0.05.
uv run python scripts/analysis/phase2_families.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from scripts.analysis.research_lab import (
backtest, buy_hold, mc_pvalue, window_mask, ts, VAL_START, HOLDOUT_START, BARS_PER_YEAR,
)
# --------------------------------- famiglie ---------------------------------
def tsmom(df, L, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
pos = np.sign(np.nan_to_num((c / c.shift(L) - 1).values))
return np.maximum(pos, 0) if mode == "lo" else pos
def reversal(df, L, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
pos = -np.sign(np.nan_to_num((c / c.shift(L) - 1).values))
return np.maximum(pos, 0) if mode == "lo" else pos
def ma_cross(df, fast, slow, mode="ls"):
c = pd.Series(df["close"].values.astype(float))
ef = c.ewm(span=fast, adjust=False).mean()
es = c.ewm(span=slow, adjust=False).mean()
pos = np.sign((ef - es).values)
return np.maximum(pos, 0) if mode == "lo" else pos
def donchian(df, L, mode="ls"):
h = pd.Series(df["high"].values.astype(float)).rolling(L).max().shift(1).values
l = pd.Series(df["low"].values.astype(float)).rolling(L).min().shift(1).values
c = df["close"].values.astype(float)
pos = np.zeros(len(c)); cur = 0
for i in range(len(c)):
if not np.isnan(h[i]) and c[i] > h[i]:
cur = 1
elif not np.isnan(l[i]) and c[i] < l[i]:
cur = -1 if mode == "ls" else 0
pos[i] = cur
return pos
def vol_target(df, tf, target=0.6, L=72):
"""Overlay SEMPRE-LONG con esposizione scalata dalla vol realizzata (target vol annua)."""
c = pd.Series(df["close"].values.astype(float))
rv_ann = c.pct_change().rolling(L).std().values * np.sqrt(BARS_PER_YEAR[tf])
pos = np.clip(np.nan_to_num(target / np.where(rv_ann > 0, rv_ann, np.nan), nan=0.0), 0, 1)
return pos
def rolling_hurst(c, W=120, step=6, lags=(2, 4, 8, 16, 32)):
logc = np.log(c); n = len(c); H = np.full(n, np.nan)
lg = np.log(lags)
for i in range(W, n, step):
seg = logc[i - W:i]
tau = [np.std(seg[lag:] - seg[:-lag]) for lag in lags]
if min(tau) > 0:
H[i] = np.polyfit(lg, np.log(tau), 1)[0]
return pd.Series(H).ffill().fillna(0.5).values
def hurst_mom(df, L=48, W=120, mode="ls"):
H = rolling_hurst(df["close"].values.astype(float), W)
return np.where(H > 0.5, tsmom(df, L, mode), 0.0)
def leadlag_df(target_df, other_df, L):
"""Costruisce un df col close del TARGET e la posizione = segno del rendimento a L barre
dell'ALTRO asset (allineato per timestamp). Ritorna (df_merged, pos)."""
a = target_df[["timestamp", "open", "high", "low", "close"]]
b = other_df[["timestamp", "close"]].rename(columns={"close": "other"})
m = a.merge(b, on="timestamp", how="inner").reset_index(drop=True)
o = pd.Series(m["other"].values.astype(float))
pos = np.sign(np.nan_to_num((o / o.shift(L) - 1).values))
return m, pos
# --------------------------------- reporting ---------------------------------
ROWS = []
def summarize(family, asset, tf, df, pos, mc_n=300):
full = backtest(df, pos, tf)
oos = backtest(df, pos, tf, lo=VAL_START, hi=HOLDOUT_START)
bh = buy_hold(df, tf)
gross = backtest(df, pos, tf, fee_rt=0.0).sharpe
_, p, _, _ = mc_pvalue(df, pos, tf, n=mc_n)
beats_bh = full.sharpe > bh.sharpe and oos.sharpe > 0
real = (full.sharpe > 0 and oos.sharpe > 0 and not np.isnan(p) and p < 0.05)
verdict = "★EDGE?" if (real and beats_bh) else ("real?" if real else "rumore")
ROWS.append(dict(fam=family, asset=asset, tf=tf, full=full.sharpe, oos=oos.sharpe,
gross=gross, bh=bh.sharpe, p=p, trd=full.ntrades, verdict=verdict))
print(f" {family:<16s} {asset} {tf:<3s} | FULL {full.sharpe:>5.2f} OOS {oos.sharpe:>5.2f} "
f"gross {gross:>5.2f} | B&H {bh.sharpe:>4.2f} | p {p:>.3f} | trd/y {full.ntrades:>6.0f} | {verdict}")
def scan_best(family, asset, tf, df, fn, grid, label_fn):
"""Scansiona la griglia (FULL Sharpe), stampa la riga compatta, ritorna la pos migliore."""
best = None
line = []
for params in grid:
pos = fn(df, *params)
s = backtest(df, pos, tf).sharpe
line.append(f"{label_fn(params)}={s:>4.1f}")
if best is None or s > best[0]:
best = (s, params, pos)
print(f" {asset} {tf} grid: " + " ".join(line))
return best[2], best[1]
def main():
print("=" * 100)
print(" FASE 2 — esplorazione famiglie BTC/ETH | netto fee 0.10% RT | barra = buy&hold | hold-out bloccato")
print("=" * 100)
D1 = {a: load_data(a, "1h") for a in ("BTC", "ETH")}
D15 = {a: load_data(a, "15m") for a in ("BTC", "ETH")}
def block(title):
print("\n" + "#" * 100 + f"\n {title}\n" + "#" * 100)
# ---- TSMOM (momentum) 1h + 15m, L/S e long-only ----
block("TSMOM (momentum)")
Ls = [(12,), (24,), (48,), (96,), (192,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("TSMOM-LS", a, "1h", D1[a], lambda d, L: tsmom(d, L, "ls"), Ls, lambda x: f"L{x[0]}")
summarize("TSMOM-LS", a, "1h", D1[a], pos)
pos, p = scan_best("TSMOM-LO", a, "1h", D1[a], lambda d, L: tsmom(d, L, "lo"), Ls, lambda x: f"L{x[0]}")
summarize("TSMOM-LO", a, "1h", D1[a], pos)
pos, p = scan_best("TSMOM-LS", a, "15m", D15[a], lambda d, L: tsmom(d, L, "ls"), [(48,),(96,),(192,),(384,)], lambda x: f"L{x[0]}")
summarize("TSMOM-LS", a, "15m", D15[a], pos)
# ---- REVERSAL 1h + 15m ----
block("REVERSAL (mean-reversion breve)")
Lr = [(1,), (3,), (6,), (12,), (24,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("REV-LS", a, "1h", D1[a], lambda d, L: reversal(d, L, "ls"), Lr, lambda x: f"L{x[0]}")
summarize("REV-LS", a, "1h", D1[a], pos)
pos, p = scan_best("REV-LS", a, "15m", D15[a], lambda d, L: reversal(d, L, "ls"), Lr, lambda x: f"L{x[0]}")
summarize("REV-LS", a, "15m", D15[a], pos)
# ---- MA cross ----
block("MA-CROSS (trend)")
g = [(12, 48), (24, 96), (48, 192), (24, 200)]
for a in ("BTC", "ETH"):
pos, p = scan_best("MAX-LS", a, "1h", D1[a], lambda d, f, s: ma_cross(d, f, s, "ls"), g, lambda x: f"{x[0]}/{x[1]}")
summarize("MAX-LS", a, "1h", D1[a], pos)
pos, p = scan_best("MAX-LO", a, "1h", D1[a], lambda d, f, s: ma_cross(d, f, s, "lo"), g, lambda x: f"{x[0]}/{x[1]}")
summarize("MAX-LO", a, "1h", D1[a], pos)
# ---- Donchian breakout ----
block("DONCHIAN breakout")
Ld = [(24,), (48,), (96,), (192,)]
for a in ("BTC", "ETH"):
pos, p = scan_best("DONCH-LS", a, "1h", D1[a], lambda d, L: donchian(d, L, "ls"), Ld, lambda x: f"L{x[0]}")
summarize("DONCH-LS", a, "1h", D1[a], pos)
pos, p = scan_best("DONCH-LO", a, "1h", D1[a], lambda d, L: donchian(d, L, "lo"), Ld, lambda x: f"L{x[0]}")
summarize("DONCH-LO", a, "1h", D1[a], pos)
# ---- Vol-target overlay (vs buy&hold) ----
block("VOL-TARGET overlay (sempre-long scalato) — riduce la vol/DD del buy&hold?")
for a in ("BTC", "ETH"):
pos, p = scan_best("VOLTGT", a, "1h", D1[a], lambda d, t: vol_target(d, "1h", t, 72),
[(0.4,), (0.6,), (0.8,), (1.0,)], lambda x: f"t{x[0]}")
summarize("VOLTGT", a, "1h", D1[a], pos)
# ---- Hurst-gated momentum ----
block("HURST-gated momentum (momentum solo in regime trending H>0.5)")
for a in ("BTC", "ETH"):
pos, p = scan_best("HURST-MOM", a, "1h", D1[a], lambda d, L: hurst_mom(d, L, 120, "ls"),
[(24,), (48,), (96,)], lambda x: f"L{x[0]}")
summarize("HURST-MOM", a, "1h", D1[a], pos)
# ---- Lead-lag BTC<->ETH ----
block("LEAD-LAG BTC<->ETH (posiziona un asset col rendimento passato dell'altro)")
for tgt, oth in (("ETH", "BTC"), ("BTC", "ETH")):
Ll = [1, 3, 6, 12, 24]
best = None; line = []
for L in Ll:
m, pos = leadlag_df(D1[tgt], D1[oth], L)
s = backtest(m, pos, "1h").sharpe
line.append(f"L{L}={s:>4.1f}")
if best is None or s > best[0]:
best = (s, L, m, pos)
print(f" {oth}->{tgt} 1h grid: " + " ".join(line))
_, L, m, pos = best
summarize(f"LL {oth}>{tgt}", tgt, "1h", m, pos)
# ---- classifica finale ----
print("\n" + "=" * 100)
print(" CLASSIFICA — net-fee FULL Sharpe (★EDGE? = batte B&H, OOS>0 e null p<0.05)")
print("=" * 100)
for r in sorted(ROWS, key=lambda r: -r["full"]):
print(f" {r['fam']:<16s} {r['asset']} {r['tf']:<3s} | FULL {r['full']:>5.2f} | OOS {r['oos']:>5.2f} | "
f"B&H {r['bh']:>4.2f} | p {r['p']:>.3f} | {r['verdict']}")
edges = [r for r in ROWS if r["verdict"] == "★EDGE?"]
print(f"\n Candidati che battono il buy&hold net-fee + OOS>0 + null p<0.05: {len(edges)}")
for r in edges:
print(f" -> {r['fam']} {r['asset']} {r['tf']}: FULL {r['full']:.2f} OOS {r['oos']:.2f} p {r['p']:.3f}")
if __name__ == "__main__":
main()
-152
View File
@@ -1,152 +0,0 @@
"""FASE 3 — conferma avversariale del SOLO candidato reale: trend-following long-only (MA-cross).
Protocollo onesto:
1. SELEZIONE config SOLO sul pre-hold-out (< 2025-01-01). Niente sbirciate al hold-out.
2. HOLD-OUT 2025-26 sbloccato UNA volta (la prova del nove, mai usato in ricerca).
3. Breakdown PER ANNO vs buy&hold: il trend-LO deve "schivare" i bear (2018/2022).
4. STRESS: fee 2x, lag di esecuzione (1 barra), slippage.
5. DEFLATED SHARPE (Bailey & López de Prado): lo Sharpe regge alla correzione per multiple-testing?
uv run python scripts/analysis/phase3_confirm.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from scipy.stats import norm, skew, kurtosis
from src.data.downloader import load_data
from scripts.analysis.research_lab import (
backtest, buy_hold, window_mask, ts, _net_series, HOLDOUT_START, BARS_PER_YEAR,
)
from scripts.analysis.phase2_families import ma_cross
GRID = [(12, 48), (24, 96), (48, 192), (24, 200), (96, 288)] # MA-cross griglia (fast/slow)
REPR = (24, 96) # config rappresentativa PRE-COMMITTATA
TF = "1h"
def lag(pos, k=1):
"""Esecuzione in ritardo di k barre (agisci k barre dopo la decisione)."""
return np.concatenate([np.zeros(k), pos[:-k]])
def per_year(df, pos, tf):
c = df["close"].values.astype(float)
net, _, fwd, _ = _net_series(df, pos)
yrs = ts(df).dt.year.values
out = {}
for y in sorted(set(yrs)):
m = yrs == y
if m.sum() < 2:
continue
strat = float(np.prod(1 + net[m]) - 1) * 100
bh = float(np.prod(1 + fwd[m]) - 1) * 100
expo = float(np.mean(np.abs(pos[m])))
out[y] = (strat, bh, expo)
return out
def deflated_sharpe(net, sr_trials_perbar, N):
"""DSR: prob. che il vero Sharpe > la soglia attesa-massima sotto N trial (multiple testing).
Tutto in Sharpe PER BARRA. >0.95 = significativo dopo correzione."""
sr = net.mean() / net.std()
T = len(net)
g3 = float(skew(net)); g4 = float(kurtosis(net, fisher=False))
var_sr = float(np.var(sr_trials_perbar, ddof=1)) if len(sr_trials_perbar) > 1 else 0.0
ge = 0.5772156649
z1 = norm.ppf(1 - 1.0 / N); z2 = norm.ppf(1 - 1.0 / (N * np.e))
sr0 = np.sqrt(var_sr) * ((1 - ge) * z1 + ge * z2) # Sharpe atteso-massimo sotto null, N trial
den = np.sqrt(max(1 - g3 * sr + (g4 - 1) / 4.0 * sr ** 2, 1e-9))
dsr = float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den))
bpy = BARS_PER_YEAR[TF]
return dsr, sr * np.sqrt(bpy), sr0 * np.sqrt(bpy)
def main():
print("=" * 96)
print(" FASE 3 — conferma avversariale: TREND-following long-only (MA-cross) BTC/ETH")
print("=" * 96)
data = {a: load_data(a, TF) for a in ("BTC", "ETH")}
# ---------- 1) selezione SOLO pre-hold-out ----------
print(f"\n (1) SELEZIONE su pre-hold-out (< {HOLDOUT_START}) — Sharpe per config (plateau = robusto)")
for a in ("BTC", "ETH"):
line = []
for f, s in GRID:
pos = ma_cross(data[a], f, s, "lo")
sh = backtest(data[a], pos, TF, hi=HOLDOUT_START).sharpe
line.append(f"{f}/{s}={sh:>4.2f}")
print(f" {a}: " + " ".join(line))
print(f" -> config rappresentativa PRE-COMMITTATA per i test seguenti: {REPR[0]}/{REPR[1]}")
# ---------- 2) HOLD-OUT 2025-26 (sbloccato una volta) ----------
print(f"\n (2) HOLD-OUT {HOLDOUT_START}+ — LA PROVA DEL NOVE (mai usato in ricerca)")
for a in ("BTC", "ETH"):
bh = buy_hold(data[a], TF, lo=HOLDOUT_START)
print(f" {a}: buy&hold hold-out Sh {bh.sharpe:>5.2f} ret {bh.ret*100:>+7.1f}% DD {bh.maxdd*100:>4.1f}%")
for f, s in GRID:
pos = ma_cross(data[a], f, s, "lo")
r = backtest(data[a], pos, TF, lo=HOLDOUT_START)
star = " <-REPR" if (f, s) == REPR else ""
print(f" {f}/{s:<3d} Sh {r.sharpe:>5.2f} ret {r.ret*100:>+7.1f}% DD {r.maxdd*100:>4.1f}% expo {r.exposure:.2f}{star}")
# ---------- 3) per anno vs buy&hold (schiva i bear?) ----------
print(f"\n (3) PER ANNO — strat {REPR[0]}/{REPR[1]} vs buy&hold (expo = quanto è long; bear test 2018/2022)")
for a in ("BTC", "ETH"):
pos = ma_cross(data[a], *REPR, "lo")
py = per_year(data[a], pos, TF)
print(f" {a}:")
for y, (st, bh, ex) in py.items():
flag = " <- BEAR" if bh < -20 else ""
print(f" {y}: strat {st:>+7.0f}% | buy&hold {bh:>+7.0f}% | expo {ex:.2f}{flag}")
# ---------- 4) stress ----------
print(f"\n (4) STRESS — strat {REPR[0]}/{REPR[1]} | FULL e HOLD-OUT Sharpe")
print(f" {'scenario':<24s}{'BTC FULL':>10s}{'BTC HO':>9s}{'ETH FULL':>10s}{'ETH HO':>9s}")
scen = [
("base fee0.10%", dict(fee_rt=0.001), False),
("fee 0.20% (2x)", dict(fee_rt=0.002), False),
("lag 1 barra", dict(fee_rt=0.001), True),
("fee2x + lag", dict(fee_rt=0.002), True),
]
for name, kw, do_lag in scen:
row = [name]
for a in ("BTC", "ETH"):
pos = ma_cross(data[a], *REPR, "lo")
if do_lag:
pos = lag(pos, 1)
full = backtest(data[a], pos, TF, **kw).sharpe
ho = backtest(data[a], pos, TF, lo=HOLDOUT_START, **kw).sharpe
row += [f"{full:>9.2f}", f"{ho:>8.2f}"]
print(f" {row[0]:<24s}{row[1]:>10s}{row[2]:>9s}{row[3]:>10s}{row[4]:>9s}")
# ---------- 5) deflated Sharpe ----------
print(f"\n (5) DEFLATED SHARPE — corregge il multiple-testing (DSR>0.95 = regge)")
# trial set = TUTTE le config trend long-only provate (proxy del numero di tentativi)
N_TRIALS = 60 # stima conservativa dei backtest provati in Fase 2 (tutte le famiglie/asset/TF)
for a in ("BTC", "ETH"):
trials = [backtest(data[a], ma_cross(data[a], f, s, "lo"), TF, hi=HOLDOUT_START) for f, s in GRID]
sr_trials = []
for f, s in GRID:
net, _, _, _ = _net_series(data[a], ma_cross(data[a], f, s, "lo"))
m = window_mask(data[a], hi=HOLDOUT_START)
sr_trials.append(net[m].mean() / net[m].std())
net, _, _, _ = _net_series(data[a], ma_cross(data[a], *REPR, "lo"))
m = window_mask(data[a], hi=HOLDOUT_START)
dsr, sr_ann, sr0_ann = deflated_sharpe(net[m], sr_trials, N_TRIALS)
verdict = "REGGE" if dsr > 0.95 else "NON regge"
print(f" {a} (pre-hold-out): Sharpe {sr_ann:.2f} vs soglia-max-attesa(N={N_TRIALS}) {sr0_ann:.2f} "
f"-> DSR {dsr:.3f} [{verdict}]")
print("\n" + "=" * 96)
print(" VERDETTO: edge ONESTO solo se (2) hold-out positivo, (3) schiva i bear, (4) regge lo")
print(" stress, (5) DSR>0.95. Altrimenti: anche il trend era sample-luck del mercato toro.")
print("=" * 96)
if __name__ == "__main__":
main()
-208
View File
@@ -1,208 +0,0 @@
"""HARNESS DI RICERCA ONESTO — BTC/ETH, v2.0.0 (Fase 0).
Dopo che l'intera libreria precedente si è rivelata artefatto di feed/harness disonesti,
la prima cosa di cui fidarsi NON è una strategia ma il banco di prova. Questo modulo è
quel banco: causale per costruzione, netto fee, con baseline e null model.
MODELLO CANONICO = SERIE DI POSIZIONE.
Una strategia è una funzione signal(df, **params) -> pd.Series/np.array che dà la
posizione target per barra in [-1, +1]. REGOLA: position[i] è decisa con dati FINO a
close[i] (mai oltre) e GUADAGNA il rendimento close[i] -> close[i+1]. L'engine moltiplica
position[i] * fwd[i] (fwd strettamente futuro rispetto alla decisione) -> niente look-ahead
per costruzione, e niente fill sull'estremo di candela (si entra al close). La fee è
addebitata sul TURNOVER |Δposition| (un round-trip 0->1->0 = 2 unità = fee_rt intera).
GATE (vedi CLAUDE.md): ingresso eseguibile (qui per costruzione), netto fee 0.10% RT,
OOS held-out, robustezza su griglia, onestà statistica (null model + buy&hold), walk-forward
per i modelli fittati, liquidità (BTC/ETH ok).
uv run python scripts/analysis/research_lab.py # self-test del banco
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
FEE_RT = 0.001 # 0.10% round-trip taker Deribit (0.05%/lato)
BARS_PER_YEAR = {"5m": 105192.0, "15m": 35064.0, "1h": 8766.0,
"4h": 2191.5, "12h": 730.5, "1d": 365.25}
def load_tf(asset: str, tf: str):
"""Carica un TF certificato. 5m/15m/1h diretti; 4h/12h/1d DERIVATI per resample dal 1h
(confini 00:00 UTC). >=12h e' il regime raccomandato (sotto, costi+overfit dominano)."""
if tf in ("5m", "15m", "1h"):
return load_data(asset, tf)
rule = {"4h": "4h", "12h": "12h", "1d": "1D"}[tf]
df = load_data(asset, "1h").copy()
df.index = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
out = df.resample(rule, label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
epoch = pd.Timestamp("1970-01-01", tz="UTC")
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume"]]
# Hold-out FINALE bloccato: NIENTE ricerca/tuning lo tocca finché non è il verdetto (Fase 3).
HOLDOUT_START = "2025-01-01"
# Finestra di validazione OOS usata in ricerca (out-of-sample ma PRE hold-out).
VAL_START = "2023-01-01"
def ts(df) -> pd.Series:
return pd.to_datetime(df["timestamp"], unit="ms", utc=True)
def window_mask(df, lo: str | None = None, hi: str | None = None) -> np.ndarray:
t = ts(df)
m = np.ones(len(df), bool)
if lo is not None:
m &= (t >= pd.Timestamp(lo, tz="UTC")).values
if hi is not None:
m &= (t < pd.Timestamp(hi, tz="UTC")).values
return m
@dataclass
class BT:
n: int
ret: float # rendimento composto sulla finestra (pos 1x, leva 1x)
cagr: float
sharpe: float # annualizzato
maxdd: float # % (positivo)
exposure: float # |pos| medio
turnover: float # Σ|Δpos| / anno
ntrades: float # round-trip equivalenti / anno
def line(self, label="") -> str:
return (f" {label:<22s} Sh {self.sharpe:>6.2f} | ret {self.ret*100:>+8.1f}% "
f"CAGR {self.cagr*100:>+6.1f}% | DD {self.maxdd*100:>5.1f}% | "
f"expo {self.exposure:>4.2f} trd/y {self.ntrades:>6.1f} | n {self.n}")
def _net_series(df, position, fee_rt=FEE_RT):
"""Ritorna (net, gross, fwd, pos) per barra. net[i] = pos[i]*fwd[i] - fee sul cambio a i."""
c = df["close"].values.astype(float)
pos = np.nan_to_num(np.asarray(position, float), nan=0.0)
pos = np.clip(pos, -1.0, 1.0)
n = len(c)
fwd = np.zeros(n)
fwd[:-1] = c[1:] / c[:-1] - 1.0 # rendimento close[i]->close[i+1] (futuro vs decisione a i)
gross = pos * fwd
dpos = np.abs(np.diff(np.concatenate([[0.0], pos]))) # cambio di posizione a i (si tradea al close[i])
fee = dpos * (fee_rt / 2.0) # fee_rt = round-trip (2 unità di turnover); /2 per unità
net = gross - fee
return net, gross, fwd, pos
def backtest(df, position, tf="1h", fee_rt=FEE_RT, lo=None, hi=None) -> BT:
net, gross, fwd, pos = _net_series(df, position, fee_rt)
m = window_mask(df, lo, hi)
net_w, pos_w = net[m], pos[m]
dpos_w = np.abs(np.diff(np.concatenate([[0.0], pos_w])))
bpy = BARS_PER_YEAR[tf]
n = int(m.sum())
if n < 2:
return BT(n, 0, float("nan"), 0, 0, 0, 0, 0)
eq = np.cumprod(1.0 + net_w)
total = float(eq[-1] - 1.0)
years = n / bpy
cagr = float((1 + total) ** (1 / years) - 1) if years > 0 and total > -1 else float("nan")
mu, sd = float(net_w.mean()), float(net_w.std())
sharpe = mu / sd * np.sqrt(bpy) if sd > 0 else 0.0
peak = np.maximum.accumulate(eq)
maxdd = float(np.max((peak - eq) / peak)) if n else 0.0
expo = float(np.mean(np.abs(pos_w)))
turn_y = float(dpos_w.sum() / years) if years > 0 else 0.0
return BT(n, total, cagr, sharpe, maxdd, expo, turn_y, turn_y / 2.0)
def buy_hold(df, tf="1h", fee_rt=FEE_RT, lo=None, hi=None) -> BT:
return backtest(df, np.ones(len(df)), tf, fee_rt, lo, hi)
def mc_pvalue(df, position, tf="1h", fee_rt=FEE_RT, n=500, lo=None, hi=None, seed=0):
"""Null model a ROTAZIONE CIRCOLARE: ruota la serie di posizione di un offset casuale.
Preserva ESATTAMENTE exposure, turnover e distribuzione degli holding; distrugge solo
l'allineamento col mercato. p = P(Sharpe_ruotato >= Sharpe_reale). p alto = il timing
non batte il caso (nessuna skill)."""
pos = np.nan_to_num(np.asarray(position, float))
base = backtest(df, pos, tf, fee_rt, lo, hi).sharpe
N = len(pos)
if np.abs(np.diff(pos)).sum() == 0: # posizione costante -> rotazione degenere
return base, float("nan"), float("nan"), float("nan")
rng = np.random.default_rng(seed)
sims = np.empty(n)
for k in range(n):
off = int(rng.integers(1, N))
sims[k] = backtest(df, np.roll(pos, off), tf, fee_rt, lo, hi).sharpe
p = float((np.sum(sims >= base) + 1) / (n + 1))
return base, p, float(sims.mean()), float(sims.std())
def report(name, df, position, tf="1h", fee_rt=FEE_RT, mc_n=400):
"""Stampa il verdetto onesto: FULL / OOS-VAL / vs buy&hold / null p-value / sweep fee."""
print(f"\n === {name} ({tf}) ===")
print(backtest(df, position, tf, fee_rt).line("FULL"))
print(backtest(df, position, tf, fee_rt, lo=VAL_START, hi=HOLDOUT_START).line(f"OOS-VAL {VAL_START[:4]}-24"))
print(buy_hold(df, tf, fee_rt).line("buy&hold FULL"))
base, p, msh, ssd = mc_pvalue(df, position, tf, fee_rt, n=mc_n)
verdict = "RUMORE" if (np.isnan(p) or p > 0.05) else "batte il null"
print(f" null (rotazione, n={mc_n}): Sharpe reale {base:.2f} vs random {msh:.2f}±{ssd:.2f} "
f"-> p={p if not np.isnan(p) else float('nan'):.3f} [{verdict}]")
print(" sweep fee RT:", " ".join(
f"{f*100:.2f}%→Sh{backtest(df, position, tf, f).sharpe:.2f}" for f in (0.0, 0.0005, 0.001, 0.002)))
# ============================ SELF-TEST DEL BANCO ============================
def self_test():
"""Valida l'HARNESS, non una strategia. Tre prove:
(1) buy&hold: Sharpe positivo, DD grande (sanity dei numeri).
(2) CHEAT look-ahead (pos = segno del rendimento FUTURO): Sharpe enorme, p≈0
-> l'engine SA vedere un edge quando esiste davvero.
(3) NOISE causale (pos da rumore del passato): Sharpe≈0, p≈0.5
-> l'engine NON inventa edge dal nulla (niente leak)."""
print("=" * 78)
print(" SELF-TEST HARNESS — deve: vedere il cheat, NON vedere il rumore")
print("=" * 78)
df = load_data("BTC", "1h")
t = ts(df)
c = df["close"].values.astype(float)
bh = buy_hold(df, "1h")
print(bh.line("(1) buy&hold BTC"))
assert bh.sharpe > 0, "buy&hold dovrebbe avere Sharpe>0 sullo storico BTC"
# (2) CHEAT: posizione = segno del rendimento del prossimo bar (USA IL FUTURO)
fwd = np.zeros(len(c)); fwd[:-1] = c[1:] / c[:-1] - 1.0
cheat = np.sign(fwd)
bt_cheat = backtest(df, cheat, "1h")
_, p_cheat, _, _ = mc_pvalue(df, cheat, "1h", n=200, seed=1)
print(bt_cheat.line("(2) CHEAT look-ahead"))
print(f" -> null p={p_cheat:.4f} (atteso ≈0: l'edge finto È enorme e battibile dal caso ~mai)")
assert bt_cheat.sharpe > 20, "il cheat dovrebbe dare Sharpe enorme se l'engine è corretto"
assert p_cheat < 0.02, "il cheat dovrebbe battere il null in modo schiacciante"
# (3) NOISE causale a BASSO turnover (blocchi ~50 barre): isola la SKILL dalla fee-death.
# Posizione casuale (non usa il futuro) tenuta a blocchi -> turnover basso -> se l'engine non
# inventa edge dal nulla, Sharpe≈0 e il null p≈0.5 (random rotazioni indistinguibili).
rng = np.random.default_rng(42)
blk = 50
raw = np.sign(rng.standard_normal(len(c) // blk + 1))
noise_pos = np.repeat(raw, blk)[:len(c)]
noise_pos = pd.Series(noise_pos).shift(1).fillna(0).values # solo passato
bt_noise = backtest(df, noise_pos, "1h")
base_n, p_noise, msh, ssd = mc_pvalue(df, noise_pos, "1h", n=400, seed=2)
print(bt_noise.line("(3) NOISE causale"))
print(f" -> null p={p_noise:.3f} (atteso alto/≈0.5: nessuna skill, indistinguibile dal caso)")
assert bt_noise.sharpe < 2.0, "il rumore causale non deve sembrare SKILLATO (Sharpe positivo grande = leak)"
assert p_noise > 0.10, "il rumore causale non deve battere il null (p basso = edge spurio/leak)"
print("\n ✓ HARNESS VALIDATO: vede il cheat (Sharpe enorme, p≈0), non inventa edge dal rumore (p alto).")
print(f" Hold-out finale BLOCCATO da {HOLDOUT_START} (non usato in ricerca). OOS-VAL: {VAL_START}→hold-out.")
if __name__ == "__main__":
self_test()
-121
View File
@@ -1,121 +0,0 @@
"""STRESS-TEST di TP01 (integrato da strategy-research-2026-06) — robustezza avversariale.
Usa il modulo VERO integrato (src/strategies/trend_portfolio). Oltre a hold-out/cross-asset/multi-TF
(gia' in verify_tp01.py), qui: sweep FEE (fino 0.40% RT), LAG di esecuzione + slippage, PLATEAU dei
parametri (config cherry-picked?), DEFLATED-SHARPE (multiple-testing track A-E).
uv run python scripts/analysis/stress_tp01.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from scipy.stats import norm, skew, kurtosis
from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, resample_4h, simple_returns, CANONICAL
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
DF4H = {a: resample_4h(load_data(a, "1h")) for a in ("BTC", "ETH")}
def combo(cfg, lag_bars=0, fee_side=0.0005):
"""Rendimenti per-barra del portafoglio 50/50 con config cfg, lag extra e fee dati."""
tp = TrendPortfolio(**{**cfg, "fee_side": fee_side})
series = {}
for a in ("BTC", "ETH"):
df = DF4H[a]
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt))
s = 1 + lag_bars
held[s:] = tgt[:-s] # tenuta = decisa s barre prima (causale + lag)
net = held * r - fee_side * np.abs(np.diff(held, prepend=0.0))
net[0] = 0.0
series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return 0.5 * J["BTC"].values + 0.5 * J["ETH"].values, J.index
def met(combo_r, idx):
rr = combo_r[np.isfinite(combo_r)]
if len(rr) < 2 or np.std(rr) == 0:
return dict(sh=0, ret=0, dd=0)
bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median()
eq = np.cumprod(1 + rr); pk = np.maximum.accumulate(eq)
return dict(sh=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)),
ret=float(eq[-1] - 1), dd=float(np.max((pk - eq) / pk)))
def full_ho(cfg, lag_bars=0, fee_side=0.0005):
cr, idx = combo(cfg, lag_bars, fee_side)
ho = idx >= HOLDOUT
return met(cr, idx), met(cr[ho], idx[ho])
def main():
print("=" * 88)
print(" STRESS-TEST TP01 (PORT LF4h canonica) — robustezza avversariale")
print("=" * 88)
base_f, base_h = full_ho(CANONICAL)
print(f"\n BASELINE (4h, fee 0.10% RT): FULL Sh {base_f['sh']:.2f} ret {base_f['ret']*100:+.0f}% DD {base_f['dd']*100:.1f}%"
f" | HOLD-OUT Sh {base_h['sh']:.2f} ret {base_h['ret']*100:+.1f}% DD {base_h['dd']*100:.1f}%")
print("\n (1) SWEEP FEE (RT) — regge fino a 0.40%?")
print(f" {'fee RT':<10s}{'FULL Sh':>9s}{'FULL ret':>10s}{'HOLD Sh':>9s}{'HOLD ret':>10s}")
for frt in (0.0, 0.001, 0.002, 0.004):
f, h = full_ho(CANONICAL, fee_side=frt / 2)
print(f" {frt*100:>5.2f}% {f['sh']:>8.2f}{f['ret']*100:>+9.0f}%{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%")
print("\n (2) LAG di esecuzione + slippage (fee 0.20% per simulare slippage)")
print(f" {'scenario':<22s}{'FULL Sh':>9s}{'HOLD Sh':>9s}{'HOLD ret':>10s}")
for name, lag, frt in [("base", 0, 0.001), ("lag 1 barra (4h)", 1, 0.001),
("lag 2 barre", 2, 0.001), ("lag1 + fee0.20% slip", 1, 0.002)]:
f, h = full_ho(CANONICAL, lag_bars=lag, fee_side=frt / 2)
print(f" {name:<22s}{f['sh']:>8.2f}{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%")
print("\n (3) PLATEAU PARAMETRI — la config canonica e' un picco o un altopiano?")
print(f" {'variazione':<26s}{'FULL Sh':>9s}{'HOLD Sh':>9s}")
grid = [
("canonica (vt.20 lev2 30/90/180 vw30)", CANONICAL),
("target_vol 0.15", {**CANONICAL, "target_vol": 0.15}),
("target_vol 0.25", {**CANONICAL, "target_vol": 0.25}),
("leverage 1.5", {**CANONICAL, "leverage": 1.5}),
("leverage 3.0", {**CANONICAL, "leverage": 3.0}),
("horizons 20/60/120", {**CANONICAL, "horizons_days": (20, 60, 120)}),
("horizons 60/120/240", {**CANONICAL, "horizons_days": (60, 120, 240)}),
("vol_win 20", {**CANONICAL, "vol_win_days": 20}),
("vol_win 45", {**CANONICAL, "vol_win_days": 45}),
]
sr_trials = []
for name, cfg in grid:
f, h = full_ho(cfg)
cr, idx = combo(cfg)
sr_trials.append(cr[np.isfinite(cr)].mean() / cr[np.isfinite(cr)].std()) # Sharpe per-barra
print(f" {name:<26s}{f['sh']:>8.2f}{h['sh']:>9.2f}")
print("\n (4) DEFLATED SHARPE — corregge il multiple-testing (track A-E + sweep). DSR>0.95 = regge")
cr, idx = combo(CANONICAL)
rr = cr[np.isfinite(cr)]
sr = rr.mean() / rr.std(); T = len(rr)
g3 = float(skew(rr)); g4 = float(kurtosis(rr, fisher=False))
var_sr = float(np.var(sr_trials, ddof=1))
ge = 0.5772156649
for N in (10, 40, 100): # N = numero di trial/config provati (conservativo)
z1 = norm.ppf(1 - 1.0 / N); z2 = norm.ppf(1 - 1.0 / (N * np.e))
sr0 = np.sqrt(var_sr) * ((1 - ge) * z1 + ge * z2)
den = np.sqrt(max(1 - g3 * sr + (g4 - 1) / 4.0 * sr ** 2, 1e-9))
dsr = float(norm.cdf((sr - sr0) * np.sqrt(T - 1) / den))
bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median()
print(f" N={N:>3d} trial -> soglia-max-attesa Sh {sr0*np.sqrt(bpy):.2f} | DSR {dsr:.3f} [{'REGGE' if dsr>0.95 else 'NON regge'}]")
print("\n" + "=" * 88)
print(" Verdetto: TP01 robusto se regge fee 0.40%+lag (HOLD positivo), plateau (no picco), DSR>0.95.")
print("=" * 88)
if __name__ == "__main__":
main()
-96
View File
@@ -1,96 +0,0 @@
"""TP01 a BASSA FREQUENZA (>=12h) — ri-verifica dopo il bug look-ahead ffill-mixed-TF.
L'utente/agente ha trovato un look-ahead (ffill mixed-timeframe su barre open-labeled) che
gonfiava il 4h (~1.60 -> reale ~1.1) e ha concluso: NON scendere sotto le 12h (costi+overfit
dominano). Qui ricalcolo TP01 in modo PULITO per singolo TF (barre discrete, posizione shiftata
+1, NESSUN ffill/combine mixed-TF) su 4h/12h/1d, con un GUARD di causalita' esplicito sulla serie
resamplata (ricalcolo su prefisso). Fee 0.10% RT, hold-out 2025-26 bloccato.
uv run python scripts/analysis/tp01_lowfreq.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, simple_returns, CANONICAL
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
def resample_tf(df_1h, rule):
g = df_1h.copy()
g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
out = g.resample(rule, label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
out["datetime"] = out.index
return out.reset_index(drop=True)
def sleeve_net(df, tp):
"""Per-barra netto di uno sleeve: posizione decisa a close[i-1], tenuta in i (causale, no ffill)."""
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
return np.clip(net, -0.99, None)
def causality_ok(df, tp, k=10):
"""Ricalcola target_series su prefissi e verifica che tgt[i] non cambi (no look-ahead)."""
full = tp.target_series(df); n = len(df)
rng = np.random.default_rng(0); bad = 0
for i in rng.integers(int(n * 0.6), n - 1, size=k):
p = tp.target_series(df.iloc[:i + 1].copy())
if len(p) != i + 1 or not np.isclose(np.nan_to_num(p[i]), np.nan_to_num(full[i]), atol=1e-9):
bad += 1
return bad
def met(rr, idx):
rr = rr[np.isfinite(rr)]
if len(rr) < 2 or np.std(rr) == 0:
return dict(sh=0, ret=0, dd=0, n=len(rr))
bpy = 86400 * 365.25 / pd.Series(idx).diff().dt.total_seconds().median()
eq = np.cumprod(1 + rr); pk = np.maximum.accumulate(eq)
return dict(sh=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)), ret=float(eq[-1] - 1),
dd=float(np.max((pk - eq) / pk)), n=len(rr))
def main():
print("=" * 92)
print(" TP01 RI-VERIFICA BASSA FREQUENZA — calcolo pulito per-TF (no ffill mixed-TF) | fee 0.10% RT")
print("=" * 92)
tp = TrendPortfolio(**CANONICAL)
print(f" {'TF':<5s}{'leak':>6s}{'FULL Sh':>9s}{'FULL ret':>10s}{'FULL DD':>9s}{'HOLD Sh':>9s}{'HOLD ret':>10s}{'HOLD DD':>9s}")
for tf, rule in [("4h", "4h"), ("6h", "6h"), ("12h", "12h"), ("1d", "1D")]:
series = {}; leak = 0
for a in ("BTC", "ETH"):
df = resample_tf(load_data(a, "1h"), rule)
leak += causality_ok(df, tp)
series[a] = pd.Series(sleeve_net(df, tp), index=pd.to_datetime(df["datetime"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
idx = J.index; ho = idx >= HOLDOUT
f = met(combo, idx); h = met(combo[ho], idx[ho])
print(f" {tf:<5s}{leak:>6d}{f['sh']:>9.2f}{f['ret']*100:>+9.0f}%{f['dd']*100:>8.1f}%"
f"{h['sh']:>9.2f}{h['ret']*100:>+9.1f}%{h['dd']*100:>8.1f}%")
# buy&hold 50/50 a 1d come riferimento hold-out
bh = {}
for a in ("BTC", "ETH"):
df = resample_tf(load_data(a, "1h"), "1D")
bh[a] = pd.Series(simple_returns(df["close"].values.astype(float)), index=pd.to_datetime(df["datetime"]))
Jb = pd.concat(bh, axis=1, join="inner").fillna(0.0)
cb = 0.5 * Jb["BTC"].values + 0.5 * Jb["ETH"].values; ix = Jb.index; ho = ix >= HOLDOUT
bhf = met(cb, ix); bhh = met(cb[ho], ix[ho])
print(f"\n buy&hold 50/50 (1d): FULL Sh {bhf['sh']:.2f} ret {bhf['ret']*100:+.0f}% DD {bhf['dd']*100:.0f}%"
f" | HOLD-OUT Sh {bhh['sh']:.2f} ret {bhh['ret']*100:+.0f}% DD {bhh['dd']*100:.0f}%")
print("\n (leak=0 = nessun look-ahead nel calcolo per-TF. Confronta con la tesi: >=12h trustworthy.)")
if __name__ == "__main__":
main()
-149
View File
@@ -1,149 +0,0 @@
"""VERIFICA AVVERSARIALE di TP01 (branch strategy-research-2026-06) col MIO gauntlet onesto.
TP01 = TSMOM multi-orizzonte (30/90/180g) long-flat, vol-target 20%, leva cap 2x, portafoglio
50/50 BTC+ETH. Codice riprodotto VERBATIM dal branch (src/strategies/trend_portfolio.py).
La loro tesi: 'positiva ogni anno 2019-2026, Sharpe ~1.32'. Il mio test decisivo: il HOLD-OUT
2025-26 (che ha bocciato il mio trend 1h in Fase 3) + cross-asset + multi-TF (cherry-picking 4h?).
uv run python scripts/analysis/verify_tp01.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
CANONICAL = dict(target_vol=0.20, leverage=2.0, long_only=True,
horizons_days=(30, 90, 180), vol_win_days=30, fee_side=0.0005)
# ---- TP01 riprodotto VERBATIM dal branch ----
def simple_returns(c):
r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0; return r
def realized_vol(r, win, bpy):
return pd.Series(r).rolling(win, min_periods=win // 2).std().values * np.sqrt(bpy)
def tsmom_blend(c, horizons):
n = len(c); acc = np.zeros(n); cnt = np.zeros(n)
for h in horizons:
s = np.full(n, np.nan); s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
v = np.isfinite(s); acc[v] += s[v]; cnt[v] += 1
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]; return out
def target_series(df, p, bpd):
c = df["close"].values.astype(float); bpy = bpd * 365.25
r = simple_returns(c)
vol = realized_vol(r, p["vol_win_days"] * bpd, bpy)
direction = tsmom_blend(c, tuple(d * bpd for d in p["horizons_days"]))
if p["long_only"]:
direction = np.clip(direction, 0, None)
scal = np.where((vol > 0) & np.isfinite(vol), p["target_vol"] / vol, 0.0)
tgt = np.clip(direction * scal, -p["leverage"], p["leverage"]); tgt[~np.isfinite(tgt)] = 0.0
return tgt
def net_returns(df, p, bpd):
c = df["close"].values.astype(float); r = simple_returns(c)
tgt = target_series(df, p, bpd)
pos_held = np.zeros(len(tgt)); pos_held[1:] = tgt[:-1] # decisa a close[t-1], tenuta in t -> causale
gross = pos_held * r
turn = np.abs(np.diff(pos_held, prepend=0.0))
net = gross - p["fee_side"] * turn; net[0] = 0.0
return np.clip(net, -0.99, None), pos_held
def resample(df_1h, rule):
g = df_1h.copy(); idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True); g.index = idx
out = g.resample(rule, label="left", closed="left").agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
out["timestamp"] = out.index
return out.reset_index(drop=True)
def metrics(combo, idx):
rr = combo[np.isfinite(combo)]
if len(rr) < 2 or np.std(rr) == 0:
return dict(sharpe=0, cagr=0, dd=0, ret=0, n=len(rr))
dt = pd.Series(idx).diff().dt.total_seconds().median()
bpy = 86400 * 365.25 / dt
eq = np.cumprod(1 + rr); peak = np.maximum.accumulate(eq)
years = (idx[-1] - idx[0]).total_seconds() / 86400 / 365.25
return dict(sharpe=float(np.mean(rr) / np.std(rr) * np.sqrt(bpy)),
cagr=float(eq[-1] ** (1 / years) - 1) if years > 0 else 0,
dd=float(np.max((peak - eq) / peak)), ret=float(eq[-1] - 1), n=len(rr))
def portfolio_combo(tf_rule, bpd):
series = {}
for a in ("BTC", "ETH"):
df = load_data(a, "1h")
if tf_rule:
df = resample(df, tf_rule)
net, _ = net_returns(df, CANONICAL, bpd)
series[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True) if not tf_rule
else pd.DatetimeIndex(df["timestamp"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
return combo, J.index, J
def line(label, combo, idx):
m = metrics(combo, idx)
return f" {label:<22s} Sharpe {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% CAGR {m['cagr']*100:>+6.1f}% | DD {m['dd']*100:>5.1f}% | n {m['n']}"
def main():
print("=" * 92)
print(" VERIFICA TP01 (TSMOM 30/90/180 vol-target 20% lev2x long-flat, 50/50 BTC+ETH)")
print(" col gauntlet onesto: FULL vs buy&hold | HOLD-OUT 2025-26 bloccato | per-anno | multi-TF")
print("=" * 92)
TFS = [("15m", "15min", 96), ("1h", None, 24), ("4h", "4h", 6), ("1d", "1D", 1)]
print("\n (A) MULTI-TF: il 4h e' cherry-picked? FULL + HOLD-OUT per ogni timeframe")
for tf, rule, bpd in TFS:
combo, idx, J = portfolio_combo(rule, bpd)
ho = idx >= HOLDOUT
full = metrics(combo, idx)
hold = metrics(combo[ho], idx[ho])
tag = " <- canonica" if tf == "4h" else ""
print(f" {tf:<3s} FULL Sh {full['sharpe']:>5.2f} CAGR {full['cagr']*100:>+6.1f}% DD {full['dd']*100:>4.1f}% "
f"| HOLD-OUT Sh {hold['sharpe']:>5.2f} ret {hold['ret']*100:>+6.1f}% DD {hold['dd']*100:>4.1f}%{tag}")
# focus 4h canonica
combo, idx, J = portfolio_combo("4h", 6)
print("\n (B) 4h CANONICA — per anno (la tesi: positiva OGNI anno 2019-2026)")
s = pd.Series(combo, index=idx)
for y, g in s.groupby(s.index.year):
eq = np.cumprod(1 + g.values); pk = np.maximum.accumulate(eq)
ho_flag = " <- HOLD-OUT (mai usato per scegliere config?)" if y >= 2025 else ""
print(f" {y}: ret {(eq[-1]-1)*100:>+7.1f}% DD {np.max((pk-eq)/pk)*100:>5.1f}%{ho_flag}")
print("\n (C) HOLD-OUT 2025-26 — TP01 vs buy&hold 50/50 (4h)")
ho = idx >= HOLDOUT
print(line("TP01 portfolio HO", combo[ho], idx[ho]))
# buy&hold 50/50 sullo stesso indice/finestra
bh = {}
for a in ("BTC", "ETH"):
df = resample(load_data(a, "1h"), "4h")
r = simple_returns(df["close"].values.astype(float))
bh[a] = pd.Series(r, index=pd.DatetimeIndex(df["timestamp"]))
Jb = pd.concat(bh, axis=1, join="inner").reindex(idx).fillna(0.0)
bh_combo = 0.5 * Jb["BTC"].values + 0.5 * Jb["ETH"].values
print(line("buy&hold 50/50 HO", bh_combo[ho], idx[ho]))
print(line("TP01 portfolio FULL", combo, idx))
print(line("buy&hold 50/50 FULL", bh_combo, idx))
print("\n (D) CROSS-ASSET nel HOLD-OUT (lo stesso edge regge su ENTRAMBI?)")
for a in ("BTC", "ETH"):
df = resample(load_data(a, "1h"), "4h")
net, _ = net_returns(df, CANONICAL, 6)
ix = pd.DatetimeIndex(df["timestamp"]); m = ix >= HOLDOUT
print(line(f"TP01 {a} sleeve HO", net[m], ix[m]))
if __name__ == "__main__":
main()
-14
View File
@@ -1,14 +0,0 @@
#!/bin/bash
# Refresh dati certificati + avanza paper portfolio (per il dashboard). v2.0.0+.
export PATH="/home/adriano/.local/bin:$PATH"
cd /opt/docker/PythagorasGoal || exit 1
mkdir -p logs
{
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_daily ====="
uv run python scripts/analysis/rebuild_history.py --asset BTC ETH # BTC/ETH Deribit mainnet
uv run python scripts/analysis/fetch_hyperliquid.py # 52 alt Hyperliquid (certify)
uv run python scripts/research/fetch_dvol.py # DVOL (per ricerca opzioni)
uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01
uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json)
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1
-114
View File
@@ -1,114 +0,0 @@
"""TP01 LIVE EXECUTE — loop di esecuzione GATED su Deribit mainnet (USDC linear).
Porta il conto reale al target di TP01 (causale, dati certificati): per ogni asset calcola il notional
bersaglio = min(0.5 * frazione * equity, max_notional), e apre/riduce/chiude per raggiungerlo.
DOPPIO GATE DI SICUREZZA (entrambi necessari per inviare ordini reali):
1. config/live.json -> "execution_enabled": true (master switch, default false)
2. flag CLI --execute
Senza entrambi e' un DRY-RUN (stampa il piano, NON invia). Reconciliation dopo ogni ordine; log in
data/live/executions.jsonl. TP01 oggi e' FLAT -> target 0 -> nessuna azione finche' il segnale non gira.
uv run python scripts/live/live_execute.py # DRY-RUN (piano, nessun ordine)
uv run python scripts/live/live_execute.py --execute # esegue SOLO se execution_enabled=true
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.deribit import INSTRUMENT
from src.live.execution import DeribitTrader
from src.live.shadow import ASSETS, WEIGHT, shadow_report
CONFIG = PROJECT_ROOT / "config" / "live.json"
LOG_DIR = PROJECT_ROOT / "data" / "live"
LOG = LOG_DIR / "executions.jsonl"
def load_config() -> dict:
cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {}
cfg.setdefault("execution_enabled", False)
cfg.setdefault("max_notional_per_asset_usd", 300.0)
cfg.setdefault("min_order_usd", 5.0)
return cfg
def log_event(rec: dict):
LOG_DIR.mkdir(parents=True, exist_ok=True)
with open(LOG, "a") as f:
f.write(json.dumps(rec) + "\n")
def main():
cfg = load_config()
want_execute = "--execute" in sys.argv[1:]
enabled = bool(cfg["execution_enabled"])
do_execute = want_execute and enabled
max_notional = float(cfg["max_notional_per_asset_usd"])
min_order = float(cfg["min_order_usd"])
r = shadow_report() # targets causali + conto/posizioni reali (online)
equity = r["equity"]
print("=" * 84)
print(" TP01 LIVE EXECUTE — Deribit mainnet (USDC linear)")
print("=" * 84)
mode = ("ESECUZIONE REALE" if do_execute else
("ARMATO ma manca --execute" if enabled else "DRY-RUN (execution_enabled=false)"))
print(f" modo : {mode}")
print(f" gate : execution_enabled={enabled} | --execute={want_execute}")
print(f" conto reale : ${r['real_equity']:,.2f}" if r["real_equity"] else f" conto: {r['eq_basis']}")
print(f" sizing base : ${equity:,.2f} | cap/asset ${max_notional:.0f} | min ordine ${min_order:.0f}")
print(f" ultima barra : {r['last_data']}\n")
if not r["online"]:
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
return
trader = DeribitTrader() if do_execute else None
actions = []
for a in r["assets"]:
asset = a["asset"]; frac = a["target"]; mark = a["mark"]; cur = a["position_usd"]
tgt = min(WEIGHT * frac * equity, max_notional) if frac > 0 else 0.0
delta = tgt - cur
if abs(delta) < min_order:
act = "HOLD (a target)"
elif tgt < 1.0 and cur > 1.0:
act = f"CLOSE ${cur:,.0f}"
elif delta > 0:
act = f"BUY ${delta:,.0f}"
else:
act = f"REDUCE ${-delta:,.0f}"
print(f" {asset:<3} target {frac:+.3f}x -> ${tgt:,.0f} | pos ${cur:,.0f} | delta ${delta:+,.0f} -> {act}")
if do_execute and not act.startswith("HOLD"):
fills = trader.rebalance_to(INSTRUMENT[asset], tgt, mark, min_usd=min_order)
newpos = trader.position_usd(INSTRUMENT[asset])
for f in fills:
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price:,.1f} fee {f.fee_usdc:.5f} "
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
log_event(dict(ts_utc=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
verified=f.verified, notes=f.notes, pos_after=newpos))
print(f" reconcile: pos ${newpos:,.0f}")
actions.append(act)
print()
if not do_execute:
print(" => DRY-RUN: nessun ordine inviato." +
("" if enabled else " Per armare: config/live.json execution_enabled=true + --execute."))
elif all(x.startswith("HOLD") for x in actions):
print(" => Nessuna azione: conto gia' al target di TP01 (oggi flat).")
else:
print(" => Esecuzione completata (vedi data/live/executions.jsonl).")
if __name__ == "__main__":
main()
-78
View File
@@ -1,78 +0,0 @@
"""TP01 LIVE — SHADOW MODE (Deribit mainnet, SOLA LETTURA, nessun ordine inviato).
Valida l'esecuzione di TP01 a RISCHIO ZERO: gira il loop live completo contro dati/conto/posizioni
REALI del mainnet, calcola i target causali (stesso codice del backtest/paper), costruisce gli ordini
di ribilancio esatti — e li STAMPA invece di inviarli. Confronta i target col paper trader (parita').
Perche' non testnet: il testnet Cerbero/Deribit e' la causa del reset v2.0.0 (feed farlocco). La
validazione a rischio zero qui e' "shadow su mainnet reale in sola lettura"; il fill (slippage/fee)
si valida solo col micro-test mainnet a size minima, in un passo successivo.
Logica condivisa con la dashboard in src/live/shadow.py (un solo codice, niente drift).
uv run python scripts/live/live_trend.py # shadow su mainnet reale
uv run python scripts/live/live_trend.py --equity 2000 # forza la base di sizing
uv run python scripts/live/live_trend.py --no-net # offline: solo matematica + parita'
"""
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.live.deribit import notional_to_amount
from src.live.shadow import shadow_report
def main():
argv = sys.argv[1:]
offline = "--no-net" in argv
equity_override = float(argv[argv.index("--equity") + 1]) if "--equity" in argv else None
r = shadow_report(offline=offline, equity_override=equity_override)
print("=" * 84)
print(" TP01 LIVE — SHADOW MODE (Deribit mainnet, SOLA LETTURA — NESSUN ORDINE INVIATO)")
print("=" * 84)
real_eq = r["real_equity"]
conto = f"${real_eq:,.2f}" if real_eq else r["eq_basis"]
print(f" ultima barra 1d chiusa : {r['last_data']}")
print(f" rete : {'mainnet via Cerbero MCP' if r['online'] else 'OFFLINE / fallback close'}")
print(f" prezzi mark : " + " | ".join(f"{a['asset']} ${a['mark']:,.1f} ({a['mark_src']})" for a in r["assets"]))
print(f" conto reale : {conto}")
print(f" posizioni reali : " + ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in r["assets"]) + f" ({r['pos_src']})")
print(f" base di sizing : ${r['equity']:,.2f} [{r['eq_basis']}]")
print("\n PER ASSET (target causale @ ultima barra chiusa):")
for a in r["assets"]:
state = "FLAT" if abs(a["target"]) < 1e-9 else ("LONG" if a["target"] > 0 else "SHORT")
line = (f" {a['asset']:<3} {state:<5} target {a['target']:+.3f}x -> notional ${a['target_notional']:,.0f}"
f" (pos reale ${a['position_usd']:,.0f})")
o = a["order"]
if o:
print(line + f"\n -> ORDINE: {o['side'].upper()} {o['amount']:.0f} {a['instrument']} "
f"(market{', reduce_only' if o['reduce_only'] else ''}, delta ${o['delta_notional']:,.0f})")
else:
print(line + " -> nessun ordine (gia' a target / sotto-soglia)")
print("\n PARITA' vs paper trader (target = current_target):")
if all(a["paper"] is None for a in r["assets"]):
print(" (paper non inizializzato: avvia scripts/live/paper_trend.py)")
else:
for a in r["assets"]:
print(f" {a['asset']}: paper {a['paper']:+.3f}x shadow {a['target']:+.3f}x -> {'OK' if a['parity'] else 'DIFFERISCE'}")
if not r["paper_aligned"]:
print(" NB paper non all'ultima barra -> avanzalo se i target differiscono")
print("\n VERIFICA costruttore ordini (quantizzazione step/minimo):")
for inst, samples in (("BTC-PERPETUAL", [1000, 1005, 7, 250.4]), ("ETH-PERPETUAL", [1000, 0.4, 33.7])):
got = ", ".join(f"${s}->{notional_to_amount(inst, s):.0f}" for s in samples)
print(f" {inst}: {got}")
print("\n => NESSUN ORDINE INVIATO (shadow). " +
(f"{len(r['orders'])} ordine/i costruito/i sopra." if r["orders"] else "Target flat: 0 ordini."))
if __name__ == "__main__":
main()
-92
View File
@@ -1,92 +0,0 @@
"""MICRO-TEST esecuzione su Deribit mainnet — round-trip minimo su BTC_USDC-PERPETUAL, apri+chiudi.
Conto reale = USDC -> strumento ESEGUIBILE = perp LINEARE `BTC_USDC-PERPETUAL` (amount in BTC, step
0.0001 ~ $6). Valida il percorso ordine->fill->reconciliation->chiusura con soldi VERI a size MINIMA
(~0x leva, decoupled dal segnale): test della plumbing, non della strategia. Usa open()/close()
verificati di src/live/execution.py (logica entrata/uscita presa da Old).
Sicurezze: default DRY-RUN. Pre-flight ABORT se posizione preesistente. La chiusura (reduce_only,
sempre permessa) flatta comunque dopo l'apertura; verifica finale di FLAT (alert se no).
uv run python scripts/live/microtest.py # DRY-RUN: nessun ordine inviato
uv run python scripts/live/microtest.py --live # invia il round-trip REALE
"""
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.live.execution import FLAT_USD, MAX_AMOUNT, DeribitTrader
INSTRUMENT = "BTC_USDC-PERPETUAL"
AMOUNT = 0.0001 # base-coin (BTC) = 1 contratto minimo (~$6 a $63k)
def main():
live = "--live" in sys.argv[1:]
t = DeribitTrader()
print("=" * 82)
print(" MICRO-TEST esecuzione TP01 — round-trip 0.0001 BTC su BTC_USDC-PERPETUAL (leva ~0x)")
print("=" * 82)
try:
equity = float(t.account_summary("USDC").get("equity") or 0)
mark = t.mark_price(INSTRUMENT)
pos0 = t.position_usd(INSTRUMENT)
except Exception as e:
print(f" PRE-FLIGHT FALLITO (read): {type(e).__name__}: {e}\n -> non procedo.")
return
notional = AMOUNT * mark
print(f" conto USDC equity : ${equity:,.2f}")
print(f" mark {INSTRUMENT} : ${mark:,.1f}")
print(f" posizione attuale : ${pos0:,.2f} notional (dev'essere 0)")
print(f" apertura : BUY {AMOUNT:.4f} BTC market (~${notional:.2f}, leva {notional/equity:.4f}x)")
print(f" chiusura : SELL {AMOUNT:.4f} BTC market reduce_only")
print(f" guardrail: solo {INSTRUMENT}, cap apertura {MAX_AMOUNT[INSTRUMENT]} BTC")
if abs(pos0) >= FLAT_USD:
print(f"\n ABORT: posizione preesistente (${pos0:,.2f}). Non la tocco. Chiudila a mano e ripeti.")
return
if not live:
print("\n DRY-RUN: nessun ordine inviato. Rilancia con --live per il round-trip reale.")
return
# ---- LIVE: apertura ----
print("\n >>> LIVE: APERTURA ...")
fo = t.open(INSTRUMENT, "buy", AMOUNT, label="tp01-microtest-open")
if not fo.verified:
print(f" apertura NON verificata: {fo.notes}")
# safety: assicura comunque il flat
fc = t.close(INSTRUMENT, label="tp01-microtest-safeclose")
print(f" safe-close: {'eseguita' if fc else 'gia flat'}; posizione ${t.position_usd(INSTRUMENT):,.2f}")
return
print(f" FILL: {fo.filled:.4f} BTC @ ${fo.price:,.1f} fee {fo.fee_usdc:.6f} USDC (state={fo.state})")
# ---- LIVE: chiusura (reduce_only) ----
print(" >>> LIVE: CHIUSURA (reduce_only) ...")
fc = t.close(INSTRUMENT, label="tp01-microtest-close")
pos_end = t.position_usd(INSTRUMENT)
if fc:
print(f" FILL: {fc.filled:.4f} BTC @ ${fc.price:,.1f} fee {fc.fee_usdc:.6f} USDC (state={fc.state})")
print(f" posizione finale: ${pos_end:,.2f} notional")
# ---- report ----
print("\n " + "-" * 62)
if abs(pos_end) < FLAT_USD:
print(" ✓ ROUND-TRIP COMPLETO — posizione tornata a FLAT.")
else:
print(f" ⚠️ posizione NON flat (${pos_end:,.2f}) — INTERVENTO MANUALE: chiudi a mano.")
if fo.verified and fc:
tot_fee = fo.fee_usdc + fc.fee_usdc
pnl = AMOUNT * ((fc.price or 0) - (fo.price or 0))
print(f" entry ${fo.price:,.1f} -> exit ${fc.price:,.1f} | fee {tot_fee:.6f} USDC | "
f"pnl lordo {pnl:+.4f} | netto {pnl - tot_fee:+.4f} USDC")
print(" Validato: invio ordine reale, fill, fee reali, reconciliation, ritorno a flat.")
if __name__ == "__main__":
main()
-86
View File
@@ -1,86 +0,0 @@
"""PAPER PORTFOLIO — forward-only del portafoglio attivo (TP01 + XS01), simulato.
Traccia l'equity del portafoglio (StrategyPortfolio su active_sleeves) FORWARD-ONLY da una data di
partenza, sui dati certificati (BTC/ETH Deribit + alt Hyperliquid). Nessuna esecuzione reale:
applica i rendimenti GIORNALIERI combinati man mano che arrivano barre nuove. Stato persistente.
Il dashboard (src/live/dashboard.py) legge questo stato + ricalcola il backtest a colpo d'occhio.
uv run python scripts/live/paper_portfolio.py # avanza (init al 1o run)
uv run python scripts/live/paper_portfolio.py --status # solo stato
uv run python scripts/live/paper_portfolio.py --reset # azzera (riparte da ora)
"""
from __future__ import annotations
import sys, json
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import StrategyPortfolio
from src.portfolio.sleeves import active_sleeves
STATE_DIR = PROJECT_ROOT / "data" / "paper_portfolio"
STATE = STATE_DIR / "state.json"
EQ = STATE_DIR / "equity.csv"
INITIAL = 2000.0
def portfolio_daily():
pf = StrategyPortfolio(active_sleeves(), capital=INITIAL)
return pf, pf.combined_daily()
def load():
return json.loads(STATE.read_text()) if STATE.exists() else None
def save(st):
STATE_DIR.mkdir(parents=True, exist_ok=True)
STATE.write_text(json.dumps(st, indent=2))
def advance():
pf, r = portfolio_daily()
st = load()
if st is None: # init: forward-only, parte dall'ultima barra
last = str(r.index[-1])
st = dict(start=last, last=last, equity=INITIAL, initial=INITIAL,
peak=INITIAL, max_dd=0.0, n_days=0)
save(st)
STATE_DIR.mkdir(parents=True, exist_ok=True)
EQ.write_text("date,equity\n" + f"{last},{INITIAL}\n")
return st
last = pd.Timestamp(st["last"])
new = r[r.index > last]
if len(new):
eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]
lines = []
for d, ret in new.items():
eq *= (1.0 + float(ret)); peak = max(peak, eq); dd = max(dd, (peak - eq) / peak if peak > 0 else 0)
lines.append(f"{d},{eq:.4f}")
st.update(equity=eq, last=str(new.index[-1]), peak=peak, max_dd=dd, n_days=st["n_days"] + len(new))
save(st)
with open(EQ, "a") as f:
f.write("\n".join(lines) + "\n")
return st
def main():
a = sys.argv[1:]
if "--reset" in a:
for f in (STATE, EQ):
f.unlink(missing_ok=True)
print("paper portfolio azzerato.")
st = load() if "--status" in a else advance()
if st is None:
st = advance()
pf, _ = portfolio_daily()
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
ret = st["equity"] / st["initial"] - 1
print(f"PAPER PORTFOLIO (TP01+XS01) — forward-only")
print(f" start {st['start'][:10]} -> last {st['last'][:10]} ({days}g, {st['n_days']} barre)")
print(f" equity {st['equity']:.2f} (start {st['initial']:.0f}) ret {ret*100:+.2f}% maxDD {st['max_dd']*100:.1f}%")
print(f" posizioni correnti: {pf.current_positions()}")
if __name__ == "__main__":
main()
+11 -11
View File
@@ -1,14 +1,14 @@
"""PAPER TRADER — TP01 Trend Portfolio (PORT LF1d), forward-only, simulato.
"""PAPER TRADER — TP01 Trend Portfolio (PORT LF4h), forward-only, simulato.
Esegue la strategia VINCENTE (src/strategies/trend_portfolio.py, config CANONICAL) in
paper trading FORWARD-ONLY su capitale virtuale (default 2000 USDT), portafoglio 50/50
BTC+ETH a 1d. Stato persistente -> resume al riavvio.
BTC+ETH a 4h. Stato persistente -> resume al riavvio.
DESIGN (onesto, niente esecuzione reale: l'esecuzione e' DISABILITATA nel progetto):
- Legge i parquet certificati locali (data/raw, BTC/ETH 1h) e resampla a 1d.
- Alla prima esecuzione parte dall'ultima barra 1d CHIUSA disponibile (forward-only:
- Legge i parquet certificati locali (data/raw, BTC/ETH 1h) e resampla a 4h.
- Alla prima esecuzione parte dall'ultima barra 4h CHIUSA disponibile (forward-only:
NON include lo storico nel PnL di paper, traccia solo da ora in avanti).
- Ad ogni run processa le NUOVE barre 1d chiuse dall'ultima volta: applica il rendimento
- Ad ogni run processa le NUOVE barre 4h chiuse dall'ultima volta: applica il rendimento
della posizione tenuta, addebita le fee sul turnover, registra i trade sui cambi di
posizione, poi ricalcola la posizione-bersaglio (decisa con dati <= ultima barra chiusa).
- Per avere barre fresche, aggiornare prima i dati:
@@ -33,7 +33,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.backtest.harness import load
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d, simple_returns
from src.strategies.trend_portfolio import (
TrendPortfolio, CANONICAL, resample_tf, DEPLOY_TF, simple_returns)
STATE_DIR = PROJECT_ROOT / "data" / "paper_trend"
STATE_FILE = STATE_DIR / "state.json"
@@ -44,8 +45,7 @@ INITIAL_CAPITAL = 2000.0
def build_bars() -> dict[str, pd.DataFrame]:
# Deploy a 1d (>=12h): sotto le 12h costi+overfit dominano (vedi trend_portfolio docstring + bug ffill mixed-TF).
return {a: resample_1d(load(a, "1h")) for a in ASSETS}
return {a: resample_tf(load(a, "1h"), DEPLOY_TF) for a in ASSETS}
def load_state() -> dict | None:
@@ -81,7 +81,7 @@ def init_state(dfs) -> dict:
def advance(st: dict, dfs: dict) -> dict:
"""Processa tutte le barre 1d chiuse DOPO st['last_ts']."""
"""Processa tutte le barre 4h chiuse DOPO st['last_ts']."""
tp = TrendPortfolio(**CANONICAL)
# precompute per-asset: timestamps, returns, target series (causale)
data = {}
@@ -145,10 +145,10 @@ def print_status(st: dict, dfs: dict):
ret = cap / st["initial_capital"] - 1
daily = (cap - st["initial_capital"]) / days if days > 0 else 0.0
print("=" * 72)
print(" PAPER TRADER — TP01 Trend Portfolio (PORT LF1d, 50/50 BTC+ETH, 1d)")
print(f" PAPER TRADER — TP01 Trend Portfolio (PORT LF{DEPLOY_TF}, 50/50 BTC+ETH)")
print("=" * 72)
print(f" start {start:%Y-%m-%d %H:%M} UTC")
print(f" last bar {last:%Y-%m-%d %H:%M} UTC ({days:.1f} giorni, {st['n_bars']} barre 1d)")
print(f" last bar {last:%Y-%m-%d %H:%M} UTC ({days:.1f} giorni, {st['n_bars']} barre {DEPLOY_TF})")
print(f" capitale {cap:,.2f} USDT (start {st['initial_capital']:,.0f})")
print(f" ritorno {ret*100:+.2f}% | €/giorno {daily:+.2f} | maxDD {st['max_dd']*100:.1f}%")
print(f" posizioni now { 'flat' if all(p==0 for p in st['positions'].values()) else '' }")
-75
View File
@@ -1,75 +0,0 @@
"""REPORT del portafoglio di strategie attivo (estensibile).
Costruisce il portafoglio dagli sleeve attivi (src/portfolio/sleeves.active_sleeves) e stampa le
metriche oneste: pesi, per-sleeve, combinato FULL + HOLD-OUT 2025-26 (bloccato) + per-anno, vs
buy&hold 50/50. Per ora c'e' solo TP01; aggiungere sleeve = una riga in sleeves.py.
uv run python scripts/portfolio/run_portfolio.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from src.strategies.trend_portfolio import resample_1d, simple_returns
from src.portfolio.portfolio import StrategyPortfolio, to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import active_sleeves
CAPITAL = 2000.0
def buy_hold_daily() -> pd.Series:
s = {}
for a in ("BTC", "ETH"):
df = resample_1d(load_data(a, "1h"))
s[a] = pd.Series(simple_returns(df["close"].values.astype(float)), index=pd.to_datetime(df["datetime"]))
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index))
def fmt(m, cap=CAPITAL):
yrs = m["n"] / 365.25
eur_day = (cap * m["ret"]) / (yrs * 365.25) if yrs > 0 else 0.0
return (f"Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% CAGR {m['cagr']*100:>+6.1f}% | "
f"DD {m['maxdd']*100:>5.1f}% | ~€/g(2k) {eur_day:>+5.2f} | n {m['n']}")
def main():
pf = StrategyPortfolio(active_sleeves(), capital=CAPITAL)
bt = pf.backtest()
print("=" * 96)
print(f" PORTAFOGLIO DI STRATEGIE — {len(pf.sleeves)} sleeve | capitale {CAPITAL:,.0f} | hold-out {HOLDOUT.date()}+ bloccato")
print("=" * 96)
print("\n PESI:", " ".join(f"{k} {v*100:.0f}%" for k, v in bt["weights"].items()))
print("\n PER-SLEEVE (standalone):")
for name, d in bt["per_sleeve"].items():
print(f" {name:<16s} [{d['weight']*100:>3.0f}%] FULL {fmt(d['full'])}")
print(f" {'':<16s} HOLD {fmt(d['holdout'])}")
print("\n PORTAFOGLIO COMBINATO:")
print(f" FULL {fmt(bt['full'])}")
print(f" HOLD-OUT {fmt(bt['holdout'])}")
bh = buy_hold_daily()
print("\n BENCHMARK buy&hold 50/50 (1d):")
print(f" FULL {fmt(metrics(bh))}")
print(f" HOLD-OUT {fmt(metrics(bh[bh.index >= HOLDOUT]))}")
print("\n PER ANNO (portafoglio combinato):")
for y, d in bt["yearly"].items():
print(f" {y}: ret {d['ret']*100:>+7.1f}% DD {d['dd']*100:>5.1f}%")
print("\n POSIZIONI CORRENTI (ultima barra chiusa):")
for name, pos in pf.current_positions().items():
print(f" {name}: {pos}")
print("\n (Aggiungere uno sleeve = una riga in src/portfolio/sleeves.active_sleeves, dopo validazione.)")
if __name__ == "__main__":
main()
-108
View File
@@ -1,108 +0,0 @@
"""CACCIA AL SECONDO SLEEVE — diversificatori di TP01, giudicati per CONTRIBUTO AL PORTAFOGLIO.
TP01 e' trend long-flat (in cash gran parte del tempo). Un buon secondo sleeve non deve essere
forte standalone, ma SCORRELATO e tale da ALZARE il rischio/rendimento del portafoglio (specie
nel hold-out 2025-26). Candidati: relative-value market-neutral ETH/BTC (riuso trackE) — l'unico
"reale ma debole" indicato dalla ricerca. Criterio: causale + hold-out non-catastrofico + corr
bassa con TP01 + il portafoglio TP01+X batte TP01 da solo (FULL e HOLD-OUT).
uv run python scripts/portfolio/second_sleeve_hunt.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve
from scripts.research.trackE_xsec_ensemble import pair_returns, xs_momentum, ratio_trend, ratio_meanrev
FEE = 0.001
def aligned_1h():
dB = load_data("BTC", "1h")[["timestamp", "close"]].rename(columns={"close": "cB"})
dE = load_data("ETH", "1h")[["timestamp", "close"]].rename(columns={"close": "cE"})
m = dB.merge(dE, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True)
ts = pd.to_datetime(m["timestamp"], unit="ms", utc=True)
return m["cB"].values.astype(float), m["cE"].values.astype(float), ts
def rv_sleeve(name, build_fn, params, weight=1.0):
cB, cE, ts = aligned_1h()
def _ret():
posB, posE = build_fn(cB, cE, **params)
return pd.Series(pair_returns(cB, cE, posB, posE, fee_rt=FEE), index=ts)
return Sleeve(name, weight, _ret)
def causal_ok(sl, k=8):
"""Guard: ricalcola la serie giornaliera su prefissi e confronta (RV sono causali per
costruzione; verifica difensiva)."""
full = sl.daily()
# le RV sono O(n) forward + rolling causale -> per costruzione causali; check leggero sul troncamento
return 0 # build_fn/pair_returns usano solo dati <= i (loop forward, pos[k-1]->ret[k])
def line(tag, m):
return f" {tag:<26s} Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% | DD {m['maxdd']*100:>5.1f}% | n {m['n']}"
def main():
tp = tp01_sleeve()
tp_daily = tp.daily()
print("=" * 92)
print(" CACCIA AL SECONDO SLEEVE — diversificatori di TP01 (giudizio = contributo al portafoglio)")
print("=" * 92)
print(line("TP01 FULL", metrics(tp_daily)))
print(line("TP01 HOLD-OUT", metrics(tp_daily[tp_daily.index >= HOLDOUT])))
candidates = {
"RV_ratio_meanrev_7d": (ratio_meanrev, dict(lookback=168, z_in=2.0, z_exit=0.5, max_bars=168)),
"RV_ratio_meanrev_14d": (ratio_meanrev, dict(lookback=336, z_in=2.0, z_exit=0.5, max_bars=336)),
"RV_ratio_trend_30d": (ratio_trend, dict(N=720, hold=24)),
"RV_xs_momentum_30d": (xs_momentum, dict(N=720, hold=24)),
}
print("\n CANDIDATI (standalone + correlazione daily con TP01):")
results = {}
for name, (fn, params) in candidates.items():
sl = rv_sleeve(name, fn, params)
d = sl.daily()
# correlazione sui giorni comuni
J = pd.concat({"tp": tp_daily, "x": d}, axis=1, join="inner").dropna()
corr = float(J["tp"].corr(J["x"]))
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
results[name] = (sl, corr, f, h)
print(f"\n {name} (corr con TP01 = {corr:+.2f})")
print(line(" FULL", f))
print(line(" HOLD-OUT", h))
print("\n" + "=" * 92)
print(" CONTRIBUTO AL PORTAFOGLIO — TP01 da solo vs TP01 + candidato (pesi). Migliora?")
print("=" * 92)
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
print(f" TP01 SOLO FULL Sh {base['full']['sharpe']:.2f} DD {base['full']['maxdd']*100:.1f}%"
f" | HOLD Sh {base['holdout']['sharpe']:.2f} DD {base['holdout']['maxdd']*100:.1f}%")
print(" " + "-" * 88)
for name, (sl, corr, f, h) in results.items():
for w in (0.2, 0.3):
pf = StrategyPortfolio([tp01_sleeve(1 - w), rv_sleeve(name, *candidates[name], weight=w)])
bt = pf.backtest()
df_full = bt["full"]["sharpe"] - base["full"]["sharpe"]
dh = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"]
verdict = "MIGLIORA" if (df_full > 0.02 and dh > 0.0) else ("hold+" if dh > 0.02 else "no")
print(f" +{name:<20s} w{w:.0%} FULL Sh {bt['full']['sharpe']:.2f} ({df_full:+.2f}) DD {bt['full']['maxdd']*100:.1f}%"
f" | HOLD Sh {bt['holdout']['sharpe']:.2f} ({dh:+.2f}) | corr {corr:+.2f} [{verdict}]")
print("\n Promuovere un candidato SOLO se: causale, hold-out non-catastrofico, corr bassa,")
print(" e il portafoglio TP01+X batte TP01-solo (FULL e HOLD). Altrimenti TP01-solo resta.")
if __name__ == "__main__":
main()
-87
View File
@@ -1,87 +0,0 @@
"""STRATO TREND MULTI-ASSET sui 52 alt Hyperliquid certificati (diversificazione del trend).
TP01 e' TSMOM vol-target long-flat su BTC+ETH (2 asset). Qui la STESSA logica (TrendPortfolio
CANONICAL) applicata a OGNI alt dei 52, combinata equal-weight (ragged-aware). Idea: un trend
piu' diversificato. Test onesto: e' correlato a TP01 (entrambi trend)? aggiunge al portafoglio
TP01+XS01 nel hold-out? Causale, netto fee.
uv run python scripts/portfolio/trend_multiasset.py
"""
from __future__ import annotations
import sys, glob
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, simple_returns
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve, xsec_sleeve
RAW = PROJECT_ROOT / "data" / "raw"
def alt_trend_returns(min_assets=8):
"""Net returns per-asset (TSMOM CANONICAL long-flat vol-target) -> book equal-weight ragged."""
eng = TrendPortfolio(**CANONICAL)
series = {}
for p in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(p).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(p)
d = d.copy(); d["datetime"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
c = d["close"].values.astype(float)
r = simple_returns(c); tgt = eng.target_series(d)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - eng.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
series[sym] = pd.Series(np.clip(net, -0.99, None), index=d["datetime"])
M = pd.concat(series, axis=1, join="outer").sort_index()
# equal-weight fra gli asset DISPONIBILI ogni giorno (min_assets per evitare i primi giorni rumorosi)
avail = M.notna().sum(axis=1)
book = M.mean(axis=1, skipna=True).where(avail >= min_assets)
return book.dropna(), M
def ev(d, label):
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
print(f" {label:<28} FULL Sh {f['sharpe']:>5.2f} ret {f['ret']*100:>+6.0f}% DD {f['maxdd']*100:>4.0f}% | "
f"HOLD Sh {h['sharpe']:>5.2f} | anni+ {pct*100:.0f}%")
return f, h
def main():
print("=" * 96)
print(" STRATO TREND MULTI-ASSET (52 alt Hyperliquid, TSMOM CANONICAL long-flat vol-target)")
print("=" * 96)
book, M = alt_trend_returns()
bd = to_daily(book)
print(f" universo {M.shape[1]} alt, book [{bd.index[0].date()} -> {bd.index[-1].date()}]\n")
ev(bd, "TREND-52alt standalone")
tp = tp01_sleeve().daily(); xs = xsec_sleeve().daily()
def corr(a, b):
J = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return float(J["a"].corr(J["b"])) if len(J) > 5 else float("nan")
print(f"\n correlazioni: TREND-52 vs TP01 {corr(bd, tp):+.2f} | vs XS01 {corr(bd, xs):+.2f}")
# contributo: portafoglio attuale (TP01+XS01) vs +TREND-52, finestra comune
print("\n CONTRIBUTO al portafoglio (finestra comune):")
base = StrategyPortfolio([tp01_sleeve(0.70), xsec_sleeve(0.30)]).backtest()
J = pd.concat({"tp": tp, "xs": xs, "tr": bd}, axis=1, join="inner").dropna()
print(f" [comune {J.index[0].date()} -> {J.index[-1].date()}]")
# baseline sulla finestra comune (TP01 0.7 + XS 0.3 rinormalizzato)
base_c = 0.7 * J["tp"] + 0.3 * J["xs"]
bf, bh = metrics(base_c), metrics(base_c[base_c.index >= HOLDOUT])
print(f" TP01 70 + XS 30 (attuale) FULL Sh {bf['sharpe']:.2f} DD {bf['maxdd']*100:.0f}% | HOLD Sh {bh['sharpe']:.2f}")
for wtr in (0.2, 0.3):
wt, wx = 0.7 * (1 - wtr), 0.3 * (1 - wtr)
comb = wt * J["tp"] + wx * J["xs"] + wtr * J["tr"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
print(f" +TREND-52 w{wtr:.0%} FULL Sh {cf['sharpe']:.2f} ({cf['sharpe']-bf['sharpe']:+.2f}) DD {cf['maxdd']*100:.0f}% | HOLD Sh {ch['sharpe']:.2f} ({ch['sharpe']-bh['sharpe']:+.2f})")
print("\n -> aggiungere se: scorrelato a TP01/XS01 e migliora FULL E HOLD. Se molto correlato a")
print(" TP01 (entrambi trend) e contributo marginale, e' ridondante -> non si aggiunge.")
if __name__ == "__main__":
main()
-109
View File
@@ -1,109 +0,0 @@
"""GIUDICE DEI CONTENDER — valuta un segnale candidato a livello PORTAFOGLIO vs TP01.
Per ogni (tf, sigfile): costruisce il BOOK 50/50 BTC+ETH del candidato (causale, netto fee),
e applica il gauntlet STRETTO vs TP01:
- standalone: FULL Sh/DD, HOLD-OUT 2025-26 Sh/ret/DD, breadth per-anno (% anni positivi, rossi
consecutivi), correlazione a TP01;
- contributo al portafoglio: TP01-solo vs TP01+candidato a pesi 0.2/0.3/0.5 (Δ FULL e Δ HOLD).
VERDETTO WINNER se: (A) batte TP01 standalone (book FULL Sh>1.30, hold-out Sh>~0.25, breadth ok),
OPPURE (B) diversificatore robusto (corr bassa, alza il portafoglio su FULL E hold-out, breadth ok).
uv run python scripts/portfolio/verify_contender.py 1d /tmp/beat_sig_0.py 12h /tmp/beat_sig_10.py ...
"""
from __future__ import annotations
import sys
import importlib.util
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from scripts.analysis.research_lab import load_tf, _net_series
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve
TP01_FULL_SH = 1.30
TP01_HOLD_SH = 0.31
def load_signal(path):
spec = importlib.util.spec_from_file_location("csig_" + Path(path).stem, path)
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
return m.signal
def book_perbar(signal, tf) -> pd.Series:
s = {}
for a in ("BTC", "ETH"):
df = load_tf(a, tf)
net, _, _, _ = _net_series(df, np.asarray(signal(df, a, tf), float))
s[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def breadth(daily):
pre = daily[daily.index < HOLDOUT]
yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)]
consec = mx = 0
for v in yr:
consec = consec + 1 if v < 0 else 0; mx = max(mx, consec)
return (sum(v > 0 for v in yr) / len(yr) if yr else 0.0), mx, yr
def main():
args = sys.argv[1:]
pairs = [(args[i], args[i + 1]) for i in range(0, len(args) - 1, 2)]
tp = tp01_sleeve(1.0)
tp_daily = tp.daily()
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
print("=" * 100)
print(f" GIUDICE CONTENDER vs TP01 (book FULL Sh {base['full']['sharpe']:.2f} / HOLD {base['holdout']['sharpe']:.2f})")
print("=" * 100)
winners = []
for tf, sig in pairs:
name = Path(sig).stem
try:
signal = load_signal(sig)
pb = book_perbar(signal, tf)
d = to_daily(pb)
except Exception as e:
print(f"\n {name} ({tf}): ERRORE {type(e).__name__}: {str(e)[:80]}"); continue
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
J = pd.concat({"tp": tp_daily, "x": d}, axis=1, join="inner").dropna()
corr = float(J["tp"].corr(J["x"])) if len(J) > 2 else float("nan")
pct, consec, yr = breadth(d)
print(f"\n {name} ({tf}) BOOK 50/50")
print(f" standalone: FULL Sh {f['sharpe']:>5.2f} DD {f['maxdd']*100:>4.1f}% | HOLD Sh {h['sharpe']:>5.2f} ret {h['ret']*100:>+6.1f}% DD {h['maxdd']*100:>4.1f}%"
f" | anni+ {pct*100:>3.0f}% rossi-consec {consec} | corr_TP01 {corr:+.2f} | turn n/a")
# contributo al portafoglio
contrib = []
for w in (0.2, 0.3, 0.5):
sl = Sleeve(name, w, lambda pb=pb: pb)
bt = StrategyPortfolio([tp01_sleeve(1 - w), sl]).backtest()
dF = bt["full"]["sharpe"] - base["full"]["sharpe"]
dH = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"]
contrib.append((w, bt["full"]["sharpe"], dF, bt["holdout"]["sharpe"], dH))
print(f" +TP01 w{w:.0%}: FULL {bt['full']['sharpe']:.2f} ({dF:+.2f}) | HOLD {bt['holdout']['sharpe']:.2f} ({dH:+.2f})")
breadth_ok = pct >= 0.6 and consec <= 1
standalone_beats = f["sharpe"] > TP01_FULL_SH and h["sharpe"] > 0.25 and breadth_ok
# diversificatore: corr<0.5, migliora FULL E hold del portafoglio ad almeno un peso, breadth ok
improves = any(dF > 0.05 and dH > 0.0 for _, _, dF, _, dH in contrib)
diversifier = (not np.isnan(corr) and corr < 0.5) and improves and breadth_ok
verdict = "WINNER-standalone" if standalone_beats else ("WINNER-diversifier" if diversifier else "no")
print(f" -> {verdict} (breadth_ok={breadth_ok}, standalone_beats={standalone_beats}, diversifier={diversifier})")
if verdict.startswith("WINNER"):
winners.append((name, tf, verdict))
print("\n" + "=" * 100)
print(f" WINNERS: {len(winners)}")
for n, tf, v in winners:
print(f" {n} ({tf}): {v}")
if not winners:
print(" nessuno batte TP01 con criterio onesto -> serve un'altra ondata.")
if __name__ == "__main__":
main()
-108
View File
@@ -1,108 +0,0 @@
"""AFFINAMENTO XS01 — blend di LOOKBACK (multi-orizzonte cross-sectional).
XS01 attuale usa un singolo lookback (L=30). Come TP01 fonde gli orizzonti 30/90/180, qui il
momentum cross-sectional fonde piu' lookback: per ogni ribilancio, z-score cross-sectional del
rendimento a ciascun L, MEDIATO -> punteggio blended -> long top-k / short bottom-k. Piu' liscio
e robusto (meno dipendente da un singolo orizzonte/regime). Causale, netto fee, vol-target.
Confronto vs singolo-L + contributo al portafoglio TP01+XS01.
uv run python scripts/portfolio/xsec_blend.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
def load_majors():
cols = {}
for sym in XS_UNIVERSE:
p = RAW / f"hl_{sym.lower()}_1d.parquet"
if p.exists():
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
def xs_signal(C, lookbacks, H=10, k=5, mode="mom", tv=0.20):
"""lookbacks = lista (blend) o singolo [L]. Score = media z-score cross-sectional dei ret_L."""
px = C.values; n, A = px.shape
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A)); w = np.zeros(A)
for i in range(n):
if i >= max(lookbacks) and i % H == 0:
score = np.zeros(A); cnt = 0
for L in lookbacks:
rL = px[i] / px[i - L] - 1.0
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A); lo, hi = order[:k], order[-k:]
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
s = pd.Series(gross - turn * (FEE / 2.0), index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * scale, index=C.index))
def ev(C, lbs, tp):
d = xs_signal(C, lbs)
f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
return d, f, o, pct, corr
def main():
C = load_majors()
tp = tp01_sleeve().daily()
print("=" * 92)
print(f" AFFINAMENTO XS01 — blend di lookback (19 major, {len(C)} giorni)")
print("=" * 92)
print(f" {'lookbacks':<22}{'FULL':>7}{'OOS25':>7}{'DD%':>6}{'anni+':>7}{'corrTP':>8}")
configs = [
("[30] (attuale)", [30]), ("[90]", [90]), ("[20]", [20]),
("[20,40]", [20, 40]), ("[20,60]", [20, 60]), ("[30,90]", [30, 90]),
("[20,40,90]", [20, 40, 90]), ("[30,60,120]", [30, 60, 120]),
("[20,60,180]", [20, 60, 180]), ("[15,30,60,120]", [15, 30, 60, 120]),
]
rows = []
for name, lbs in configs:
d, f, o, pct, corr = ev(C, lbs, tp)
rows.append((name, lbs, d, f, o, pct, corr))
print(f" {name:<22}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{f['maxdd']*100:>6.0f}{pct*100:>6.0f}%{corr:>+8.2f}")
# candidato: miglior blend per (FULL+OOS) con breadth 100% e corr bassa
cand = [r for r in rows if r[5] >= 0.99 and r[6] < 0.4]
cand.sort(key=lambda r: -(r[3]["sharpe"] + r[4]["sharpe"]))
print("\n CONTRIBUTO al portafoglio — attuale (XS [30]) vs miglior blend")
base_xs = rows[0][2] # [30]
for label, dxs in [("XS [30] attuale", base_xs)] + ([(cand[0][0], cand[0][2])] if cand else []):
J = pd.concat({"tp": tp, "xs": dxs}, axis=1, join="inner").dropna()
for w in (0.3,):
comb = (1 - w) * J["tp"] + w * J["xs"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
xf = metrics(J["xs"]); xo = metrics(J["xs"][J["xs"].index >= HOLDOUT])
print(f" {label:<22} XS-solo FULL {xf['sharpe']:.2f}/OOS {xo['sharpe']:.2f} | TP01 70+XS 30: FULL {cf['sharpe']:.2f} HOLD {ch['sharpe']:.2f}")
if cand:
print(f"\n -> blend migliore: {cand[0][0]} (lookbacks {cand[0][1]}). Promuovere se batte [30] su")
print(" FULL+OOS+robustezza E migliora il portafoglio. Sennò resta [30].")
if __name__ == "__main__":
main()
-101
View File
@@ -1,101 +0,0 @@
"""AFFINAMENTO XS01 — GATE DI DISPERSIONE.
Il momentum cross-sectional vive nella DISPERSIONE (winners/losers distanti). In regime compatto
(tutti gli asset si muovono insieme) non ha segnale -> churn/rumore. Gate: entra SOLO se la
dispersione cross-section del momentum supera una soglia CAUSALE (percentile espandente della
dispersione passata); altrimenti flat. Sul blend [30,90] dei 19 major. Sweep soglia + contributo.
uv run python scripts/portfolio/xsec_dispgate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
LOOKBACKS = (30, 90); H = 10; K = 5; TV = 0.20
def load_majors():
cols = {}
for sym in XS_UNIVERSE:
p = RAW / f"hl_{sym.lower()}_1d.parquet"
if p.exists():
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
def xs_gated(C, disp_pct=0, min_hist=20):
px = C.values; n, A = px.shape
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
mlb = max(LOOKBACKS)
# dispersione del momentum a ogni barra: media (su lookback) della std cross-section di ret_L
disp = np.full(n, np.nan)
for i in range(mlb, n):
acc = 0.0; c = 0
for L in LOOKBACKS:
acc += (px[i] / px[i - L] - 1.0).std(); c += 1
disp[i] = acc / c
W = np.zeros((n, A)); w = np.zeros(A)
hist = []
gated_flat = 0; total = 0
for i in range(n):
if i >= mlb and i % H == 0:
thr = np.percentile(hist, disp_pct) if (disp_pct > 0 and len(hist) >= min_hist) else -np.inf
total += 1
if disp[i] >= thr:
score = np.zeros(A)
for L in LOOKBACKS:
rL = px[i] / px[i - L] - 1.0; sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd
order = np.argsort(score); w = np.zeros(A); lo, hi = order[:K], order[-K:]
w[hi] = 0.5 / K; w[lo] = -0.5 / K
else:
w = np.zeros(A); gated_flat += 1
hist.append(disp[i])
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
s = pd.Series(gross - turn * (FEE / 2.0), index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(TV / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * scale, index=C.index)), (gated_flat / total if total else 0)
def main():
C = load_majors(); tp = tp01_sleeve().daily()
print("=" * 92)
print(f" AFFINAMENTO XS01 — gate di dispersione (blend [30,90], 19 major, {len(C)}g)")
print("=" * 92)
print(f" {'soglia pctile':<16}{'FULL':>7}{'OOS25':>7}{'DD%':>6}{'anni+':>7}{'corrTP':>8}{'%flat':>8}")
res = {}
for p in (0, 30, 40, 50, 60, 70):
d, flat = xs_gated(C, p)
f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
res[p] = (d, f, o, pct, corr)
lab = "0 (no gate)" if p == 0 else f"p{p}"
print(f" {lab:<16}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{f['maxdd']*100:>6.0f}{pct*100:>6.0f}%{corr:>+8.2f}{flat*100:>7.0f}%")
print("\n CONTRIBUTO al portafoglio (TP01 70 + XS 30, finestra comune):")
for p in (0, 40, 50, 60):
d = res[p][0]
J = pd.concat({"tp": tp, "xs": d}, axis=1, join="inner").dropna()
comb = 0.7 * J["tp"] + 0.3 * J["xs"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
lab = "no gate (attuale)" if p == 0 else f"gate p{p}"
print(f" {lab:<18} FULL Sh {cf['sharpe']:.2f} DD {cf['maxdd']*100:.0f}% | HOLD Sh {ch['sharpe']:.2f}")
print("\n -> promuovere il gate se migliora Sharpe/DD/robustezza E il contributo. Sennò no-gate resta.")
if __name__ == "__main__":
main()
-133
View File
@@ -1,133 +0,0 @@
"""XS cross-sectional con UNIVERSO TOP-LIQUIDITÀ DINAMICO (Hyperliquid 52 certificati).
Invece di 19 nomi fissi, a ogni ribilancio: seleziona i top-N per liquidità (dollar-volume 30g
causale), poi fra quelli long i k più forti / short i k più deboli (momentum, market-neutral),
vol-target. Idea: cross-section pulita e ADATTIVA (i token entrano quando maturano in liquidità),
escludendo il long-tail rumoroso che diluiva il 52-all. Gestione ragged (asset a date diverse:
si classifica solo fra i disponibili). Causale. Confronto vs fisso-19 + 52-all + contributo TP01.
uv run python scripts/portfolio/xsec_dynuniverse.py
"""
from __future__ import annotations
import sys, glob
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
def load_close_vol():
close, vol = {}, {}
for p in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(p).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(p)
ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
close[sym] = pd.Series(d["close"].values.astype(float), index=ix)
vol[sym] = pd.Series(d["volume"].values.astype(float), index=ix)
C = pd.concat(close, axis=1, join="outer").sort_index()
V = pd.concat(vol, axis=1, join="outer").sort_index().reindex(C.index)
return C, V
def xs_dynamic(C, V, N=20, lb=60, hold=10, k=5, mode="mom", tv=0.20, fixed=None):
"""fixed=lista simboli -> universo statico (ignora liquidità). Altrimenti top-N per liquidità."""
cols = list(C.columns); A = len(cols)
px = C.values; n = len(px)
dret = np.full((n, A), 0.0); dret[1:] = np.where(np.isfinite(px[1:]) & np.isfinite(px[:-1]), px[1:] / px[:-1] - 1.0, 0.0)
dvol = V.values * px
liq = pd.DataFrame(dvol, index=C.index, columns=cols).rolling(30, min_periods=15).mean().shift(1).values
fixed_mask = np.array([c in fixed for c in cols]) if fixed else None
W = np.zeros((n, A)); w = np.zeros(A)
for i in range(n):
if i >= lb and i % hold == 0:
retlb = np.where(np.isfinite(px[i]) & np.isfinite(px[i - lb]), px[i] / px[i - lb] - 1.0, np.nan)
avail = np.isfinite(retlb) & np.isfinite(px[i])
if fixed is not None:
avail &= fixed_mask
cand = np.where(avail)[0]
else:
avail &= np.isfinite(liq[i])
idx = np.where(avail)[0]
if len(idx) > N:
cand = idx[np.argsort(liq[i][idx])[-N:]] # top-N per liquidità
else:
cand = idx
w = np.zeros(A)
ke = min(k, len(cand) // 2)
if ke >= 1:
order = cand[np.argsort(retlb[cand])]
lo, hi = order[:ke], order[-ke:]
if mode == "mom": w[hi] = 0.5 / ke; w[lo] = -0.5 / ke
else: w[lo] = 0.5 / ke; w[hi] = -0.5 / ke
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (FEE / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * scale, index=C.index))
def ev(d):
f = metrics(d); o = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
return f, o, pct
def main():
C, V = load_close_vol()
print("=" * 96)
print(f" XS UNIVERSO TOP-LIQUIDITÀ DINAMICO — {len(C.columns)} asset certificati [{C.index[0].date()} -> {C.index[-1].date()}]")
print("=" * 96)
tp = tp01_sleeve().daily()
print("\n (1) SWEEP N (top-liquidità) x config (mom) — FULL Sh / OOS25 Sh / anni+ / corrTP")
print(f" {'config':<28}{'FULL':>7}{'OOS25':>7}{'anni+':>7}{'corrTP':>8}")
best = None
for N in (12, 15, 20, 25):
for lb, hold, k in [(30, 10, 5), (60, 10, 5), (90, 10, 5)]:
d = xs_dynamic(C, V, N=N, lb=lb, hold=hold, k=k)
f, o, pct = ev(d)
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
tag = f"top{N} L{lb}H{hold}k{k}"
print(f" {tag:<28}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{pct*100:>6.0f}%{corr:>+8.2f}")
if (best is None or f['sharpe'] > best[1]['sharpe']) and corr < 0.4 and o['sharpe'] > 0:
best = (tag, f, o, corr, d, (N, lb, hold, k))
print("\n (2) BASELINE di confronto (stessa finestra):")
for name, kw in [("fisso-19 major (L30H10k5)", dict(lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))),
("fisso-19 major (L90H10k5)", dict(lb=90, hold=10, k=5, fixed=set(XS_UNIVERSE))),
("52-all (L60H10k5)", dict(lb=60, hold=10, k=5))]:
d = xs_dynamic(C, V, **kw); f, o, pct = ev(d)
print(f" {name:<28} FULL {f['sharpe']:.2f} OOS25 {o['sharpe']:.2f} anni+ {pct*100:.0f}%")
if best is None:
print("\n Nessuna config dinamica scorrelata+positiva. Il top-liquidità non aiuta.")
return
tag, f, o, corr, d, cfg = best
print(f"\n === MIGLIOR DINAMICO: {tag} | FULL {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.0f}% | OOS25 {o['sharpe']:.2f} | corrTP {corr:+.2f} ===")
per = [(int(y), round(float((1 + g).prod() - 1), 3)) for y, g in d.groupby(d.index.year)]
print(f" per-anno: {per}")
# contributo al portafoglio vs fisso-19 (XS01 attuale)
xs19 = xs_dynamic(C, V, lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))
J = pd.concat({"tp": tp, "dyn": d, "x19": xs19}, axis=1, join="inner").dropna()
print(f"\n CONTRIBUTO (finestra comune {J.index[0].date()}->{J.index[-1].date()}):")
for nm, col in [("TP01 solo", None), ("TP01+XS19 (attuale) 70/30", "x19"), ("TP01+DYN 70/30", "dyn")]:
if col is None:
comb = J["tp"]
else:
comb = 0.7 * J["tp"] + 0.3 * J[col]
mf = metrics(comb); mh = metrics(comb[comb.index >= HOLDOUT])
print(f" {nm:<28} FULL Sh {mf['sharpe']:.2f} DD {mf['maxdd']*100:.0f}% | HOLD Sh {mh['sharpe']:.2f}")
print("\n -> DINAMICO meglio del fisso-19? guarda FULL/OOS + contributo. Sennò: fisso-19 resta.")
if __name__ == "__main__":
main()
-123
View File
@@ -1,123 +0,0 @@
"""CROSS-SECTIONAL su universo Hyperliquid certificato (19 alt, 1d, 2024-2026).
Strategia market-neutral: ogni H giorni classifica gli asset per rendimento a L giorni (causale),
va long i top-k / short i bottom-k (momentum) o viceversa (reversal), dollar-neutral, vol-target.
Mira a DIVERSIFICARE TP01 (long-trend): se scorrelata e robusta, migliora il portafoglio.
Gauntlet onesto: FULL (2024-26) + within-window OOS (2025+) + per-anno + corr TP01 + contributo.
Caveat: storia corta (~2.5 anni). Risultati suggestivi, non robusti come BTC/ETH 6 anni.
uv run python scripts/portfolio/xsec_research.py
"""
from __future__ import annotations
import sys, glob
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001
def load_universe():
cols = {}
for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
s = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(f)
cols[s] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
return C
def xs_book(C, L, H, k, mode="mom", target_vol=0.20):
"""Rendimenti netti giornalieri di un book cross-sectional market-neutral. Causale."""
assets = list(C.columns); A = len(assets)
px = C.values; n = len(px)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A)) # peso per asset per giorno (deciso a close[i], tenuto in i+1)
w = np.zeros(A)
for i in range(n):
if i >= L and i % H == 0:
lb = px[i] / px[i - L] - 1.0
order = np.argsort(lb)
w = np.zeros(A)
lo, hi = order[:k], order[-k:] # peggiori / migliori
if mode == "mom":
w[hi] = 0.5 / k; w[lo] = -0.5 / k # long forti / short deboli
else:
w[lo] = 0.5 / k; w[hi] = -0.5 / k # reversal
W[i] = w
# rendimento book: peso[i-1] guadagna dret[i]; fee su turnover
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) # W[i-1] guadagna dret[i]
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) # turnover per (ri)settare W[i]
net = gross - turn * (FEE / 2.0)
s = pd.Series(net, index=C.index)
# vol-target (causale): scala per target/vol_realizzata(30) shiftata
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return pd.Series(s.values * scale, index=C.index)
def yr_breadth(daily):
pre = daily
yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)]
consec = mx = 0
for v in yr: consec = consec + 1 if v < 0 else 0; mx = max(mx, consec)
return yr, (sum(v > 0 for v in yr) / len(yr) if yr else 0), mx
def main():
C = load_universe()
print("=" * 96)
print(f" CROSS-SECTIONAL Hyperliquid — {len(C.columns)} asset, {len(C)} giorni [{C.index[0].date()} -> {C.index[-1].date()}]")
print("=" * 96)
tp = tp01_sleeve(1.0); tp_daily = tp.daily()
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
print(f"\n {'config':<24}{'FULL Sh':>9}{'OOS25 Sh':>10}{'ret%':>8}{'DD%':>7}{'corrTP':>8}{'anni+':>7}")
cands = []
grid = [("mom",L,H,k) for L in (30,60,90) for H in (5,10,20) for k in (3,5)] \
+ [("rev",L,H,k) for L in (3,7,14) for H in (3,5) for k in (3,5)]
for mode,L,H,k in grid:
d = to_daily(xs_book(C,L,H,k,mode))
f=metrics(d); oos=metrics(d[d.index>=HOLDOUT])
J=pd.concat({"tp":tp_daily,"x":d},axis=1,join="inner").dropna(); corr=float(J["tp"].corr(J["x"])) if len(J)>5 else float("nan")
yr,pct,consec=yr_breadth(d)
tag=f"{mode} L{L} H{H} k{k}"
cands.append((tag,mode,L,H,k,f,oos,corr,pct,consec,d))
if f["sharpe"]>0.6 or oos["sharpe"]>0.8:
print(f" {tag:<24}{f['sharpe']:>9.2f}{oos['sharpe']:>10.2f}{f['ret']*100:>+8.0f}{f['maxdd']*100:>7.1f}{corr:>+8.2f}{pct*100:>6.0f}%")
# migliore per OOS Sharpe (con corr bassa) come candidato diversificatore
good=[c for c in cands if not np.isnan(c[7]) and abs(c[7])<0.4 and c[5]["sharpe"]>0.5 and c[6]["sharpe"]>0]
good.sort(key=lambda c:-(c[6]["sharpe"]))
print(f"\n Candidati scorrelati(<0.4) e positivi (FULL>0.5, OOS>0): {len(good)}")
print("\n === TOP candidato come DIVERSIFICATORE di TP01 ===")
if not good:
print(" nessun candidato cross-sectional robusto+scorrelato. Universo corto.")
return
tag,mode,L,H,k,f,oos,corr,pct,consec,d = good[0]
print(f" {tag}: FULL Sh {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.1f}% | OOS25 Sh {oos['sharpe']:.2f} | corr TP01 {corr:+.2f} | anni+ {pct*100:.0f}% rossi-consec {consec}")
per=[(y,round(v,3)) for y,(v) in zip([yy for yy,_ in d.groupby(d.index.year)], yr_breadth(d)[0])]
print(f" per-anno: {per}")
# CONFRONTO EQUO: sulla finestra COMUNE (2024-2026), TP01-solo vs TP01+XS
J = pd.concat({"tp": tp_daily, "xs": d}, axis=1, join="inner").dropna()
tpw, xsw = J["tp"], J["xs"]
bw_f = metrics(tpw); bw_h = metrics(tpw[tpw.index >= HOLDOUT])
print(f"\n [finestra comune {J.index[0].date()}->{J.index[-1].date()}]")
print(f" TP01 SOLO (su finestra comune): FULL Sh {bw_f['sharpe']:.2f} DD {bw_f['maxdd']*100:.1f}% | HOLD Sh {bw_h['sharpe']:.2f}")
for w in (0.2, 0.3, 0.5):
comb = (1 - w) * tpw + w * xsw
cf = metrics(comb); ch = metrics(comb[comb.index >= HOLDOUT])
print(f" +XS w{w:.0%}: FULL {cf['sharpe']:.2f} ({cf['sharpe']-bw_f['sharpe']:+.2f}) DD {cf['maxdd']*100:.1f}%"
f" | HOLD {ch['sharpe']:.2f} ({ch['sharpe']-bw_h['sharpe']:+.2f})")
print("\n WINNER-diversifier se: corr bassa, e TP01+XS batte TP01-solo (FULL E HOLD) sulla finestra comune,")
print(" con breadth per-anno ok. Altrimenti no (e attenzione: storia XS solo ~2.5 anni).")
if __name__=="__main__":
main()
-58
View File
@@ -1,58 +0,0 @@
"""FETCH storia DVOL (Deribit Volatility Index) — input IV per lo sleeve opzioni VRP.
DVOL = vol implicita 30d annualizzata di Deribit (l'IV "ATM" del mercato). Public API, no auth.
Limite 1000 punti/richiesta -> paginazione all'indietro. Salva data/raw/dvol_<asset>.parquet
(colonne: timestamp ms, close = DVOL%). Usato come IV per prezzare BS le opzioni nel backtest VRP;
la RV viene dai nostri prezzi certificati. VRP = IV - RV.
uv run python scripts/research/fetch_dvol.py
"""
from __future__ import annotations
import sys, time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import requests, pandas as pd
URL = "https://www.deribit.com/api/v2/public/get_volatility_index_data"
RAW = PROJECT_ROOT / "data" / "raw"
def fetch(cur, res=86400):
end = int(time.time() * 1000)
floor = int(pd.Timestamp("2020-06-01", tz="UTC").timestamp() * 1000)
rows = {}
guard = 0
while end > floor and guard < 60:
guard += 1
r = requests.get(URL, params={"currency": cur, "start_timestamp": floor,
"end_timestamp": end, "resolution": res}, timeout=40)
data = r.json().get("result", {}).get("data", [])
if not data:
break
for ts, o, h, l, c in data:
rows[int(ts)] = float(c)
earliest = min(int(x[0]) for x in data)
if earliest >= end:
break
end = earliest - 1
if not rows:
return pd.DataFrame()
df = pd.DataFrame(sorted(rows.items()), columns=["timestamp", "close"])
return df
def main():
for cur in ("BTC", "ETH"):
df = fetch(cur)
if df.empty:
print(f"{cur}: VUOTO"); continue
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
df.to_parquet(RAW / f"dvol_{cur.lower()}.parquet", index=False)
print(f"{cur}: {len(df)} giorni [{ts.iloc[0].date()} -> {ts.iloc[-1].date()}] "
f"DVOL media {df['close'].mean():.1f} range [{df['close'].min():.1f}, {df['close'].max():.1f}] "
f"-> data/raw/dvol_{cur.lower()}.parquet")
if __name__ == "__main__":
main()
-83
View File
@@ -1,83 +0,0 @@
"""CALIBRAZIONE VRP su quote REALI cerbero-bite — misura f e skew, non li assume.
cerbero-bite accumula la catena Deribit mainnet reale (option_chain_snapshots). Qui, per ogni
snapshot, prendo la put piu' vicina a delta -0.28 (DTE settimanale), confronto il BID REALE
(vendita conservativa) col premio MODELLATO (BS su DVOL, IV-ATM) -> fattore f = reale/modellato,
e skew = IV_put_reale - DVOL. Pinna empiricamente dove sta il VRP sleeve sullo sweep f.
Input: /tmp/cb_puts.csv (export da cerbero-bite). Finestra ~2026-05 -> oggi (un regime, mainnet).
uv run python scripts/research/options_vrp_calibrate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from scripts.research.options_vrp_lab import bs_put
from scripts.analysis.research_lab import load_tf
CSV = "/tmp/cb_puts.csv"
def spot_series(asset):
px = load_tf(asset, "1h")
return pd.Series(px["close"].values.astype(float),
index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index()
def dvol_series(asset):
d = pd.read_parquet(PROJECT_ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet")
return pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
def main():
df = pd.read_csv(CSV, names=["ts", "asset", "strike", "expiry", "bid", "mid", "iv", "delta"])
df["ts"] = pd.to_datetime(df["ts"], utc=True, errors="coerce")
df["expiry"] = pd.to_datetime(df["expiry"], utc=True, errors="coerce")
for c in ("strike", "bid", "mid", "iv", "delta"):
df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=["ts", "expiry", "strike", "bid", "iv", "delta"])
df["dte"] = (df["expiry"] - df["ts"]).dt.total_seconds() / 86400.0
df = df[(df["dte"] >= 4) & (df["dte"] <= 10) & (df["bid"] > 0)]
print("=" * 92)
print(" CALIBRAZIONE VRP su QUOTE REALI (cerbero-bite mainnet) — put weekly ~delta -0.28")
print("=" * 92)
for asset in ("BTC", "ETH"):
d = df[df["asset"] == asset].copy()
if d.empty:
print(f"\n {asset}: nessun dato"); continue
# per snapshot, la put piu' vicina a delta -0.28
d["dd"] = (d["delta"] - (-0.28)).abs()
pick = d.sort_values("dd").groupby("ts").first().reset_index().sort_values("ts")
S = spot_series(asset); V = dvol_series(asset)
Sdf = pd.DataFrame({"ts": S.index.as_unit("ns"), "spot": S.values}).sort_values("ts")
Vdf = pd.DataFrame({"ts": V.index.as_unit("ns"), "dvol": V.values}).sort_values("ts")
pick = pick.sort_values("ts").reset_index(drop=True)
pts = pick[["ts"]].copy()
pts["ts"] = pts["ts"].dt.as_unit("ns")
pick["spot"] = pd.merge_asof(pts, Sdf, on="ts", direction="nearest", tolerance=pd.Timedelta("2h"))["spot"].values
pick["dvol"] = pd.merge_asof(pts, Vdf, on="ts", direction="nearest", tolerance=pd.Timedelta("2D"))["dvol"].values
pick = pick.dropna(subset=["spot", "dvol"])
# premio reale (vendo al BID, in coin -> frazione del sottostante) vs modellato BS@DVOL
pick["real_pct"] = pick["bid"] * 100.0
pick["model_pct"] = pick.apply(lambda r: bs_put(r["spot"], r["strike"], r["dte"] / 365.25, r["dvol"] / 100.0) / r["spot"] * 100.0, axis=1)
pick = pick[pick["model_pct"] > 0]
pick["f"] = pick["real_pct"] / pick["model_pct"]
pick["skew"] = pick["iv"] - pick["dvol"]
print(f"\n {asset} (snapshot validi={len(pick)}, {pick['ts'].iloc[0].date()} -> {pick['ts'].iloc[-1].date()})")
print(f" delta medio {pick['delta'].mean():+.2f} | DTE medio {pick['dte'].mean():.1f}g | moneyness medio {(pick['strike']/pick['spot']).mean():.3f}")
print(f" IV put reale {pick['iv'].mean():.1f}% vs DVOL {pick['dvol'].mean():.1f}% -> SKEW medio {pick['skew'].mean():+.1f} pt")
print(f" premio reale(BID) {pick['real_pct'].mean():.2f}% vs modellato(IV-ATM) {pick['model_pct'].mean():.2f}%")
print(f" FATTORE f = reale/modellato: mediana {pick['f'].median():.2f} IQR [{pick['f'].quantile(.25):.2f}, {pick['f'].quantile(.75):.2f}] (range {pick['f'].min():.2f}-{pick['f'].max():.2f})")
print("\n -> f e' il punto reale sullo sweep di options_vrp_lab (Sh: f1.0=0.71, f1.29=1.70).")
print(" CAVEAT: finestra mag-giu 2026 = UN regime (niente crash) -> f calmo. In stress lo skew")
print(" sale (piu' premio) MA la coda colpisce: il f di stress va misurato quando arriva un crash.")
if __name__ == "__main__":
main()
-150
View File
@@ -1,150 +0,0 @@
"""OPTIONS VRP LAB — sleeve income: vendita put settimanali (CSP) che incassa il VRP (IV>RV).
Aggira il muro "niente catena storica gratis" come crypto_backtest: prezza le put con Black-Scholes
sulla DVOL REALE (IV storica Deribit, data/raw/dvol_*.parquet) + CALIBRAZIONE su quote reali
(fattore f: la verifica su quote reali ha trovato premio reale ~1.29x il modellato a IV-ATM per via
dello skew, al netto dello spread). Payoff sul path REALIZZATO dei prezzi certificati. Causale: la
decisione (strike/premio) usa solo dati <= sell-date; il payoff realizza a scadenza.
Onesto: e' SHORT-VOL, il rischio vero e' la CODA (crash). Riporto worst-weeks (LUNA/FTX), per-anno,
sweep su f (sensitivity del premio reale) e delta. NON e' un deploy: e' la prima validazione del lead.
uv run python scripts/research/options_vrp_lab.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from scipy.stats import norm
from scripts.analysis.research_lab import load_tf
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WK_PER_YEAR = 365.25 / 7.0
def bs_put(S, K, T, sig):
if T <= 0 or sig <= 0:
return max(K - S, 0.0)
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
d2 = d1 - sig * np.sqrt(T)
return K * norm.cdf(-d2) - S * norm.cdf(-d1) # r=0
def strike_from_delta(S, T, sig, target_delta=-0.28):
# delta_put = -N(-d1) = target -> d1 = -N^{-1}(-target)
d1 = -norm.ppf(-target_delta)
return S * np.exp(0.5 * sig ** 2 * T - d1 * sig * np.sqrt(T))
def load_series(asset):
px = load_tf(asset, "1d")
s = pd.Series(px["close"].values.astype(float), index=pd.to_datetime(px["timestamp"], unit="ms", utc=True))
dv = pd.read_parquet(PROJECT_ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet")
d = pd.Series(dv["close"].values.astype(float), index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
return J
def put_sell_weekly(asset, delta=-0.28, f=1.0, tenor_d=7):
"""Vendita CSP settimanale. Ritorna serie di rendimenti SETTIMANALI (su collaterale K) indicizzata
alla data di scadenza. Causale: strike/premio da DVOL e prezzo a sell-date; payoff a scadenza."""
J = load_series(asset)
px = J["px"].values; dv = J["dvol"].values / 100.0; idx = J.index
n = len(px); T = tenor_d / 365.25
rets = {}
i = 30
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
K = strike_from_delta(S0, T, sig, delta)
prem = bs_put(S0, K, T, sig) * f
S1 = px[i + tenor_d]
pnl = prem - max(0.0, K - S1) # short put: incassi premio, paghi se finisce ITM
rets[idx[i + tenor_d]] = pnl / K # rendimento su collaterale cash-secured
i += tenor_d
return pd.Series(rets)
def m_weekly(r):
r = r.dropna()
if len(r) < 3 or r.std() == 0:
return dict(sh=0, cagr=0, dd=0, n=len(r))
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
yrs = len(r) / WK_PER_YEAR
return dict(sh=float(r.mean() / r.std() * np.sqrt(WK_PER_YEAR)),
cagr=float(eq[-1] ** (1 / yrs) - 1) if yrs > 0 and eq[-1] > 0 else 0,
dd=float(np.max((pk - eq) / pk)), n=len(r))
def per_year(r):
out = {}
for y, g in r.groupby(r.index.year):
eq = np.cumprod(1 + g.values)
out[int(y)] = float(eq[-1] - 1)
return out
def main():
print("=" * 96)
print(" OPTIONS VRP LAB — vendita put settimanali (CSP), premio BS su DVOL reale + calibrazione f")
print("=" * 96)
# contesto VRP: IV (DVOL) vs RV realizzata
for a in ("BTC", "ETH"):
J = load_series(a)
rv = J["px"].pct_change().rolling(30).std() * np.sqrt(365.25) * 100
vrp = (J["dvol"] - rv).dropna()
print(f" {a}: DVOL media {J['dvol'].mean():.0f}% | RV30 media {rv.mean():.0f}% | VRP media {vrp.mean():+.1f} pt, >0 nel {100*(vrp>0).mean():.0f}% del tempo")
print("\n (1) SWEEP CALIBRAZIONE f (delta -0.28, weekly) — book 50/50 BTC+ETH")
print(f" {'f':>6}{'Sh':>7}{'CAGR':>8}{'maxDD':>8}{'worst-wk':>10}")
for f in (0.70, 0.85, 1.0, 1.15, 1.29):
rB = put_sell_weekly("BTC", f=f); rE = put_sell_weekly("ETH", f=f)
book = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
mm = m_weekly(book); worst = book.min()
tag = " <- reale(calm)" if f == 1.29 else (" <- conservativo" if f == 1.0 else "")
print(f" {f:>6.2f}{mm['sh']:>7.2f}{mm['cagr']*100:>+7.0f}%{mm['dd']*100:>7.1f}%{worst*100:>+9.1f}%{tag}")
print("\n (2) SWEEP DELTA (f=1.0 conservativo) — book 50/50")
print(f" {'delta':>7}{'Sh':>7}{'CAGR':>8}{'maxDD':>8}")
for dl in (-0.15, -0.28, -0.40):
rB = put_sell_weekly("BTC", delta=dl); rE = put_sell_weekly("ETH", delta=dl)
book = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
mm = m_weekly(book)
print(f" {dl:>7.2f}{mm['sh']:>7.2f}{mm['cagr']*100:>+7.0f}%{mm['dd']*100:>7.1f}%")
# config centrale: delta -0.28, f=1.0 (conservativo) e f=1.29 (reale misurato)
print("\n (3) PER ANNO + WORST WEEKS (delta -0.28, book 50/50) — il rischio e' la CODA")
for f in (1.0, 1.29):
rB = put_sell_weekly("BTC", f=f); rE = put_sell_weekly("ETH", f=f)
book = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
py = per_year(book)
worst = book.nsmallest(5)
print(f"\n f={f}: per-anno " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
print(f" worst weeks: " + " ".join(f"{d.date()}:{v*100:.0f}%" for d, v in worst.items()))
full = m_weekly(book); ho = m_weekly(book[book.index >= HOLDOUT])
print(f" FULL Sh {full['sh']:.2f} CAGR {full['cagr']*100:+.0f}% DD {full['dd']*100:.0f}% | HOLD-OUT Sh {ho['sh']:.2f}")
# correlazione e contributo vs TP01 (resampling settimanale)
print("\n (4) CORRELAZIONE + CONTRIBUTO vs TP01 (settimanale; f=1.0 conservativo)")
from src.portfolio.sleeves import tp01_sleeve
tp = tp01_sleeve().daily()
tp_wk = (1 + tp).resample("7D").prod() - 1
rB = put_sell_weekly("BTC"); rE = put_sell_weekly("ETH")
opt = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
opt_wk = opt.copy(); opt_wk.index = opt_wk.index.to_period("W").to_timestamp()
tp_wk2 = tp_wk.copy(); tp_wk2.index = tp_wk2.index.to_period("W").to_timestamp()
Jc = pd.concat({"tp": tp_wk2, "opt": opt_wk}, axis=1, join="inner").dropna()
corr = float(Jc["tp"].corr(Jc["opt"])) if len(Jc) > 5 else float("nan")
print(f" corr settimanale opt vs TP01 = {corr:+.2f} (atteso ~0.2)")
for w in (0.3, 0.5):
comb = (1 - w) * Jc["tp"] + w * Jc["opt"]
mt = m_weekly(Jc["tp"]); mc = m_weekly(comb)
print(f" TP01 {1-w:.0%} + OPT {w:.0%}: Sh {mc['sh']:.2f} (TP01-solo {mt['sh']:.2f}) DD {mc['dd']*100:.0f}%")
print("\n NB onesto: short-vol -> guarda i worst-weeks e gli anni di crash. Premio MODELLATO; il")
print(" rischio coda/roll in stress NON e' pienamente catturato. Lead, non deploy.")
if __name__ == "__main__":
main()
-180
View File
@@ -1,180 +0,0 @@
"""OPTIONS VRP v2 — migliora lo sleeve short-vol con le idee di FinanceOld/OptionsAgent.
Baseline (options_vrp_lab): vendita put NUDA settimanale delta -0.28, premio BS su DVOL reale.
f=1.0 -> Sh 0.71, DD 33%, worst-week -26%, HOLD-OUT Sh 0.04 (muore OOS). Il rischio e' la CODA.
OptionsAgent (Bear Call Spread + VIX hedge su IWM) porta 3 idee testabili qui:
(A) RISCHIO DEFINITO: invece della put nuda, PUT CREDIT SPREAD (vendi put delta -0.28, COMPRI put
piu' OTM delta -0.10). Cap la coda: max perdita = width - premio netto. Capitale = width (margine
reale di un defined-risk). Lo Sharpe e' scale-free; DD/worst-week sul width (capitale vero a rischio).
(B) GATE VRP/IV-RANK: vendi vol SOLO quando e' ricca. Gate causale su:
- vrp: DVOL[i] - RV30(causale) > 0 (premio > vol realizzata recente)
- ivr: IV-rank = percentile espandente di DVOL[i] in DVOL[:i] > soglia
"Solo se IV Rank > 30%" e' una delle 5 condizioni d'ingresso di OptionsAgent.
(C) CRASH-SKIP: vai flat se DVOL gia' esploso sopra un percentile alto (vol-spike = NO-GO, come
"VIX>35 -> NO-GO" di OptionsAgent). Evita di vendere nel pieno del crash.
Tutto CAUSALE: strike/premio/gate usano solo dati <= sell-date; payoff realizza a scadenza sui prezzi
certificati. Fee Deribit opzioni: 0.03% del NOTIONAL per gamba (cap 12.5% del premio) -> qui modellate
come costo per-trade sul premio. NON deploy: lead quantificato e onesto.
uv run python scripts/research/options_vrp_v2.py
"""
from __future__ import annotations
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series, m_weekly, per_year
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WK_PER_YEAR = 365.25 / 7.0
# fee Deribit opzioni: 0.0003 * spot per contratto, cap 12.5% del premio. Per uno spread sono 2 gambe.
# Modellata come frazione del premio netto incassato (conservativa: usa il cap come ordine di grandezza).
FEE_FRAC_OF_PREMIUM = 0.125 # 12.5% del premio netto, per ROUND-TRIP delle gambe (worst-case del cap)
def _rv30(px: np.ndarray, i: int) -> float:
"""RV annualizzata causale dagli ultimi 30 rendimenti giornalieri (fino a i incluso)."""
if i < 31:
return np.nan
r = np.diff(np.log(px[i - 30:i + 1]))
return float(np.std(r) * np.sqrt(365.25))
def _ivrank(dv: np.ndarray, i: int) -> float:
"""IV-rank causale: percentile di dv[i] nella storia espandente dv[:i]."""
if i < 60:
return np.nan
hist = dv[:i]
return float((hist < dv[i]).mean())
def vrp_spread_weekly(asset, short_delta=-0.28, long_delta=-0.10, f=1.0, tenor_d=7,
defined_risk=True, gate_vrp=False, gate_ivr=0.0, crash_skip=1.01,
with_fee=True):
"""Vendita settimanale di put credit spread (o nuda se defined_risk=False), con gate causali.
Ritorna serie di rendimenti settimanali su CAPITALE A RISCHIO (width per lo spread, K per la nuda)."""
J = load_series(asset)
px = J["px"].values; dv_pct = J["dvol"].values; dv = dv_pct / 100.0; idx = J.index
n = len(px); T = tenor_d / 365.25
rets = {}
i = 60 # serve storia per RV/IV-rank
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
# --- GATE causali (decisi a sell-date) ---
skip = False
if gate_vrp:
rv = _rv30(px, i)
if not np.isnan(rv) and (sig - rv) <= 0: # VRP non positivo -> non vendere
skip = True
if gate_ivr > 0:
ivr = _ivrank(dv, i)
if not np.isnan(ivr) and ivr < gate_ivr: # IV troppo bassa -> non vendere
skip = True
if crash_skip < 1.0:
ivr = _ivrank(dv, i)
if not np.isnan(ivr) and ivr > crash_skip: # vol gia' esplosa -> NO-GO
skip = True
if skip:
rets[idx[i + tenor_d]] = 0.0 # flat: nessun rischio quella settimana
i += tenor_d
continue
# --- struttura ---
Ks = strike_from_delta(S0, T, sig, short_delta) # put venduta
prem_s = bs_put(S0, Ks, T, sig) * f
S1 = px[i + tenor_d]
if defined_risk:
Kl = strike_from_delta(S0, T, sig, long_delta) # put comprata (piu' OTM, strike piu' basso)
prem_l = bs_put(S0, Kl, T, sig) * f
net_prem = prem_s - prem_l
width = Ks - Kl
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1) # quanto pago netto a scadenza
pnl = net_prem - payoff
cap = Ks # cash-secured: stesso capitale del baseline nudo -> DD/worst comparabili,
# il long wing CAPPA la coda (la differenza dal nudo e' solo la coda tagliata)
else:
pnl = prem_s - max(0.0, Ks - S1)
cap = Ks
net_prem = prem_s
if with_fee:
pnl -= FEE_FRAC_OF_PREMIUM * abs(net_prem)
rets[idx[i + tenor_d]] = pnl / cap
i += tenor_d
return pd.Series(rets)
def book(fn, **kw):
rB = fn("BTC", **kw); rE = fn("ETH", **kw)
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
def report(name, b):
full = m_weekly(b); ho = m_weekly(b[b.index >= HOLDOUT])
worst = b.min(); active = float((b != 0).mean())
py = per_year(b)
print(f" {name:<34} FULL Sh {full['sh']:>5.2f} CAGR {full['cagr']*100:>+4.0f}% DD {full['dd']*100:>3.0f}% "
f"worst {worst*100:>+5.1f}% | HOLD Sh {ho['sh']:>5.2f} | attivo {active*100:>3.0f}%")
return full, ho, py
def main():
print("=" * 104)
print(" OPTIONS VRP v2 — defined-risk spread + gate VRP/IV-rank + crash-skip (idee OptionsAgent)")
print("=" * 104)
print(" Fee opzioni Deribit modellate: 12.5%% del premio netto per round-trip (cap del fee reale).\n")
print(" (0) BASELINE — put NUDA delta -0.28 (riproduce options_vrp_lab, ora CON fee)")
report("naked f=1.0 (no gate)", book(vrp_spread_weekly, defined_risk=False, f=1.0))
report("naked f=1.29 (reale-calm)", book(vrp_spread_weekly, defined_risk=False, f=1.29))
print("\n (1) RISCHIO DEFINITO — put credit spread -0.28/-0.10 (cap coda), capitale=width")
for f in (1.0, 1.29):
report(f"spread f={f}", book(vrp_spread_weekly, defined_risk=True, f=f))
print("\n (2) + GATE VRP>0 (vendi solo se DVOL>RV30 causale)")
for f in (1.0, 1.29):
report(f"spread+vrp f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, gate_vrp=True))
print("\n (3) + GATE IV-RANK > 0.30 (vendi solo vol ricca; cond. d'ingresso OptionsAgent)")
for f in (1.0, 1.29):
report(f"spread+ivr30 f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, gate_ivr=0.30))
print("\n (4) + CRASH-SKIP IV-rank>0.90 (NO-GO se vol gia' esplosa)")
for f in (1.0, 1.29):
report(f"spread+crashskip f={f}", book(vrp_spread_weekly, defined_risk=True, f=f, crash_skip=0.90))
print("\n (5) COMBO — spread + vrp + ivr30 + crash-skip (tutti i filtri, f=1.0 conservativo)")
full, ho, py = report("COMBO f=1.0", book(vrp_spread_weekly, defined_risk=True, f=1.0,
gate_vrp=True, gate_ivr=0.30, crash_skip=0.90))
print(" per-anno: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
full, ho, py = report("COMBO f=1.29", book(vrp_spread_weekly, defined_risk=True, f=1.29,
gate_vrp=True, gate_ivr=0.30, crash_skip=0.90))
print(" per-anno: " + " ".join(f"{y}:{v*100:+.0f}%" for y, v in py.items()))
# contributo al portafoglio TP01
print("\n (6) CORRELAZIONE + CONTRIBUTO vs TP01 (COMBO f=1.0)")
from src.portfolio.sleeves import tp01_sleeve
tp = tp01_sleeve().daily()
tp_wk = (1 + tp).resample("7D").prod() - 1
opt = book(vrp_spread_weekly, defined_risk=True, f=1.0, gate_vrp=True, gate_ivr=0.30, crash_skip=0.90)
opt_wk = opt.copy(); opt_wk.index = opt_wk.index.to_period("W").to_timestamp()
tp_wk2 = tp_wk.copy(); tp_wk2.index = tp_wk2.index.to_period("W").to_timestamp()
Jc = pd.concat({"tp": tp_wk2, "opt": opt_wk}, axis=1, join="inner").dropna()
corr = float(Jc["tp"].corr(Jc["opt"])) if len(Jc) > 5 else float("nan")
print(f" corr settimanale opt vs TP01 = {corr:+.2f}")
for w in (0.3, 0.5):
comb = (1 - w) * Jc["tp"] + w * Jc["opt"]
mt = m_weekly(Jc["tp"]); mc = m_weekly(comb)
print(f" TP01 {1-w:.0%} + OPT {w:.0%}: Sh {mc['sh']:.2f} (TP01-solo {mt['sh']:.2f}) DD {mc['dd']*100:.0f}%")
print("\n NB onesto: capitale=strike corto (cash-secured) per entrambe -> DD/worst comparabili al nudo.")
print(" Il defined-risk CAPPA la coda (-16.6%->-7.4% worst, DD 33%->14-21%) RIDUCENDO la dipendenza")
print(" dal f di stress (la coda e' tagliata per costruzione). Il gate IV-rank e' l'alpha: vendere")
print(" solo vol ricca (58%% delle settimane) ribalta l'HOLD-OUT da -0.25 a +0.28 (f=1.0). Premio")
print(" MODELLATO su DVOL ATM (no skew). Lead quantificato, non deploy (serve catena reale + f di stress).")
if __name__ == "__main__":
main()
-1
View File
@@ -1 +0,0 @@
"""Monitoraggio paper (dashboard). Lo stack live REALE resta in Old/."""
-190
View File
@@ -1,190 +0,0 @@
"""DASHBOARD web del portafoglio attivo (TP01 + XS01) — monitoraggio PAPER, stdlib only.
Mostra: metriche (FULL/HOLD Sharpe, DD, CAGR), per-sleeve, posizioni correnti, equity (backtest +
paper forward da scripts/live/paper_portfolio.py), ultima data dato. Nessuna auth -> solo rete
interna. Esecuzione REALE disabilitata: e' un monitor, non un trader.
uv run python -m src.live.dashboard --port 8787
"""
from __future__ import annotations
import sys, json, time
from pathlib import Path
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np, pandas as pd
from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT
from src.portfolio.sleeves import active_sleeves
from src.live.shadow import shadow_report, tp01_trades
from src.version import APP_VERSION
PAPER = PROJECT_ROOT / "data" / "paper_portfolio" / "state.json"
_CACHE = {"t": 0.0, "data": None}
_TTL = 120.0
def build():
if _CACHE["data"] is not None and time.time() - _CACHE["t"] < _TTL:
return _CACHE["data"]
pf = StrategyPortfolio(active_sleeves(), capital=2000.0)
bt = pf.backtest()
eq = bt["equity"]; idx = bt["index"]
# sparkline: subsample ~400 punti
step = max(1, len(eq) // 400)
spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)]
paper = json.loads(PAPER.read_text()) if PAPER.exists() else None
try:
shadow = shadow_report() # mainnet sola lettura, best-effort
except Exception as e:
shadow = {"error": f"{type(e).__name__}: {e}"}
try:
trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale
except Exception:
trades = []
data = dict(
version=APP_VERSION,
last_data=str(idx[-1].date()),
full=bt["full"], holdout=bt["holdout"], weights=bt["weights"],
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
positions=pf.current_positions(), spark=spark, paper=paper, shadow=shadow, trades=trades,
bh=None,
)
_CACHE.update(t=time.time(), data=data)
return data
def svg_spark(spark, w=900, h=220):
ys = [v for _, v in spark]
lo, hi = min(ys), max(ys)
rng = hi - lo or 1
pts = []
for i, (_, v) in enumerate(spark):
x = i / (len(spark) - 1) * w
y = h - (v - lo) / rng * (h - 10) - 5
pts.append(f"{x:.1f},{y:.1f}")
return (f'<svg viewBox="0 0 {w} {h}" width="100%" height="{h}" preserveAspectRatio="none">'
f'<polyline fill="none" stroke="#2ecc71" stroke-width="2" points="{" ".join(pts)}"/></svg>')
def html():
d = build()
f, ho = d["full"], d["holdout"]
rows = ""
for name, s in d["per_sleeve"].items():
rows += (f"<tr><td>{name}</td><td>{s['weight']*100:.0f}%</td>"
f"<td>{s['full']['sharpe']:.2f}</td><td>{s['full']['maxdd']*100:.0f}%</td>"
f"<td>{s['holdout']['sharpe']:.2f}</td></tr>")
yrs = "".join(f"<span class=y>{y}: {v['ret']*100:+.0f}%</span>" for y, v in sorted(d["yearly"].items()))
pos = ""
for sl, p in d["positions"].items():
pos += f"<tr><td>{sl}</td><td>{'flat (in cash)' if p == {'BTC': 0.0, 'ETH': 0.0} else (p if p is not None else 'stat-mode (book 19 gambe)')}</td></tr>"
pp = d["paper"]
if pp:
days = (pd.Timestamp(pp["last"]) - pd.Timestamp(pp["start"])).days
ret = pp["equity"] / pp["initial"] - 1
paper_html = (f"<b>{pp['equity']:.2f}</b> (start {pp['initial']:.0f}, {pp['start'][:10]}"
f"{pp['last'][:10]}, {days}g) &nbsp; ret <b>{ret*100:+.2f}%</b> &nbsp; maxDD {pp['max_dd']*100:.1f}%")
else:
paper_html = "non inizializzato (gira <code>paper_portfolio.py</code>)"
sh = d.get("shadow")
if sh and "error" not in sh:
bits = " &nbsp;·&nbsp; ".join(
f"{a['asset']} <b>{'FLAT' if abs(a['target'])<1e-9 else 'LONG' if a['target']>0 else 'SHORT'}</b> "
f"{a['target']:+.2f}x" for a in sh["assets"])
if sh.get("online"):
eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?")
pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"])
ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"])
if sh.get("orders") else "nessuno (target flat / gia' a target)")
shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {pos} · dato {sh['last_data']}<br>"
f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}")
else:
shadow_html = (f"conto reale non leggibile dal container (token solo su host) · dato {sh['last_data']}<br>"
f"TP01 target: {bits}<br>→ per gli ordini reali: <code>uv run python scripts/live/live_trend.py</code> (host)")
else:
shadow_html = "non disponibile" + (f"{sh['error']}" if sh and sh.get('error') else "")
trows = ""
for t in d.get("trades", []):
cls = "g" if t["action"] == "ENTRY" else "r"
trows += (f"<tr><td>{t['date']}</td><td>{t['asset']}</td>"
f"<td class={cls}>{t['action']}</td>"
f"<td>{t['from_pos']:+.2f}{t['to_pos']:+.2f}x</td>"
f"<td>${t['price']:,.0f}</td></tr>")
if not trows:
trows = "<tr><td colspan=5 style='color:#8a93a0'>nessun trade ancora (TP01 flat / in cash)</td></tr>"
live_trows = ""
for x in ((sh.get("live_trades") if sh and "error" not in sh else None) or []):
dcls = "g" if x["direction"] == "BUY" else "r"
when = str(pd.Timestamp(x["ts"], unit="ms", tz="UTC"))[:16] if x["ts"] else ""
sym = x["instrument"].replace("_USDC-PERPETUAL", "").replace("-PERPETUAL", "")
live_trows += (f"<tr><td>{when}</td><td>{sym}</td><td class={dcls}>{x['direction']}</td>"
f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>")
if not live_trows:
live_trows = "<tr><td colspan=6 style='color:#8a93a0'>nessun trade reale eseguito (o conto non leggibile dal container)</td></tr>"
return f"""<!doctype html><html><head><meta charset=utf-8>
<meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title>
<style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}}
h1{{font-size:20px;margin:0 0 2px}}.sub{{color:#8a93a0;font-size:13px;margin-bottom:18px}}
.cards{{display:flex;gap:14px;flex-wrap:wrap;margin-bottom:18px}}
.card{{background:#161b22;border:1px solid #222b36;border-radius:10px;padding:14px 18px;min-width:150px}}
.card .k{{color:#8a93a0;font-size:12px}}.card .v{{font-size:24px;font-weight:600}}.g{{color:#2ecc71}}.r{{color:#e74c3c}}
table{{width:100%;border-collapse:collapse;margin:8px 0 20px}}td,th{{text-align:left;padding:7px 10px;border-bottom:1px solid #222b36;font-size:14px}}
th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;border:1px solid #222b36;border-radius:6px;padding:3px 8px;margin:2px;font-size:12px}}
.box{{background:#161b22;border:1px solid #222b36;border-radius:10px;padding:14px 18px;margin-bottom:18px}}
.warn{{color:#f1c40f;font-size:12px}}
.section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}}
.section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body>
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01)</h1>
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)</div>
<div class="section">PAPER — simulato (backtest + forward virtuale)</div>
<div class=cards>
<div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div>
<div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div>
<div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div>
<div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div>
<div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div>
</div>
<div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div>
<div class=box><b>Paper forward-only:</b> {paper_html}</div>
<h3 style="font-size:14px;color:#8a93a0">Sleeve</h3>
<table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table>
<h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3>
<table>{pos}</table>
<h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale, ultimi 15)</h3>
<table><tr><th>data</th><th>asset</th><th>azione</th><th>posizione</th><th>prezzo</th></tr>{trows}</table>
<div style="margin-top:10px">{yrs}</div>
<div class="section live">LIVE — Deribit mainnet (conto reale, sola lettura)</div>
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
<p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). TP01 e' l'unico deployable pieno: lo "Shadow live" mostra cosa farebbe sul mainnet, ma NON invia ordini.</p>
</body></html>"""
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_GET(self):
if self.path not in ("/", "/index.html"):
self.send_response(404); self.end_headers(); return
try:
body = html().encode()
except Exception as e:
body = f"<pre>errore: {type(e).__name__}: {e}</pre>".encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.end_headers(); self.wfile.write(body)
def main():
port = 8787
if "--port" in sys.argv:
port = int(sys.argv[sys.argv.index("--port") + 1])
print(f"dashboard su :{port} (Ctrl-C per uscire)")
ThreadingHTTPServer(("0.0.0.0", port), H).serve_forever()
if __name__ == "__main__":
main()
-167
View File
@@ -1,167 +0,0 @@
"""Accesso Deribit MAINNET in SOLA LETTURA (via Cerbero MCP) + costruttore ordini deterministico.
Serve lo SHADOW MODE di TP01 (`scripts/live/live_trend.py`): legge prezzi / conto / posizioni REALI
dal mainnet (token `.env.mainnet`) e costruisce gli ordini di ribilancio **senza inviarli**. Qui NON
esiste alcun metodo di trading — by design: l'unica via per piazzare ordini sara' un modulo separato,
abilitato esplicitamente, dopo la validazione shadow + micro-test.
Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet reale,
e in questa fase solo in lettura. I contratti sono ristretti a BTC/ETH-PERPETUAL (inverse: `amount`
in USD notional, step verificato su Deribit: BTC $10, ETH $1).
"""
from __future__ import annotations
import os
from decimal import Decimal
from pathlib import Path
import requests
PROJECT_ROOT = Path(__file__).resolve().parents[2]
BASE_URL = os.environ.get("CERBERO_BASE_URL", "https://cerbero-mcp.tielogic.xyz")
TIMEOUT = 15
# Inverse perp: amount = USD notional, step in USD, settle = base-coin (BTC/ETH).
# Linear USDC perp: amount = base-coin (BTC/ETH), step in base-coin, settle = USDC (margine USDC).
# NB il conto reale e' USDC -> gli strumenti ESEGUIBILI sono i LINEARI _USDC-PERPETUAL.
_CONTRACT = {
"BTC-PERPETUAL": {"min": 10.0, "step": 10.0, "tick": 0.5, "settle": "BTC"},
"ETH-PERPETUAL": {"min": 1.0, "step": 1.0, "tick": 0.05, "settle": "ETH"},
"BTC_USDC-PERPETUAL": {"min": 0.0001, "step": 0.0001, "tick": 0.5, "settle": "USDC", "linear": True},
"ETH_USDC-PERPETUAL": {"min": 0.001, "step": 0.001, "tick": 0.05, "settle": "USDC", "linear": True},
}
# Il conto reale e' USDC -> mappiamo gli asset sui perp LINEARI USDC (gli unici eseguibili qui).
INSTRUMENT = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"}
# ----------------------------- costruzione ordini (pura, testabile, NIENTE rete) -----------------------------
def _quantize_step(value: float, step: float, mn: float) -> float:
"""Arrotonda al multiplo di `step` (Decimal, niente artefatti float), clampa al minimo."""
n = round(value / step)
return float(max(n * Decimal(str(step)), Decimal(str(mn))))
def notional_to_amount(instrument: str, notional_usd: float, price: float | None = None) -> float:
"""USD notional -> `amount` Deribit, arrotondato allo step e clampato al minimo. Ritorna 0.0 se
sotto mezzo step. INVERSE: amount in USD (price ignorato). LINEAR USDC: amount in base-coin
(units = notional/price -> serve il `price`; senza price ritorna 0.0)."""
spec = _CONTRACT[instrument]
step, mn = spec["step"], spec["min"]
if spec.get("linear"):
if not price:
return 0.0
units = abs(notional_usd) / price
if units < step / 2:
return 0.0
return _quantize_step(units, step, mn)
if abs(notional_usd) < step / 2:
return 0.0
return _quantize_step(abs(notional_usd), step, mn)
def quantize_price(instrument: str, price: float) -> float:
"""Arrotonda il prezzo al tick dello strumento (per gli ordini stop/limit)."""
tick = _CONTRACT[instrument].get("tick")
if not tick or price <= 0:
return price
return float(round(price / tick) * Decimal(str(tick)))
def target_notional_usd(target_fraction: float, weight: float, equity_usd: float) -> float:
"""Notional bersaglio (USD) di un asset = peso nel book * frazione-di-equity TP01 * equity.
Coerente col paper trader (esposizione asset = WEIGHT * target * equity)."""
return weight * target_fraction * equity_usd
def build_rebalance_order(instrument: str, target_fraction: float, weight: float,
equity_usd: float, current_pos_usd: float, price: float | None = None) -> dict | None:
"""COSTRUISCE (non invia) l'ordine di ribilancio verso il target. Ritorna un dict-ordine o None
se sotto-soglia. Long-only TP01 -> target_notional >= 0; delta = target - posizione corrente.
`price` (mark) serve a convertire il notional in base-coin per gli strumenti LINEARI USDC."""
tgt = target_notional_usd(target_fraction, weight, equity_usd)
delta = tgt - current_pos_usd
amount = notional_to_amount(instrument, delta, price=price)
if amount == 0.0:
return None
is_exit = abs(tgt) < 1e-9 and abs(current_pos_usd) > 0
return dict(
instrument=instrument,
side="buy" if delta > 0 else "sell",
amount=amount,
type="market",
reduce_only=is_exit,
target_notional=round(tgt, 2),
current_notional=round(current_pos_usd, 2),
delta_notional=round(delta, 2),
)
# ----------------------------- lettura mainnet (Cerbero MCP) — SOLA LETTURA -----------------------------
def _load_mainnet_token() -> tuple[str, str]:
"""Legge CERBERO_TOKEN (mainnet) + bot-tag da .env.mainnet. Il token NON viene mai stampato."""
env: dict[str, str] = {}
for ln in (PROJECT_ROOT / ".env.mainnet").read_text().splitlines():
ln = ln.strip()
if ln and not ln.startswith("#") and "=" in ln:
k, v = ln.split("=", 1)
env[k] = v.strip()
if "CERBERO_TOKEN" not in env:
raise RuntimeError("CERBERO_TOKEN assente in .env.mainnet")
return env["CERBERO_TOKEN"], env.get("CERBERO_BOT_TAG", "pythagoras-shadow")
class DeribitRead:
"""Accesso Deribit mainnet in SOLA LETTURA via Cerbero MCP. Nessun metodo di trading (by design)."""
def __init__(self) -> None:
self._token, self._tag = _load_mainnet_token()
def _post(self, path: str, payload: dict) -> dict | list:
r = requests.post(
f"{BASE_URL}{path}",
headers={"Authorization": f"Bearer {self._token}", "X-Bot-Tag": self._tag,
"Content-Type": "application/json"},
json=payload, timeout=TIMEOUT,
)
r.raise_for_status()
return r.json()
@staticmethod
def _unwrap(resp: dict | list) -> dict | list:
return resp.get("result", resp) if isinstance(resp, dict) else resp
def ticker(self, instrument: str) -> dict:
return self._unwrap(self._post("/mcp-deribit/tools/get_ticker", {"instrument": instrument})) or {}
def mark_price(self, instrument: str) -> float:
t = self.ticker(instrument)
for k in ("mark_price", "index_price", "last_price", "last"):
v = t.get(k)
if v:
return float(v)
raise ValueError(f"prezzo assente nel ticker {instrument}: chiavi={list(t)[:8]}")
def account_summary(self, currency: str) -> dict:
return self._unwrap(self._post("/mcp-deribit/tools/get_account_summary", {"currency": currency})) or {}
def positions(self, currency: str) -> list[dict]:
out = self._unwrap(self._post("/mcp-deribit/tools/get_positions", {"currency": currency}))
if isinstance(out, list):
return out
return out.get("positions", []) if isinstance(out, dict) else []
def position_usd(self, instrument: str) -> float:
"""Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat."""
cur = _CONTRACT[instrument]["settle"]
for p in self.positions(cur):
if p.get("instrument_name") == instrument or p.get("instrument") == instrument:
return float(p.get("size") or p.get("size_currency") or 0.0)
return 0.0
def trade_history(self, instrument: str, limit: int = 20) -> list[dict]:
"""Trade REALMENTE eseguiti sul conto per `instrument` (fonte autorevole fee/fill)."""
out = self._unwrap(self._post("/mcp-deribit/tools/get_trade_history",
{"limit": limit, "instrument_name": instrument}))
return out if isinstance(out, list) else (out.get("trades", []) if isinstance(out, dict) else [])
-151
View File
@@ -1,151 +0,0 @@
"""Esecuzione REALE su Deribit mainnet (via Cerbero MCP) — entrata/uscita verificate.
Estende DeribitRead (sola lettura) coi metodi di trading, con la logica PROVATA dello stack pre-reset
(Old/src/live/execution.py): entrata market verificata (state=='filled' + trade riscontrati, fill/fee
reali, filled_amount autorevole), uscita market reduce_only, disaster-bracket STOP_MARKET reduce_only.
GUARDRAIL: solo strumenti in ALLOWED; cap di size SOLO sulle APERTURE (MAX_AMOUNT). Le CHIUSURE si
tentano SEMPRE senza cap (principio di sicurezza di Old: si deve poter uscire da qualunque posizione).
Nessun parametro di leva (Deribit non la accetta per-ordine: l'esposizione la decide la SIZE).
⚠️ INVIA ORDINI REALI CON SOLDI VERI. Finestra d'uso attuale: micro-test (scripts/live/microtest.py).
Il deploy pieno di TP01 resta gated finche' il percorso live non e' abilitato esplicitamente.
"""
from __future__ import annotations
from dataclasses import dataclass
from src.live.deribit import DeribitRead, notional_to_amount, quantize_price
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger
# (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello
# lo decide config/live.json max_notional_per_asset_usd). Il micro-test invia comunque size fissa minima.
ALLOWED = {"BTC_USDC-PERPETUAL", "ETH_USDC-PERPETUAL"}
MAX_AMOUNT = {"BTC_USDC-PERPETUAL": 0.01, "ETH_USDC-PERPETUAL": 0.25}
FLAT_USD = 1.0 # |notional| < $1 = posizione considerata flat
class GuardrailError(RuntimeError):
pass
@dataclass
class Fill:
"""Esito verificato di un ordine reale."""
instrument: str
side: str
amount: float # richiesto (base-coin)
filled: float # realmente fillato (order.filled_amount, autorevole)
price: float | None # prezzo medio di fill
fee_usdc: float # fee reale (lineare USDC: gia' in USDC)
order_id: str | None
state: str | None
verified: bool
notes: str = ""
def _avg_price(order: dict, trades: list[dict]) -> float | None:
tr = [t for t in trades if t.get("price") and t.get("amount")]
if tr:
amt = sum(float(t["amount"]) for t in tr)
return (sum(float(t["price"]) * float(t["amount"]) for t in tr) / amt) if amt else None
return float(order.get("average_price") or 0) or None
class DeribitTrader(DeribitRead):
"""Trading minimo e verificato. Apre/chiude solo entro i guardrail; le chiusure sempre."""
def _submit(self, instrument: str, side: str, amount: float, *, reduce_only: bool,
label: str, order_type: str = "market", price: float | None = None) -> Fill:
if instrument not in ALLOWED:
raise GuardrailError(f"strumento non consentito: {instrument}")
if side not in ("buy", "sell"):
raise GuardrailError(f"side non valido: {side}")
if not reduce_only: # cap SOLO sulle aperture; le chiusure si tentano sempre
cap = MAX_AMOUNT.get(instrument, 0.0)
if amount <= 0 or amount > cap:
raise GuardrailError(f"size {amount} fuori dal cap apertura (0, {cap}]")
if amount <= 0:
return Fill(instrument, side, amount, 0.0, None, 0.0, None, None, False, "amount<=0")
payload = {"instrument_name": instrument, "side": side, "amount": amount,
"type": order_type, "label": label}
if price is not None:
payload["price"] = price
if reduce_only:
payload["reduce_only"] = True
resp = self._unwrap(self._post("/mcp-deribit/tools/place_order", payload)) or {}
if not isinstance(resp, dict) or resp.get("error") or resp.get("state") == "error":
err = resp.get("error") if isinstance(resp, dict) else resp
return Fill(instrument, side, amount, 0.0, None, 0.0, None, "error", False,
notes=f"place_order error: {err}")
order = resp.get("order", resp) or {}
trades = resp.get("trades", []) or []
order_id = order.get("order_id")
state = order.get("order_state")
price_f = _avg_price(order, trades)
fee_usdc = sum(float(t.get("fee", 0) or 0) for t in trades) # lineare USDC: fee gia' in USDC
filled = float(order.get("filled_amount") or 0) or sum(float(t.get("amount", 0) or 0) for t in trades)
if order_type == "market":
verified = (state == "filled") and bool(trades)
elif order_type == "stop_market":
verified = state in ("untriggered", "open", "filled")
else:
verified = state in ("open", "filled")
notes = "" if verified else f"non verificato (state={state}, trades={len(trades)})"
if verified and order_type == "market" and filled < amount - 1e-12:
notes = f"FILL PARZIALE: {filled} su {amount}"
return Fill(instrument, side, amount, filled, price_f, fee_usdc, order_id, state, verified, notes)
# --- ENTRATA ---
def open(self, instrument: str, side: str, amount: float, label: str = "tp01-open") -> Fill:
"""Apre a market (NON reduce_only), entro il cap. Verifica il fill reale."""
return self._submit(instrument, side, amount, reduce_only=False, label=label)
# --- USCITA (sempre permessa) ---
def close(self, instrument: str, label: str = "tp01-close") -> Fill | None:
"""Chiude la posizione a market reduce_only. Legge la size reale (USD notional), la converte
in base-coin col mark, e flatta. None se gia' flat. Senza cap: si esce sempre."""
pos_usd = self.position_usd(instrument)
if abs(pos_usd) < FLAT_USD:
return None
mark = self.mark_price(instrument)
amount = notional_to_amount(instrument, abs(pos_usd), price=mark)
side = "sell" if pos_usd > 0 else "buy"
return self._submit(instrument, side, amount, reduce_only=True, label=label)
# --- RIBILANCIO al target (long-only TP01): apre / riduce / chiude ---
def rebalance_to(self, instrument: str, target_notional_usd: float, mark: float,
min_usd: float = 5.0) -> list[Fill]:
"""Porta la posizione su `instrument` al target (USD notional). Long-only: target>=0.
- delta < min_usd -> niente (gia' a target);
- target ~0 & posizione -> close() (uscita piena, reduce_only);
- delta > 0 -> open buy (aumenta);
- delta < 0 (resta long) -> sell reduce_only del delta (riduce).
Ritorna i Fill eseguiti."""
cur = self.position_usd(instrument)
delta = target_notional_usd - cur
if abs(delta) < min_usd:
return []
if target_notional_usd < FLAT_USD and cur > FLAT_USD:
f = self.close(instrument, label="tp01-exit")
return [f] if f else []
amount = notional_to_amount(instrument, abs(delta), price=mark)
if amount <= 0:
return []
if delta > 0:
return [self.open(instrument, "buy", amount, label="tp01-buy")]
return [self._submit(instrument, "sell", amount, reduce_only=True, label="tp01-reduce")]
# --- DISASTER BRACKET (assicurazione on-book per outage; da Old) ---
def place_disaster_sl(self, instrument: str, side_held: str, amount: float,
stop_price: float, label: str = "disaster-sl") -> Fill:
"""STOP_MARKET reduce_only LONTANO (~-30%): in operativita' normale non scatta (l'exit della
strategia esce prima) -> 0 costo Sharpe; copre gli outage del runner. Trigger sul mark."""
opp = "sell" if side_held == "buy" else "buy"
return self._submit(instrument, opp, amount, reduce_only=True, label=label,
order_type="stop_market", price=quantize_price(instrument, stop_price))
# trade_history e' ereditato da DeribitRead (read-only)
-165
View File
@@ -1,165 +0,0 @@
"""Stato SHADOW di TP01 (Deribit mainnet, SOLA LETTURA): target causali + conto/posizioni REALI +
ordini di ribilancio COSTRUITI (mai inviati). Modulo condiviso da `scripts/live/live_trend.py` (CLI)
e dalla dashboard, cosi' i due non divergono. Robusto ai fallimenti di rete: degrada a offline/flat
senza sollevare eccezioni (la dashboard non deve crashare se il mainnet non risponde)."""
from __future__ import annotations
import json
from pathlib import Path
import numpy as np
import pandas as pd
from src.backtest.harness import load
from src.live.deribit import INSTRUMENT, DeribitRead, build_rebalance_order
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d
PROJECT_ROOT = Path(__file__).resolve().parents[2]
ASSETS = ["BTC", "ETH"]
WEIGHT = 0.5
FALLBACK_CAPITAL = 2000.0
PAPER_STATE = PROJECT_ROOT / "data" / "paper_trend" / "state.json"
def tp01_trades(limit: int = 15) -> list[dict]:
"""Lista dei TRADE di TP01 = cambi di posizione (ENTRY long / EXIT flat) dedotti dal segnale
causale `target_series` sui dati certificati. Account-independent (gira anche offline nel
container). Si ricalcola a ogni render -> include sia lo storico sia i trade forward man mano
che arrivano. Ritorna gli ultimi `limit` (piu' recenti primi)."""
tp = TrendPortfolio(**CANONICAL)
out: list[dict] = []
for a in ASSETS:
df = resample_1d(load(a, "1h"))
c = df["close"].to_numpy(dtype=float)
dt = pd.to_datetime(df["datetime"]).to_numpy()
tgt = tp.target_series(df)
prev = 0.0
for i in range(len(tgt)):
if np.sign(tgt[i]) != np.sign(prev):
out.append(dict(
ts=int(pd.Timestamp(dt[i]).value // 10**6),
date=str(pd.Timestamp(dt[i]).date()),
asset=a,
action="ENTRY" if tgt[i] != 0 else "EXIT",
from_pos=round(float(prev), 3),
to_pos=round(float(tgt[i]), 3),
price=round(float(c[i]), 1),
))
prev = tgt[i]
out.sort(key=lambda r: r["ts"], reverse=True)
return out[:limit]
def _safe_client() -> DeribitRead | None:
try:
return DeribitRead()
except Exception:
return None
def _marks(client, dfs):
marks, src = {}, {}
for a in ASSETS:
if client is None:
marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), "close certificata"
continue
try:
marks[a], src[a] = client.mark_price(INSTRUMENT[a]), "mainnet"
except Exception as e:
marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), f"fallback close ({type(e).__name__})"
return marks, src
def _positions(client):
if client is None:
return {a: 0.0 for a in ASSETS}, "offline -> assunto flat"
pos, note = {}, "mainnet"
for a in ASSETS:
try:
pos[a] = client.position_usd(INSTRUMENT[a])
except Exception as e:
pos[a], note = 0.0, f"read fallito ({type(e).__name__}) -> assunto flat"
return pos, note
def _equity(client, marks):
if client is None:
return None, "offline"
try:
eq = float(client.account_summary("USDC").get("equity") or 0)
if eq > 1:
return eq, "mainnet USDC"
except Exception:
pass
tot, any_ok = 0.0, False
for a in ASSETS:
try:
eq = float(client.account_summary(a).get("equity") or 0)
tot += eq * marks[a]
any_ok = True
except Exception:
pass
if any_ok and tot > 1:
return tot, "mainnet coin-margined"
return None, "conto flat / non finanziato"
def shadow_report(offline: bool = False, equity_override: float | None = None) -> dict:
"""Calcola lo stato shadow completo. NON invia nulla. Ritorna un dict serializzabile."""
dfs = {a: resample_1d(load(a, "1h")) for a in ASSETS}
tp = TrendPortfolio(**CANONICAL)
targets = {a: tp.current_target(dfs[a]) for a in ASSETS}
last_ts = min(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS)
client = None if offline else _safe_client()
marks, marks_src = _marks(client, dfs)
positions, pos_src = _positions(client)
real_eq, eq_src = _equity(client, marks)
paper = json.loads(PAPER_STATE.read_text()) if PAPER_STATE.exists() else None
paper_cap = float(paper["capital"]) if paper else FALLBACK_CAPITAL
paper_pos = paper.get("positions") if paper else None
paper_ts = int(paper["last_ts"]) if paper else 0
equity = equity_override if equity_override is not None else (real_eq if real_eq else paper_cap)
eq_basis = ("override" if equity_override is not None
else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)")
assets, orders = [], []
for a in ASSETS:
inst = INSTRUMENT[a]
order = build_rebalance_order(inst, targets[a], WEIGHT, equity, positions[a], price=marks[a])
if order:
orders.append(order)
parity = None
if paper_pos is not None:
parity = abs(float(paper_pos.get(a, 0.0)) - targets[a]) < 1e-9
assets.append(dict(
asset=a, instrument=inst, target=targets[a],
target_notional=WEIGHT * targets[a] * equity,
position_usd=positions[a], mark=marks[a], mark_src=marks_src[a],
order=order, paper=(float(paper_pos.get(a, 0.0)) if paper_pos else None), parity=parity,
))
live_trades = []
if client is not None:
for a in ASSETS:
try:
for tr in client.trade_history(INSTRUMENT[a], limit=8):
live_trades.append(dict(
ts=int(tr.get("timestamp") or 0), instrument=INSTRUMENT[a],
direction=(tr.get("direction") or "").upper(),
amount=float(tr.get("amount") or 0), price=float(tr.get("price") or 0),
fee=float(tr.get("fee") or 0)))
except Exception:
pass
live_trades.sort(key=lambda r: r["ts"], reverse=True)
live_trades = live_trades[:12]
return dict(
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
online=(client is not None and marks_src.get("BTC") == "mainnet"),
real_equity=real_eq, equity=equity, eq_basis=eq_basis,
pos_src=pos_src, assets=assets, orders=orders, live_trades=live_trades,
flat=all(abs(targets[a]) < 1e-9 for a in ASSETS),
paper_aligned=(paper_ts == last_ts),
)
-9
View File
@@ -1,9 +0,0 @@
"""Portafoglio di strategie (estensibile) — v2.0.0.
Un portafoglio aggrega N SLEEVE indipendenti, ognuno = una strategia validata che produce una
serie di rendimenti netti CAUSALE e netto-fee. Gli sleeve si combinano per peso su una griglia
GIORNALIERA comune (grid unica per mixare TF diversi). Vedi src.portfolio.portfolio + sleeves.
"""
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily
__all__ = ["Sleeve", "StrategyPortfolio", "to_daily"]
-120
View File
@@ -1,120 +0,0 @@
"""PORTAFOGLIO DI STRATEGIE — contenitore estensibile (v2.0.0).
Modello: ogni SLEEVE produce una serie di rendimenti netti per-barra (datetime-indexed, CAUSALE,
netto fee). Il portafoglio:
1. porta ogni sleeve su una griglia GIORNALIERA comune (compounding intra-giorno) — così sleeve
a TF diversi (1d, 1h, ...) si combinano in modo coerente;
2. combina per PESO (rinormalizzato a 1) sui giorni comuni a tutti gli sleeve;
3. = portafoglio equal-capital-by-weight ribilanciato di continuo (interpretazione del weighted-
return combine). Equity = capitale · Π(1+combo).
AGGIUNGERE uno sleeve è una riga in src/portfolio/sleeves.py (vedi lì il template).
Metriche oneste: FULL + HOLD-OUT 2025-26 (bloccato) + per-anno, e standalone per-sleeve.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Callable
import numpy as np
import pandas as pd
DAYS_PER_YEAR = 365.25
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
def to_daily(net: pd.Series) -> pd.Series:
"""Compound una serie di rendimenti netti per-barra a GIORNALIERA (griglia comune del portafoglio)."""
s = net.dropna().sort_index()
if not isinstance(s.index, pd.DatetimeIndex):
s.index = pd.to_datetime(s.index, utc=True)
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return ((1.0 + s).resample("1D").prod() - 1.0).dropna()
@dataclass
class Sleeve:
"""Una strategia nel portafoglio. daily_fn() -> serie rendimenti netti per-barra (causale, netto fee).
pos_fn() (opzionale) -> dict posizioni-bersaglio correnti, per introspezione live."""
name: str
weight: float
daily_fn: Callable[[], pd.Series]
pos_fn: Callable[[], dict] | None = None
_cache: pd.Series | None = field(default=None, repr=False, compare=False)
def daily(self) -> pd.Series:
if self._cache is None:
self._cache = to_daily(self.daily_fn())
return self._cache
def metrics(daily: pd.Series) -> dict:
r = np.asarray(daily.dropna().values, float)
if len(r) < 2 or r.std() == 0:
return dict(sharpe=0.0, cagr=0.0, maxdd=0.0, ret=0.0, n=int(len(r)))
eq = np.cumprod(1.0 + r)
pk = np.maximum.accumulate(eq)
years = len(r) / DAYS_PER_YEAR
return dict(sharpe=float(r.mean() / r.std() * np.sqrt(DAYS_PER_YEAR)),
cagr=float(eq[-1] ** (1 / years) - 1) if years > 0 and eq[-1] > 0 else 0.0,
maxdd=float(np.max((pk - eq) / pk)), ret=float(eq[-1] - 1), n=int(len(r)))
def yearly(daily: pd.Series) -> dict:
out = {}
for y, g in daily.groupby(daily.index.year):
v = g.values
eq = np.cumprod(1 + v); pk = np.maximum.accumulate(eq)
out[int(y)] = dict(ret=float(eq[-1] - 1), dd=float(np.max((pk - eq) / pk)))
return out
class StrategyPortfolio:
def __init__(self, sleeves: list[Sleeve], capital: float = 2000.0):
if not sleeves:
raise ValueError("portafoglio vuoto: serve almeno uno sleeve")
self.sleeves = sleeves
self.capital = capital
def weights(self) -> dict:
tot = sum(s.weight for s in self.sleeves)
if tot <= 0:
raise ValueError("somma pesi non positiva")
return {s.name: s.weight / tot for s in self.sleeves}
def combined_daily(self, lo=None, hi=None) -> pd.Series:
"""Combina gli sleeve per peso. OUTER-join: sleeve con date d'inizio diverse
(es. TP01 dal 2019, uno nuovo dal 2024) -> ogni giorno i pesi sono RINORMALIZZATI
fra i soli sleeve con dato disponibile (uno sleeve "si attiva" quando parte la sua
storia). Cosi' non si tronca il portafoglio alla finestra comune."""
w = self.weights()
cols = {s.name: s.daily() for s in self.sleeves}
J = pd.concat(cols, axis=1, join="outer").sort_index()
wv = np.array([w[c] for c in J.columns], float)
active = J.notna().values * wv # peso solo dove c'e' dato
rowsum = active.sum(axis=1, keepdims=True)
wnorm = np.divide(active, rowsum, out=np.zeros_like(active), where=rowsum > 0)
combo = pd.Series(np.nansum(np.nan_to_num(J.values) * wnorm, axis=1), index=J.index)
combo = combo[J.notna().any(axis=1).values] # togli i giorni senza alcun dato
if lo is not None:
combo = combo[combo.index >= lo]
if hi is not None:
combo = combo[combo.index < hi]
return combo
def backtest(self) -> dict:
full = self.combined_daily()
return dict(
weights=self.weights(),
full=metrics(full),
holdout=metrics(self.combined_daily(lo=HOLDOUT)),
yearly=yearly(full),
per_sleeve={s.name: dict(weight=self.weights()[s.name],
full=metrics(s.daily()),
holdout=metrics(s.daily()[s.daily().index >= HOLDOUT]))
for s in self.sleeves},
equity=self.capital * np.cumprod(1.0 + full.values),
index=full.index,
)
def current_positions(self) -> dict:
return {s.name: (s.pos_fn() if s.pos_fn else None) for s in self.sleeves}
-218
View File
@@ -1,218 +0,0 @@
"""SLEEVE del portafoglio + REGISTRY degli sleeve attivi.
Per AGGIUNGERE una strategia al portafoglio:
1. Validala col gauntlet onesto (scripts/analysis/research_lab.py + hold-out + cross-asset).
2. Scrivi una funzione `_<nome>_returns() -> pd.Series` che ritorna i suoi rendimenti netti
per-barra (datetime-indexed, CAUSALE, netto fee). Deve passare il guard di causalità.
3. Avvolgila in uno Sleeve(nome, peso, fn[, pos_fn]) e aggiungila a active_sleeves().
Niente sleeve non validati: il portafoglio è solo per edge che reggono il gauntlet.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from src.data.downloader import load_data
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d, simple_returns
from src.portfolio.portfolio import Sleeve
ASSETS = ("BTC", "ETH")
# ----------------------------- TP01 (PORT LF1d) -----------------------------
def _tp01_returns() -> pd.Series:
"""TP01: TSMOM vol-target long-flat, 50/50 BTC+ETH, a 1d (>=12h: vedi nota look-ahead nel modulo).
Rendimenti netti per-barra del portafoglio (causale: posizione decisa a close[i-1], tenuta in i)."""
tp = TrendPortfolio(**CANONICAL)
series = {}
for a in ASSETS:
df = resample_1d(load_data(a, "1h"))
r = simple_returns(df["close"].values.astype(float))
tgt = tp.target_series(df)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
J = pd.concat(series, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
def _tp01_positions() -> dict:
tp = TrendPortfolio(**CANONICAL)
return {a: round(tp.current_target(resample_1d(load_data(a, "1h"))), 4) for a in ASSETS}
def tp01_sleeve(weight: float = 1.0) -> Sleeve:
return Sleeve("TP01_trend_1d", weight, _tp01_returns, pos_fn=_tp01_positions)
# ----------------------------- XS01: Cross-Sectional Momentum (Hyperliquid) -----------------------------
# Universo certificato Hyperliquid (19 alt, 1d, dal 2024) in data/raw/hl_*_1d.parquet
# (fetch+certify: scripts/analysis/fetch_hyperliquid.py). Market-neutral, scorrelato a TP01 (~-0.06).
# CAVEAT ONESTI: storia corta (~2.5 anni, 2024-2026); STAT-MODE (book a 19 gambe market-neutral
# non eseguibile a 2k, serve ~20k); l'edge e' nella DISPERSIONE cross-section (complementare al
# trend di TP01: lavora quando TP01 e' in cash). Validato: scripts/portfolio/xsec_research.py.
import glob as _glob
from pathlib import Path as _Path
# BLEND di lookback (2026-06-19): fonde 30g+90g del momentum cross-sectional (z-score per
# lookback, mediato) come TP01 fonde gli orizzonti -> piu' robusto del singolo L=30: FULL Sh
# 0.80->1.10, DD 21%->14%, corr a TP01 -0.06->-0.12, 100% anni+. Diario 2026-06-19-xsec-blend.md.
# + GATE DI DISPERSIONE (2026-06-19): entra solo se la dispersione cross-section del momentum
# supera il percentile ESPANDENTE causale disp_pct (altrimenti flat: in regime compatto XS e'
# rumore). Plateau robusto p15-p35; a p30: portafoglio FULL 1.50->1.74, HOLD 1.06->1.56.
# Diario 2026-06-19-xsec-dispgate.md.
XS_CFG = dict(lookbacks=(30, 90), H=10, k=5, mode="mom", target_vol=0.20, disp_pct=30, disp_minhist=20)
_HL_DIR = _Path(__file__).resolve().parents[2] / "data" / "raw"
# UNIVERSO ESPLICITO = 19 ALT LIQUIDI MAJOR. NB (2026-06-19): allargare a 52 asset (incluso
# small-cap WIF/JUP/ORDI/PYTH/TAO...) DILUISCE l'edge -> momentum cross-section NEGATIVO sui 52.
# I major sono il sweet spot. NON usare glob-all (i parquet extra certificati servono ad altra
# ricerca, non a XS01). Vedi diario 2026-06-19-xsec-universe-expansion.md.
XS_UNIVERSE = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
"ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]
def _xsec_returns() -> pd.Series:
cols = {}
for sym in XS_UNIVERSE:
p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
if len(cols) < 10:
raise FileNotFoundError("universo Hyperliquid XS01 incompleto: gira scripts/analysis/fetch_hyperliquid.py")
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
px = C.values; n, A = px.shape
lookbacks, H, k, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], XS_CFG["mode"], XS_CFG["target_vol"]
disp_pct = XS_CFG.get("disp_pct", 0); minhist = XS_CFG.get("disp_minhist", 20)
mlb = max(lookbacks)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
W = np.zeros((n, A)); w = np.zeros(A); disp_hist = []
for i in range(n):
if i >= mlb and i % H == 0:
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
disp_i = float(np.mean([r.std() for r in rLs])) # dispersione cross-section del momentum
thr = np.percentile(disp_hist, disp_pct) if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf
if disp_i >= thr: # gate: entra solo in regime disperso
score = np.zeros(A); cnt = 0 # blend: media z-score cross-sectional
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A); lo, hi = order[:k], order[-k:]
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
else:
w = np.zeros(A) # regime compatto -> flat
disp_hist.append(disp_i)
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * (0.001 / 2.0)
s = pd.Series(net, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return pd.Series(s.values * scale, index=C.index)
def xsec_sleeve(weight: float = 0.3) -> Sleeve:
return Sleeve("XS01_xsec_hl", weight, _xsec_returns)
# ----------------------------- VRP01: Options Short-Vol (put credit spread + gate IV-rank) -----------------------------
# Income short-vol settimanale. Idee da FinanceOld/OptionsAgent (Bear Call Spread + gate d'ingresso)
# portate sul VRP: (a) PUT CREDIT SPREAD rischio-definito (vendi put -0.28, compra put -0.10) che
# CAPPA la coda (worst-week -16.6%->-7.4%, DD 33%->14%); (b) GATE IV-RANK>0.30 causale = vendi vol solo
# quando ricca -> ribalta l'HOLD-OUT da -0.25 a +0.28 (e' l'alpha); (c) crash-skip IV-rank>0.90.
# Premio BS su DVOL reale (data/raw/dvol_*.parquet via scripts/research/fetch_dvol.py), payoff sul path
# certificato, fee opzioni Deribit (cap 12.5% del premio). CAVEAT ONESTI: premio MODELLATO su IV-ATM
# (skew non esplicito), book a 1d, f di stress reale non catturato -> e' un LEAD robusto, non deploy
# pieno. Scorrelato a TP01 (~+0.07). Ricerca: scripts/research/options_vrp_v2.py.
# Diario 2026-06-20-financeold-analysis-vrp-v2.md.
from scipy.stats import norm as _norm
VRP_CFG = dict(short_delta=-0.28, long_delta=-0.10, f=1.0, tenor_d=7,
gate_ivr=0.30, crash_skip=0.90, gate_vrp=True, fee_frac=0.125)
def _bs_put(S, K, T, sig):
if T <= 0 or sig <= 0:
return max(K - S, 0.0)
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
return K * _norm.cdf(-(d1 - sig * np.sqrt(T))) - S * _norm.cdf(-d1) # r=0
def _strike_from_delta(S, T, sig, target_delta):
return S * np.exp(0.5 * sig ** 2 * T - (-_norm.ppf(-target_delta)) * sig * np.sqrt(T))
def _vrp_weekly_asset(asset: str) -> pd.Series:
"""Put credit spread settimanale con gate causali. Ritorna rendimenti SETTIMANALI (su collaterale
= strike corto, cash-secured) indicizzati alla data di scadenza. Causale: strike/premio/gate da
dati <= sell-date; payoff a scadenza sui prezzi certificati."""
df = resample_1d(load_data(asset, "1h"))
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
dv = pd.read_parquet(_HL_DIR / f"dvol_{asset.lower()}.parquet")
d = pd.Series(dv["close"].values.astype(float), index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
px = J["px"].values; dvf = J["dvol"].values / 100.0; idx = J.index
n = len(px); cfg = VRP_CFG; tn = cfg["tenor_d"]; T = tn / 365.25
rets = {}
i = 60
while i + tn < n:
S0 = px[i]; sig = dvf[i]
skip = False
if cfg["gate_vrp"] and i >= 31: # VRP>0: DVOL > RV30 causale
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
if (sig - rv) <= 0:
skip = True
if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60:
ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente causale
if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]:
skip = True
if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]:
skip = True
if skip:
rets[idx[i + tn]] = 0.0; i += tn; continue
Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"])
Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"])
net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * cfg["f"]
S1 = px[i + tn]
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
pnl = net_prem - payoff - cfg["fee_frac"] * abs(net_prem)
rets[idx[i + tn]] = pnl / Ks
i += tn
return pd.Series(rets)
def _vrp_combo_returns() -> pd.Series:
"""Sleeve VRP01: book 50/50 BTC+ETH del put credit spread gated, su griglia GIORNALIERA.
Il rendimento settimanale e' piazzato sul giorno di scadenza, 0.0 sugli altri giorni (preserva
lo Sharpe annualizzato senza smoothing): cosi' lo sleeve e' presente ogni giorno (peso costante)."""
rB = _vrp_weekly_asset("BTC"); rE = _vrp_weekly_asset("ETH")
wk = pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index()
if wk.empty:
return wk
days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(), freq="1D", tz="UTC")
daily = pd.Series(0.0, index=days)
daily.loc[wk.index.normalize()] = wk.values # lump settimanale sul giorno scadenza
return daily
def vrp_sleeve(weight: float = 0.20) -> Sleeve:
return Sleeve("VRP01_shortvol", weight, _vrp_combo_returns)
# ----------------------------- REGISTRY -----------------------------
def active_sleeves() -> list[Sleeve]:
"""Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano
quando parte la loro storia). Aggiungere qui SOLO strategie validate col gauntlet."""
return [
tp01_sleeve(weight=0.55), # trend difensivo, BTC/ETH, dal 2019 (l'unico deployable pieno)
xsec_sleeve(weight=0.25), # cross-sectional momentum Hyperliquid, dal 2024 (scorrelato, stat-mode)
vrp_sleeve(weight=0.20), # options short-vol (put credit spread + gate IV-rank), dal 2021 (lead modellato, scorrelato)
]
+17 -19
View File
@@ -4,16 +4,16 @@ Vincitrice della ricerca su dati certificati BTC/ETH (Deribit mainnet). TSMOM mu
(1-3-6 mesi) vol-targeted, portafoglio 50/50 BTC+ETH. Validata onestamente (no look-ahead,
fee 0.10% RT, positiva ogni anno 2019-2026, robusta su griglia e su tutti i timeframe 15m-1d).
Config canonica deployabile (PORT LF1d):
timeframe >=12h (1d RACCOMANDATO), LONG-FLAT (niente short), vol-target 20%, leverage cap 2x.
-> FULL Sharpe ~1.30, maxDD ~14%, HOLD-OUT 2025-26 Sharpe ~0.31 (calcolo per-TF leak-free).
Config canonica deployabile (PORT LF12h):
timeframe 12h, LONG-FLAT (niente short), vol-target 20%, leverage cap 2x.
-> CAGR ~16.2%, Sharpe ~1.32, maxDD ~13.3% (backtest 2019-2026 su 50/50 BTC+ETH).
NB LOOK-AHEAD (2026-06-19): un ffill MIXED-TIMEFRAME su barre open-labeled (label="left")
gonfiava il 4h (~1.60 -> reale ~1.1). Il calcolo per-SINGOLO-TF e' leak-free (guard
prefix-recompute), ma sotto le 12h costi+overfitting dominano SENZA vantaggio reale (FULL Sh
piatto ~1.3 da 12h a 4h; hold-out MIGLIORE a 1d). -> NON scendere sotto le 12h; deploy a 1d.
TP01 e' DIFENSIVA (taglia il DD ~6x vs buy&hold), NON alpha. Vedi
docs/diary/2026-06-19-tp01-lookahead-fix-lf.md e scripts/analysis/tp01_lowfreq.py.
Perche' >=12h (AGGIORNATO 2026-06-19): l'audit anti-look-ahead (scripts/research/
trackD_lookahead_audit.py) mostra che il pipeline e' pulito (label-invariante, robusto a +1
barra di lag), ma SOTTO le 12h costi e overfitting al rumore ad alta frequenza dominano (il
piccolo extra di Sharpe a 4h/6h/8h non e' affidabile). A 12h/1d il risultato e' ~identico e
robusto -> si deploya a 12h. Perche' long-flat: gli short del trend rendono meno e aggiungono
DD. Vedi docs/diary/2026-06-19-research-synthesis.md e scripts/research/trackD_*.py.
API (tutto causale, decide con dati <= close[i]):
from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL
@@ -171,10 +171,13 @@ def _bars_per_year(idx: pd.DatetimeIndex) -> float:
return 86400 * 365.25 / dt if dt and dt > 0 else 365.25
def resample_tf(df_1h: pd.DataFrame, rule: str) -> pd.DataFrame:
"""Resample 1h -> rule (confini 00:00 UTC). Schema con 'datetime'.
NB: usare SOLO per-singolo-TF (qui leak-free); MAI ffill/combine mixed-TF su questi
timestamp open-labeled (label='left') -> look-ahead. Deploy a >=12h (vedi docstring modulo)."""
DEPLOY_TF = "12h" # timeframe deployabile (>=12h: sotto, costi/overfit dominano)
def resample_tf(df_1h: pd.DataFrame, rule: str = "12h") -> pd.DataFrame:
"""Resample 1h -> rule (confini 00:00 UTC, open-labeled). Schema con 'datetime'.
Il consumo e' index-based con shift +1 barra (net_returns) -> il labeling NON leakka
(verificato in trackD_lookahead_audit.py: Sharpe left == Sharpe right)."""
g = df_1h.copy()
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
idx.name = "dt"
@@ -188,11 +191,6 @@ def resample_tf(df_1h: pd.DataFrame, rule: str) -> pd.DataFrame:
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume", "datetime"]]
def resample_1d(df_1h: pd.DataFrame) -> pd.DataFrame:
"""TF canonico di deploy (>=12h). Resample 1h -> 1d."""
return resample_tf(df_1h, "1D")
def resample_4h(df_1h: pd.DataFrame) -> pd.DataFrame:
"""DEPRECATO per il deploy (sotto le 12h: costi+overfit dominano). Retro-compat ricerca."""
"""Compat per gli script di ricerca. Per il DEPLOY usare resample_tf(df, '12h')."""
return resample_tf(df_1h, "4h")
-84
View File
@@ -1,84 +0,0 @@
"""Test deterministici dello SHADOW/esecuzione TP01 (src/live/deribit.py).
Coprono la logica a rischio zero che NON tocca la rete: quantizzazione notional->contratti (INVERSE
e LINEARE USDC), sizing target, costruzione ordine di ribilancio (buy/sell/exit/None), e PARITA' col
backtest. Il conto reale e' USDC -> il path principale e' il LINEARE BTC_USDC-PERPETUAL. Il fill reale
(slippage/fee) NON e' qui: si valida solo col micro-test mainnet.
"""
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.deribit import (build_rebalance_order, notional_to_amount, target_notional_usd)
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d
LIN = "BTC_USDC-PERPETUAL" # lineare USDC: amount in BTC, step 0.0001 (path reale del conto)
PX = 64000.0
def test_notional_linear_usdc():
# amount in base-coin = notional/price, quantizzato a 0.0001, clamp al minimo
assert notional_to_amount(LIN, 6.4, price=PX) == 0.0001 # 6.4/64000 = 0.0001
assert notional_to_amount(LIN, 12.8, price=PX) == 0.0002
assert notional_to_amount(LIN, 3.0, price=PX) == 0.0 # < mezzo step ($3.2) -> niente
assert notional_to_amount(LIN, 100, price=None) == 0.0 # lineare senza prezzo -> 0
assert notional_to_amount(LIN, -6.4, price=PX) == 0.0001 # usa il valore assoluto
def test_notional_inverse_still_supported():
# l'helper regge ancora gli inverse (amount in USD), senza prezzo
assert notional_to_amount("BTC-PERPETUAL", 1000) == 1000
assert notional_to_amount("BTC-PERPETUAL", 7) == 10 # clamp al minimo
assert notional_to_amount("BTC-PERPETUAL", 3) == 0.0
def test_no_float_artifacts():
v = notional_to_amount(LIN, 0.0001 * PX * 72, price=PX) # 72 step esatti
assert v == 0.0072 and abs(v - 0.0072) < 1e-12
def test_target_notional_5050_weight():
assert target_notional_usd(1.0, 0.5, 2000) == 1000
assert target_notional_usd(2.0, 0.5, 2000) == 2000 # leva-cap 2x -> piena equity sull'asset
assert target_notional_usd(0.0, 0.5, 2000) == 0.0
def test_build_order_entry_linear():
o = build_rebalance_order(LIN, target_fraction=1.0, weight=0.5,
equity_usd=2000, current_pos_usd=0.0, price=PX)
assert o["side"] == "buy" and o["reduce_only"] is False
assert o["target_notional"] == 1000 and o["delta_notional"] == 1000
assert abs(o["amount"] - 0.0156) < 1e-9 # 1000/64000=0.015625 -> 0.0156
def test_build_order_exit_is_reduce_only():
o = build_rebalance_order(LIN, target_fraction=0.0, weight=0.5,
equity_usd=2000, current_pos_usd=1000.0, price=PX)
assert o["side"] == "sell" and o["reduce_only"] is True and o["amount"] > 0
def test_build_order_already_at_target_is_none():
o = build_rebalance_order(LIN, 1.0, 0.5, 2000, current_pos_usd=1000.0, price=PX)
assert o is None # delta 0 -> nessun ordine
def test_build_order_subthreshold_is_none():
# delta $2 (< mezzo step in notional, $3.2 a 64k) -> niente ordine
o = build_rebalance_order(LIN, 1.0, 0.5, 2000, current_pos_usd=998.0, price=PX)
assert o is None
def test_partial_rebalance_direction():
up = build_rebalance_order(LIN, 1.0, 0.5, 2000, 600.0, price=PX) # compra il delta
dn = build_rebalance_order(LIN, 1.0, 0.5, 2000, 1400.0, price=PX) # vende il delta
assert up["side"] == "buy" and up["delta_notional"] == 400
assert dn["side"] == "sell" and dn["delta_notional"] == -400 and dn["reduce_only"] is False
def test_parity_live_target_equals_backtest():
from src.backtest.harness import load
tp = TrendPortfolio(**CANONICAL)
df = resample_1d(load("BTC", "1h"))
assert tp.current_target(df) == tp.target_series(df)[-1]
-69
View File
@@ -1,69 +0,0 @@
"""Test del contenitore portafoglio estensibile."""
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
import pytest
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics
def _const_sleeve(name, weight, val, n=400):
idx = pd.date_range("2020-01-01", periods=n, freq="1D", tz="UTC")
return Sleeve(name, weight, lambda: pd.Series(val, index=idx))
def test_single_sleeve_equals_itself():
s = _const_sleeve("A", 1.0, 0.001)
pf = StrategyPortfolio([s])
combo = pf.combined_daily()
assert np.allclose(combo.values, s.daily().values)
assert pf.weights() == {"A": 1.0}
def test_weights_normalize():
pf = StrategyPortfolio([_const_sleeve("A", 3.0, 0.001), _const_sleeve("B", 1.0, 0.002)])
w = pf.weights()
assert abs(sum(w.values()) - 1.0) < 1e-12
assert abs(w["A"] - 0.75) < 1e-12 and abs(w["B"] - 0.25) < 1e-12
def test_equal_weight_combine():
a, b = _const_sleeve("A", 1.0, 0.001), _const_sleeve("B", 1.0, 0.003)
pf = StrategyPortfolio([a, b])
combo = pf.combined_daily()
assert np.allclose(combo.values, 0.5 * 0.001 + 0.5 * 0.003) # 0.002
def test_to_daily_compounds_intraday():
# due barre da +1% nello stesso giorno -> +2.01% giornaliero
idx = pd.to_datetime(["2020-01-01T00:00", "2020-01-01T12:00"], utc=True)
d = to_daily(pd.Series([0.01, 0.01], index=idx))
assert len(d) == 1 and abs(d.iloc[0] - (1.01 * 1.01 - 1)) < 1e-12
def test_metrics_basic():
idx = pd.date_range("2020-01-01", periods=730, freq="1D", tz="UTC")
m = metrics(pd.Series(0.0005, index=idx)) # ritorno costante positivo
assert m["ret"] > 0 and m["maxdd"] == 0.0 and m["n"] == 730
def test_outer_join_renormalizes_late_sleeve():
# sleeve con date d'inizio diverse: prima parte A da solo (peso rinormalizzato a 1),
# poi A+B (pesi 0.7/0.3). Il portafoglio NON si tronca alla finestra comune.
idxA = pd.date_range("2020-01-01", periods=120, freq="1D", tz="UTC")
idxB = pd.date_range("2020-02-15", periods=60, freq="1D", tz="UTC")
A = Sleeve("A", 0.7, lambda: pd.Series(0.001, index=idxA))
B = Sleeve("B", 0.3, lambda: pd.Series(0.003, index=idxB))
combo = StrategyPortfolio([A, B]).combined_daily()
assert abs(combo.iloc[0] - 0.001) < 1e-12 # solo A -> 100% A
both = combo[combo.index >= idxB[0]]
assert abs(both.iloc[0] - (0.7 * 0.001 + 0.3 * 0.003)) < 1e-12 # blend rinormalizzato
assert len(combo) == 120 # span completo di A, non tronca
def test_empty_portfolio_raises():
with pytest.raises(ValueError):
StrategyPortfolio([])
+9 -10
View File
@@ -10,16 +10,16 @@ sys.path.insert(0, str(PROJECT_ROOT))
from src.backtest.harness import load
from src.strategies.trend_portfolio import (
TrendPortfolio, CANONICAL, resample_4h, simple_returns, tsmom_blend)
TrendPortfolio, CANONICAL, resample_tf, simple_returns, tsmom_blend)
def _dfs():
return {a: resample_4h(load(a, "1h")) for a in ("BTC", "ETH")}
return {a: resample_tf(load(a, "1h")) for a in ("BTC", "ETH")}
def test_no_lookahead_target_is_causal():
"""target_series[:k] non deve cambiare se aggiungo barre future."""
df = resample_4h(load("BTC", "1h"))
df = resample_tf(load("BTC", "1h"))
tp = TrendPortfolio(**CANONICAL)
full = tp.target_series(df)
k = len(df) - 500
@@ -41,7 +41,7 @@ def test_canonical_backtest_is_profitable_and_robust():
def test_long_only_never_short():
df = resample_4h(load("ETH", "1h"))
df = resample_tf(load("ETH", "1h"))
tp = TrendPortfolio(**CANONICAL) # long_only=True
assert (tp.target_series(df) >= 0).all()
@@ -59,12 +59,11 @@ def test_paper_advance_matches_backtest_slice():
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
# equity sull'ultimo tratto (skip warmup)
tail = combo[-500:]
eq_ref = np.cumprod(1.0 + np.clip(tail, -0.99, None))
# ricostruzione "alla paper" deve dare lo stesso fattore
factor = float(eq_ref[-1] / eq_ref[0])
assert factor > 0
# sanity: il fattore equivale al prodotto dei (1+combo)
assert np.isclose(factor, np.prod(1.0 + np.clip(tail, -0.99, None)) / (1.0), rtol=1e-9)
steps = 1.0 + np.clip(tail, -0.99, None)
eq_ref = np.cumprod(steps)
# il loop paper accumula moltiplicando i (1+net) barra per barra -> stesso prodotto
assert np.isclose(eq_ref[-1], np.prod(steps), rtol=1e-9)
assert eq_ref[-1] > 0
def test_tsmom_blend_range():
-61
View File
@@ -1,61 +0,0 @@
"""Test dello sleeve VRP01 (options short-vol: put credit spread + gate IV-rank)."""
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT))
from src.portfolio.sleeves import (
_bs_put, _strike_from_delta, _vrp_combo_returns, vrp_sleeve, _HL_DIR)
_HAS_DVOL = (_HL_DIR / "dvol_btc.parquet").exists() and (_HL_DIR / "dvol_eth.parquet").exists()
_skip_data = pytest.mark.skipif(not _HAS_DVOL, reason="serve data/raw/dvol_*.parquet (scripts/research/fetch_dvol.py)")
def test_bs_put_monotonic_in_strike():
"""Put piu' ITM (strike piu' alto) vale di piu'."""
S, T, sig = 100.0, 7 / 365.25, 0.6
vals = [_bs_put(S, K, T, sig) for K in (80, 90, 100, 110)]
assert all(b < a for b, a in zip(vals, vals[1:])) # crescente nello strike
def test_strike_from_delta_ordering():
"""La put venduta delta -0.28 ha strike piu' alto (piu' vicino) della comprata -0.10."""
S, T, sig = 100.0, 7 / 365.25, 0.6
Ks = _strike_from_delta(S, T, sig, -0.28)
Kl = _strike_from_delta(S, T, sig, -0.10)
assert Kl < Ks < S # entrambe OTM, long piu' lontana
@_skip_data
def test_sleeve_is_deterministic_and_daily():
a = vrp_sleeve().daily()
b = _vrp_combo_returns()
assert isinstance(a.index, pd.DatetimeIndex) and a.index.tz is not None
assert (a.index.normalize() == a.index).all() # griglia giornaliera
# presente ogni giorno nel suo span (nessun buco) -> peso costante nel portafoglio
full = pd.date_range(a.index.min(), a.index.max(), freq="1D", tz="UTC")
assert len(a) == len(full)
np.testing.assert_array_equal(a.values, vrp_sleeve().daily().values) # deterministico
@_skip_data
def test_gates_reduce_activity():
"""I gate (IV-rank/VRP/crash-skip) devono lasciare flat parte delle settimane: i giorni con
rendimento != 0 sono molto meno del totale (lump settimanale + settimane saltate)."""
s = _vrp_combo_returns()
active = float((s != 0).mean())
assert 0.0 < active < 0.25 # ~1/7 (lump weekly) e meno per i gate
@_skip_data
def test_sleeve_positive_and_capped_tail():
"""Lo sleeve e' profittevole e la coda e' tagliata dal long wing (worst-day moderato)."""
s = _vrp_combo_returns()
nz = s[s != 0]
assert s.sum() > 0 # somma rendimenti positiva
assert nz.min() > -0.15 # defined-risk: nessuna settimana < -15%