research(wave-0702bis): ondata video-claims — Elliott 3/3 e Albimarini 2/2 scartati, mappa capital scaling 2-5k

Sei agenti, nessun sopravvissuto:
- ELL-A range-cycle: rumore (0/24 Bonferroni; nessuna cella weekly regge
  a tutte le 7 ancore). Lezione pandas: resample("7D", origin) IGNORA
  origin -> usare "168h" per le bande d'ancora weekly.
- ELL-B Fibonacci: l'edge apparente e' la POSIZIONE dei livelli, non i
  numeri (null location-matched: pctl 0.39-0.68); confluenza FAIL 4/4.
- ELL-C canale: Donchian travestito (non batte il Donchian equivalente,
  DSR 0.685, IS 1.40 -> HOLD -0.87; target 1.618 = caso; anchor-luck 4h).
- ALB-A diagonale: il condor stessa-scadenza la batte a ogni f; senza
  gate IV-rank tutte le strutture perdono (3a conferma: l'alpha del VRP
  e' il gate); fee-negativa su Deribit a qualsiasi size; 2o caso
  "0-perdite = Sharpe implausibile" dopo CC01.
- ALB-B claims: 82%/PF 5.16/"420%" consistente con zero skill (P=20-45%,
  78.6% delle finestre 6-mesi lo produce); replay con code reali =
  rovina 1998/2002/2020; la diagonale passa il 12-40% della perdita naked.
- Capital scaling 600->2-5k: unico vincolo binding = cap $300/asset
  (a 5k book al 49% del target) -> AL DEPOSITO alzare a equity/2;
  min_order $5 lasciare; XS01 ~20k confermata; aspettativa onesta
  de-luckata 2k ~EUR 0.6-0.8/g, 5k ~EUR 1.4-2/g.

Nessun nuovo sleeve, book live invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-02 23:31:38 +00:00
parent a74cc69583
commit 76120b59c2
8 changed files with 3453 additions and 0 deletions
+28
View File
@@ -248,6 +248,34 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
(conferma indipendente del lead prevday in forward-monitor). Lezione: il timing-luck d'ancora è
multiple-testing che il deflated-Sharpe NON conta (candidato gate futuro `anchor_luck_band`).
Diario `2026-07-02-timing-crt-wave.md`; script `scripts/research/r0702_*.py` (9 file).
- **Ondata 2026-07-02-bis ("video claims": Elliott 3 filoni + Albimarini 2 + capital scaling) — 0 nuovi
sleeve, 0 forward-monitor, 1 azione config pendente sul deposito.** Meccanizzazione onesta di claim da
video didattici: (1) **Elliott range-cycle** (onda1 compressa→onda3 ampia): rumore, 0/24 celle a
Bonferroni, nessuna cella weekly regge a tutte le 7 ancore → SCARTATO. (2) **Confluenza Fibonacci**:
vs null ingenui sembra buona (pctl 0.82-1.00), vs null **location-matched** (Fib±jitter: "0.618 vs
0.58?") crolla a 0.39-0.68 = l'apparente edge è la POSIZIONE dei livelli, non i numeri; confluenza
FAIL 4/4 → SCARTATO (il null location-matched è IL test per ogni claim su livelli "speciali").
(3) **Tecnica del canale Elliott**: Donchian travestito — non batte il Donchian a pari geometria
(corr 0.43-0.53), DSR 0.685, cella in-sample collassa in hold-out (1.40→−0.87), target 1.618 = caso
(5/6 celle), anchor-luck di nuovo (4h banda hold [0.35,1.54], 00:00 la migliore) → SCARTATO.
(4) **Albimarini double-diagonal** (short T + long T+1, deep OTM, via motore DVOL di VRP01): il
condor stessa-scadenza la batte a ogni f (la long T+1 = assicurazione di coda +12/33bps nel tail,
~1bps costo medio, non dominanza); senza gate IV-rank TUTTE le strutture perdono (3ª conferma:
l'alpha del VRP è il gate); su Deribit fee-negativa a QUALSIASI size (fee 8 gambe = 194-221% del
theta); celle deep-OTM 0-perdite/142 trade = 2° caso "Sharpe implausibile" dopo CC01 → gate
`implausible_sharpe` raccomandato con più forza. VRP01 resta superiore su tutta la banda skew →
nessun LEAD. (5) **Audit claims** (28 trade, 82% win, PF 5.16, "420%/anno"): consistente con ZERO
skill (P=20-45%; il 78.6% delle finestre 6-mesi 1996-2026 lo produce); replay con code reali =
rovina 1998/2002/2020 col sizing dichiarato; la diagonale lascia passare il 12-40% della perdita
naked. (6) **Capital scaling 600→2-5k** (`r0702_capital_scaling.py`): l'unico vincolo binding è
`max_notional_per_asset_usd=300` (a 5k il book live girerebbe al 49% del target) → **al deposito
alzare il cap a equity/2** ($1000/$1750/$2500 a 2k/3.5k/5k); min_order $5 da LASCIARE; tranching
K=2 non cablare (blocco feed intraday); opzioni ETH eseguibili da ~2.6k ma la regola no-short-vol-
da-modello non decade col capitale; XS01 ~20k confermata, CC01 fuori per struttura. Aspettativa
onesta col CAGR de-luckato (10-15%): 2k ≈ €0.6-0.8/g, 5k ≈ €1.4-2/g (€50/g resta ≈130k).
⚠️ Lezione pandas: `resample("7D", origin=...)` IGNORA origin (pandas 2.x, solo RuntimeWarning) →
bande d'ancora weekly finte; usare `"168h"`. Diario `2026-07-02-elliott-albimarini-capital.md`;
script `scripts/research/r0702_{ell_*,alb_*,capital_scaling}.py` (6 file).
- **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.
- **Sweep "strategie alternative" (2026-06-20) — 104 ipotesi / 153 agenti / NIENTE di nuovo regge.**
@@ -0,0 +1,168 @@
# 2026-07-02 — Ondata "video claims": Elliott (3 filoni), Albimarini (2), capital scaling 2-5k
**Contesto.** Su richiesta: meccanizzare e testare le claim falsificabili di 4 video didattici
italiani (3 sulle onde di Elliott, 1 ricostruzione della "strategia Albimarini" — double diagonal
a credito su SPX), più l'audit dei muri di scala visto l'annuncio che il conto live verrà portato
a **2.000-5.000 $**. Sei agenti indipendenti, harness altlib, fee 0.10% RT, hold-out 2025-26 mai
usato per selezionare, banda d'ancora obbligatoria (regola dello stesso giorno). Nessun PASS →
nessuno scettico necessario. Nessun file di produzione toccato.
**Scripts:** `scripts/research/r0702_ell_{rangecycle,fibconfluence,channel}.py`,
`r0702_alb_{structure,claims}.py`, `r0702_capital_scaling.py`.
---
## Verdetto in una riga
**Elliott 3/3 SCARTATO** (range-cycle = rumore 0/24 Bonferroni; canale = Donchian travestito che
non batte il Donchian; Fibonacci = effetto posizione, non numeri magici). **Albimarini 2/2
SCARTATO** (la diagonale non domina nemmeno nel modello ed è fee-negativa su Deribit; il track
record dichiarato è consistente con zero skill al 20-45%, e va in rovina in ogni finestra storica
con una coda). **Capital scaling: l'unico vincolo vero a 2-5k è il cap $300/asset** — da alzare a
equity/2 al momento del deposito; tutto il resto si scioglie da solo o resta fuori comunque.
---
## ELL-A — "range-cycle" (onda 1 compressa → onda 3 ampia): RUMORE
Claim di Frost&Prechter/Larry Williams meccanizzato senza conteggio soggettivo: 2 definizioni di
segmento (flip TSMOM, zigzag k·ATR causale), impulso iniziale (N∈{3,5,10} barre, range/ATR
pre-segmento) vs ampiezza del seguito (claim A) e troncatura dell'ultimo terzo (claim B).
BTC/ETH 1d+1w, Spearman con null permutato (2000), Bonferroni m=24.
- **0/24 celle significative** (miglior p raw: A 0.061, B 0.058; Bonferroni 1.00 ovunque, FULL e
pre-2025). 7-9/12 celle col segno "giusto" = moneta su celle correlate.
- **Nessuna cella weekly regge a tutte e 7 le ancore** (p 0.02-0.05 di singole ancore = il
timing-luck d'ancora che da oggi non crediamo più; alcune celle flippano segno tra ancore).
- **Artefatto previsto e smascherato**: normalizzando col range POST-impulso la correlazione
diventa negativa per pura meccanica del denominatore — chi "conferma" il claim così misura
l'artefatto.
- Coerente con le famiglie di compressione già fallite nello sweep 104 (BRK08 NR7, BRK10
BB-squeeze, CMB05, VOL12). Campione strutturalmente piccolo (mediana 78 segmenti daily, ~30
weekly). Strategizzazione correttamente non eseguita (statistica prima della strategia).
⚠️ **LEZIONE PANDAS (gemella di `view("int64")` del 2026-07-01):** `resample("7D",
origin=...)` in pandas 2.x **ignora silenziosamente `origin`** ("D" non è Tick-like, solo un
RuntimeWarning) → tutte le ancore weekly uscivano identiche e la banda d'ancora sarebbe stata una
banda finta di larghezza zero. Serve **`"168h"`**. Da ricordare per ogni futura banda d'ancora
settimanale.
## ELL-B — Confluenza di Fibonacci: EFFETTO POSIZIONE, non numeri magici
Zigzag causale non-repainting (livelli usabili solo dalla conferma del pivot, causality_ok
max_tail_diff=0.0), tocco fresh intrabar, reazione ATR-normalizzata a 5/20 barre, ~24k/14k tocchi
IS a 1h. **Tre null in scala di durezza**: (A) rapporti uniformi casuali, (B) ri-sorteggiati per
swing, **(C) location-matched** (Fib ± jitter 2-8%: "0.618 è speciale vs 0.58/0.66?").
- Vs null A/B i Fib sembrano buoni (pctl 0.82-1.00) — **vs null C crollano a 0.39-0.68 = puro
caso**. L'apparente vantaggio è interamente l'effetto di *posizione* (i rapporti Fib cadono in
zone mediamente decenti del range), non dei numeri di Fibonacci.
- **Confluenza** (2 griglie entro 0.25·ATR): FAIL 4/4 celle — non reagisce più delle confluenze
di rapporti casuali. Break-rate al tocco ~46-50% (moneta); su 1h i Fib veri vengono bucati più
spesso del placebo.
- In assoluto comunque nulla da tradare: +2.6/+5.6 bps a 5 barre (fee = 10 bps), negativo a 20
barre e su 1d. Strategizzazione correttamente saltata da gate a priori.
- **Pattern di test riusabile**: il null location-matched è IL test giusto per ogni claim su
"livelli speciali" (Fib, pivot, round numbers) — separa la qualità della posizione dal numero.
## ELL-C — Tecnica del canale (Ftaonline): DONCHIAN TRAVESTITO
Implementazione onesta completa (zigzag causale, vincolo onda-2>origine, canale 0→min-2 con
parallela dal max-1, entry al close della barra fuori, target 1.618×, stop=invalidazione,
speculare short, 27 celle TF×k×varianti).
- **family_honest boccia**: cella scelta in-sample (1h k4 long, IS 1.40) → hold-out **0.87**;
DSR 0.685 < 0.95; earns_slot=False. Marginale vs TP01: uplift positivo solo 2020-21 =
beta-trend del toro (corr 0.35).
- **Il Donchian banale a pari geometria/frequenza eguaglia o batte il canale a ogni TF**
(corr 0.43-0.53): vincoli d'onda + 1.618 non aggiungono nulla. Il segnale-2 ("onda 5") è
quasi inerte (+0.00-0.07).
- **Target 1.618 = geometria, non magia**: toccato con la frequenza di distanze identiche
piazzate a caso (5/6 celle p>0.18). Claim discriminante "in canale = correttivo":
direzionalmente sensata, mai significativa (p 0.08-0.45); l'uscita dal canale è momentum da
breakout, già in casa.
- **Anchor-luck di nuovo**: la miglior cella 4h fa hold 1.54 all'ancora 00:00, la migliore delle
4 (banda [0.35, 1.54]) — quarta conferma della firma nello stesso giorno.
## ALB-A — Double diagonal "Albimarini" su BTC/ETH: NON DOMINA, niente LEAD
Portata su Deribit (che ha le scadenze giornaliere per la long T+1) col motore DVOL di VRP01
(riprodotto esatto: 1.09/0.59/11.8%), strike scalati in vol (2σ/5g ≈ il "9% su SPY"), gate
IV-rank canonico non riottimizzato, 2021-26 con LUNA/FTX/2022, banda skew f∈{0.6,0.8,1.0,1.3}.
- **Il claim strutturale del corso è falsificato**: gated, l'iron condor stessa-scadenza batte la
diagonale a ogni f (1.05 vs 0.95 a f=1.0), Δ negativo ogni anno. La long T+1 è **assicurazione
di coda quantificata** (+12/+33 bps nei 5% peggiori vs ~1 bps di costo medio), non dominanza.
- **Senza gate tutte le strutture perdono** a f≤1.0 → terza conferma indipendente: nel VRP
l'alpha è il filtro di regime, non la vendita di premio. VRP01 resta superiore su tutta la
banda f (CAGR 8.3% vs 1.4%).
- **Il meccanismo dell'illusione riprodotto in vitro**: celle z≥2.5 = 0 perdite su 142 trade,
Sharpe 5.9-∞ (il punto cieco CC01 in forma opzioni); la best in-sample (7.88) crolla a 0.51
in hold-out. Seconda occorrenza del pattern "0-perdite = Sharpe implausibile" in una settimana
**si rafforza il caso per il gate `implausible_sharpe` in altlib** (raccomandato dal filone
CC01, ora con 2 casi d'uso).
## ALB-B — Audit dei numeri dichiarati (28 trade, 82%, PF 5.16, "420% annuo"): ZERO SKILL
SPY 1d 1996-2026 (parquet già in repo dal filone GTAA) + BTC/ETH certificati; MC 200k; replay
claim-anchored calibrato per riprodurre esattamente il video.
- **L'82% è incoerente col "deep OTM" in entrambe le letture**: strike a 9% → win strutturale
99.5% (P(|move 4td|>9%) = 0.53%, fat-tail 88× la normale); 82% letterale → strike a 2.63% ≈
1.2σ, non deep. In entrambi i casi win-rate = delta venduto.
- **P(track record del video senza edge) = 20-45%**; il **78.6%** di tutte le finestre 6-mesi
1996-2026 lo avrebbe prodotto (lettura deep). Il PF ~5.1 esce per costruzione quando la coda
non è campionata.
- **Replay attraverso le code reali**: rovina nel 1998/2002/2020 col sizing 1→4 dichiarato;
perfino regalando +20% di premio, 1 contratto fisso fa CAGR +4.1% con maxDD 95%. La stessa
semestrale del video annualizza a +73-77%, non 420%. Su BTC/ETH a EV=0: +12%/DD68% e
+0%/DD88%.
- **La diagonale lascia passare il 12-40%** della perdita naked in un 10% overnight (dipende dal
vol-spike). Su Deribit la double diagonal è **fee-negativa a qualsiasi size** (fee 8 gambe =
194-221% del theta raccolto a questi strike): il capitale sposta solo il muro di margine.
- Che cosa distingue VRP01 (sopravvissuto allo stesso tipo di audit): defined-risk per
costruzione, gate IV-rank che ribalta l'hold-out, sizing fisso — l'opposto esatto su tutti e
tre gli assi.
## Capital scaling 600 → 2000/3500/5000 (r0702_capital_scaling.py)
| capitale | TP01 haircut Sh | book investibile % (cap $300) | K=2 exec-turn % | statarb haircut | spread ETH conc. | XS01 exec-turn % |
|---|---|---|---|---|---|---|
| 600 | +0.010 | 100.0% | 90.5 | 0.004 | 0 | 56.9 |
| 2000 | 0.002 | 85.9% | 95.7 | 0.000 | 3 | 87.4 |
| 3500 | 0.002 | 63.7% | 97.5 | 0.000 | 5 | 89.3 |
| 5000 | 0.002 | **48.9%** | 98.2 | 0.000 | 7 | 90.8 |
- **Il vincolo dominante è UNO: `max_notional_per_asset_usd`=300.** A 5k il book girerebbe a
metà del target (cap binding nel 39-46% delle barre). **Proposta config (da applicare AL
deposito): cap = equity/2** → $1000 a 2k, $1750 a 3.5k, $2500 a 5k. `min_order_usd`=5 va
LASCIATO (il fee drag risale solo al tetto noto ~0.4%/anno; l'isteresi artificiale costerebbe
più in lag — lezione ondata timing).
- Min-order/haircut TP01 ≈ 0 a ogni capitale (conferma indipendente del "74% ordini" a 600 →
39-43% saltati a 5k, tutti micro). Statarb: haircut 0 da 2k (il muro era l'edge, non la scala).
- Tranching K=2: matematicamente regge a 5k (98.2% turnover-tranche), ma il blocco vero resta il
feed intraday fuori path certificato + beneficio solo di varianza → **non cablare**.
- Opzioni: il muro d'ingresso ETH cade a ~2.6k (3-7 spread concorrenti a 2-5k, fee solo 5-6% del
credito grazie al cap 12.5%; rettifica vs ALB-A: margine spread ETH oggi $66-76). BTC granulare
da ~$4.7k. **La regola "niente short-vol da modello" non decade col capitale.**
- XS01: soglia ~20k confermata (a 5k il 9%+ del turnover non esegue). CC01: il capitale non è
mai stato il muro (fuori per ragioni strutturali).
- **Aspettativa onesta**: col CAGR de-luckato del book live (10-15%, audit SKH01): **2k ≈
€0.55-0.82/giorno; 5k ≈ €1.37-2.05/giorno**. Il salto di capitale cambia cosa è eseguibile,
non l'ordine di grandezza del ritorno (€50/g ≈ 130k, invariato).
## Lezioni
1. **Il null giusto smonta il livello "speciale"**: location-matched (ELL-B) per i livelli,
Donchian-equivalente (ELL-C) per i pattern, EV=0-calibrato (ALB-B) per i track record. In
tutti e tre i casi il claim sopravviveva ai null ingenui e moriva su quello giusto.
2. **`resample("7D", origin=...)` ignora origin in pandas 2.x** → usare `"168h"` per le bande
d'ancora settimanali.
3. Secondo caso "0-perdite = Sharpe implausibile" (celle deep-OTM di ALB-A, dopo CC01) → il gate
`implausible_sharpe` in altlib sale in coda ai lavori insieme ad `anchor_luck_band`.
4. L'anchor-luck è comparso spontaneamente anche in questa ondata (celle weekly ELL-A, 4h ELL-C)
— la regola della banda d'ancora sta già pagando.
**Stato:** nessun nuovo sleeve, nessun forward-monitor aggiunto, book live INVARIATO. Un'azione
operativa pende sul FUTURO deposito: alzare `max_notional_per_asset_usd` a equity/2 (decisione
utente al momento del funding). Test suite verde. 6 script committati.
+662
View File
@@ -0,0 +1,662 @@
"""r0702_alb_claims — Audit STATISTICO delle claim "strategia Albimarini" (video didattici).
CLAIM DICHIARATE (input, dal video):
- Struttura: double diagonal DEEP OTM su SPX/SPY (short strangle ~6DTE + long strangle
a scadenza successiva, strike piu' lontani), durata trade 3-6 giorni di calendario.
- Track record: n=28 trade, win-rate 82% (23W/5L), P&L medio +$113/trade,
perdite totali $765 (loss medio $153), profit factor 5.16, capitale ~$10k,
estrapolazione "420% annuo", sizing a compounding 1->2->3->4 contratti.
Derivati: totale +$3.164; gross win $3.929 (avg win $171); PF = 3929/765 = 5.14 ok.
OBIETTIVO: QUANTIFICARE la critica (win-rate strutturale, coda non campionata, PF non
significativo su n=28), non riformularla. 6 test, ognuno con numeri.
DATI: SOLO locali gia' certificati/presenti nel progetto:
- data/raw/eq_spy_1d.parquet (SPY daily 1996-2026, fetch IB del filone GTAA)
- BTC/ETH 1d via altlib (Deribit mainnet certificato)
Nessuna rete. Nessun file scritto fuori dallo scratchpad. Nessun DatetimeIndex.view.
Convenzioni oneste:
- 6 giorni di calendario ~= 4 trading day (h=4 primario; h=2..5 come robustezza).
- Le probabilita' empiriche usano finestre rolling OVERLAPPING (stima della prob.
per-finestra); i test di significativita' usano le 28 finestre NON sovrapposte
del track record e cicli non sovrapposti nel replay.
- Il replay del Test 3 e' CLAIM-ANCHORED: usa l'economia per-trade dichiarata dal
video (massimo beneficio al claim); l'unico parametro nostro e' la severita'
della coda, calibrata in due modi (cap BS della diagonale / EV=0).
- BS senza skew nei Test 4-5: sottostima il premio degli OTM (a favore del claim).
Uso: uv run python scripts/research/r0702_alb_claims.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm, t as student_t
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "research" / "alt"))
import altlib as al # noqa: E402
RNG = np.random.default_rng(20260702)
ROOT = Path(__file__).resolve().parents[2]
# ---- claim dichiarate ------------------------------------------------------
N_TRADES = 28
WINRATE_CLAIM = 23 / 28 # 0.821
AVG_PNL = 113.0 # $/trade
TOT_LOSS = 765.0 # $ totali (5 loss -> avg $153)
AVG_WIN = (N_TRADES * AVG_PNL + TOT_LOSS) / 23 # $171
AVG_LOSS = TOT_LOSS / 5 # $153
PF_CLAIM = (23 * AVG_WIN) / TOT_LOSS # 5.14
CAPITAL = 10_000.0
H_TD = 4 # 6 giorni calendario ~ 4 trading day
OUT: dict = {}
def p(msg=""):
print(msg, flush=True)
# ===========================================================================
# BS helpers (no dividendi/tassi — orizzonte 6-13 giorni)
# ===========================================================================
def bs_put(S, K, T, sigma):
T = max(T, 1e-9)
d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
return K * norm.cdf(-d2) - S * norm.cdf(-d1)
def bs_call(S, K, T, sigma):
T = max(T, 1e-9)
d1 = (np.log(S / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
return S * norm.cdf(d1) - K * norm.cdf(d2)
# ===========================================================================
# DATA
# ===========================================================================
def load_spy() -> pd.DataFrame:
df = pd.read_parquet(ROOT / "data" / "raw" / "eq_spy_1d.parquet")
df = df.sort_values("timestamp").reset_index(drop=True)
df["datetime"] = pd.to_datetime(df["timestamp"].astype("int64"), unit="ms", utc=True)
return df
def hmoves(close: np.ndarray, h: int) -> np.ndarray:
return close[h:] / close[:-h] - 1.0
def cal_moves(df: pd.DataFrame, days: int = 6) -> np.ndarray:
"""Move su ESATTAMENTE `days` giorni di calendario (ultimo close <= t+days).
Epoca ms esplicita (mai .view su DatetimeIndex)."""
ts = df["timestamp"].to_numpy(dtype="int64") # ms
c = df["close"].to_numpy(dtype=float)
tgt = ts + days * 86_400_000
j = np.searchsorted(ts, tgt, side="right") - 1 # ultimo close <= t+days
ok = j > np.arange(len(ts))
return c[j[ok]] / c[ok] - 1.0
# ===========================================================================
# TEST 1 — WIN-RATE STRUTTURALE
# ===========================================================================
def test1():
p("=" * 88)
p("TEST 1 — WIN-RATE STRUTTURALE: P(move >9% in ~6 giorni) e strike impliciti dall'82%")
p("=" * 88)
spy = load_spy()
c, lo, hi = spy["close"].values, spy["low"].values, spy["high"].values
r1 = np.diff(np.log(c))
vol_ann = r1.std() * np.sqrt(252)
res = {}
for h in (2, 3, 4, 5):
m = hmoves(c, h)
res[h] = dict(p_abs9=float(np.mean(np.abs(m) > 0.09)),
p_dn9=float(np.mean(m < -0.09)),
sigma=float(m.std()),
p_2sig=float(np.mean(np.abs(m) > 2 * m.std())))
m6c = cal_moves(spy, 6)
m4 = hmoves(c, H_TD)
# touch (management intra-finestra): min low / max high nei prossimi h giorni
n = len(c) - H_TD
lo_w = np.array([lo[i + 1:i + 1 + H_TD].min() for i in range(n)])
hi_w = np.array([hi[i + 1:i + 1 + H_TD].max() for i in range(n)])
p_touch9 = float(np.mean((lo_w < c[:n] * 0.91) | (hi_w > c[:n] * 1.09)))
# analitico: normale a vol 16% e alla vol campione; t-Student fittata sui move 4td
sig6_16 = 0.16 * np.sqrt(6 / 365)
p_norm16 = 2 * (1 - norm.cdf(0.09 / sig6_16))
sig4 = res[4]["sigma"]
p_norm_emp = 2 * (1 - norm.cdf(0.09 / sig4))
tdf, tloc, tscale = student_t.fit(m4)
p_t = float(2 * student_t.sf((0.09 - tloc) / tscale, tdf))
# strike implicito dall'82% di win (quantile 82% di |move|) e win-rate a 9% OTM
q82_4 = float(np.quantile(np.abs(m4), WINRATE_CLAIM))
q82_6c = float(np.quantile(np.abs(m6c), WINRATE_CLAIM))
win_at_9 = 1 - res[4]["p_abs9"]
p(f"SPY 1996-2026 ({len(spy)} barre, vol ann {vol_ann:.1%})")
for h in (2, 3, 4, 5):
r = res[h]
p(f" h={h}td: P(|m|>9%)={r['p_abs9']:.4%} P(m<-9%)={r['p_dn9']:.4%} "
f"sigma={r['sigma']:.2%} P(|m|>2sig)={r['p_2sig']:.2%}")
p(f" 6 giorni CALENDARIO esatti: P(|m|>9%)={np.mean(np.abs(m6c) > 0.09):.4%} "
f"sigma={m6c.std():.2%}")
p(f" TOUCH intra-finestra (h=4td, strike a +-9%): P={p_touch9:.4%}")
p(f" Analitico: normale vol16% -> {p_norm16:.2e} | normale vol camp. -> {p_norm_emp:.2e} | "
f"t-Student fit (df={tdf:.2f}) -> {p_t:.4%} | empirico {res[4]['p_abs9']:.4%}")
p(f" -> fat-tail factor empirico/normale = {res[4]['p_abs9'] / p_norm_emp:,.0f}x")
p()
p(f" INCONSISTENZA QUANTIFICATA delle claim:")
p(f" (a) se gli strike sono DAVVERO ~9% OTM: win-rate strutturale a scadenza = "
f"{win_at_9:.2%} (>=99%), non 82% -> le 5 perdite non sono breach, sono gestione;")
p(f" (b) se il win-rate 82% e' letterale (breach a scadenza): gli strike distano "
f"q82(|m4|) = {q82_4:.2%} (6gg cal: {q82_6c:.2%}) ~= 1.2 sigma, NON 'deep OTM'.")
p(f" In entrambi i casi il win-rate e' il quantile della distribuzione (il delta "
f"venduto), non skill: qualunque venditore agli stessi strike ottiene lo stesso numero.")
# BTC/ETH dai NOSTRI dati certificati
crypto = {}
for a in ("BTC", "ETH"):
d = al.get(a, "1d")
cc = d["close"].values
m6 = hmoves(cc, 6)
crypto[a] = dict(p_abs9=float(np.mean(np.abs(m6) > 0.09)),
sigma=float(m6.std()),
p_2sig=float(np.mean(np.abs(m6) > 2 * m6.std())),
q82=float(np.quantile(np.abs(m6), WINRATE_CLAIM)),
span=f"{d['datetime'].iloc[0].date()}->{d['datetime'].iloc[-1].date()}",
last=float(cc[-1]))
p(f" {a} (certificato, {crypto[a]['span']}): P(|m6d|>9%)={crypto[a]['p_abs9']:.1%} "
f"sigma6d={crypto[a]['sigma']:.1%} P(>2sig)={crypto[a]['p_2sig']:.1%} "
f"strike per win82% = {crypto[a]['q82']:.1%} OTM")
p(f" -> la stessa struttura trasposta su crypto: strike a 9% OTM = win-rate "
f"{1 - crypto['BTC']['p_abs9']:.0%} (BTC) / {1 - crypto['ETH']['p_abs9']:.0%} (ETH), "
f"cioe' il 9% che su SPY e' 'deep' su crypto e' ~1 sigma.")
OUT["test1"] = dict(spy=res, spy_6cal_p9=float(np.mean(np.abs(m6c) > 0.09)),
p_touch9=p_touch9, p_norm16=float(p_norm16),
p_norm_emp=float(p_norm_emp), p_t=p_t, t_df=float(tdf),
q82_4td=q82_4, q82_6cal=q82_6c, win_at_9otm=float(win_at_9),
crypto=crypto)
return spy, m4, q82_4
# ===========================================================================
# TEST 2 — SIGNIFICATIVITA' DEL PF 5.16 SU n=28 (null EV=0)
# ===========================================================================
def test2(spy: pd.DataFrame, q82: float):
p("\n" + "=" * 88)
p("TEST 2 — PF 5.16 su n=28: distribuzione sotto il null 'vendita premio a EV=0 netto'")
p("=" * 88)
c = spy["close"].values
m4 = hmoves(c, H_TD)
NSIM = 200_000
W = 126 # 6 mesi di trading day
def branch(name: str, thr: float) -> dict:
"""Null EV=0: win +$171 (p=0.821), small loss -$153, tail loss -L con
p_tail = P(|m4|>thr) empirica; L calibrata perche' EV=0 netto."""
p_loss = 1 - WINRATE_CLAIM
p_tail = float(np.mean(np.abs(m4) > thr))
p_small = max(p_loss - p_tail, 0.0)
L_tail = (WINRATE_CLAIM * AVG_WIN - p_small * AVG_LOSS) / p_tail
wins = RNG.normal(AVG_WIN, 60, (NSIM, N_TRADES)).clip(20, None)
smalls = RNG.normal(AVG_LOSS, 60, (NSIM, N_TRADES)).clip(30, None)
u = RNG.random((NSIM, N_TRADES))
is_win = u < min(WINRATE_CLAIM, 1 - p_tail)
is_tail = u > 1 - p_tail
pnl = np.where(is_win, wins, np.where(is_tail, -L_tail, -smalls))
gp = np.where(pnl > 0, pnl, 0).sum(1)
gl = -np.where(pnl < 0, pnl, 0).sum(1)
pf = gp / np.maximum(gl, 1e-9)
wr = is_win.mean(1)
p_joint = float(np.mean((pf >= PF_CLAIM) & (wr >= WINRATE_CLAIM - 1e-9)))
p_notail = float(np.mean(~is_tail.any(1)))
tail_day = np.abs(m4) > thr
frac_clean = float(np.mean([not tail_day[i:i + W].any()
for i in range(0, len(tail_day) - W)]))
r = dict(name=name, thr=thr, p_tail=p_tail, p_small=p_small,
L_tail=float(L_tail), ev_check=float(pnl.mean()),
pf_median=float(np.median(pf)),
p_pf_ge=float(np.mean(pf >= PF_CLAIM)), p_joint=p_joint,
p_no_tail_28=p_notail,
p_no_tail_analytic=float((1 - p_tail) ** N_TRADES),
frac_6m_clean=frac_clean)
p(f" Branch {name}:")
p(f" tail = |m4| > {thr:.2%}: P(tail/trade)={p_tail:.2%} -> L_tail per EV=0 = "
f"${L_tail:,.0f} ({L_tail / CAPITAL:.0%} del capitale per unita' di size); "
f"EV MC {pnl.mean():+.1f}$/trade")
p(f" PF su 28 trade: mediana {r['pf_median']:.2f} | P(PF>=5.16)={r['p_pf_ge']:.1%} | "
f"P(PF>=5.16 E win>=82%)={p_joint:.1%}")
p(f" P(zero tail in 28 trade)={p_notail:.1%} (analitico {r['p_no_tail_analytic']:.1%}); "
f"quota storica finestre 6 mesi SENZA tail = {frac_clean:.1%}")
return r
p(f"Null 'venditore di premio a EV=0 netto' (tanti +$171, -$153 gestite, code rare):")
b_obs = branch(f"OBS (strike {q82:.2%} OTM, breach oltre il long +2%)", q82 + 0.02)
b_deep = branch("DEEP (strike 9% OTM come dichiarato)", 0.09)
p(f" -> Il track record dichiarato (PF 5.16, win 82%, zero code) e' un esito da "
f"P={b_obs['p_joint']:.0%} (lettura OBS) a P={b_deep['p_joint']:.0%} (lettura DEEP) "
f"sotto ZERO skill: tra 'comune' e 'esito mediano'.")
p(f" Il PF senza coda campionata misura solo 23/5 * avg_win/avg_smallloss = "
f"{(23 * AVG_WIN) / (5 * AVG_LOSS):.2f}: e' un rapporto STRUTTURALE, non una statistica "
f"di edge (n=28 non tocca mai la coda che paga tutto).")
OUT["test2"] = dict(obs=b_obs, deep=b_deep)
return b_obs["L_tail"]
# ===========================================================================
# TEST 3 — IL "420% ANNUO" ATTRAVERSO UNA CODA REALE (replay claim-anchored)
# ===========================================================================
def _replay(dates, moves, q82: float, width: float, L_tail: float,
win_amt: float, small_amt: float,
sizing: str = "video", start: float = CAPITAL, label: str = ""):
"""Replay CLAIM-ANCHORED sulla sequenza REALE di move (cicli non sovrapposti).
PER CONTRATTO:
|m| <= q82 -> +win_amt (win, calibrato sui numeri del video)
q82 < |m| <= q82+w -> -small_amt (perdita gestita, come le 5 del video)
|m| > q82+w -> -L_tail (breach oltre il long: coda)
sizing: 'video' = +1 contratto ogni +$1000 di profitto (1->2->3->4 come
dichiarato), cap margine equity/$2500; 'prop' = leva costante iniziale
(n = equity//10k); 'fixed' = 1 contratto. Ruin se equity <= 0."""
eq = start
path, pnl_hist, npos = [], [], []
ruined = None
for i, m in enumerate(moves):
if abs(m) <= q82:
out = win_amt
elif abs(m) <= q82 + width:
out = -small_amt
else:
out = -L_tail
if sizing == "video":
n = int(np.clip(1 + max(eq - start, 0) // 1000, 1, max(1, eq // 2500)))
elif sizing == "prop":
n = int(max(1, eq // 10_000))
else:
n = 1
pnl = n * out
eq += pnl
pnl_hist.append(pnl)
npos.append(n)
path.append(eq)
if eq <= 0:
ruined = dates[i]
break
path = np.asarray(path, dtype=float)
peak = np.maximum.accumulate(np.maximum(path, 1e-9))
maxdd = float((path / peak - 1.0).min()) if len(path) else 0.0
last_i = len(path) - 1
yrs = max(1e-9, (pd.Timestamp(dates[last_i]) - pd.Timestamp(dates[0])).days / 365.25)
cagr = float((max(path[-1], 1e-9) / start) ** (1 / yrs) - 1) if len(path) else 0.0
wi = int(np.argmin(pnl_hist)) if pnl_hist else 0
return dict(label=label, final=float(path[-1]) if len(path) else start,
cagr=cagr, maxdd=maxdd,
ruined=str(pd.Timestamp(ruined).date()) if ruined is not None else None,
worst_cycle=float(min(pnl_hist)) if pnl_hist else 0.0,
worst_date=str(pd.Timestamp(dates[wi]).date()) if pnl_hist else None,
n_at_worst=int(npos[wi]) if npos else 0,
n_cycles=len(pnl_hist),
n_tails=int(np.sum(np.abs(np.asarray(moves[:len(pnl_hist)])) > q82 + width)),
avg_pnl_cycle=float(np.mean(pnl_hist)) if pnl_hist else 0.0,
winrate=float(np.mean(np.asarray(pnl_hist) > 0)) if pnl_hist else 0.0,
years=float(yrs))
def _cycles(df: pd.DataFrame, h: int):
c = df["close"].values
idx = np.arange(0, len(c) - h, h)
moves = c[idx + h] / c[idx] - 1.0
dates = df["datetime"].values[idx + h]
return dates, moves
def _calib_win_per_contract() -> float:
"""Calibra il win PER CONTRATTO cosi' che 28 cicli TUTTI win con il sizing
video (+1 contratto/$1000, 1->2->3->4) riproducano il totale dichiarato
(+$3.164): i +$171/+$113 del video sono medie a livello CONTO su 1-4
contratti, non per contratto."""
lo, hi = 10.0, 171.0
for _ in range(60):
w = 0.5 * (lo + hi)
eq, tot = CAPITAL, 0.0
for _k in range(N_TRADES):
n = int(np.clip(1 + max(eq - CAPITAL, 0) // 1000, 1, max(1, eq // 2500)))
eq += n * w
tot += n * w
if tot > N_TRADES * AVG_PNL:
hi = w
else:
lo = w
return round(0.5 * (lo + hi), 1)
def test3(spy: pd.DataFrame, q82: float):
p("\n" + "=" * 88)
p("TEST 3 — '420% ANNUO': la stessa strategia attraverso le code reali, con compounding")
p("=" * 88)
# Replay CLAIM-ANCHORED: economia PER CONTRATTO calibrata sui numeri del video,
# guidata dalla sequenza REALE dei move ~6gg. Unico parametro nostro: la
# severita' della coda per contratto, boxata da due letture convergenti:
# cap fisico = width 2% x notional $60k = $1.200 (cap della diagonale, Test 4)
# EV=0 = la severita' che rende il gioco fair alle p empiriche
w_c = _calib_win_per_contract()
s_c = round(w_c * AVG_LOSS / AVG_WIN, 1)
m4 = hmoves(spy["close"].values, H_TD)
p_tail = float(np.mean(np.abs(m4) > q82 + 0.02))
p_small = max(1 - WINRATE_CLAIM - p_tail, 0.0)
L_ev0 = round((WINRATE_CLAIM * w_c - p_small * s_c) / p_tail, 0)
L_bs = 1200.0
p(f"Replay claim-anchored (cicli 4td non sovrapposti, strike q82={q82:.2%}, long +2%):")
p(f"PER CONTRATTO: win +${w_c} (|m|<=q82), gestita -${s_c} (<=q82+2%), coda -$L")
p(f"(|m|>q82+2%). Calibrazione: 28 cicli senza coda col sizing video 1->4 = +$3.164")
p(f"(riproduce il video PER COSTRUZIONE: win 82%, PF 5.1). Severita' coda boxata:")
p(f"cap fisico width2%x$60k = ${L_bs:,.0f} | EV=0 = ${L_ev0:,.0f} (convergenti).")
p(f"Sizing 'video' = +1 contratto/$1000 profitto (cap eq/$2500); 'prop' = eq//10k.")
windows = [("FULL 1996-2026", None, None),
("2019-2026 (era book)", "2019-01-01", None),
("H2-2023 (finestra tipo video)", "2023-07-01", "2023-12-31"),
("2020 (COVID)", "2020-01-01", "2020-12-31"),
("2022 (bear)", "2022-01-01", "2022-12-31"),
("2008 (GFC)", "2008-01-01", "2008-12-31")]
rows = []
for sizing, L, tag in (("video", L_bs, "video, cap fisico $1.200"),
("video", L_ev0, f"video, EV=0 ${L_ev0:,.0f}"),
("prop", L_bs, "leva costante, cap fisico")):
p(f"\n -- sizing {tag} --")
for label, a, b in windows:
d = spy
if a:
d = d[d["datetime"] >= pd.Timestamp(a, tz="UTC")]
if b:
d = d[d["datetime"] <= pd.Timestamp(b, tz="UTC")]
dates, moves = _cycles(d.reset_index(drop=True), H_TD)
out = _replay(dates, moves, q82, 0.02, L, w_c, s_c, sizing=sizing,
label=f"{label} [{tag}]")
rows.append(out)
ann = f"{out['cagr']:+.0%}" if not out["ruined"] else "RUIN"
p(f" {label:30s} finale ${out['final']:>8,.0f} CAGR {ann:>7s} "
f"maxDD {out['maxdd']:>5.0%} win {out['winrate']:.0%} "
f"code {out['n_tails']:>2d} worst ${out['worst_cycle']:>7,.0f} "
f"({out['worst_date']}, {out['n_at_worst']}c)"
+ (f" ** ROVINA {out['ruined']} **" if out["ruined"] else ""))
# controllo: size FISSA 1 contratto, e scenario 'premio generoso' (+20% sui win)
dates, moves = _cycles(spy, H_TD)
fx = _replay(dates, moves, q82, 0.02, L_ev0, w_c, s_c, sizing="fixed",
label="FULL fixed-1c EV=0")
gen_fx = _replay(dates, moves, q82, 0.02, L_ev0, w_c * 1.2, s_c, sizing="fixed",
label="FULL fixed-1c premio+20%")
gen = _replay(dates, moves, q82, 0.02, L_ev0, w_c * 1.2, s_c, sizing="video",
label="FULL video premio+20%")
p(f"\n Controlli (FULL 1996-2026, coda EV=0):")
p(f" - size FISSA 1 contratto: finale ${fx['final']:>8,.0f} CAGR {fx['cagr']:+.1%} "
f"maxDD {fx['maxdd']:.0%}" + (f" ROVINA {fx['ruined']}" if fx['ruined'] else ""))
p(f" - fixed-1c, premio +20%: finale ${gen_fx['final']:>8,.0f} CAGR "
f"{gen_fx['cagr']:+.1%} maxDD {gen_fx['maxdd']:.0%}"
+ (f" ROVINA {gen_fx['ruined']}" if gen_fx['ruined'] else ""))
p(f" - sizing video, premio +20%: finale ${gen['final']:>8,.0f} CAGR "
f"{gen['cagr']:+.1%} maxDD {gen['maxdd']:.0%}"
+ (f" ROVINA {gen['ruined']}" if gen['ruined'] else ""))
p(f" -> anche regalando un VRP persistente del +20%, il CAGR onesto multi-anno e'")
p(f" a una cifra, NON 420%; e il sizing del video lo converte comunque in rovina")
p(f" (scala dopo i win -> la coda colpisce sempre vicino alla size massima).")
rows += [fx, gen_fx, gen]
# BTC/ETH dai NOSTRI dati: stesso 82% strutturale (q82 proprio), width vol-scalata,
# severita' EV=0 ricalibrata sulla p_tail dell'asset.
p()
for aname in ("BTC", "ETH"):
d = al.get(aname, "1d")
m6 = hmoves(d["close"].values, 6)
q82a = float(np.quantile(np.abs(m6), WINRATE_CLAIM))
wa = float(0.9 * m6.std())
p_tail_a = float(np.mean(np.abs(m6) > q82a + wa))
La = round((WINRATE_CLAIM * w_c
- max(1 - WINRATE_CLAIM - p_tail_a, 0) * s_c) / p_tail_a, 0)
dates_a, moves_a = _cycles(d, 6)
out = _replay(dates_a, moves_a, q82a, wa, La, w_c, s_c, sizing="video",
label=f"{aname} strike {q82a:.0%} width {wa:.0%} EV=0")
rows.append(out)
ann = f"{out['cagr']:+.0%}" if not out["ruined"] else "RUIN"
p(f" {out['label']:38s} P(coda)={p_tail_a:.1%} L=${La:,.0f} -> finale "
f"${out['final']:>8,.0f} CAGR {ann:>6s} maxDD {out['maxdd']:>5.0%} "
f"code {out['n_tails']}"
+ (f" ** ROVINA {out['ruined']} **" if out["ruined"] else ""))
p()
p(" -> Il '420%' e' l'annualizzazione della finestra senza coda con il sizing gia'")
p(" scalato. Sulla storia intera, con la STESSA economia dichiarata dal video e")
p(" una coda fair, il compounding del video incontra la coda alla size massima.")
OUT["test3"] = dict(win_per_contract=w_c, small_per_contract=s_c,
L_bs=L_bs, L_ev0=L_ev0, p_tail=p_tail, rows=rows)
# ===========================================================================
# TEST 4 — QUANTO COPRE LA DIAGONALE IL GIORNO DEL CRASH
# ===========================================================================
def test4(q82: float):
p("\n" + "=" * 88)
p("TEST 4 — DIAGONALE COME PROTEZIONE: loss_con_diagonale / loss_naked (BS, gap+vol spike)")
p("=" * 88)
S0 = 100.0
iv0 = 0.16
def scenario(d_short, width, gap, iv1):
"""Ritorna (loss_naked, loss_diag, ratio) in frazione di S0 (positivi=perdita)."""
Kp_s = S0 * (1 - d_short)
Kp_l = S0 * (1 - d_short - width)
v_s0 = bs_put(S0, Kp_s, 6 / 365, iv0)
v_l0 = bs_put(S0, Kp_l, 13 / 365, iv0)
S1 = S0 * (1 + gap)
v_s1 = bs_put(S1, Kp_s, 5 / 365, iv1)
v_l1 = bs_put(S1, Kp_l, 12 / 365, iv1)
loss_naked = float(v_s1 - v_s0) # mark contro lo short
loss_diag = float((v_s1 - v_s0) - (v_l1 - v_l0)) # long compensa
ratio = loss_diag / loss_naked if loss_naked > 1e-9 else np.nan
return loss_naked, loss_diag, ratio
rows = []
p(f"Struttura put-side branch OBS: short K={100 * (1 - q82):.1f} T=6g, long T=13g piu' OTM.")
p(f"Scenario: gap overnight (resta 1g di vita in meno), IV 16% -> IV_crash. Perdite % notional.")
p(f" {'gap':>5s} {'IV->':>5s} {'width':>6s} | {'naked':>7s} {'diag':>7s} {'ratio':>6s} "
f"{'equity hit @6x':>14s}")
for width in (0.02, 0.03):
for gap in (-0.05, -0.10, -0.15):
for iv1 in (0.16, 0.40, 0.50, 0.70):
ln, ld, ratio = scenario(q82, width, gap, iv1)
rows.append(dict(d=q82, width=width, gap=gap, iv1=iv1,
naked=ln, diag=ld, ratio=ratio))
p(f" {gap:>5.0%} {iv1:>5.0%} {width:>6.0%} | {ln / S0:>7.2%} "
f"{ld / S0:>7.2%} {ratio:>6.0%} {6 * ld / S0:>13.1%}")
# branch DEEP (9% OTM come dichiarato), width 3%
deep = []
for gap in (-0.10, -0.15):
for iv1 in (0.40, 0.50, 0.70):
ln, ld, ratio = scenario(0.09, 0.03, gap, iv1)
deep.append(dict(d=0.09, width=0.03, gap=gap, iv1=iv1,
naked=ln, diag=ld, ratio=ratio))
p()
p(" Branch DEEP (short 9% OTM, long 12% OTM, width 3%):")
for r in deep:
p(f" {r['gap']:>5.0%} IV->{r['iv1']:.0%} | naked {r['naked'] / S0:>6.2%} "
f"diag {r['diag'] / S0:>6.2%} ratio {r['ratio']:>4.0%} "
f"equity hit @6x {6 * r['diag'] / S0:>6.1%}")
r10 = [r for r in rows if r["gap"] == -0.10 and r["iv1"] == 0.50]
r10_novol = [r for r in rows if r["gap"] == -0.10 and r["iv1"] == 0.16
and r["width"] == 0.02][0]
p()
p(f" -> La diagonale NON e' un hedge pieno e NON e' gratis: a gap -10%/IV 50 lascia")
p(f" passare il {r10[0]['ratio']:.0%} (width 2%) / {r10[1]['ratio']:.0%} (width 3%) "
f"della perdita naked; SENZA vol spike (IV ferma) il {r10_novol['ratio']:.0%}.")
p(f" A 6x notional un -10% costa {6 * r10[0]['diag'] / S0:.0%}-"
f"{6 * r10_novol['diag'] / S0:.0%} di equity PER UNITA' (x4 unita' post-scaling = "
f"{24 * r10[0]['diag'] / S0:.0%}-{24 * r10_novol['diag'] / S0:.0%}).")
p(f" La protezione dipende dal vol-spike che gonfia il long (vega del T+7g): e'")
p(f" un cap alla ROVINA, non alla perdita — e il costo del cap (long ricomprato")
p(f" ogni ciclo) e' il motivo per cui l'EV netto resta ~0 (Test 2/3).")
OUT["test4"] = dict(obs=rows, deep=deep)
# ===========================================================================
# TEST 5 — ESEGUIBILITA' PER NOI (Deribit BTC/ETH, $600)
# ===========================================================================
def test5():
p("\n" + "=" * 88)
p("TEST 5 — ESEGUIBILITA' su Deribit BTC/ETH al NOSTRO capitale ($600)")
p("=" * 88)
res = {}
for a, minsz in (("BTC", 0.1), ("ETH", 0.1)):
d = al.get(a, "1d")
S = float(d["close"].values[-1])
m6 = hmoves(d["close"].values, 6)
q82a = float(np.quantile(np.abs(m6), WINRATE_CLAIM))
leg_notional = minsz * S
sig = 0.55 if a == "BTC" else 0.70
w = float(0.9 * m6.std()) # width vol-scalata (come il 2% su SPY)
short0 = bs_put(S, S * (1 - q82a), 6 / 365, sig) + bs_call(S, S * (1 + q82a), 6 / 365, sig)
long0 = (bs_put(S, S * (1 - q82a - w), 13 / 365, sig)
+ bs_call(S, S * (1 + q82a + w), 13 / 365, sig))
long7 = (bs_put(S, S * (1 - q82a - w), 7 / 365, sig)
+ bs_call(S, S * (1 + q82a + w), 7 / 365, sig))
# raccolto theta atteso per ciclo a mercato FERMO (spread 5%/gamba)
harvest = (0.95 * short0 - 1.05 * long0 + 0.95 * long7) / S * leg_notional
# fee Deribit opzioni: min(0.0003*S, 12.5% premio) per gamba; 8 esecuzioni/ciclo
avg_leg = (short0 / 2 + long0 / 2) / 2 / S * leg_notional
fee = 8 * min(0.0003 * S * minsz, 0.125 * max(avg_leg, 1e-9))
# margine short (standard margin, senza netting sulle diagonali):
# ~max(0.15-OTM, 0.10)*S*size + mark, x2 gambe short
margin = 2 * (max(0.15 - q82a, 0.10) * S * minsz + short0 / 2 * minsz)
# tail netta: full-breach del cap (gap = -(q82+w), IV x1.5), lato put
gap_full = -(q82a + w)
Sc = S * (1 + gap_full)
vs = bs_put(Sc, S * (1 - q82a), 1 / 365, 1.5 * sig)
vl = bs_put(Sc, S * (1 - q82a - w), 8 / 365, 1.5 * sig)
vs0 = bs_put(S, S * (1 - q82a), 6 / 365, sig)
vl0 = bs_put(S, S * (1 - q82a - w), 13 / 365, sig)
tail = float(((vs - vs0) - (vl - vl0)) / S * leg_notional)
net_cycle = harvest - fee
fee_pct = f"{fee / harvest:.0%} del raccolto" if harvest > 0 else "raccolto <=0!"
res[a] = dict(S=S, q82=q82a, width=w, leg_notional=leg_notional,
harvest_cycle=float(harvest), fee_rt=float(fee),
net_cycle=float(net_cycle), margin=float(margin), tail_net=tail,
gap_full=float(gap_full))
p(f" {a} (close {S:,.0f}, strike {q82a:.0%} OTM per win 82%, width {w:.0%}): "
f"min {minsz}/gamba -> {leg_notional:,.0f}$/gamba, 4 gambe = "
f"{4 * leg_notional:,.0f}$ notional")
p(f" raccolto theta/ciclo (mercato fermo, netto spread 5%) ~${harvest:.2f}, "
f"fee Deribit 8 gambe ~${fee:.2f} ({fee_pct}) -> NETTO ${net_cycle:+.2f}/ciclo;")
p(f" margine short ~${margin:,.0f}; tail netta (full-breach {gap_full:.0%} 6d, "
f"IV x1.5) ~${tail:,.0f}")
p()
b, e = res["BTC"], res["ETH"]
p(f" Fee strutturale: le fee Deribit sono PROPORZIONALI alla size -> il rapporto")
p(f" fee/raccolto ({b['fee_rt'] / max(b['harvest_cycle'], 1e-9):.0%} BTC, "
f"{e['fee_rt'] / max(e['harvest_cycle'], 1e-9):.0%} ETH) NON migliora scalando:")
p(f" a questi strike la DOUBLE DIAGONAL e' fee-negativa su Deribit A QUALSIASI size")
p(f" (netto {b['net_cycle']:+.2f}$/ciclo BTC, {e['net_cycle']:+.2f}$/ciclo ETH per "
f"unita' minima). Il capitale sposta il muro di margine, non questo.")
p()
# ---- soglie di capitale: $600 oggi, 2k/3.5k/5k prospettici --------------
# Granularita' sensata = >=3 posizioni concorrenti + scalabile 1->2 contratti
# (>=6 slot di margine), budget margine 25% equity, rischio/posizione <=5%.
# ETH CREDIT SPREAD: numeri del filone gemello ALB-A (margine ~$107/spread =
# max-loss defined-risk, credito ~$2/spread) + fee 2 gambe.
sp_margin, sp_credit = 107.0, 2.0
sp_fee = 2 * min(0.0003 * e["S"] * 0.1, 0.125 * sp_credit) # entry 2 gambe, 0.1 ETH
p(f" SOGLIE DI CAPITALE (budget margine 25%, rischio/pos <=5%, granularita' sensata")
p(f" = 3 posizioni concorrenti scalabili 1->2 = 6 slot):")
p(f" {'conto':>7s} | {'DD BTC (m.$' + format(b['margin'], ',.0f') + ')':>22s} | "
f"{'DD ETH min (m.$' + format(e['margin'], ',.0f') + ')':>22s} | "
f"{'spread ETH (m.$107)':>20s}")
tiers = {}
for cap in (600.0, 2000.0, 3500.0, 5000.0):
mbud = 0.25 * cap
n_btc = int(mbud // b["margin"])
n_eth = int(mbud // e["margin"])
n_sp = int(mbud // sp_margin)
risk_ok_sp = sp_margin <= 0.05 * cap # max-loss spread <= 5% equity
risk_ok_eth = e["tail_net"] <= 0.05 * cap
tiers[int(cap)] = dict(margin_budget=mbud, n_btc=n_btc, n_eth_dd=n_eth,
n_spread=n_sp, spread_risk_ok=bool(risk_ok_sp),
eth_dd_risk_ok=bool(risk_ok_eth),
spread_pct_per_pos=sp_margin / cap,
spread_income_yr=n_sp * (sp_credit - sp_fee) * 52)
p(f" {cap:>7,.0f} | {n_btc:>4d} unita' ({b['margin'] / cap:>4.0%}/u) | "
f"{n_eth:>4d} unita' ({e['margin'] / cap:>4.0%}/u) | "
f"{n_sp:>3d} spread ({sp_margin / cap:>4.0%}/u, risk5% "
f"{'OK' if risk_ok_sp else 'NO'})")
p()
p(f" Lettura (con upgrade conto 2-5k):")
p(f" - DOUBLE DIAGONAL BTC: margine ${b['margin']:,.0f}/unita' -> 1a unita' dentro il")
p(f" budget 25% solo da ~${4 * b['margin']:,.0f}; 'granularita' sensata' (6 slot) da "
f"~${24 * b['margin'] / 1000:,.0f}k. Nemmeno 5k basta: resta il muro gamma-scalp")
p(f" (diario 2026-06-26: 'opzione BTC min $5.968 >> $600'), che si sposta a ~10-30k.")
p(f" - DOUBLE DIAGONAL ETH min 0.1: gia' a 2k il margine non e' il vincolo "
f"({int(0.25 * 2000 / e['margin'])} unita' nel budget), ma resta FEE-NEGATIVA "
f"({e['net_cycle']:+.2f}$/ciclo): eseguibile != conveniente, a ogni soglia.")
p(f" - CREDIT SPREAD ETH (ALB-A): il PRIMO oggetto con soglia reale. A $600: 1 spread")
p(f" = {sp_margin / 600:.0%} del conto (max-loss {sp_margin / 600:.0%} > 5% -> NO). "
f"A 2k: {int(0.25 * 2000 // sp_margin)} spread nel budget, max-loss {sp_margin / 2000:.1%}")
p(f" ~ok ma niente scalata 1->2 su 3 posizioni; a 3.5k: {int(0.25 * 3500 // sp_margin)} slot "
f"(3 pos x2 non ancora); a 5k: {int(0.25 * 5000 // sp_margin)} slot -> granularita' OK")
p(f" (3 concorrenti scalabili 1->2, {6 * sp_margin / 5000:.0%} del conto impegnato).")
p(f" SOGLIA ~${int(np.ceil(6 * sp_margin / 0.25 / 100) * 100):,} per il set completo; "
f"~$2.1k per 5 slot senza scalata. Income lordo a 5k: "
f"{tiers[5000]['spread_income_yr']:.0f}$/anno ({tiers[5000]['spread_income_yr'] / 5000:.1%}) "
f"PRIMA delle code (Test 2-3: EV~0).")
p(f" - Confronto scala video: 10k su SPX = 1 contratto SPY (~$60k notional, 6x leva).")
p(f" L'equivalente Deribit a parita' di prudenza (tail 5%) e' ~${20 * e['tail_net']:,.0f}+")
p(f" su ETH e ~${20 * b['tail_net']:,.0f}+ su BTC: anche a 5k il conto e' SOTTO la")
p(f" scala minima del gioco del video su BTC, e ci sta su ETH solo in versione")
p(f" credit-spread (che e' VRP01, non 'Albimarini').")
OUT["test5"] = dict(assets=res, tiers=tiers,
spread=dict(margin=sp_margin, credit=sp_credit, fee=float(sp_fee)))
# ===========================================================================
# TEST 6 — CONFRONTO CON VRP01 (perche' un theta-harvest sopravvive)
# ===========================================================================
def test6():
p("\n" + "=" * 88)
p("TEST 6 — Che cosa distingue un theta-harvest che sopravvive (VRP01) da uno che no")
p("=" * 88)
p(" VRP01 (audit superato, diario 2026-06-20): (1) DEFINED-RISK come struttura, non")
p(" come accessorio: il long wing cappa worst-week -16.6% -> -7.4% e DD 33% -> 21%;")
p(" (2) GATE di regime IV-rank>0.30 (vende solo vol RICCA): ribalta l'HOLD-OUT da")
p(" -0.25 a +0.28 — l'alpha e' il filtro, non il theta; (3) SIZING: sleeve al 12%")
p(" del book, niente compounding del numero di contratti; positivo/flat anche 2022.")
p(" La 'Albimarini' e' l'opposto quantificato: vende SEMPRE (nessun gate: incassa il")
p(" premio anche quando IV<RV), il rischio e' cappato solo dalla width della diagonale")
p(" (a 6x notional un gap -10% = ~-5/-12% equity PER UNITA', x4 post-scaling, Test 4),")
p(" e il sizing SCALA sui profitti proprio mentre la coda si avvicina (Test 3).")
p(" Win-rate 82% e PF 5.16 su 28 trade sono la FIRMA STRUTTURALE del venditore di")
p(" quantile a EV=0 in una finestra senza code (P 20-45%, Test 2), non un edge.")
# ===========================================================================
def main():
spy, m4, q82 = test1()
test2(spy, q82)
test3(spy, q82)
test4(q82)
test5()
test6()
p("\n" + "=" * 88)
p("JSON")
p("=" * 88)
p(json.dumps(OUT, indent=1, default=float))
if __name__ == "__main__":
main()
+333
View File
@@ -0,0 +1,333 @@
"""R0702 ALB-STRUCTURE — double diagonal deep-OTM "Albimarini" vs struttura VRP01.
FILONE: video didattici claimano su SPY una vendita sistematica di DOUBLE DIAGONAL a credito
(2 short scadenza T a ~9% di distanza su ~6 giorni + 2 long scadenza T+1g, entrambi i lati) con
82% win, PF 5.16, "420% annuo". Qui la STRUTTURA (non il gate — lezione 2026-07-01: il gate
IV-rank>0.30 canonico NON si riottimizza) viene portata su BTC/ETH Deribit (che ha scadenze
giornaliere) e modellata onestamente sul nostro stack DVOL, contro il VRP01 canonico
(put credit spread settimanale -0.28/-0.10, gate vrp>0 + ivr>=0.30 + crash-skip 0.90).
⚠️ CAVEAT SKEW — IN TESTA, NON IN FONDO: il pricing e' Black-Scholes FLAT sulla DVOL (IV ATM 30g).
Il deep-OTM (qui 1.5-3.0 sigma ~ 8-25% di distanza) e' ESATTAMENTE dove il flat-vol sbaglia di piu':
su crypto lo smile e' ripido su ENTRAMBE le ali (su equity solo put). Il premio reale delle ali e'
probabilmente > modello (f>1) per le put e variabile per le call; la calibrazione reale che abbiamo
(f~1.0) e' ATM-ish delta -0.28 su finestra calma, NON copre il deep-OTM. Quindi OGNI numero va letto
come BANDA sul fattore premio f in {0.6, 0.8, 1.0, 1.3} + uno scenario skew asimmetrico
(f_put=1.3 / f_call=0.7), MAI come stima puntuale. In piu' la DVOL e' IV a 30g usata per tenor 3-6g
(term structure ignorata; snapshot vol_term 2026-06: iv_7d vs iv_30d entro ~±3pt in calma, ma in
stress il front-end esplode -> il modello SOTTOSTIMA il mark-to-market avverso in crash).
Distanza in unita' di VOL, non il 9% fisso di SPY: 9%/6g su SPY (IV~16%) = ~3.5-4 sigma; BTC si
muove del 9% in 6 giorni spesso. Cella centrale dichiarata A PRIORI: z=2.0 sigma, ali dz=+1.0 sigma,
tenor 5g (centro del range 3-6g del video). z=3.0 in griglia = cella "fedele al video".
Riusa: options_vrp_lab (bs_put, load_series, per_year), options_vrp_v2 (vrp_spread_weekly = VRP01,
_ivrank, _rv30), altlib.marginal_vs_tp01. Fee Deribit opzioni per gamba: 0.03% del notional cap
12.5% del premio (+ delivery 0.015% cap 12.5% sull'ITM a scadenza; il diagonale paga anche la fee
di USCITA per vendere le long residue a T). NON deploy: regola standing "niente short-vol da
modello in deploy" — l'esito massimo e' conoscenza sulla struttura.
uv run python scripts/research/r0702_alb_structure.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))
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
import numpy as np, pandas as pd
from scipy.stats import norm
from scripts.research.options_vrp_lab import bs_put, load_series, per_year
from scripts.research.options_vrp_v2 import vrp_spread_weekly, _ivrank, _rv30
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
DAY = 1.0 / 365.25
# cella centrale DICHIARATA A PRIORI (prima di guardare qualsiasi risultato)
CENTRAL = dict(z=2.0, dz=1.0, tenor_d=5)
F_SWEEP = (0.6, 0.8, 1.0, 1.3) # banda skew simmetrica
F_SKEW = dict(f_put=1.3, f_call=0.7) # scenario skew crypto-shaped (put ricche, call povere)
def bs_call(S, K, T, sig):
if T <= 0 or sig <= 0:
return max(S - K, 0.0)
d1 = (np.log(S / K) + 0.5 * sig ** 2 * T) / (sig * np.sqrt(T))
return S * norm.cdf(d1) - K * norm.cdf(d1 - sig * np.sqrt(T)) # r=0
def _fee_frac(prem_frac, notional_ratio=1.0, rate=0.0003):
"""Fee Deribit per gamba come frazione di S0: rate*notional cap 12.5% del premio."""
return min(rate * notional_ratio, 0.125 * max(prem_frac, 0.0))
def run_structure(asset, kind, z=2.0, dz=1.0, tenor_d=5, f_put=1.0, f_call=1.0,
gated=False, collect=None):
"""Vendita sistematica non-overlapping della struttura, cadenza = tenor_d.
kind: 'diag' = double diagonal Albimarini (short T entrambi i lati, long T+1g piu' OTM)
'condor' = iron condor STESSA scadenza (controllo del claim '+1 giorno')
'vert' = vertical put credit spread deep-OTM (solo lato put, stessa scadenza)
Strike: K = S0*exp(±z·σ√T) (z in sigma dell'orizzonte SHORT); ali a z+dz.
gated=True -> gate CANONICO VRP01 (vrp>0 AND ivr>=0.30 AND ivr<=0.90), NON riottimizzato.
Ritorna Series di rendimenti per-periodo su capitale = S0 (spot a entry), indice = scadenza.
collect (dict) accumula diagnostica per la decomposizione diag-vs-condor."""
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
T_long = T + DAY if kind == "diag" else T
has_call = kind in ("diag", "condor")
rets = {}
i = 60
while i + tenor_d < n:
S0 = px[i]; sig = dv[i]
if gated:
rv = _rv30(px, i); ivr = _ivrank(dv, i)
skip = ((not np.isnan(rv) and (sig - rv) <= 0)
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
if skip:
rets[idx[i + tenor_d]] = 0.0
i += tenor_d
continue
m = sig * np.sqrt(T)
Kp_s = S0 * np.exp(-z * m); Kp_l = S0 * np.exp(-(z + dz) * m)
Kc_s = S0 * np.exp(+z * m); Kc_l = S0 * np.exp(+(z + dz) * m)
# premi a entry (frazione di S0), f per lato
ps = bs_put(S0, Kp_s, T, sig) / S0 * f_put # short put
pl = bs_put(S0, Kp_l, T_long, sig) / S0 * f_put # long put (T o T+1g)
legs = [ps, pl]
cs = cl = 0.0
if has_call:
cs = bs_call(S0, Kc_s, T, sig) / S0 * f_call
cl = bs_call(S0, Kc_l, T_long, sig) / S0 * f_call
legs += [cs, cl]
credit = (ps + cs) - (pl + cl)
# exit a scadenza degli short
j = i + tenor_d
S1 = px[j]; sig1 = dv[j]
short_pay = max(0.0, Kp_s - S1) / S0 + (max(0.0, S1 - Kc_s) / S0 if has_call else 0.0)
if kind == "diag": # long con 1 giorno residuo: mark BS alla DVOL di uscita (vega!)
lp = bs_put(S1, Kp_l, DAY, sig1) / S0 * f_put
lc = bs_call(S1, Kc_l, DAY, sig1) / S0 * f_call
long_val = lp + lc
exit_fee = sum(_fee_frac(v, notional_ratio=S1 / S0) for v in (lp, lc))
else: # stessa scadenza: valore = solo intrinseco
long_val = max(0.0, Kp_l - S1) / S0 + (max(0.0, S1 - Kc_l) / S0 if has_call else 0.0)
exit_fee = 0.0
entry_fee = sum(_fee_frac(v) for v in legs)
# delivery fee su gambe short ITM a scadenza (0.015% cap 12.5%)
deliv = _fee_frac(max(0.0, Kp_s - S1) / S0, rate=0.00015)
if has_call:
deliv += _fee_frac(max(0.0, S1 - Kc_s) / S0, rate=0.00015)
pnl = credit - short_pay + long_val - entry_fee - exit_fee - deliv
rets[idx[j]] = pnl
if collect is not None:
# costo extra del T+1 a entry e valore residuo recuperato a exit (vs intrinseco)
pl_T = bs_put(S0, Kp_l, T, sig) / S0 * f_put
cl_T = (bs_call(S0, Kc_l, T, sig) / S0 * f_call) if has_call else 0.0
intr = max(0.0, Kp_l - S1) / S0 + (max(0.0, S1 - Kc_l) / S0 if has_call else 0.0)
collect.setdefault("extra_cost", []).append((pl + cl) - (pl_T + cl_T))
collect.setdefault("recovered", []).append((long_val - intr) if kind == "diag" else 0.0)
collect.setdefault("short_pay", []).append(short_pay)
collect.setdefault("credit", []).append(credit)
i += tenor_d
return pd.Series(rets)
def book(kind, **kw):
rB = run_structure("BTC", kind, **kw); rE = run_structure("ETH", kind, **kw)
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1)
def metrics(r, tenor_d):
"""Metriche su rendimenti per-periodo (cadenza tenor_d). win/PF solo sui periodi ATTIVI."""
r = r.dropna()
ppy = 365.25 / tenor_d
if len(r) < 3 or r.std() == 0:
return dict(sh=0.0, sh_h=0.0, cagr=0.0, dd=0.0, worst=0.0, win=0.0, pf=0.0, act=0.0)
def _sh(x):
return float(x.mean() / x.std() * np.sqrt(ppy)) if len(x) > 2 and x.std() > 0 else 0.0
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
yrs = len(r) / ppy
act = r[r != 0.0]
pos = act[act > 0].sum(); neg = -act[act < 0].sum()
return dict(
sh=_sh(r), sh_h=_sh(r[r.index >= HOLDOUT]),
cagr=float(eq[-1] ** (1 / yrs) - 1) if yrs > 0 and eq[-1] > 0 else -1.0,
dd=float(np.max((pk - eq) / pk)), worst=float(r.min()),
win=float((act > 0).mean()) if len(act) else 0.0,
pf=float(pos / neg) if neg > 0 else float("inf"),
act=float((r != 0.0).mean()), n_act=int(len(act)))
def row(label, r, tenor_d):
mm = metrics(r, tenor_d)
pf = f"{mm['pf']:5.2f}" if np.isfinite(mm["pf"]) else " inf"
print(f" {label:<40} {mm['sh']:>6.2f} {mm['sh_h']:>6.2f} {mm['cagr']*100:>+6.1f}% "
f"{mm['dd']*100:>5.1f}% {mm['worst']*100:>+6.2f}% {pf} {mm['win']*100:>4.0f}% {mm['act']*100:>4.0f}%")
return mm
def to_daily_lumped(wk):
"""Rendimenti per-periodo -> griglia giornaliera con lump alla scadenza (convenzione
_vrp_combo_returns: preserva lo Sharpe annualizzato, niente smoothing)."""
wk = wk.sort_index()
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
return daily
HDR = f" {'struttura':<40} {'ShF':>6} {'ShH':>6} {'CAGR':>7} {'maxDD':>6} {'worst':>7} {'PF':>5} {'win%':>5} {'att%':>5}"
def main():
print("=" * 110)
print(" R0702 ALB-STRUCTURE — double diagonal deep-OTM (Albimarini) vs vertical vs condor vs VRP01")
print(" Capitale = SPOT a entry (S0) per le strutture nuove; VRP01 canonico = strike corto (sua convenzione).")
print(" ⚠️ SKEW: pricing BS FLAT su DVOL-30g; il deep-OTM e' banda-f, non stima puntuale. Term structure ignorata.")
print("=" * 110)
z, dz, tn = CENTRAL["z"], CENTRAL["dz"], CENTRAL["tenor_d"]
for a in ("BTC", "ETH"):
J = load_series(a)
sig = J["dvol"].mean() / 100.0
d5 = sig * np.sqrt(tn / 365.25)
print(f" {a}: DVOL media {sig*100:.0f}% -> 1σ su {tn}g = {d5*100:.1f}% | z=2.0 = {2*d5*100:.1f}% "
f"| z=3.0 = {3*d5*100:.1f}% (il '9% su SPY/6g' ≈ 3.5-4σ equity)")
# ------------------------------------------------------------------ (1) VRP01 canonico
print(f"\n (1) VRP01 CANONICO (riproduzione options_vrp_v2 COMBO: spread -0.28/-0.10 7g, vrp>0+ivr30+cs90)")
print(HDR)
vrp = {}
for f in F_SWEEP:
vrp[f] = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=f, gate_vrp=True,
gate_ivr=0.30, crash_skip=0.90),
"E": vrp_spread_weekly("ETH", defined_risk=True, f=f, gate_vrp=True,
gate_ivr=0.30, crash_skip=0.90)},
axis=1, join="inner").mean(axis=1)
row(f"VRP01 gated f={f}", vrp[f], 7)
vrp_nog = pd.concat({"B": vrp_spread_weekly("BTC", defined_risk=True, f=1.0),
"E": vrp_spread_weekly("ETH", defined_risk=True, f=1.0)},
axis=1, join="inner").mean(axis=1)
row("VRP01 NO-gate f=1.0", vrp_nog, 7)
# bridge di validazione del motore: vertical -0.28-equivalente (z~0.58, dz~0.70, 7g) ~ VRP01
zb = float(-norm.ppf(0.28)); dzb = float(-norm.ppf(0.10)) - zb
br = book("vert", z=zb, dz=dzb, tenor_d=7, f_put=1.0, f_call=1.0, gated=True)
row(f"[bridge] vert z={zb:.2f} dz={dzb:.2f} 7g gated", br, 7)
print(" (bridge ~ VRP01 a meno di convenzione strike/capitale: valida il motore nuovo)")
# ------------------------------------------------------------------ (2) tabella principale
print(f"\n (2) STRUTTURE ALLA CELLA CENTRALE A PRIORI (z={z}σ, ali +{dz}σ, tenor {tn}g) — banda f")
scen = [(f, f, f"f={f}") for f in F_SWEEP] + [(F_SKEW["f_put"], F_SKEW["f_call"], "SKEW fp=1.3/fc=0.7")]
streams = {}
for gated, gtag in ((False, "NO-GATE"), (True, "GATE canonico (vrp>0+ivr30+cs90)")):
print(f"\n --- {gtag} ---")
print(HDR)
for kind, ktag in (("diag", "DIAG double-diagonal T+1g"),
("condor", "CONDOR iron condor stessa T"),
("vert", "VERT put spread stessa T")):
for fp, fc, ftag in scen:
r = book(kind, z=z, dz=dz, tenor_d=tn, f_put=fp, f_call=fc, gated=gated)
row(f"{ktag} {ftag}", r, tn)
streams[(kind, gated, ftag)] = r
# distanze alternative (sweep trasparente, selezione SOLO in-sample; f=1.0 gated)
print(f"\n (3) SWEEP DISTANZA/TENOR (gated, f=1.0) — selezione cella SOLO su Sharpe PRE-holdout")
print(f" {'cella':<40} {'ShF-IS':>7} {'ShH':>6} {'DD':>6} {'worst':>7} {'PF':>5} {'win%':>5}")
best = None
for kind in ("diag", "condor", "vert"):
for zz in (1.5, 2.0, 2.5, 3.0):
for tt in (3, 5):
r = book(kind, z=zz, dz=dz, tenor_d=tt, f_put=1.0, f_call=1.0, gated=True)
ris = r[r.index < HOLDOUT]
mm = metrics(r, tt); mi = metrics(ris, tt)
pf = f"{mm['pf']:5.2f}" if np.isfinite(mm["pf"]) else " inf"
act = r[r != 0.0]
nloss = int((act < 0).sum())
tag = " <- video ~3.5σ" if zz == 3.0 and tt == 5 and kind == "diag" else ""
if nloss == 0:
tag += " ⚠️ 0 perdite su tutta la storia = coda MAI campionata (lezione CC01: Sharpe implausibile -> rischio nascosto)"
print(f" {kind:<7} z={zz} dz={dz} tenor={tt}g{'':<14} {mi['sh']:>7.2f} {mm['sh_h']:>6.2f} "
f"{mm['dd']*100:>5.1f}% {mm['worst']*100:>+6.2f}% {pf} {mm['win']*100:>4.0f}% n={len(act):>3}{tag}")
if best is None or mi["sh"] > best[1]:
best = ((kind, zz, tt), mi["sh"], mm["sh_h"])
print(f" -> cella best IN-SAMPLE: {best[0]} (ShF-IS {best[1]:.2f}) | suo hold-out ShH {best[2]:.2f}")
print(" Le celle z>=2.5/5g vendono un evento ~1% mai occorso nel subsample gated (~140 trade):")
print(" Sharpe 'inf/5.9' = premio senza coda osservata, NON edge. E' il punto cieco CC01 in forma opzioni.")
# ------------------------------------------------------------------ (4) claim del video
print(f"\n (4) TEST CLAIM VIDEO: la long a T+1g domina la long a STESSA T? (z={z}, dz={dz}, {tn}g, f=1.0)")
for gated in (False, True):
colD, colC = {}, {}
rD = pd.concat({a[0]: run_structure(a, "diag", z=z, dz=dz, tenor_d=tn, gated=gated, collect=colD)
for a in ("BTC", "ETH")}, axis=1, join="inner").mean(axis=1)
rC = pd.concat({a[0]: run_structure(a, "condor", z=z, dz=dz, tenor_d=tn, gated=gated, collect=colC)
for a in ("BTC", "ETH")}, axis=1, join="inner").mean(axis=1)
mD, mC = metrics(rD, tn), metrics(rC, tn)
ec = np.array(colD["extra_cost"]); rec = np.array(colD["recovered"])
sp = np.array(colD["short_pay"]); crd = np.array(colD["credit"])
crash = sp > np.quantile(sp, 0.95)
gt = "GATE" if gated else "NO-GATE"
print(f" [{gt}] DIAG ShF {mD['sh']:+.2f}/ShH {mD['sh_h']:+.2f} worst {mD['worst']*100:+.2f}% | "
f"CONDOR ShF {mC['sh']:+.2f}/ShH {mC['sh_h']:+.2f} worst {mC['worst']*100:+.2f}%")
print(f" costo extra T+1 a entry: {ec.mean()*1e4:+.1f} bps/trade | residuo recuperato a exit: "
f"{rec.mean()*1e4:+.1f} bps (nei 5% peggiori: {rec[crash].mean()*1e4:+.1f} bps vs extra {ec[crash].mean()*1e4:+.1f})")
print(f" trade a CREDITO netto: {(crd>0).mean()*100:.0f}% (credito medio {crd.mean()*1e4:+.1f} bps di S0)")
dY = per_year(rD); cY = per_year(rC)
print(" Δ(diag-condor) per anno: " + " ".join(f"{y}:{(dY[y]-cY.get(y,0))*100:+.2f}%" for y in sorted(dY)))
# ------------------------------------------------------------------ (5) per-anno
print(f"\n (5) PER-ANNO (gated, f=1.0) — 2022 = LUNA+FTX e' il banco di prova")
for tag, r, tt in (("DIAG", streams[("diag", True, "f=1.0")], tn),
("CONDOR", streams[("condor", True, "f=1.0")], tn),
("VERT", streams[("vert", True, "f=1.0")], tn),
("VRP01", vrp[1.0], 7)):
py = per_year(r)
print(f" {tag:<7} " + " ".join(f"{y}:{v*100:+5.1f}%" for y, v in sorted(py.items())))
# ------------------------------------------------------------------ (6) marginale
print(f"\n (6) MARGINALE vs TP01 e vs VRP01 (daily-lumped; corr su griglia settimanale)")
import altlib as al
tp = al.tp01_baseline_daily()
dv_daily = to_daily_lumped(streams[("diag", True, "f=1.0")])
vr_daily = to_daily_lumped(vrp[1.0])
tp_w = (1 + tp).resample("W").prod() - 1
di_w = (1 + dv_daily).resample("W").prod() - 1
vr_w = (1 + vr_daily).resample("W").prod() - 1
Jw = pd.concat({"tp": tp_w, "di": di_w, "vr": vr_w}, axis=1, join="inner").dropna()
print(f" corr settimanale: DIAG~TP01 {Jw['tp'].corr(Jw['di']):+.2f} | DIAG~VRP01 {Jw['di'].corr(Jw['vr']):+.2f} "
f"| VRP01~TP01 {Jw['tp'].corr(Jw['vr']):+.2f}")
for name, dd in (("DIAG gated f=1.0", dv_daily), ("VRP01 gated f=1.0 (riferimento)", vr_daily)):
mv = al.marginal_vs_tp01(dd)
print(f" marginal_vs_tp01[{name}]: verdict={mv.get('marginal_verdict')} corr={mv.get('corr_full')} "
f"uplift w25 full/hold={mv['blends']['w25']['uplift_full']}/{mv['blends']['w25']['uplift_hold']} "
f"IS-Sh={mv.get('cand_insample_sharpe')} insample_edge={mv.get('has_insample_edge')} "
f"hedge={mv.get('is_hedge')} robust_oos={mv.get('robust_oos')} multicut={mv.get('multicut_uplift')}")
print(" ⚠️ Un verdetto ADDS qui NON promuove: lo stream vende coda che nel subsample gated non ha mai")
print(" colpito (hold-out Sh 3+ = assenza di eventi, non alpha) — vale la lezione CC01, e vale la regola")
print(" standing 'niente short-vol da modello in deploy'.")
# ------------------------------------------------------------------ (7) eseguibilita'
print(f"\n (7) ESEGUIBILITA' DERIBIT (min 0.1 BTC / 1 ETH per gamba; diag = 4 gambe/asset, book = 8)")
for a, minc in (("BTC", 0.1), ("ETH", 1.0)):
J = load_series(a); S = float(J["px"].iloc[-1]); sig = float(J["dvol"].iloc[-1]) / 100.0
w = dz * sig * np.sqrt(tn / 365.25) # larghezza ala in frazione di S
notional = minc * S
maxloss = w * notional # margine ~ max loss defined-risk (per lato)
col = {}
run_structure(a, "diag", z=z, dz=dz, tenor_d=tn, gated=True, collect=col)
cbps = np.mean(col["credit"]) * 1e4 if col.get("credit") else float("nan")
print(f" {a}: spot ~${S:,.0f} -> notional min {minc} = ${notional:,.0f}/gamba | ala {w*100:.1f}% "
f"-> margine/max-loss min ~${maxloss:,.0f} | credito tipico {cbps:+.0f} bps = ${notional*cbps/1e4:,.0f}/trade")
print(" -> a $600: UN diagonale BTC min-size impegna >50% del capitale su un trade 5g = NON eseguibile.")
print(" Scala minima: sleeve opzioni al ~12% con margine <= peso richiede >~$3-5k per il solo BTC")
print(" min-size; book 50/50 con granularita' (>=3-5 step di size) ~= $15-25k. STAT-MODE, come VRP01.")
print("\n NB ONESTO: win-rate alto e' STRUTTURALE nel deep-OTM (vendi eventi rari), non e' edge. Il verdetto")
print(" sta in Sharpe/PF/coda attraverso la banda f e il 2022. Regola standing: niente short-vol da modello")
print(" in deploy — esito massimo = aggiornamento di conoscenza sulla STRUTTURA.")
if __name__ == "__main__":
main()
+594
View File
@@ -0,0 +1,594 @@
#!/usr/bin/env python
"""r0702_capital_scaling.py — RI-PRICING dei MURI DI SCALA a capitale {600, 2000, 3500, 5000}.
NON e' ricerca di strategie nuove: e' la ri-quantificazione ONESTA dei vincoli di scala gia'
documentati (tutti quantificati a $600) in vista del funding del conto live a 2-5k $:
(1) TP01 smallcap haircut — al.eval_weights_smallcap ai 4 capitali (budget per-asset = C/2,
book 50/50): haircut Sharpe modellato→eseguibile, ordini
eseguiti/saltati, turnover eseguito, fee drag (tetto noto
~0.4%/anno: a $600 il min-order fa da banda d'isteresi gratuita
— ondata timing 2026-07-02 — a 2-5k l'effetto si riduce).
(2) Book live TP01+SKH01 75/25 — replica CONCETTUALE dei target di src/live/book.py (formula
net = clamp(0.5*E*(0.75*tp_frac + 0.25*skh_sign), ±cap)) sulla
griglia 230m storica, SENZA importare il modulo live: notional
tipici, % ordini sub-min, e quanto book resta NON investito se
il cap $300/asset non viene alzato.
(3) Tranching TP01 K=2/K=4 — (diario 2026-07-02: "NO deploy a $600, rivalutare a >=5-10k"):
% di ribilanciamenti-tranche >= min-order $5 a ogni capitale.
NB il blocco feed-intraday-fuori-path-certificato e' SEPARATO
e resta (dichiarato in output).
(4) STATARB-RESID — stesso smallcap a 2 gambe (eval_spread_smallcap di
orthogonal_signals, W=45/sgn=+1 CONGELATI come in
scripts/live/paper_statarb.py — qui solo LETTI, mai toccati).
(5) Opzioni Deribit — matematica STATICA (nessun backtest): min 0.1 BTC / 1 ETH per
gamba, margine defined-risk dello spread VRP01, fee Deribit
0.03% notional cap 12.5% premio. Solo pricing del muro: regola
standing "niente short-vol da modello in deploy" INVARIATA.
(6) XS01 (19 gambe HL, "~20k") — replica della matrice pesi di sleeves._xsec_returns (copia
locale, sola lettura del modulo) + min order HL ~$10/gamba:
conferma/rettifica della soglia. CC01: conti statici gambe.
(7) SINTESI — tabella capitale × vincoli + raccomandazioni CONFIG (solo
proposte: config/live.json NON viene toccato) + aspettativa
onesta EUR/giorno col CAGR de-luckato del book (10-15%, audit
SKH01 2026-07-02 path orario — NON i numeri canonici).
Causalita'/pandas: niente DatetimeIndex.view("int64") — epoca ms esplicita ovunque (timestamp gia'
int64 nei frame certificati). Fee 0.10% RT (0.05%/lato). Nessun file di produzione toccato.
uv run python scripts/research/r0702_capital_scaling.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT))
import altlib as al # noqa: E402
from src.data.downloader import load_data # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
from src.strategies.skyhook import LTF_MIN, SKH01_V2_DD, build_frames, skyhook_entries # noqa: E402
from orthogonal_signals import build_joint, eval_spread, eval_spread_smallcap, f_statarb_resid # noqa: E402
CAPITALS = (600.0, 2000.0, 3500.0, 5000.0)
MIN_ORDER = 5.0 # Deribit min order USD (config/live.json)
MIN_ORDER_HL = 10.0 # Hyperliquid min order USD (~$10)
CAP_NOW = 300.0 # max_notional_per_asset_usd corrente
ASSETS = ("BTC", "ETH")
MS_D = 86_400_000
MS_LTF = LTF_MIN * 60_000
FEE_SIDE = al.FEE_SIDE # 0.0005
TP = TrendPortfolio(**CANONICAL)
def _years(ts_ms: np.ndarray) -> float:
return max((int(ts_ms[-1]) - int(ts_ms[0])) / (MS_D * 365.25), 1e-9)
# ===========================================================================
# (1) TP01 smallcap haircut ai 4 capitali (budget per-asset = C/2, book 50/50)
# ===========================================================================
def smallcap_counts(tgt: np.ndarray, capital: float, min_order: float = MIN_ORDER) -> dict:
"""Replica il path di skip di al.eval_weights_smallcap per CONTARE ordini eseguiti vs
desiderati (la funzione ufficiale riporta metriche, non i saltati)."""
tgt = np.clip(np.nan_to_num(np.asarray(tgt, float)), -10, 10)
cur = 0.0
n_exec = 0
n_want = 0
for x in tgt:
d = abs(x - cur)
if d * capital >= min_order:
cur = x
n_exec += 1
n_want += 1
elif d * capital >= 0.01: # un cambio era desiderato ma sub-min-order
n_want += 1
return dict(n_exec=n_exec, n_want=n_want)
def section1():
print("=" * 100)
print("(1) TP01 CANONICO 1d — smallcap haircut ai 4 capitali (budget per-asset = C/2, book 50/50)")
print(" min order $5 Deribit; fee 0.05%/lato; fee-drag = fee_side * turnover-eseguito/anno")
print("=" * 100)
out = {}
d1 = {a: al.get(a, "1d") for a in ASSETS}
tgt = {a: np.nan_to_num(TP.target_series(d1[a])) for a in ASSETS}
modeled = {a: al.eval_weights(d1[a], tgt[a]) for a in ASSETS}
for a in ASSETS:
m = modeled[a]
print(f" [{a}] modellato (fiction ribilanciamento continuo): Sh FULL {m['full']['sharpe']:.2f} "
f"turnover {m['turnover_per_year']:.1f}x/anno -> fee drag modellato "
f"{m['turnover_per_year'] * FEE_SIDE * 100:.2f}%/anno")
hdr = (f" {'capitale':>8} {'asset':>5} {'Sh mod':>7} {'Sh real':>8} {'haircut':>8} "
f"{'ordini/anno':>12} {'saltati%':>9} {'turn exec':>10} {'feedrag%':>9}")
print(hdr)
for C in CAPITALS:
rows = []
for a in ASSETS:
r = al.eval_weights_smallcap(d1[a], tgt[a], capital=C / 2.0, min_order=MIN_ORDER)
cnt = smallcap_counts(tgt[a], C / 2.0)
yrs = _years(d1[a]["timestamp"].values.astype("int64"))
skipped = 1.0 - cnt["n_exec"] / max(cnt["n_want"], 1)
drag = r["executed_turnover_per_year"] * FEE_SIDE * 100
rows.append(dict(asset=a, mod=r["modeled"]["sharpe"], real=r["realistic"]["sharpe"],
hc=r["sharpe_haircut"], opy=cnt["n_exec"] / yrs, skip=skipped,
turn=r["executed_turnover_per_year"], drag=drag))
print(f" {C:>8.0f} {a:>5} {r['modeled']['sharpe']:>7.2f} {r['realistic']['sharpe']:>8.2f} "
f"{r['sharpe_haircut']:>8.3f} {cnt['n_exec'] / yrs:>12.0f} {skipped * 100:>8.1f}% "
f"{r['executed_turnover_per_year']:>10.1f} {drag:>9.2f}")
out[C] = dict(haircut=float(np.mean([x["hc"] for x in rows])),
drag=float(np.mean([x["drag"] for x in rows])),
skipped=float(np.mean([x["skip"] for x in rows])))
print(" NB contesto (ondata timing 2026-07-02): a $600 il min-order E' la banda d'isteresi ottimale")
print(" (ordini -74% a costo ~0). Ai capitali alti la banda implicita si stringe (5$/2500$ = 0.2% del")
print(" budget-asset) -> si eseguono quasi tutti i micro-ribilanci e il fee drag risale verso il")
print(" modellato (tetto noto ~0.4%/anno) — e' il costo, atteso e piccolo, della fedelta' al modello.")
return out
# ===========================================================================
# (2) BOOK LIVE TP01+SKH01 75/25 — replica concettuale dei target (griglia 230m)
# ===========================================================================
def skh_sign_series(asset: str) -> tuple[pd.DataFrame, np.ndarray]:
"""Segno SKH01 (+1/-1/0) alla DECISIONE di ogni barra 230m chiusa, replicando la logica
non-overlap entry+exit (TP/SL/max_bars) di sleeves._skyhook_positions su tutta la storia."""
ltf, htf = build_frames(load_data(asset, "5m"))
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
H = ltf["high"].values
L = ltf["low"].values
n = len(ltf)
sgn = np.zeros(n)
i = 0
while i < n:
e = ent[i]
if e is None:
i += 1
continue
d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
exit_idx = None
for s in range(1, mb + 1):
j = i + s
if j >= n:
break
hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp)
if hit or s == mb:
exit_idx = j
break
if exit_idx is None: # trade ancora aperto a fine storia
sgn[i:] = d
break
sgn[i:exit_idx] = d # alla decisione di exit_idx il trade e' gia' chiuso
i = exit_idx + 1
return ltf, sgn
def tp_frac_on_ltf(asset: str, ltf: pd.DataFrame) -> np.ndarray:
"""tp_frac (target TP01 daily, causale) mappato sulla griglia 230m: per ogni chiusura 230m
l'ultimo target daily la cui CHIUSURA nominale (open-label + 24h, epoca ms) e' <= chiusura 230m."""
d1 = al.get(asset, "1d")
tgt = np.nan_to_num(TP.target_series(d1))
close_d = d1["timestamp"].values.astype("int64") + MS_D
close_l = ltf["timestamp"].values.astype("int64") + MS_LTF
idx = np.searchsorted(close_d, close_l, side="right") - 1
return np.where(idx >= 0, tgt[np.maximum(idx, 0)], 0.0)
def book_sim(tpf: np.ndarray, sgn: np.ndarray, ts_ms: np.ndarray,
C: float, cap: float, min_order: float = MIN_ORDER) -> dict:
"""Replica PURA della formula di book.book_net_target + build_book_order (senza import live):
equity fissa = C (isola l'effetto scala; il live usa l'equity reale), ordini market al delta."""
raw = 0.5 * C * (0.75 * np.maximum(np.nan_to_num(tpf), 0.0) + 0.25 * np.nan_to_num(sgn))
net = np.clip(raw, -cap, cap)
pos = 0.0
executed = []
n_skip = 0
for x in net:
d = x - pos
if abs(d) >= min_order:
executed.append(abs(d))
pos = x
elif abs(d) >= 0.01:
n_skip += 1
yrs = _years(ts_ms)
ex = np.asarray(executed) if executed else np.asarray([0.0])
nz = np.abs(raw) > 1e-9
mean_raw = float(np.mean(np.abs(raw[nz]))) if nz.any() else 0.0
mean_net = float(np.mean(np.minimum(np.abs(raw[nz]), cap))) if nz.any() else 0.0
return dict(orders_py=len(executed) / yrs, med_order=float(np.median(ex)),
p90_order=float(np.percentile(ex, 90)),
sub_min=n_skip / max(n_skip + len(executed), 1),
cap_bind=float(np.mean(np.abs(raw) > cap)),
invested=(mean_net / mean_raw) if mean_raw > 0 else 1.0,
mean_raw=mean_raw)
def section2():
print()
print("=" * 100)
print("(2) BOOK LIVE TP01+SKH01 75/25 — replica formula book.py su griglia 230m storica")
print(" net = clamp(0.5*C*(0.75*tp_frac + 0.25*skh_sign), +/-cap); equity fissa = C; min order $5")
print("=" * 100)
data = {}
for a in ASSETS:
ltf, sgn = skh_sign_series(a)
tpf = tp_frac_on_ltf(a, ltf)
data[a] = (ltf["timestamp"].values.astype("int64"), tpf, sgn)
print(f" [{a}] barre 230m: {len(sgn)} tempo con SKH aperto: {np.mean(sgn != 0) * 100:.1f}% "
f"tp_frac medio: {np.mean(np.maximum(tpf, 0)):.2f}")
out = {}
hdr = (f" {'capitale':>8} {'cap/asset':>10} {'asset':>5} {'ordini/anno':>12} {'medONot$':>9} "
f"{'p90$':>7} {'submin%':>8} {'capbind%':>9} {'investito%':>11}")
print(hdr)
for C in CAPITALS:
for cap, lbl in ((CAP_NOW, "300 (oggi)"), (C / 2.0, "C/2 (prop)")):
if C == 600.0 and cap != CAP_NOW:
continue # a 600 i due scenari coincidono
inv = []
for a in ASSETS:
ts, tpf, sgn = data[a]
r = book_sim(tpf, sgn, ts, C, cap)
inv.append(r["invested"])
print(f" {C:>8.0f} {lbl:>10} {a:>5} {r['orders_py']:>12.0f} {r['med_order']:>9.0f} "
f"{r['p90_order']:>7.0f} {r['sub_min'] * 100:>7.1f}% {r['cap_bind'] * 100:>8.1f}% "
f"{r['invested'] * 100:>10.1f}%")
out[(C, lbl)] = float(np.mean(inv))
print(" Lettura: 'investito%' = quota del target-notional desiderato che il cap/asset lascia")
print(" effettivamente a mercato (media sulle barre con target != 0). Col cap fermo a $300 il book")
print(" a 2-5k gira sotto-investito in modo strutturale; col cap = C/2 il rapporto attuale (300/600)")
print(" e' preservato e il vincolo torna a mordere solo sulle leve alte (tp_frac -> 2x).")
return out
# ===========================================================================
# (3) TRANCHING TP01 K=2/K=4 — eseguibilita' delle tranche a 2-5k
# ===========================================================================
def section3():
print()
print("=" * 100)
print("(3) TRANCHING TP01 (K ancore daily sfasate, 1/K del capitale per tranche)")
print(" ordine-tranche per asset = |Delta tgt| * C/(2K); eseguibile se >= $5")
print("=" * 100)
out = {}
dd = {}
for a in ASSETS:
d1 = al.get(a, "1d")
tgt = np.nan_to_num(TP.target_series(d1))
d = np.abs(np.diff(tgt, prepend=0.0))
dd[a] = d[d > 1e-12] # solo i giorni in cui un ribilancio e' desiderato
print(f" [{a}] |Delta tgt| giornaliero (giorni con cambio): mediana {np.median(dd[a]):.4f} "
f"p25 {np.percentile(dd[a], 25):.4f} p75 {np.percentile(dd[a], 75):.4f}")
alld = np.concatenate([dd[a] for a in ASSETS])
print(f" {'capitale':>8} {'K':>3} {'$/tranche-asset':>16} {'ordine mediano $':>17} "
f"{'exec-eventi%':>13} {'exec-turnover%':>15}")
for C in CAPITALS:
for K in (1, 2, 4):
pt = C / (2.0 * K)
mask = alld * pt >= MIN_ORDER
ex_ev = float(np.mean(mask)) # % degli EVENTI di ribilancio
ex_tw = float(alld[mask].sum() / alld.sum()) # % del TURNOVER (massa) eseguibile
med = float(np.median(alld)) * pt
out[(C, K)] = ex_tw
print(f" {C:>8.0f} {K:>3} {pt:>16.0f} {med:>17.2f} {ex_ev * 100:>12.1f}% {ex_tw * 100:>14.1f}%")
print(" Lettura: 'exec-eventi%' basso e' in parte FISIOLOGICO (i micro-ribilanci vol-target saltati")
print(" = la banda d'isteresi gratuita); il degrado vero e' 'exec-turnover%': la quota della MASSA")
print(" di ribilanciamento che ogni tranche riesce a eseguire (i cambi grossi = entrate/uscite).")
print(" Il 'degenera in K=1 a $600' del diario e' la granularita' EVENTO: l'ordine-tranche mediano")
print(" K=2 resta sotto $5 perfino a 5k ($3.7) -> le tranche non fanno il fine-tuning giornaliero,")
print(" ma da ~2k in su eseguono >95% della massa (entrate/uscite) ciascuna alla propria ancora.")
print(" NB: distribuzione |Delta| presa dall'ancora canonica (proxy: le altre ancore hanno")
print(" distribuzioni simili — r0702_tp01_offset). BLOCCO SEPARATO E INVARIATO: il tranching")
print(" richiede decisioni intraday (ancore != 00:00) => feed intraday FUORI dal path certificato")
print(" daily del cron attuale. Anche dove il min-order non degenera piu' K=2 in K=1, il deploy")
print(" resta condizionato a quel lavoro di feed/infra (diario 2026-07-02-timing-crt-wave).")
return out
# ===========================================================================
# (4) STATARB-RESID — haircut REAL ai 4 capitali (2 gambe, W=45/sgn=+1 congelati)
# ===========================================================================
def section4():
print()
print("=" * 100)
print("(4) STATARB-RESID (paper_statarb, config CONGELATA W=45 sgn=+1) — haircut min-order a scala")
print("=" * 100)
j = build_joint("1d")
pos = f_statarb_resid(W=45, sgn=+1)(j) # identica a paper_statarb (solo lettura parametri)
mod = eval_spread(j, pos)
print(f" modellato (2 gambe, fee 0.05%/lato x2): Sh FULL {mod['full']['sharpe']:.2f} "
f"turnover {mod['turnover']:.1f}x/anno (per gamba)")
out = {}
print(f" {'capitale':>8} {'Sh mod':>7} {'Sh real':>8} {'haircut':>8} {'trade eseguiti':>15}")
for C in CAPITALS:
r = eval_spread_smallcap(j, pos, capital=C, min_order=MIN_ORDER)
out[C] = r["sharpe_haircut"]
print(f" {C:>8.0f} {r['modeled']['sharpe']:>7.2f} {r['realistic']['sharpe']:>8.2f} "
f"{r['sharpe_haircut']:>8.3f} {r['n_executed_trades']:>15d}")
print(" (il vincolo binding e' il nozionale PER-GAMBA |Delta pos|*C >= $5; a 1d il turnover e'")
print(" bassissimo -> haircut gia' ~0 a $600, a 2-5k e' rumore. Il muro di statarb NON e' la scala:")
print(" e' l'EDGE — DSR 0.929 < 0.95, forward-monitor in corso.)")
return out
# ===========================================================================
# (5) OPZIONI DERIBIT — matematica statica del muro (VRP01 spread / min size)
# ===========================================================================
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)
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 section5():
print()
print("=" * 100)
print("(5) OPZIONI DERIBIT — pricing STATICO del muro (nessun backtest, nessuna proposta di deploy)")
print(" struttura VRP01: put credit spread 7g, short delta -0.28 / long -0.10, defined-risk")
print(" min size: 0.1 BTC/gamba, 1 ETH/gamba; fee 0.03% notional cap 12.5% premio (per gamba)")
print("=" * 100)
T = 7.0 / 365.25
res = {}
for a, minc in (("ETH", 1.0), ("BTC", 0.1)):
d1 = al.get(a, "1d")
S = float(d1["close"].iloc[-1])
dv = pd.read_parquet(ROOT / "data" / "raw" / f"dvol_{a.lower()}.parquet")
iv_last = float(dv["close"].iloc[-1]) / 100.0
iv_med = float(dv["close"].iloc[-365:].median()) / 100.0
rows = {}
for lbl, sig in (("DVOL oggi", iv_last), ("DVOL mediana 1y", iv_med)):
Ks = _strike_from_delta(S, T, sig, -0.28)
Kl = _strike_from_delta(S, T, sig, -0.10)
prem_s = _bs_put(S, Ks, T, sig) * minc
prem_l = _bs_put(S, Kl, T, sig) * minc
credit = prem_s - prem_l
width = (Ks - Kl) * minc
maxloss = width - credit # margine defined-risk ~ max loss
fee = (min(0.0003 * S * minc, 0.125 * prem_s)
+ min(0.0003 * S * minc, 0.125 * prem_l))
rows[lbl] = dict(S=S, sig=sig, credit=credit, width=width, maxloss=maxloss,
fee=fee, net=credit - fee)
print(f" [{a} x{minc}] {lbl}: spot ${S:,.0f} IV {sig * 100:.0f}% strike {Ks:,.0f}/{Kl:,.0f}"
f" credito ${credit:,.2f} width ${width:,.2f} margine/max-loss ${maxloss:,.2f}"
f" fee 2 gambe ${fee:,.2f} credito NETTO ${credit - fee:,.2f}"
f" ({(credit - fee) / max(credit, 1e-9) * 100:.0f}% del lordo)")
res[a] = rows["DVOL mediana 1y"]
print(f" + fee delivery a scadenza se ITM: 0.015% notional cap 12.5% (non inclusa sopra).")
m_eth = res["ETH"]["maxloss"]
m_btc = res["BTC"]["maxloss"]
print(f"\n {'capitale':>8} {'spread ETH conc. @12% peso':>27} {'@100% conto':>12} "
f"{'% conto/spread':>15}")
out = {}
for C in CAPITALS:
n12 = int((0.12 * C) // m_eth)
nfull = int(C // m_eth)
out[C] = n12
print(f" {C:>8.0f} {n12:>27d} {nfull:>12d} {m_eth / C * 100:>14.1f}%")
c_btc_1 = m_btc / 0.12
c_btc_3 = 3 * m_btc / 0.12
print(f"\n BTC options (0.1 BTC min): margine/max-loss ~${m_btc:,.0f}/spread -> a peso 12% servono"
f" ~${c_btc_1:,.0f} per 1 spread, ~${c_btc_3:,.0f} per granularita' minima (3 step di size).")
print(" Muro fee: il credito ETH sopravvive alle fee (cap 12.5% del premio ~ perdi al massimo un")
print(" quarto del credito con entry+delivery; il numero esatto sopra). Il muro VERO resta la regola")
print(" standing: NIENTE short-vol da modello in deploy (f di stress reale mai osservato).")
return out, m_eth, m_btc
# ===========================================================================
# (6) XS01 (19 gambe Hyperliquid) e CC01 — soglie di scala con conti espliciti
# ===========================================================================
def xs01_positions() -> tuple[pd.DatetimeIndex, np.ndarray]:
"""COPIA di sola-lettura della matrice pesi di src/portfolio/sleeves._xsec_returns
(stessi parametri XS_CFG / XS_UNIVERSE), che ritorna le POSIZIONI finali per gamba
P = W * scale(vol-target) invece dei rendimenti. Nessun modulo di produzione modificato."""
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE, _HL_DIR
cols = {}
for sym in XS_UNIVERSE:
p = _HL_DIR / 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))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
px = C.values
n, A = px.shape
lookbacks, H, k = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"]
mode, tv = 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]))
thr = np.percentile(disp_hist, disp_pct) if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf
if disp_i >= thr:
score = np.zeros(A)
cnt = 0
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)
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 C.index, W * scale[:, None]
def xs01_exec(P: np.ndarray, idx: pd.DatetimeIndex, sleeve_cap: float,
min_order: float = MIN_ORDER_HL) -> dict:
"""Skip sequenziale per gamba: un Delta di nozionale < min_order NON si esegue."""
n, A = P.shape
yrs = max((idx[-1] - idx[0]).days / 365.25, 1e-9)
tot_mod = 0.0
n_orders = 0
orders = []
for a in range(A):
tgt = P[:, a]
tot_mod += float(np.abs(np.diff(tgt, prepend=0.0)).sum())
cur = 0.0
for x in tgt:
d = abs(x - cur)
if d * sleeve_cap >= min_order:
orders.append(d * sleeve_cap)
cur = x
n_orders += 1
exec_turn = float(np.sum(orders) / sleeve_cap) if orders else 0.0
return dict(exec_share=exec_turn / tot_mod if tot_mod > 0 else 0.0,
orders_py=n_orders / yrs,
med_order=float(np.median(orders)) if orders else 0.0)
def section6():
print()
print("=" * 100)
print("(6) XS01 (19 gambe HL, min order ~$10) e CC01 — le soglie '~20k' ricontate")
print("=" * 100)
idx, P = xs01_positions()
gross = np.abs(P).sum(axis=1)
print(f" XS01: gross tipico {np.median(gross[gross > 0]):.2f}x del capitale sleeve; "
f"10 gambe attive (5 long + 5 short da 19), ribilancio ogni 10g + vol-target giornaliero")
print(f" {'capitale':>8} {'sleeve@15%':>11} {'exec-turnover%':>15} {'ordini/anno':>12} {'medONot$':>9}")
out = {}
for Ctot in list(CAPITALS) + [10000.0, 20000.0, 50000.0]:
sc = 0.15 * Ctot
r = xs01_exec(P, idx, sc)
out[Ctot] = r["exec_share"]
tag = " <- dichiarato" if Ctot == 20000 else ""
print(f" {Ctot:>8.0f} {sc:>11.0f} {r['exec_share'] * 100:>14.1f}% {r['orders_py']:>12.0f} "
f"{r['med_order']:>9.0f}{tag}")
print(" Lettura: exec-turnover% = quota del turnover modellato che supera il min-order $10 per gamba.")
print(" Sotto ~80-90% il libro reale diverge dal backtest (tracking error non modellato).")
print("\n CC01 (cash-and-carry HL, spot+perp stesso asset — Sharpe modellato = ARTEFATTO, v. diario):")
for Ctot in CAPITALS:
for N in (2, 4, 19):
D = 0.75 * Ctot # deploy carry: spot cash-funded + margine perp ~D/3
per_leg = D / (2 * N)
yld = (0.08 * D, 0.14 * D)
if N == 4:
print(f" C={Ctot:>5.0f} N={N:>2} asset ({2 * N} gambe): ${per_leg:,.0f}/gamba; "
f"funding 8-14% su ${D:,.0f} = ${yld[0]:,.0f}-{yld[1]:,.0f}/anno")
print(" -> il MIN-ORDER non e' il muro di CC01 gia' a 2k (gambe > $90); i muri sono strutturali:")
print(" (a) Sharpe 11-13 artefatto (manca il 2022: deleveraging/funding-negativo/basis blowout),")
print(" (b) funding HL non eseguibile da Deribit (secondo venue + travaso capitale),")
print(" (c) liquidazione short e slippage non modellati. A 5k il carry atteso ($300-525/anno")
print(" lordi, prociclico) NON paga il rischio operativo: resta LEAD da rivedere a ~20k+.")
return out
# ===========================================================================
# (7) SINTESI OPERATIVA
# ===========================================================================
def book_cagr_canonical() -> float:
"""CAGR canonico del book Deribit (0.75*TP01 + 0.25*SKH01, daily) — SOLO per confronto col
band de-luckato 10-15% (audit SKH01 2026-07-02: hourly-path + fase mediana)."""
from src.portfolio.sleeves import _skyhook_returns, _tp01_returns # sola lettura
tp = _tp01_returns()
sk = _skyhook_returns()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
J = pd.concat({"tp": tp, "sk": sk}, axis=1, join="inner").fillna(0.0)
r = 0.75 * J["tp"] + 0.25 * J["sk"]
eq = float(np.prod(1.0 + r.values))
yrs = len(r) / 365.25
return eq ** (1.0 / yrs) - 1.0
def section7(s1, s2, s3, s4, s5, s6):
n_eth_12, m_eth, m_btc = s5
print()
print("=" * 100)
print("(7) SINTESI OPERATIVA — capitale x vincoli (numeri dalle sezioni sopra)")
print("=" * 100)
hdr = (f" {'capitale':>8} | {'TP01 haircut':>12} | {'book inv.% cap300':>17} | {'K=2 turn%':>9} | "
f"{'statarb hc':>10} | {'spreadETH@12%':>13} | {'XS01 exec%':>10}")
print(hdr)
print(" " + "-" * (len(hdr) - 2))
for C in CAPITALS:
inv300 = s2.get((C, "300 (oggi)"), float("nan"))
print(f" {C:>8.0f} | {s1[C]['haircut']:>12.3f} | {inv300 * 100:>16.1f}% | "
f"{s3[(C, 2)] * 100:>8.1f}% | {s4[C]:>10.3f} | {n_eth_12[C]:>13d} | "
f"{s6[C] * 100:>9.1f}%")
try:
cagr = book_cagr_canonical()
print(f"\n CAGR canonico book Deribit (0.75 TP01 + 0.25 SKH01): {cagr * 100:.1f}%/anno "
f"(lens research, ancora canonica)")
except Exception as e: # pragma: no cover — il resto del report resta valido
print(f"\n (CAGR canonico book non calcolabile in questo run: {e})")
print(" ASPETTATIVA ONESTA (CAGR de-luckato 10-15%/anno — audit SKH01 2026-07-02, path orario +")
print(" fase mediana, NON i numeri canonici):")
for C in (2000.0, 5000.0):
lo, hi = C * 0.10 / 365.25, C * 0.15 / 365.25
print(f" a ${C:,.0f}: ~EUR {lo:.2f}-{hi:.2f}/giorno (=${C * 0.10:,.0f}-{C * 0.15:,.0f}/anno)")
print(" Regola di onesta' del progetto INVARIATA: EUR 50/giorno richiede ~130k di capitale a questo")
print(" CAGR — il funding a 2-5k NON cambia l'ordine di grandezza, cambia solo cosa e' ESEGUIBILE.")
print()
print(" RACCOMANDAZIONI CONFIG (SOLO PROPOSTE — config/live.json non viene toccato da questo script):")
print(" 1. max_notional_per_asset_usd: alzarlo INSIEME al funding mantenendo il rapporto attuale")
print(" cap = equity/2 -> $1000 a 2k, $1750 a 3.5k, $2500 a 5k. Col cap fermo a $300 il book")
print(" resta strutturalmente sotto-investito (vedi colonna 'book inv.% cap300' sopra).")
print(" 2. min_order_usd $5: lasciarlo (floor Deribit). Nessuna banda artificiale in piu': il fee")
print(" drag risale verso il modellato (~0.4%/anno max, sez.1) ed e' il costo corretto della")
print(" fedelta' al target (l'ondata timing ha mostrato che il lag costa piu' del risparmio).")
print(" 3. Tranching K=2: la matematica degli ordini regge da ~2k (exec-turnover >95%/tranche,")
print(" sez.3) e a 5k e' matura (98%), MA (a) il beneficio atteso e' solo riduzione di varianza")
print(" della STIMA (Delta-Sharpe n.s., diario timing) e (b) il blocco feed-intraday-fuori-path-")
print(" certificato resta intero -> NON cablarlo ora; rivalutare solo con feed intraday validato.")
print(" 4. Opzioni: a 3.5-5k il singolo spread ETH min-size e' sostenibile a peso ~12% (sez.5),")
print(" ma la regola standing 'niente short-vol da modello' NON decade col capitale.")
print(" 5. XS01/CC01: restano STAT-MODE/LEAD anche a 5k (sez.6). Soglia ~20k confermata per XS01")
print(" come ordine di grandezza; per CC01 il capitale non e' comunque il muro binding.")
def main():
s1 = section1()
s2 = section2()
s3 = section3()
s4 = section4()
s5 = section5()
s6 = section6()
section7(s1, s2, s3, s4, s5, s6)
if __name__ == "__main__":
main()
+692
View File
@@ -0,0 +1,692 @@
"""r0702_ell_channel — "tecnica del canale" Elliott (Ftaonline): falsificazione onesta.
FILONE (2026-07-02): l'unica parte pienamente meccanica/falsificabile del metodo Elliott
dell'analista Ftaonline:
- Swing meccanici (zigzag causale k*ATR): pivot 0 (origine), massimo "onda 1", minimo
"onda 2" (vincolo: sopra l'origine, altrimenti conteggio NULLO).
- Canale: retta 0 -> minimo onda 2; parallela dal massimo di onda 1.
- SEGNALE 1: close FUORI dal lato alto del canale = "onda 3" -> long al close della barra
di rottura; target = min(onda2) + 1.618 * ampiezza(onda 1); stop = min(onda 2).
- SEGNALE 2 (variante): rottura del massimo di onda 3 dopo un pivot di onda 4 che NON
sovrappone il territorio di onda 1 -> target = min(onda4) + 1.0 * ampiezza(onda 1).
- REGOLA DISCRIMINANTE: movimento che NON esce mai dal canale = correttivo (nessun trade;
segnale opposto alla violazione della base). Testata separatamente con null permutato.
- Speculare per lo short.
COVERAGE (scripts/research/alt/runs, sweep 104 famiglie 2026-06-20): BRK01 (Donchian LS/LF),
BRK02 (Donchian+chandelier), BRK03 (Keltner), BRK04 (Bollinger), BRK05 (ATR-range), BRK08
(NR7), BRK09 (inside-bar), BRK10 (squeeze) + SKH01 coprono la famiglia breakout-canale, ma
NESSUNO costruisce canali da pivot zigzag con vincoli d'onda e target 1.618 -> non identico,
pero' stessa famiglia: per giudizio si confronta ANCHE contro un Donchian a pari geometria
(stop = base canale, target = base + 1.618*larghezza) e pari frequenza di trade.
ONESTA':
- Pivot noti solo alla CONFERMA (reversal k*ATR dal running extreme); il canale usa solo
pivot confermati al tempo t. Guard: al.causality_ok (prefix-recompute).
- Entry a close[i] della barra che CHIUDE fuori dal canale (mai fill sull'estremo).
- Exit a target/stop/timeout ESEGUITE AL CLOSE della barra che li tocca (gap-through-stop
reale, lezione SKH01). Il fill-al-livello e' riportato SOLO come lens ottimista dichiarata.
- Fee 0.10% RT + sweep 0.00-0.20% (al.study_weights).
- Selezione cella IN-SAMPLE-ONLY + deflated Sharpe su TUTTA la griglia
(al.study_family_honest); marginale vs TP01 (al.study_marginal).
- 4h: banda d'ancora su offset 0/1/2/3h (regola anchor-luck 2026-07-02). Epoca ms
ESPLICITA nel resample (MAI DatetimeIndex.view("int64")).
- Hold-out 2025+ mai usato per selezionare. Timeout 150 barre FISSO (non cercato).
Output temporanei: scratchpad ell_c_*. Diario: da scrivere a valle (a cura del chiamante).
"""
from __future__ import annotations
import json
import sys
import time
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
SCRATCH = ("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad")
TFS = ("1d", "4h", "1h")
KS = (2.0, 3.0, 4.0) # zigzag: reversal = k * ATR(14)
VARIANTS = ("s1_long", "s1_ls", "s12_ls")
TIMEOUT = 150 # barre, fisso e dichiarato (non cercato)
ATR_WIN = 14
TGT1, TGT2 = 1.618, 1.0
SEED = 20260702
_ZZ_CACHE: dict = {}
_SIM_CACHE: dict = {}
def _dfkey(df: pd.DataFrame, asset: str):
return (asset, int(df["timestamp"].iloc[0]), int(df["timestamp"].iloc[-1]), len(df))
# ===========================================================================
# 1) ZIGZAG CAUSALE — pivot confermato SOLO quando il close ritraccia k*ATR
# dal running extreme. Ogni valore usa dati <= i (prefix-stable).
# ===========================================================================
def zigzag(df: pd.DataFrame, k: float, asset: str = "?"):
"""Ritorna lista di pivot confermati (conf_i, piv_i, piv_price, kind); kind +1=high,
-1=low. Un pivot e' utilizzabile solo da conf_i in poi."""
key = _dfkey(df, asset) + (k,)
if key in _ZZ_CACHE:
return _ZZ_CACHE[key]
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
a = al.atr(df, ATR_WIN)
n = len(c)
piv = []
dir_ = 0 # 0=unknown, +1=gamba su (caccio un high), -1=gamba giu
hi_v, hi_i = h[0], 0
lo_v, lo_i = l[0], 0
for i in range(1, n):
thr = k * a[i]
if dir_ >= 0 and h[i] > hi_v:
hi_v, hi_i = h[i], i
if dir_ <= 0 and l[i] < lo_v:
lo_v, lo_i = l[i], i
if dir_ >= 0 and c[i] < hi_v - thr:
piv.append((i, hi_i, float(hi_v), +1))
dir_ = -1
j0 = hi_i + 1
if j0 <= i:
seg = l[j0:i + 1]
off = int(np.argmin(seg))
lo_v, lo_i = float(seg[off]), j0 + off
else:
lo_v, lo_i = l[i], i
elif dir_ <= 0 and c[i] > lo_v + thr:
piv.append((i, lo_i, float(lo_v), -1))
dir_ = +1
j0 = lo_i + 1
if j0 <= i:
seg = h[j0:i + 1]
off = int(np.argmax(seg))
hi_v, hi_i = float(seg[off]), j0 + off
else:
hi_v, hi_i = h[i], i
_ZZ_CACHE[key] = piv
return piv
# ===========================================================================
# 2) STATE MACHINE canale Elliott — forward-only, un trade alla volta.
# ===========================================================================
def simulate(df: pd.DataFrame, k: float, variant: str, asset: str = "?") -> dict:
key = _dfkey(df, asset) + (k, variant)
if key in _SIM_CACHE:
return _SIM_CACHE[key]
piv = zigzag(df, k, asset)
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
n = len(c)
pos = np.zeros(n)
trades: list = []
events: list = []
cnt = dict(setups_long=0, setups_short=0, null_count=0, superseded=0)
allow_short = variant in ("s1_ls", "s12_ls")
allow_s2 = variant == "s12_ls"
LS = SS = None # setup long / short attivo
S2L = S2S = None # stato onda-5 (dopo S1 chiuso a target)
tr = None # trade aperto
pi = 0
for i in range(1, n):
# --- (a) gestione trade aperto: exit AL CLOSE della barra che tocca ------------
if tr is not None and i > tr["e"]:
reason = None
if tr["dir"] == +1:
if l[i] <= tr["stp"]:
reason = "stop" # stop prioritario se tocca entrambi
elif h[i] >= tr["tgt"]:
reason = "target"
else:
if h[i] >= tr["stp"]:
reason = "stop"
elif l[i] <= tr["tgt"]:
reason = "target"
if reason is None and i - tr["e"] >= TIMEOUT:
reason = "timeout"
if reason:
tr["x"], tr["exit_px"], tr["reason"] = i, float(c[i]), reason
trades.append(tr)
if allow_s2 and tr["sig"] == "S1" and reason == "target":
if tr["dir"] == +1:
S2L = dict(stage="w3h", P1p=tr["P1p"], amp=tr["amp"], after=tr["e"])
else:
S2S = dict(stage="w3l", P1p=tr["P1p"], amp=tr["amp"], after=tr["e"])
tr = None
pos[i] = 0.0
else:
pos[i] = tr["dir"]
# --- (b) pivot confermati a questa barra ----------------------------------------
while pi < len(piv) and piv[pi][0] == i:
_, p_i, p_px, kind = piv[pi]
pi += 1
if kind == -1: # nuovo pivot LOW
if S2L is not None and S2L.get("stage") == "w4l" and p_i > S2L["w3h_i"]:
if p_px > S2L["P1p"]: # onda 4 NON sovrappone onda 1
S2L["w4l_px"], S2L["stage"] = p_px, "brk"
else:
S2L = None
if S2S is not None and S2S.get("stage") == "w3l" and p_i > S2S["after"]:
S2S["w3l_px"], S2S["w3l_i"], S2S["stage"] = p_px, p_i, "w4h"
if pi >= 3:
t0, t1, t2 = piv[pi - 3], piv[pi - 2], piv[pi - 1]
if t0[3] == -1 and t1[3] == +1 and t2[3] == -1:
if t2[2] > t0[2]: # vincolo: onda 2 sopra l'origine
if LS is not None and not LS["done"]:
cnt["superseded"] += 1
m = (t2[2] - t0[2]) / (t2[1] - t0[1])
LS = dict(P0i=t0[1], P0p=t0[2], P1i=t1[1], P1p=t1[2],
P2i=t2[1], P2p=t2[2], m=m, conf=i, done=False)
cnt["setups_long"] += 1
else:
cnt["null_count"] += 1
LS = None
else: # nuovo pivot HIGH
if S2L is not None and S2L.get("stage") == "w3h" and p_i > S2L["after"]:
S2L["w3h_px"], S2L["w3h_i"], S2L["stage"] = p_px, p_i, "w4l"
if S2S is not None and S2S.get("stage") == "w4h" and p_i > S2S["w3l_i"]:
if p_px < S2S["P1p"]:
S2S["w4h_px"], S2S["stage"] = p_px, "brk"
else:
S2S = None
if pi >= 3:
t0, t1, t2 = piv[pi - 3], piv[pi - 2], piv[pi - 1]
if t0[3] == +1 and t1[3] == -1 and t2[3] == +1:
if t2[2] < t0[2]: # speculare: onda 2 sotto l'origine
if SS is not None and not SS["done"]:
cnt["superseded"] += 1
m = (t2[2] - t0[2]) / (t2[1] - t0[1])
SS = dict(P0i=t0[1], P0p=t0[2], P1i=t1[1], P1p=t1[2],
P2i=t2[1], P2p=t2[2], m=m, conf=i, done=False)
cnt["setups_short"] += 1
else:
cnt["null_count"] += 1
SS = None
# --- (c) monitoraggio setup + entry a close[i] ----------------------------------
if LS is not None and not LS["done"]:
up = LS["P1p"] + LS["m"] * (i - LS["P1i"])
base = LS["P0p"] + LS["m"] * (i - LS["P0i"])
if c[i] > up:
LS["done"] = True
events.append(dict(kind="impulse", side=+1, bar=i))
if tr is None:
amp = LS["P1p"] - LS["P0p"]
tr = dict(dir=+1, sig="S1", e=i, entry_px=float(c[i]),
tgt=LS["P2p"] + TGT1 * amp, stp=LS["P2p"],
P1p=LS["P1p"], amp=amp)
pos[i] = 1.0
elif c[i] < base:
LS["done"] = True
events.append(dict(kind="corrective", side=+1, bar=i))
if SS is not None and not SS["done"]:
dn = SS["P1p"] + SS["m"] * (i - SS["P1i"])
base = SS["P0p"] + SS["m"] * (i - SS["P0i"])
if c[i] < dn:
SS["done"] = True
events.append(dict(kind="impulse", side=-1, bar=i))
if tr is None and allow_short:
amp = SS["P0p"] - SS["P1p"]
tr = dict(dir=-1, sig="S1", e=i, entry_px=float(c[i]),
tgt=SS["P2p"] - TGT1 * amp, stp=SS["P2p"],
P1p=SS["P1p"], amp=amp)
pos[i] = -1.0
elif c[i] > base:
SS["done"] = True
events.append(dict(kind="corrective", side=-1, bar=i))
# --- (d) SEGNALE 2 (onda 5) -----------------------------------------------------
if allow_s2 and S2L is not None and S2L.get("stage") == "brk":
if c[i] < S2L["w4l_px"]:
S2L = None
elif c[i] > S2L["w3h_px"] and tr is None:
tr = dict(dir=+1, sig="S2", e=i, entry_px=float(c[i]),
tgt=S2L["w4l_px"] + TGT2 * S2L["amp"], stp=S2L["w4l_px"],
P1p=S2L["P1p"], amp=S2L["amp"])
pos[i] = 1.0
S2L = None
if allow_s2 and S2S is not None and S2S.get("stage") == "brk":
if c[i] > S2S["w4h_px"]:
S2S = None
elif c[i] < S2S["w3l_px"] and tr is None:
tr = dict(dir=-1, sig="S2", e=i, entry_px=float(c[i]),
tgt=S2S["w4h_px"] - TGT2 * S2S["amp"], stp=S2S["w4h_px"],
P1p=S2S["P1p"], amp=S2S["amp"])
pos[i] = -1.0
S2S = None
out = dict(pos=pos, trades=trades, events=events, counters=cnt)
_SIM_CACHE[key] = out
return out
def make_target(k: float, variant: str):
def target_fn(df, asset):
return simulate(df, k, variant, asset)["pos"]
return target_fn
def factory(tf=None, k=3.0, variant="s1_ls"):
# tf consumata da study_family_honest/candidate_daily (carica il df giusto)
return make_target(k, variant)
# ===========================================================================
# 3) COMPARATORE DONCHIAN "banale" a pari geometria (stop=base canale,
# target = base + 1.618*larghezza) — stesso engine close-exec.
# ===========================================================================
def donch_sim(df: pd.DataFrame, N: int, allow_short: bool, asset: str = "?") -> dict:
key = _dfkey(df, asset) + ("donch", N, allow_short)
if key in _SIM_CACHE:
return _SIM_CACHE[key]
hi, lo = al.donchian(df, N) # shiftati -> causali
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
n = len(c)
pos = np.zeros(n)
trades = []
tr = None
for i in range(1, n):
if tr is not None and i > tr["e"]:
reason = None
if tr["dir"] == +1:
if l[i] <= tr["stp"]:
reason = "stop"
elif h[i] >= tr["tgt"]:
reason = "target"
else:
if h[i] >= tr["stp"]:
reason = "stop"
elif l[i] <= tr["tgt"]:
reason = "target"
if reason is None and i - tr["e"] >= TIMEOUT:
reason = "timeout"
if reason:
tr["x"], tr["exit_px"], tr["reason"] = i, float(c[i]), reason
trades.append(tr)
tr = None
pos[i] = 0.0
else:
pos[i] = tr["dir"]
if tr is None and np.isfinite(hi[i]) and np.isfinite(lo[i]):
W = hi[i] - lo[i]
if c[i] > hi[i] and W > 0:
tr = dict(dir=+1, sig="D", e=i, entry_px=float(c[i]),
tgt=lo[i] + TGT1 * W, stp=lo[i])
pos[i] = 1.0
elif allow_short and c[i] < lo[i] and W > 0:
tr = dict(dir=-1, sig="D", e=i, entry_px=float(c[i]),
tgt=hi[i] - TGT1 * W, stp=hi[i])
pos[i] = -1.0
out = dict(pos=pos, trades=trades)
_SIM_CACHE[key] = out
return out
# ===========================================================================
# 4) 4h con ancora spostata (offset 0/1/2/3h) — epoca ms ESPLICITA.
# ===========================================================================
def get_4h_anchor(asset: str, off: int) -> pd.DataFrame:
g = al.get(asset, "1h").copy()
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
idx.name = "dt"
g.index = idx
out = g.resample("4h", label="left", closed="left",
offset=pd.Timedelta(hours=off)).agg(
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
out = out.dropna(subset=["open"])
out["datetime"] = out.index
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", "datetime"]]
# ===========================================================================
# 5) STATISTICHE: claim discriminante + target 1.618 vs null vol-matched.
# ===========================================================================
def discriminant_test(tf: str, k: float, H: int = 10, B: int = 2000) -> dict:
"""Claim: un movimento che NON esce dal canale (violazione base senza uscita alta) e'
correttivo -> follow-through nella direzione del conteggio NEGATIVO (reversal).
Statistica: media del forward-return H-barre ALLINEATO al conteggio, normalizzato ATR,
vs null di barre casuali (stessi segni). p_low piccolo => claim supportata."""
rng = np.random.default_rng(SEED)
out = {}
for a in al.CERTIFIED:
df = al.get(a, tf)
sim = simulate(df, k, "s1_ls", a)
c = df["close"].values.astype(float)
atr_ = al.atr(df, ATR_WIN)
n = len(c)
f = np.full(n, np.nan)
f[:n - H] = (c[H:] / c[:n - H] - 1.0) / np.maximum(atr_[:n - H] / c[:n - H], 1e-9) / np.sqrt(H)
res = {}
for kind in ("corrective", "impulse"):
ev = [(e["bar"], e["side"]) for e in sim["events"]
if e["kind"] == kind and e["bar"] + H < n and e["bar"] >= 50]
if len(ev) < 5:
res[kind] = dict(n=len(ev), mean=None, p_low=None, p_high=None)
continue
bars = np.array([b for b, _ in ev])
sides = np.array([s for _, s in ev], float)
obs = float(np.mean(f[bars] * sides))
valid = np.arange(50, n - H - 1)
draws = rng.choice(valid, size=(B, len(ev)))
null = (f[draws] * sides[None, :]).mean(axis=1)
res[kind] = dict(n=len(ev), mean=round(obs, 4),
null_mean=round(float(np.mean(null)), 4),
p_low=round(float(np.mean(null <= obs)), 4),
p_high=round(float(np.mean(null >= obs)), 4))
out[a] = res
return out
def _first_touch(h, l, s, dirn, tgt, stp):
wh = h[s + 1:s + 1 + TIMEOUT]
wl = l[s + 1:s + 1 + TIMEOUT]
if dirn == +1:
mT, mS = wh >= tgt, wl <= stp
else:
mT, mS = wl <= tgt, wh >= stp
iT = int(np.argmax(mT)) if mT.any() else 10 ** 9
iS = int(np.argmax(mS)) if mS.any() else 10 ** 9
return 1 if iT < iS else 0 # pari barra -> stop prioritario (come l'engine)
def target_hit_vs_null(tf: str, k: float, variant: str, B: int = 300) -> dict:
"""Freq. con cui il target 1.618 viene toccato prima dello stop nei trade REALI vs
null: stessa geometria (distanze % target/stop, direzione, timeout) da barre CASUALI.
p_high piccolo => la struttura d'onda tempa meglio del caso a parita' di geometria."""
rng = np.random.default_rng(SEED + 1)
out = {}
for a in al.CERTIFIED:
df = al.get(a, tf)
sim = simulate(df, k, variant, a)
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
n = len(c)
trs = [t for t in sim["trades"] if t["sig"] == "S1"]
if len(trs) < 5:
out[a] = dict(n=len(trs), real=None)
continue
real = float(np.mean([1 if t["reason"] == "target" else 0 for t in trs]))
geo = [(t["dir"], abs(t["tgt"] / t["entry_px"] - 1.0),
abs(1.0 - t["stp"] / t["entry_px"])) for t in trs]
if len(geo) > 300: # cap dichiarato per il bootstrap (compute)
sel = rng.choice(len(geo), size=300, replace=False)
geo = [geo[int(s)] for s in sel]
nulls = np.empty(B)
lo_s, hi_s = 50, n - TIMEOUT - 2
for b in range(B):
starts = rng.integers(lo_s, hi_s, size=len(geo))
hits = 0
for (dirn, dT, dS), s in zip(geo, starts):
e = c[s]
if dirn == +1:
hits += _first_touch(h, l, s, +1, e * (1 + dT), e * (1 - dS))
else:
hits += _first_touch(h, l, s, -1, e * (1 - dT), e * (1 + dS))
nulls[b] = hits / len(geo)
out[a] = dict(n=len(trs), real=round(real, 3),
null_mean=round(float(np.mean(nulls)), 3),
p_high=round(float(np.mean(nulls >= real)), 4),
p_low=round(float(np.mean(nulls <= real)), 4))
return out
# ===========================================================================
# 6) LENS OTTIMISTA (fill al livello) — dichiarata, solo per confronto.
# ===========================================================================
def lens_compare(tf: str, k: float, variant: str, fee_rt: float = 0.001) -> dict:
out = {}
for a in al.CERTIFIED:
df = al.get(a, tf)
trs = simulate(df, k, variant, a)["trades"]
if not trs:
out[a] = dict(n=0)
continue
close_r, level_r = [], []
for t in trs:
rc = t["dir"] * (t["exit_px"] / t["entry_px"] - 1.0) - fee_rt
if t["reason"] == "target":
px = t["tgt"]
elif t["reason"] == "stop":
px = t["stp"]
else:
px = t["exit_px"]
rl = t["dir"] * (px / t["entry_px"] - 1.0) - fee_rt
close_r.append(rc)
level_r.append(rl)
out[a] = dict(n=len(trs),
exp_close=round(float(np.mean(close_r)), 5),
exp_level=round(float(np.mean(level_r)), 5),
tot_close=round(float(np.prod(1 + np.array(close_r)) - 1), 4),
tot_level=round(float(np.prod(1 + np.array(level_r)) - 1), 4))
return out
# ===========================================================================
# MAIN
# ===========================================================================
def main():
t0 = time.time()
report = []
def say(s=""):
print(s, flush=True)
report.append(s)
say("=" * 88)
say("r0702_ell_channel — canale Elliott (Ftaonline), falsificazione onesta")
say("=" * 88)
# ---------- TABELLA CELLE ----------------------------------------------------------
rows = []
say("\n[1] TABELLA CELLE (TF x k x variante) — net 0.10% RT, exit al close (onesto)")
hdr = (f"{'tf':>4} {'k':>3} {'variante':>8} | "
f"{'BTC f/h':>12} {'nT':>4} {'DD%':>5} | {'ETH f/h':>12} {'nT':>4} {'DD%':>5} | "
f"{'COMB full':>9} {'hold':>6} {'inS':>6} {'DD%':>5}")
say(hdr)
say("-" * len(hdr))
for tf in TFS:
for k in KS:
for v in VARIANTS:
per, parts = {}, {}
for a in al.CERTIFIED:
df = al.get(a, tf)
sim = simulate(df, k, v, a)
ev = al.eval_weights(df, sim["pos"])
per[a] = dict(full=ev["full"], hold=ev["holdout"],
ntr=len(sim["trades"]), cnt=sim["counters"])
parts[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(parts, axis=1, join="inner").fillna(0.0)
comb = al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"])
ins = comb[comb.index < al.HOLDOUT]
hold = comb[comb.index >= al.HOLDOUT]
r = dict(tf=tf, k=k, var=v,
btc_full=per["BTC"]["full"]["sharpe"],
btc_hold=per["BTC"]["hold"].get("sharpe", 0.0),
btc_dd=per["BTC"]["full"]["maxdd"], btc_n=per["BTC"]["ntr"],
eth_full=per["ETH"]["full"]["sharpe"],
eth_hold=per["ETH"]["hold"].get("sharpe", 0.0),
eth_dd=per["ETH"]["full"]["maxdd"], eth_n=per["ETH"]["ntr"],
comb_full=round(al._sh(comb), 3), comb_hold=round(al._sh(hold), 3),
comb_ins=round(al._sh(ins), 3), comb_dd=round(al._dd_ret(comb), 4),
counters=dict(BTC=per["BTC"]["cnt"], ETH=per["ETH"]["cnt"]))
rows.append(r)
flag = " (<30 trade!)" if min(r["btc_n"], r["eth_n"]) < 30 else ""
say(f"{tf:>4} {k:>3.0f} {v:>8} | "
f"{r['btc_full']:>+5.2f}/{r['btc_hold']:>+5.2f} {r['btc_n']:>4d} "
f"{r['btc_dd']*100:>5.1f} | "
f"{r['eth_full']:>+5.2f}/{r['eth_hold']:>+5.2f} {r['eth_n']:>4d} "
f"{r['eth_dd']*100:>5.1f} | "
f"{r['comb_full']:>+9.2f} {r['comb_hold']:>+6.2f} {r['comb_ins']:>+6.2f} "
f"{r['comb_dd']*100:>5.1f}{flag}")
say(f"\n (tempo tabella: {time.time()-t0:.0f}s)")
# ---------- FAMILY HONEST (selezione in-sample + deflated Sharpe) ------------------
say("\n[2] study_family_honest — cella scelta IN-SAMPLE-ONLY + DSR su tutta la griglia")
grid = [dict(k=k, variant=v) for k in KS for v in VARIANTS]
fam = al.study_family_honest("ELLCH", factory, grid, TFS)
ch = fam["chosen"]
say(f" celle valutate: {fam['n_cells']} cella in-sample: tf={ch['tf']} "
f"params={ch['params']} (inS Sharpe {ch['insample_sharpe']}, full {ch['full_sharpe']})")
say(f" deflated Sharpe = {fam['deflated_sharpe']} (null max atteso "
f"{fam['expected_null_max']}) dsr_pass={fam['dsr_pass']}")
say(f" earns_slot_marginal={fam['earns_slot_marginal']} "
f"EARNS_SLOT_HONEST={fam['earns_slot_honest']}")
say(al.fmt_marginal(fam["marginal"]))
ck, cv, ctf = ch["params"]["k"], ch["params"]["variant"], ch["tf"]
# ---------- CAUSALITA' -------------------------------------------------------------
say("\n[3] causality_ok (prefix-recompute) sulla cella scelta")
for tf_chk in {ctf, "1h"}:
cz = al.causality_ok(make_target(ck, cv), tf=tf_chk)
say(f" tf={tf_chk}: ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} "
f"checked={cz['checked']}")
# ---------- FEE SWEEP + SMALLCAP sulla cella scelta ---------------------------------
say("\n[4] fee sweep 0.00-0.20% RT + haircut small-cap ($600, min order $5) — cella scelta")
sw = al.study_weights(f"ELLCH k={ck} {cv}", make_target(ck, cv), tfs=(ctf,))
cell = sw["cells"][0]
for a in al.CERTIFIED:
say(f" {a}: fee_sweep={cell['per_asset'][a]['fee_sweep']}")
df = al.get(a, ctf)
sc = al.eval_weights_smallcap(df, simulate(df, ck, cv, a)["pos"])
say(f" smallcap: modeled Sh {sc['modeled']['sharpe']} -> real "
f"{sc['realistic']['sharpe']} (haircut {sc['sharpe_haircut']}, "
f"{sc['n_executed_trades']} ordini)")
# ---------- COMPARATORE DONCHIAN ----------------------------------------------------
say("\n[5] Donchian 'banale' a pari geometria (stop=base, target=base+1.618*W), "
"close-exec, stesso timeout")
donch_rows = []
allow_short = cv in ("s1_ls", "s12_ls")
for tf in TFS:
ell_r = next(r for r in rows if r["tf"] == tf and r["k"] == ck and r["var"] == cv)
ell_n = (ell_r["btc_n"] + ell_r["eth_n"]) / 2
for N in (20, 55, 100):
per, parts = {}, {}
for a in al.CERTIFIED:
df = al.get(a, tf)
sim = donch_sim(df, N, allow_short, a)
ev = al.eval_weights(df, sim["pos"])
per[a] = dict(full=ev["full"]["sharpe"], hold=ev["holdout"].get("sharpe", 0.0),
ntr=len(sim["trades"]))
parts[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(parts, axis=1, join="inner").fillna(0.0)
comb = al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"])
dn = (per["BTC"]["ntr"] + per["ETH"]["ntr"]) / 2
donch_rows.append(dict(tf=tf, N=N, per=per, ntr=dn,
comb_full=round(al._sh(comb), 3),
comb_hold=round(al._sh(comb[comb.index >= al.HOLDOUT]), 3),
comb=comb, freq_gap=abs(dn - ell_n)))
best = min([d for d in donch_rows if d["tf"] == tf], key=lambda d: d["freq_gap"])
# corr Elliott(cella scelta k,cv su questo tf) vs Donchian matched
parts_e = {}
for a in al.CERTIFIED:
df = al.get(a, tf)
ev = al.eval_weights(df, simulate(df, ck, cv, a)["pos"])
parts_e[a] = pd.Series(ev["net"], index=ev["idx"])
JE = pd.concat(parts_e, axis=1, join="inner").fillna(0.0)
comb_e = al._to_daily(0.5 * JE["BTC"] + 0.5 * JE["ETH"])
JJ = pd.concat({"E": comb_e, "D": best["comb"]}, axis=1, join="inner").dropna()
corr = round(float(JJ["E"].corr(JJ["D"])), 3) if len(JJ) > 30 else None
for d in [x for x in donch_rows if x["tf"] == tf]:
mark = " <-- freq-matched" if d is best else ""
say(f" {tf:>4} N={d['N']:>3d}: comb full {d['comb_full']:>+5.2f} hold "
f"{d['comb_hold']:>+5.2f} nT/asset~{d['ntr']:.0f}{mark}")
say(f" {tf:>4} ELLIOTT (k={ck:.0f},{cv}): comb full {ell_r['comb_full']:>+5.2f} "
f"hold {ell_r['comb_hold']:>+5.2f} nT/asset~{ell_n:.0f} "
f"corr(Elliott,Donch-matched)={corr}")
# ---------- BANDA D'ANCORA 4h -------------------------------------------------------
say("\n[6] Banda d'ancora 4h (offset 0/1/2/3h) — cella 4h migliore IN-SAMPLE")
r4 = [r for r in rows if r["tf"] == "4h"]
best4 = max(r4, key=lambda r: r["comb_ins"])
say(f" cella 4h in-sample: k={best4['k']:.0f} var={best4['var']} "
f"(inS {best4['comb_ins']}, full {best4['comb_full']}, hold {best4['comb_hold']})")
anchor = {}
for off in (0, 1, 2, 3):
parts = {}
for a in al.CERTIFIED:
df = get_4h_anchor(a, off)
sim = simulate(df, best4["k"], best4["var"], f"{a}@+{off}h")
ev = al.eval_weights(df, sim["pos"])
parts[a] = pd.Series(ev["net"], index=ev["idx"])
J = pd.concat(parts, axis=1, join="inner").fillna(0.0)
comb = al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"])
anchor[off] = dict(full=round(al._sh(comb), 3),
hold=round(al._sh(comb[comb.index >= al.HOLDOUT]), 3))
say(f" offset +{off}h: comb full {anchor[off]['full']:>+5.2f} "
f"hold {anchor[off]['hold']:>+5.2f}")
fulls = [v["full"] for v in anchor.values()]
holds = [v["hold"] for v in anchor.values()]
say(f" banda full [{min(fulls):+.2f},{max(fulls):+.2f}] "
f"hold [{min(holds):+.2f},{max(holds):+.2f}]")
# ---------- CLAIM DISCRIMINANTE -----------------------------------------------------
say("\n[7] Claim discriminante: 'mai fuori dal canale = correttivo' (fwd 10 barre "
"allineato al conteggio, ATR-norm, null permutato B=2000)")
disc = {}
for tf in TFS:
disc[tf] = discriminant_test(tf, ck)
for a in al.CERTIFIED:
d = disc[tf][a]
co, im = d["corrective"], d["impulse"]
say(f" {tf:>4} {a}: corrective n={co['n']} mean={co.get('mean')} "
f"(null {co.get('null_mean')}) p_low={co.get('p_low')} | "
f"impulse n={im['n']} mean={im.get('mean')} "
f"(null {im.get('null_mean')}) p_high={im.get('p_high')}")
# ---------- TARGET 1.618 vs NULL ----------------------------------------------------
say("\n[8] Target 1.618 toccato prima dello stop: freq reale vs null vol/geometry-"
"matched (B=300 bootstrap, stessa distanza %/direzione/timeout da barre casuali)")
thit = {}
for tf in TFS:
thit[tf] = target_hit_vs_null(tf, ck, cv)
for a in al.CERTIFIED:
t = thit[tf][a]
if t.get("real") is None:
say(f" {tf:>4} {a}: n={t['n']} (troppo pochi trade)")
else:
say(f" {tf:>4} {a}: n={t['n']} real={t['real']} null={t['null_mean']} "
f"p_high={t['p_high']} p_low={t['p_low']}")
# ---------- LENS OTTIMISTA ----------------------------------------------------------
say("\n[9] Lens fill-al-livello (OTTIMISTA, dichiarata) vs close-exec — cella scelta, "
f"tf={ctf}")
lc = lens_compare(ctf, ck, cv)
for a in al.CERTIFIED:
d = lc[a]
if d.get("n", 0) == 0:
say(f" {a}: 0 trade")
else:
say(f" {a}: n={d['n']} expectancy/trade close={d['exp_close']:+.4f} "
f"level={d['exp_level']:+.4f} tot close={d['tot_close']:+.2%} "
f"level={d['tot_level']:+.2%}")
say(f"\n(tempo totale {time.time()-t0:.0f}s)")
# ---------- SALVATAGGI ---------------------------------------------------------------
donch_save = [{kk: vv for kk, vv in d.items() if kk not in ("comb", "freq_gap")}
for d in donch_rows]
payload = dict(rows=rows, family=al._clean(fam), donchian=al._clean(donch_save),
anchor_4h=anchor, discriminant=disc, target_hit=thit, lens=lc,
chosen=dict(tf=ctf, k=ck, variant=cv))
with open(f"{SCRATCH}/ell_c_results.json", "w") as f:
json.dump(al._clean(payload), f, default=str, indent=1)
with open(f"{SCRATCH}/ell_c_report.txt", "w") as f:
f.write("\n".join(report))
say(f"salvato: {SCRATCH}/ell_c_results.json + ell_c_report.txt")
if __name__ == "__main__":
main()
+562
View File
@@ -0,0 +1,562 @@
"""r0702_ell_fibconfluence.py — FIBONACCI CONFLUENCE: il prezzo reagisce ai livelli Fib
piu' che a livelli QUALSIASI? (2026-07-02, filone ELL-B)
CLAIM (video didattico, Casario): proiettando su swing di prezzo griglie di ritracciamento
(0.382/0.5/0.618) ed estensione (1.272/1.414/1.618/2.618) di Fibonacci, i livelli e in
particolare le CONFLUENZE dove due griglie convergono sarebbero zone di reazione del prezzo.
Claim testabile: la reazione ai livelli Fib batte quella a livelli con rapporti CASUALI
proiettati sugli STESSI swing? IL NULL E' TUTTO: senza placebo, "il prezzo ha reagito al 61.8%"
e' solo apofenia (qualunque livello dentro il range viene "toccato e rispettato" a volte).
METODO (tutto a priori, dichiarato PRIMA di guardare i numeri):
* Swing MECCANICI e causali: zigzag a soglia k*ATR14 (k in {2,4}), NON-REPAINTING un pivot
e' confermato solo quando close si e' mosso k*ATR dall'estremo; i livelli dello swing
(A->B) sono utilizzabili solo da confirm_idx+1 (timestamp di conferma esplicito).
* Griglia attiva per finestra [confirm(B)+1, confirm(pivot successivo)]: ritracciamenti
L = pB - r*M e estensioni L = pA + e*M (M = pB-pA). Finestre disgiunte per costruzione.
* TOCCO = low<=L<=high con la barra precedente che NON conteneva L (fresh touch); direzione
attesa dal lato di approccio (close[i-1]>L -> supporto -> atteso rimbalzo SU; <L -> giu').
Decisione a close[i] (high/low noti a fine barra), reazione misurata da close[i] a
close[i+H], H in {5,20} barre ESEGUIBILE (niente entry sugli estremi). La variante
"fill al livello" (da L invece che da close) e' riportata SOLO come lens ottimista.
* Statistica primaria: media della reazione segnata NORMALIZZATA per ATR (react/(ATR/close))
sui tocchi IN-SAMPLE (pre-2025); raw bps riportati. Il drift da buy-the-dip e' identico
per Fib e placebo -> il percentile vs placebo lo neutralizza.
* NULL (a): 100 set di rapporti PLACEBO fissi 3 ritracciamenti uniformi in [0.2,0.9] +
4 estensioni uniformi in [1.05,3.0], esclusa banda max(0.02, 2%rel) attorno ai Fib veri
proiettati sugli STESSI swing. NULL (b): 100 repliche con rapporti casuali RI-SORTEGGIATI
per ogni swing (stessa densita', nessuna coerenza cross-swing). NULL (c) — AGGIUNTO dopo
il run 1 come indurimento dichiarato: 100 set LOCATION-MATCHED (ogni rapporto = Fib vero
+/- jitter appena fuori la banda esclusa, entro ~6-8%) il null uniforme (a) non e'
density-matched (la reazione media dipende da DOVE sta il rapporto, non solo da quale
numero e'), quindi (c) e' il test affilato di "0.618 e' speciale vs 0.58/0.66".
Verdetto = percentile del Fib vero nelle tre distribuzioni placebo.
* CONFLUENZA: ritracciamenti dello swing corrente x estensioni dello swing s-2 (stesso
verso, il precedente ha verso opposto e proietta sempre FUORI dal range corrente bug
geometrico del run 1, corretto e dichiarato) entro eps=0.25*ATR(w0); reazione in zona
confluente vs livelli singoli delle stesse griglie, stesso placebo.
* MULTIPLE TESTING: 16 celle (2 asset x 2 TF x 2 k x 2 H). Soglia single-cell Bonferroni
~0.997; verdetto famiglia (a priori): esiste (k,H) con percentile POOLED IS >= 0.99 su
ENTRAMBI i null E tutte le 4 celle asset x TF a pctl >= 0.90. Solo allora si strategizza
(fade ai livelli, fee 0.10% RT) con al.study_family_honest + al.study_marginal.
* Hold-out 2025-26 MAI usato per selezionare (qui non si seleziona nulla: rapporti fissati
dal claim; k/H riportati per cella, il verdetto e' sul pooled IS).
CAUSALITA': pivot ricalcolati su prefisso troncato == pivot full con confirm<cut (check [0]);
niente DatetimeIndex.view("int64") (mai usato; date via pandas tz-aware).
Uso: uv run python scripts/research/r0702_ell_fibconfluence.py
"""
from __future__ import annotations
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
import numpy as np # noqa: E402
import pandas as pd # noqa: E402
# ----------------------------- costanti (a priori) --------------------------
SEED = 20260702
ASSETS = ("BTC", "ETH")
TFS = ("1h", "1d")
K_GRID = (2.0, 4.0) # soglia zigzag = k * ATR14
ATR_WIN = 14
RET_TRUE = (0.382, 0.5, 0.618)
EXT_TRUE = (1.272, 1.414, 1.618, 2.618)
HORIZONS = (5, 20) # barre di reazione
N_PA = 100 # placebo (a): set di rapporti fissi
N_PB = 100 # placebo (b): rapporti ri-sorteggiati per swing
N_PC = 100 # placebo (c): set fissi LOCATION-MATCHED (jitter attorno ai Fib)
CONF_EPS_ATR = 0.25 # tolleranza confluenza = 0.25 * ATR14(w0)
MIN_CELL_TOUCH = 50 # tocchi IS minimi perche' una cella conti nel verdetto
MIN_CONF_TOUCH = 30
RET_RANGE = (0.20, 0.90)
EXT_RANGE = (1.05, 3.00)
NL = len(RET_TRUE) + len(EXT_TRUE) # 7 livelli per griglia
N_SETS = 1 + N_PA + N_PB + N_PC # riga 0 = Fib vero
# ===========================================================================
# ZIGZAG causale non-repainting + segmenti
# ===========================================================================
def zigzag_pivots(df: pd.DataFrame, k_atr: float, atr_win: int = ATR_WIN) -> list:
"""Pivot alternati H/L: (pivot_idx, price, type +1=high/-1=low, confirm_idx).
Un pivot e' confermato quando close si muove k*ATR14[i] oltre l'estremo corrente.
Causale: usa solo dati <= i; il pivot NON viene mai spostato dopo la conferma."""
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
a = al.atr(df, atr_win)
n = len(c)
piv: list = []
start = atr_win
if n <= start + 2:
return piv
ehi, ehi_i = h[start], start
elo, elo_i = l[start], start
dir_ = 0
i = start + 1
while i < n and dir_ == 0: # bootstrap: direzione ignota
if h[i] > ehi:
ehi, ehi_i = h[i], i
if l[i] < elo:
elo, elo_i = l[i], i
if c[i] <= ehi - k_atr * a[i]:
piv.append((ehi_i, float(ehi), +1, i))
dir_ = -1
seg = l[ehi_i:i + 1]
elo_i = ehi_i + int(np.argmin(seg)); elo = float(seg.min())
elif c[i] >= elo + k_atr * a[i]:
piv.append((elo_i, float(elo), -1, i))
dir_ = +1
seg = h[elo_i:i + 1]
ehi_i = elo_i + int(np.argmax(seg)); ehi = float(seg.max())
i += 1
while i < n:
if dir_ == -1: # in discesa: cerco swing low
if l[i] < elo:
elo, elo_i = l[i], i
if c[i] >= elo + k_atr * a[i]:
piv.append((elo_i, float(elo), -1, i))
dir_ = +1
seg = h[elo_i:i + 1]
ehi_i = elo_i + int(np.argmax(seg)); ehi = float(seg.max())
else: # in salita: cerco swing high
if h[i] > ehi:
ehi, ehi_i = h[i], i
if c[i] <= ehi - k_atr * a[i]:
piv.append((ehi_i, float(ehi), +1, i))
dir_ = -1
seg = l[ehi_i:i + 1]
elo_i = ehi_i + int(np.argmin(seg)); elo = float(seg.min())
i += 1
return piv
def build_segments(piv: list, n: int) -> list:
"""Segmenti: swing A->B (pivot s-1 -> s), griglia attiva in [confirm(B)+1, confirm(next)].
prev2 = (qA, qB) dello swing s-2 (STESSO verso del corrente, per la confluenza: lo swing
s-1 ha verso opposto e le sue estensioni proiettano sempre fuori dal range corrente)."""
segs = []
for s in range(1, len(piv)):
A, B = piv[s - 1], piv[s]
w0 = B[3] + 1
w1 = piv[s + 1][3] if s + 1 < len(piv) else n - 1
prev2 = (piv[s - 3][1], piv[s - 2][1]) if s >= 3 else None
segs.append(dict(w0=w0, w1=w1, pA=A[1], pB=B[1], prev2=prev2))
return segs
# ===========================================================================
# RATIO SET (vero + placebo)
# ===========================================================================
def _draw_valid(rng, lo: float, hi: float, size, forbid) -> np.ndarray:
"""Uniformi in [lo,hi] con banda esclusa max(0.02, 2% relativo) attorno ai Fib veri."""
out = rng.uniform(lo, hi, size=size)
for _ in range(200):
bad = np.zeros(out.shape, bool)
for f in forbid:
bad |= np.abs(out - f) < max(0.02, 0.02 * f)
if not bad.any():
break
out[bad] = rng.uniform(lo, hi, size=int(bad.sum()))
return out
def make_placebo_a(rng):
"""(N_PA,3) ritracciamenti + (N_PA,4) estensioni: set FISSI cross-swing."""
r = _draw_valid(rng, *RET_RANGE, (N_PA, len(RET_TRUE)), RET_TRUE)
e = _draw_valid(rng, *EXT_RANGE, (N_PA, len(EXT_TRUE)), EXT_TRUE)
return r, e
def make_placebo_b(rng, n_seg: int):
"""(n_seg,N_PB,3) + (n_seg,N_PB,4): rapporti RI-SORTEGGIATI per ogni swing."""
r = _draw_valid(rng, *RET_RANGE, (n_seg, N_PB, len(RET_TRUE)), RET_TRUE)
e = _draw_valid(rng, *EXT_RANGE, (n_seg, N_PB, len(EXT_TRUE)), EXT_TRUE)
return r, e
def make_placebo_c(rng):
"""Set fissi LOCATION-MATCHED: ogni rapporto = Fib vero +/- jitter appena FUORI la banda
esclusa (max(0.02,2%rel)) ed entro ~6-8% -> vicini di casa dei Fib. Se i Fib sono davvero
speciali (il mercato ancora ESATTAMENTE a 0.618), devono battere anche questi."""
def jit(true_vals, lo, hi):
f = np.array(true_vals, float)[None, :]
band = np.maximum(0.02, 0.02 * f)
delta = band + rng.uniform(0.0, 0.06 * np.maximum(1.0, f), size=(N_PC, f.shape[1]))
sign = rng.choice([-1.0, 1.0], size=(N_PC, f.shape[1]))
return np.clip(f + sign * delta, lo, hi)
return jit(RET_TRUE, *RET_RANGE), jit(EXT_TRUE, *EXT_RANGE)
# ===========================================================================
# TOUCH SCAN vettoriale + accumulo
# ===========================================================================
def _touch_scan(h, l, c, w0: int, w1: int, L: np.ndarray, ids: np.ndarray):
"""Fresh touch dei livelli L nella finestra [w0,w1]: ritorna (bar globali, direzione
attesa dal lato di approccio, livello, id-set). Direzione: sign(close[i-1] - L)."""
lo = l[w0:w1 + 1]; hi = h[w0:w1 + 1]
contains = (lo[:, None] <= L[None, :]) & (hi[:, None] >= L[None, :])
fresh = contains.copy()
fresh[1:] &= ~contains[:-1]
fresh[0] &= ~((l[w0 - 1] <= L) & (h[w0 - 1] >= L))
tt, kk = np.nonzero(fresh)
if not len(tt):
z = np.zeros(0)
return z.astype(int), z, z, z.astype(int)
gi = w0 + tt
Lk = L[kk]
dirs = np.sign(c[gi - 1] - Lk)
m = dirs != 0
return gi[m], dirs[m], Lk[m], ids[kk[m]]
def _new_store():
return dict(
acc={(H, s): dict(sn=np.zeros(N_SETS), sr=np.zeros(N_SETS),
sl=np.zeros(N_SETS), c=np.zeros(N_SETS))
for H in HORIZONS for s in ("IS", "FULL")},
brk={s: dict(b=np.zeros(N_SETS), t=np.zeros(N_SETS)) for s in ("IS", "FULL")},
)
def _acc_touches(store, gi, dirs, Lk, sid, c, a14, is_pre, n):
if not len(gi):
return
relatr = a14[gi] / c[gi]
ok = np.isfinite(relatr) & (relatr > 0)
gi, dirs, Lk, sid, relatr = gi[ok], dirs[ok], Lk[ok], sid[ok], relatr[ok]
if not len(gi):
return
broke = ((dirs > 0) & (c[gi] < Lk)) | ((dirs < 0) & (c[gi] > Lk))
for scope, sm in (("FULL", np.ones(len(gi), bool)), ("IS", is_pre[gi])):
b = store["brk"][scope]
np.add.at(b["t"], sid[sm], 1.0)
np.add.at(b["b"], sid[sm & broke], 1.0)
for H in HORIZONS:
m = sm & ((gi + H) < n)
if not m.any():
continue
g2, d2 = gi[m], dirs[m]
react = d2 * (c[g2 + H] / c[g2] - 1.0)
a = store["acc"][(H, scope)]
np.add.at(a["sn"], sid[m], react / relatr[m])
np.add.at(a["sr"], sid[m], react)
np.add.at(a["sl"], sid[m], d2 * (c[g2 + H] / Lk[m] - 1.0))
np.add.at(a["c"], sid[m], 1.0)
def scan_config(df: pd.DataFrame, segs: list, PA_r, PA_e, PB_r, PB_e, PC_r, PC_e) -> dict:
"""Scansione completa di un config (asset,tf,k): test principale (griglia dello swing
corrente: ret+ext) e confluenza (ret corrente x ext dello swing s-2, stesso verso)."""
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
a14 = al.atr(df, ATR_WIN)
n = len(c)
is_pre = (pd.to_datetime(df["datetime"], utc=True) < al.HOLDOUT).values
R_true_r = np.array(RET_TRUE)[None, :]
R_true_e = np.array(EXT_TRUE)[None, :]
main, conf, sing = _new_store(), _new_store(), _new_store()
czc = np.zeros(N_SETS) # numero di zone confluenti per set
set_rep = np.repeat(np.arange(N_SETS), NL)
for si, seg in enumerate(segs):
w0, w1 = seg["w0"], seg["w1"]
if w0 < 1 or w1 < w0 or w0 >= n:
continue
w1 = min(w1, n - 1)
pA, pB = seg["pA"], seg["pB"]
M = pB - pA
if abs(M) < 1e-12:
continue
Rr = np.vstack([R_true_r, PA_r, PB_r[si], PC_r]) # (N_SETS,3)
Re = np.vstack([R_true_e, PA_e, PB_e[si], PC_e]) # (N_SETS,4)
L = np.concatenate([pB - Rr * M, pA + Re * M], axis=1).ravel()
_acc_touches(main, *_touch_scan(h, l, c, w0, w1, L, set_rep), c, a14, is_pre, n)
# ------- confluenza: ret(cur) x ext(swing s-2, stesso verso) entro eps -------
if seg["prev2"] is None:
continue
qA, qB = seg["prev2"]
Mp = qB - qA
if abs(Mp) < 1e-12:
continue
eps = CONF_EPS_ATR * a14[w0]
if not np.isfinite(eps) or eps <= 0:
continue
Lr = pB - Rr * M # (N_SETS,3)
Le = qA + Re * Mp # (N_SETS,4)
dmat = np.abs(Lr[:, :, None] - Le[:, None, :]) # (N_SETS,3,4)
cs, cr, ce = np.nonzero(dmat <= eps)
if len(cs):
Lc = 0.5 * (Lr[cs, cr] + Le[cs, ce])
np.add.at(czc, cs, 1.0)
_acc_touches(conf, *_touch_scan(h, l, c, w0, w1, Lc, cs), c, a14, is_pre, n)
used_r = np.zeros((N_SETS, Lr.shape[1]), bool); used_r[cs, cr] = True
used_e = np.zeros((N_SETS, Le.shape[1]), bool); used_e[cs, ce] = True
sr_, rr_ = np.nonzero(~used_r)
se_, ee_ = np.nonzero(~used_e)
Ls = np.concatenate([Lr[sr_, rr_], Le[se_, ee_]])
ids_s = np.concatenate([sr_, se_])
_acc_touches(sing, *_touch_scan(h, l, c, w0, w1, Ls, ids_s), c, a14, is_pre, n)
return dict(main=main, conf=conf, sing=sing, czc=czc, n_seg=len(segs))
# ===========================================================================
# STATISTICHE dai contatori
# ===========================================================================
def _means(store, H: int, scope: str, field: str = "sn"):
a = store["acc"][(H, scope)]
with np.errstate(invalid="ignore", divide="ignore"):
return np.where(a["c"] > 0, a[field] / a["c"], np.nan)
def _pctl(true_val: float, plc: np.ndarray):
v = plc[np.isfinite(plc)]
if len(v) < 20 or not np.isfinite(true_val):
return None
return float(np.mean(v <= true_val))
IDX_A = slice(1, 1 + N_PA)
IDX_B = slice(1 + N_PA, 1 + N_PA + N_PB)
IDX_C = slice(1 + N_PA + N_PB, N_SETS)
def _cell_stats(store, H: int):
out = {}
for scope in ("IS", "FULL"):
mn = _means(store, H, scope)
mr = _means(store, H, scope, "sr")
ml = _means(store, H, scope, "sl")
cnt = store["acc"][(H, scope)]["c"]
out[scope] = dict(
n=int(cnt[0]),
raw_bps=float(mr[0] * 1e4) if np.isfinite(mr[0]) else None,
lvl_bps=float(ml[0] * 1e4) if np.isfinite(ml[0]) else None,
norm=float(mn[0]) if np.isfinite(mn[0]) else None,
plcA_med=float(np.nanmedian(mn[IDX_A])),
plcC_med=float(np.nanmedian(mn[IDX_C])),
pctlA=_pctl(mn[0], mn[IDX_A]),
pctlB=_pctl(mn[0], mn[IDX_B]),
pctlC=_pctl(mn[0], mn[IDX_C]),
)
b = store["brk"]["IS"]
with np.errstate(invalid="ignore", divide="ignore"):
rate = np.where(b["t"] > 0, b["b"] / b["t"], np.nan)
out["brk_rate"] = float(rate[0]) if np.isfinite(rate[0]) else None
# "il livello tiene" = break-rate BASSO -> pctl_hold alto se true < placebo
v = rate[IDX_A][np.isfinite(rate[IDX_A])]
out["brk_pctl_hold"] = (float(np.mean(v >= rate[0]))
if len(v) >= 20 and np.isfinite(rate[0]) else None)
return out
def _pool_stores(stores: list):
"""Somma i contatori di piu' config (stesso significato dei set -> pooling coerente)."""
pooled = _new_store()
for st in stores:
for key, a in st["acc"].items():
for f in ("sn", "sr", "sl", "c"):
pooled["acc"][key][f] += a[f]
for s, b in st["brk"].items():
pooled["brk"][s]["b"] += b["b"]
pooled["brk"][s]["t"] += b["t"]
return pooled
# ===========================================================================
# STRATEGIA (solo se il claim batte il placebo) — fade ai livelli Fib
# ===========================================================================
def fib_fade_positions(df: pd.DataFrame, k_atr: float, H: int) -> np.ndarray:
"""Posizione long/short: al fresh-touch di un livello Fib vero prendi la direzione
del rimbalzo atteso e tienila H barre (tocchi sovrapposti sommati, clip a +/-1).
Decisione a close[i] -> eval_weights la applica dalla barra i+1 (nessun leak)."""
piv = zigzag_pivots(df, k_atr)
n = len(df)
segs = build_segments(piv, n)
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
sig = np.zeros(n)
ids0 = np.zeros(NL, dtype=int)
for seg in segs:
w0, w1 = seg["w0"], min(seg["w1"], n - 1)
if w0 < 1 or w1 < w0:
continue
pA, pB = seg["pA"], seg["pB"]
M = pB - pA
if abs(M) < 1e-12:
continue
L = np.concatenate([pB - np.array(RET_TRUE) * M, pA + np.array(EXT_TRUE) * M])
gi, dirs, _, _ = _touch_scan(h, l, c, w0, w1, L, ids0)
np.add.at(sig, gi, dirs)
return np.clip(pd.Series(sig).rolling(H, min_periods=1).sum().values, -1.0, 1.0)
def make_fib_target(tf: str = "1d", k_atr: float = 2.0, H: int = 20):
def target(df):
return fib_fade_positions(df, k_atr, H)
return target
# ===========================================================================
# MAIN
# ===========================================================================
def main():
print("=" * 104)
print("r0702 ELL-B FIBONACCI CONFLUENCE — reazione ai livelli Fib vs rapporti placebo "
"(il null e' tutto)")
print(f"zigzag k*ATR14 k={K_GRID}, ratios ret={RET_TRUE} ext={EXT_TRUE}, H={HORIZONS} barre,"
f" placebo A={N_PA} set fissi / B={N_PB} per-swing / C={N_PC} location-matched,"
f" seed={SEED}")
print("statistica = media reazione segnata/ATR sui tocchi IS (pre-2025); "
"pctl alto = Fib meglio del placebo")
print("=" * 104)
# ---------- [0] CAUSALITY: pivot su prefisso troncato + target strategy ----------
print("\n[0] CAUSALITY CHECK zigzag (pivot confermati < cut identici su prefisso 90%)")
for a in ASSETS:
for tf in TFS:
df = al.get(a, tf)
cut = int(len(df) * 0.9)
pf = [p for p in zigzag_pivots(df, K_GRID[0]) if p[3] < cut]
pp = [p for p in zigzag_pivots(df.iloc[:cut].reset_index(drop=True), K_GRID[0])
if p[3] < cut]
ok = pf == pp
print(f" {a} {tf}: pivots full<cut={len(pf)} prefix={len(pp)} "
f"identici={'OK' if ok else 'FAIL'}")
cz = al.causality_ok(make_fib_target("1d", K_GRID[0], HORIZONS[1]), tf="1d")
print(f" target fade 1d (al.causality_ok): ok={cz['ok']} "
f"max_tail_diff={cz['max_tail_diff']}")
# ---------- [1] SCANSIONE tutti i config ----------
rng_a = np.random.default_rng(SEED)
PA_r, PA_e = make_placebo_a(rng_a)
PC_r, PC_e = make_placebo_c(np.random.default_rng(SEED + 7))
results = {}
print("\n[1] SWING E TOCCHI per config")
for ia, a in enumerate(ASSETS):
for itf, tf in enumerate(TFS):
df = al.get(a, tf)
n = len(df)
for ik, k in enumerate(K_GRID):
piv = zigzag_pivots(df, k)
segs = build_segments(piv, n)
rng_b = np.random.default_rng([SEED, ia, itf, ik])
PB_r, PB_e = make_placebo_b(rng_b, len(segs))
res = scan_config(df, segs, PA_r, PA_e, PB_r, PB_e, PC_r, PC_e)
results[(a, tf, k)] = res
nIS = int(res["main"]["acc"][(HORIZONS[0], "IS")]["c"][0])
nF = int(res["main"]["acc"][(HORIZONS[0], "FULL")]["c"][0])
print(f" {a} {tf} k={k:.0f}: bars={n} pivots={len(piv)} swings={len(segs)} "
f"tocchi_true IS={nIS} FULL={nF} zone_confluenti_true={int(res['czc'][0])}")
# ---------- [2] TABELLA PRINCIPALE per cella ----------
print("\n[2] REAZIONE AL LIVELLO vs PLACEBO — per cella (statistica: media react/ATR, IS)")
print(f" pctlA = vs {N_PA} set fissi uniformi; pctlB = vs {N_PB} per-swing; "
f"pctlC = vs {N_PC} location-matched (jitter attorno ai Fib);"
f" Bonferroni 16 celle -> single-cell sig ~>=0.997")
hdr = (f" {'asset':<5s} {'tf':<3s} {'k':>2s} {'H':>3s} {'nIS':>6s} {'nFULL':>6s} "
f"{'rawIS(bps)':>10s} {'lvlIS(bps)':>10s} {'normIS':>8s} {'plcAmed':>8s} "
f"{'plcCmed':>8s} {'pctlA_IS':>8s} {'pctlB_IS':>8s} {'pctlC_IS':>8s} "
f"{'pctlA_F':>8s} {'brkIS':>6s} {'brkPctl':>7s}")
print(hdr)
cells = {}
for (a, tf, k), res in results.items():
for H in HORIZONS:
st = _cell_stats(res["main"], H)
cells[(a, tf, k, H)] = st
i_, f_ = st["IS"], st["FULL"]
print(f" {a:<5s} {tf:<3s} {k:>2.0f} {H:>3d} {i_['n']:>6d} {f_['n']:>6d} "
f"{i_['raw_bps']:>10.1f} {i_['lvl_bps']:>10.1f} {i_['norm']:>8.3f} "
f"{i_['plcA_med']:>8.3f} {i_['plcC_med']:>8.3f} "
f"{i_['pctlA']:>8.2f} {i_['pctlB']:>8.2f} {i_['pctlC']:>8.2f} "
f"{f_['pctlA']:>8.2f} {st['brk_rate']:>6.2f} "
f"{st['brk_pctl_hold'] if st['brk_pctl_hold'] is not None else float('nan'):>7.2f}")
# ---------- [3] POOLED per (k,H) su asset x TF ----------
print("\n[3] POOLED (somma tocchi su 2 asset x 2 TF) — il test famiglia")
pooled_pass = {}
for k in K_GRID:
pooled = _pool_stores([results[(a, tf, k)]["main"] for a in ASSETS for tf in TFS])
for H in HORIZONS:
st = _cell_stats(pooled, H)
i_ = st["IS"]
cell_ps = [cells[(a, tf, k, H)]["IS"]["pctlA"] for a in ASSETS for tf in TFS
if cells[(a, tf, k, H)]["IS"]["n"] >= MIN_CELL_TOUCH]
min_cell = min(cell_ps) if cell_ps else None
ok = (i_["pctlA"] is not None and i_["pctlA"] >= 0.99
and i_["pctlB"] is not None and i_["pctlB"] >= 0.99
and i_["pctlC"] is not None and i_["pctlC"] >= 0.95
and min_cell is not None and min_cell >= 0.90)
pooled_pass[(k, H)] = ok
print(f" k={k:.0f} H={H:>2d}: nIS={i_['n']:>6d} normIS={i_['norm']:+.3f} "
f"(plcA med {i_['plcA_med']:+.3f} / plcC med {i_['plcC_med']:+.3f}) "
f"pctlA_IS={i_['pctlA']:.2f} pctlB_IS={i_['pctlB']:.2f} "
f"pctlC_IS={i_['pctlC']:.2f} pctlA_FULL={st['FULL']['pctlA']:.2f} "
f"min_cell_pctlA={min_cell if min_cell is not None else 'n/a'} "
f"PASS(A,B>=0.99 & C>=0.95 & cells>=0.90)={ok}")
# ---------- [4] CONFLUENZA vs livello singolo ----------
print("\n[4] CONFLUENZA (ret swing corrente x ext swing s-2 stesso verso, eps=0.25*ATR)")
print(" diff = norm(conf) - norm(single); pctl del diff vero vs placebo A")
conf_ok_cells = {}
for (a, tf, k), res in results.items():
for H in HORIZONS:
mc = _means(res["conf"], H, "IS")
ms = _means(res["sing"], H, "IS")
nc = int(res["conf"]["acc"][(H, "IS")]["c"][0])
ns = int(res["sing"]["acc"][(H, "IS")]["c"][0])
diff = mc - ms
p = _pctl(diff[0], diff[IDX_A])
czc_true = int(res["czc"][0])
czc_plc = float(np.mean(res["czc"][IDX_A]))
conf_ok_cells[(a, tf, k, H)] = (p is not None and p >= 0.95
and nc >= MIN_CONF_TOUCH)
d0 = f"{diff[0]:+.3f}" if np.isfinite(diff[0]) else " n/a"
print(f" {a} {tf} k={k:.0f} H={H:>2d}: zone true={czc_true} (plcA med~{czc_plc:.0f}) "
f"tocchiIS conf={nc} sing={ns} "
f"norm conf={mc[0] if np.isfinite(mc[0]) else float('nan'):+.3f} "
f"sing={ms[0] if np.isfinite(ms[0]) else float('nan'):+.3f} diff={d0} "
f"pctl_diff={p if p is not None else 'n/a'}")
conf_real = {}
for k in K_GRID:
for H in HORIZONS:
conf_real[(k, H)] = all(conf_ok_cells[(a, tf, k, H)] for a in ASSETS for tf in TFS)
print(" verdetto confluenza per (k,H) [tutte le 4 celle pctl>=0.95 e n>=30]: "
+ " ".join(f"k={k:.0f},H={H}:{'PASS' if v else 'FAIL'}"
for (k, H), v in conf_real.items()))
# ---------- [5] VERDETTO + eventuale strategizzazione ----------
fib_real = any(pooled_pass.values())
any_conf = any(conf_real.values())
print("\n[5] VERDETTO")
print(f" fib_real (pooled IS: A,B>=0.99, C>=0.95, tutte le celle >=0.90, "
f"per qualche k,H): {fib_real} -> {pooled_pass}")
print(f" confluenza > singolo livello (robusto 4/4 celle): {any_conf}")
if fib_real:
print("\n[6] STRATEGIA (il claim regge il placebo) — fade ai livelli, "
"study_family_honest + marginal")
grid = [dict(k_atr=k, H=H) for k in K_GRID for H in HORIZONS]
fam = al.study_family_honest("FIB-FADE", make_fib_target, grid, tfs=TFS)
ch = fam.get("chosen")
print(f" chosen(IS)={ch} n_cells={fam.get('n_cells')} "
f"DSR={fam.get('deflated_sharpe')} (null-max {fam.get('expected_null_max')}) "
f"dsr_pass={fam.get('dsr_pass')}")
print(f" earns_slot_marginal={fam.get('earns_slot_marginal')} "
f"EARNS_SLOT_HONEST={fam.get('earns_slot_honest')}")
if fam.get("marginal"):
print(al.fmt_marginal(fam["marginal"]))
else:
print("\n[6] STRATEGIA: SALTATA — la reazione ai livelli Fib NON batte i rapporti "
"casuali in modo robusto (regola a priori). Nessun edge da strategizzare.")
print("\nFine. Selezione: nessuna (rapporti dal claim, verdetto su pooled IS); "
"hold-out mai usato per scegliere.")
if __name__ == "__main__":
main()
+414
View File
@@ -0,0 +1,414 @@
"""r0702_ell_rangecycle — Test ONESTO del claim "Elliott/Larry Williams range-cycle".
CLAIM (da video didattico, operazionalizzato SENZA conteggio soggettivo di onde):
(A) un segmento di trend che inizia con un impulso a RANGE COMPRESSO tende a produrre
un'estensione successiva AMPIA ("onda 3 larga");
(B) un segmento che inizia con impulso a RANGE AMPIO produce una fase finale TRONCATA
(ultimo terzo debole -> uscire prima).
Entrambi i claim, come operazionalizzati, predicono rho(impulso, seguito) NEGATIVA:
A: rho(range_impulso_norm, ritorno_normalizzato del RESTO del segmento) < 0
B: rho(range_impulso_norm, ritorno_normalizzato dell'ULTIMO TERZO) < 0
PRIOR ART (sweep 104-famiglie 2026-06-20): la compressione come *setup di breakout* è
già stata testata e NON regge BRK08 (NR7), BRK10 (BB-squeeze long), CMB05 (squeeze->
breakout), VOL12 (low-vol timing). Qui la domanda è DIVERSA e a livello di SEGMENTO:
il range dell'impulso iniziale predice l'ampiezza/troncatura del seguito? Statistica
PRIMA della strategia: si strategizza SOLO se il segnale regge Bonferroni sul pre-2025.
METODO:
- Dati certificati BTC/ETH 1d (al.get) + settimanali costruiti dal 1d con resample
"7D", label='left', closed='left', origin ESPLICITA (7 ancore, banda riportata).
Niente DatetimeIndex.view("int64") (lezione pandas 2026-07-01).
- 2 definizioni MECCANICHE di segmento:
(i) TSMOM-flip: run massimali di sign(close[t]-close[t-L]) (L=30 bar 1d, 4 bar 1w).
Causale sia in start sia in end.
(ii) zigzag causale k*ATR: pivot = estremo confermato solo dopo retracement k*ATR
(k=3.0/ATR14 su 1d, k=2.0/ATR8 su 1w FISSATI a priori, non cercati).
NB: il pivot è noto solo DOPO (lag di conferma) -> le statistiche sono
event-aligned ex-post (test di ASSOCIAZIONE); un overlay tradabile dovrebbe
scontare il lag. L'impulso (prime N barre) precede comunque il seguito ->
nessuna sovrapposizione meccanica impulso/seguito.
- Impulso = mean(TrueRange prime N barre) / ATR[pre-segmento], N in {3,5,10}.
- Seguito A = dir*(close_end - close_impulso) / (ATR_norm * sqrt(n_resto))
Seguito B = dir*(close_end - close_2/3) / (ATR_norm * sqrt(n_ultimo_terzo))
Normalizzazione PRIMARIA con ATR pre-segmento (stesso denominatore di impulso ->
bias comune-divisore POSITIVO su rho = CONSERVATIVO vs claim che predicono rho<0).
Sensibilità: anche ATR post-impulso (ANTI-conservativa: un impulso ampio gonfia
l'ATR post -> sgonfia il seguito -> fabbrica rho<0). Riportate entrambe, si testa
la primaria.
- Test: Spearman pooled BTC+ETH con NULL PERMUTATO (shuffle seguito<->impulso ENTRO
asset, 2000 permutazioni, two-sided). Secondario: differenza terzili (compresso -
ampio, terzili entro asset). Bonferroni su 24 celle primarie (2 def x 2 TF x 3 N x
2 claim; weekly = ancora lunedì, le altre 6 ancore = banda robustezza).
- Campione PICCOLO per costruzione (decine di segmenti, weekly ~390 barre) -> n
riportato per cella; p marginali NON vanno creduti.
Output: tabella celle + CSV nello scratchpad (ell_a_cells.csv). Strategizzazione
(study_marginal / study_family_honest) SOLO se una cella passa Bonferroni con il segno
giusto sul campione pre-2025.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import numpy as np
import pandas as pd
import altlib as al
SCRATCH = ("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad")
SEED = 20260702
N_PERM = 2000
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
N_LIST = (3, 5, 10)
# Parametri di segmentazione FISSATI A PRIORI (nessuna ricerca su di essi):
TSMOM_L = {"1d": 30, "1w": 4} # 30 giorni / 4 settimane (~28g)
ZZ_K = {"1d": 3.0, "1w": 2.0} # soglia retracement in multipli di ATR
ZZ_ATRWIN = {"1d": 14, "1w": 8}
BONF_M = 24 # 2 def x 2 TF x 3 N x 2 claim (weekly=ancora lun)
# ---------------------------------------------------------------------------
# Dati
# ---------------------------------------------------------------------------
def daily(asset: str) -> pd.DataFrame:
df = al.get(asset, "1d")
return df[["datetime", "open", "high", "low", "close", "volume"]].copy()
def weekly_from_daily(dfd: pd.DataFrame, anchor_day: int) -> pd.DataFrame:
"""Barre settimanali dal 1d. anchor_day=0 -> settimane che iniziano lunedì
(origin=2018-01-01, un lunedì); anchor_day=k sposta l'origine di k giorni.
resample('168h', label='left', closed='left', origin=esplicita) '168h' e non
'7D': in pandas 2.x 'D' non è Tick-like e IGNOREREBBE origin (warning silenzioso
-> tutte le ancore identiche). Bins parziali (<7 giorni) scartati."""
origin = pd.Timestamp("2018-01-01", tz="UTC") + pd.Timedelta(days=anchor_day)
x = dfd.set_index(pd.DatetimeIndex(pd.to_datetime(dfd["datetime"], utc=True)))
g = x.resample("168h", label="left", closed="left", origin=origin)
w = g.agg(open=("open", "first"), high=("high", "max"), low=("low", "min"),
close=("close", "last"), volume=("volume", "sum"))
cnt = g["close"].count()
w = w[cnt >= 7].dropna().reset_index(names="datetime")
return w
def true_range(df: pd.DataFrame) -> np.ndarray:
h, l, c = (df[k].values.astype(float) for k in ("high", "low", "close"))
pc = np.roll(c, 1)
pc[0] = c[0]
return np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
# ---------------------------------------------------------------------------
# Definizioni di segmento (meccaniche)
# ---------------------------------------------------------------------------
def tsmom_segments(close: np.ndarray, L: int) -> list[tuple[int, int, int]]:
"""Run massimali di sign(close[i]-close[i-L]). Ritorna (start, end, dir) inclusivi.
Causale: il segno a i usa solo close<=i; start noto allo start, end noto all'end."""
n = len(close)
s = np.zeros(n)
s[L:] = np.sign(close[L:] - close[:-L])
for i in range(1, n): # 0 -> segno precedente (nessun flip)
if s[i] == 0:
s[i] = s[i - 1]
segs, start = [], L
for i in range(L + 1, n + 1):
if i == n or s[i] != s[start]:
if s[start] != 0:
segs.append((start, i - 1, int(s[start])))
start = i
return segs
def zigzag_segments(df: pd.DataFrame, k: float, atr_win: int) -> list[tuple[int, int, int]]:
"""Zigzag causale a soglia k*ATR: pivot (estremo) confermato solo quando il prezzo
ritraccia k*ATR[i] dall'estremo corrente. Segmento = (pivot_prec+1 .. pivot), dir =
segno del leg. Il pivot è noto solo alla CONFERMA (lag): qui usato solo per
statistiche di associazione ex-post, non come segnale tradabile."""
c = df["close"].values.astype(float)
a = al.atr(df, atr_win)
n = len(c)
piv: list[int] = []
hi, hi_i, lo, lo_i, d = c[0], 0, c[0], 0, 0
for i in range(1, n):
if d >= 0:
if c[i] > hi:
hi, hi_i = c[i], i
if d == 0 and c[i] < lo:
lo, lo_i = c[i], i
if hi - c[i] > k * a[i]:
piv.append(hi_i)
d = -1
lo, lo_i = c[i], i
continue
if d == 0 and c[i] - lo > k * a[i]:
piv.append(lo_i)
d = 1
hi, hi_i = c[i], i
else:
if c[i] < lo:
lo, lo_i = c[i], i
if c[i] - lo > k * a[i]:
piv.append(lo_i)
d = 1
hi, hi_i = c[i], i
segs = []
for j in range(1, len(piv)):
p, q = piv[j - 1], piv[j]
if q > p + 1:
segs.append((p + 1, q, int(np.sign(c[q] - c[p]))))
return [s for s in segs if s[2] != 0]
# ---------------------------------------------------------------------------
# Misure per segmento
# ---------------------------------------------------------------------------
def measure(df: pd.DataFrame, segs, N: int, atr_win: int):
"""Per ogni segmento: impulso (range prime N barre / ATR pre-segmento) e seguito
per claim A (resto) e B (ultimo terzo). Normalizzazione primaria = ATR pre-segmento
(conservativa); sensibilità = ATR post-impulso. Ritorna DataFrame righe-segmento."""
c = df["close"].values.astype(float)
a = al.atr(df, atr_win)
tr = true_range(df)
ts = pd.to_datetime(df["datetime"], utc=True).values
rows = []
for (s, e, d) in segs:
seg_len = e - s + 1
if s - 1 < atr_win or seg_len < N + 3:
continue
a_pre = a[s - 1]
if not np.isfinite(a_pre) or a_pre <= 0:
continue
imp = float(np.mean(tr[s:s + N]) / a_pre)
i_end = s + N - 1 # ultima barra dell'impulso
a_post = a[i_end] if a[i_end] > 0 else np.nan
# Claim A: resto del segmento (da fine impulso a fine segmento), in dir. trend
n_rest = e - i_end
move_a = d * (c[e] - c[i_end])
seqA_pre = float(move_a / (a_pre * np.sqrt(n_rest)))
seqA_post = float(move_a / (a_post * np.sqrt(n_rest))) if np.isfinite(a_post) else np.nan
# ampiezza massima favorevole del resto (descrittiva)
if d > 0:
mfe = np.max(df["high"].values[i_end + 1:e + 1]) - c[i_end]
else:
mfe = c[i_end] - np.min(df["low"].values[i_end + 1:e + 1])
mfe_pre = float(mfe / (a_pre * np.sqrt(n_rest)))
# Claim B: ultimo terzo del segmento (deve iniziare dopo l'impulso)
l0 = s + int(np.ceil(seg_len * 2.0 / 3.0)) # prima barra dell'ultimo terzo
seqB_pre = seqB_post = np.nan
if l0 - 1 >= i_end and e - (l0 - 1) >= 2:
n_last = e - (l0 - 1)
move_b = d * (c[e] - c[l0 - 1])
a_b = a[l0 - 1]
seqB_pre = float(move_b / (a_pre * np.sqrt(n_last)))
seqB_post = float(move_b / (a_b * np.sqrt(n_last))) if a_b > 0 else np.nan
rows.append(dict(t0=ts[s], seg_len=seg_len, dir=d, imp=imp,
seqA=seqA_pre, seqA_post=seqA_post, mfe=mfe_pre,
seqB=seqB_pre, seqB_post=seqB_post))
return pd.DataFrame(rows)
# ---------------------------------------------------------------------------
# Statistica: Spearman pooled + null permutato entro-asset; terzili
# ---------------------------------------------------------------------------
def _spearman(x: np.ndarray, y: np.ndarray) -> float:
rx = pd.Series(x).rank().values
ry = pd.Series(y).rank().values
sx, sy = rx.std(), ry.std()
if sx == 0 or sy == 0:
return 0.0
return float(np.corrcoef(rx, ry)[0, 1])
def _tercile_diff(imp, seq, aid):
"""mean(seguito | terzile compresso) - mean(seguito | terzile ampio), terzili
per-asset (>=9 segmenti per asset, altrimenti asset escluso). Claim -> diff>0."""
lo_v, hi_v = [], []
for a in np.unique(aid):
m = aid == a
if m.sum() < 9:
continue
q1, q2 = np.quantile(imp[m], [1 / 3, 2 / 3])
lo_v.append(seq[m & (imp <= q1)])
hi_v.append(seq[m & (imp >= q2)])
if not lo_v:
return np.nan
return float(np.concatenate(lo_v).mean() - np.concatenate(hi_v).mean())
def perm_test(imp, seq, aid, n_perm=N_PERM, seed=SEED):
"""Shuffle del seguito rispetto all'impulso ENTRO asset. p two-sided su Spearman
(primario) e su tercile diff (secondario)."""
ok = np.isfinite(imp) & np.isfinite(seq)
imp, seq, aid = imp[ok], seq[ok], aid[ok]
n = len(imp)
if n < 12:
return dict(rho=np.nan, p=np.nan, ter=np.nan, p_ter=np.nan, n=n)
rho = _spearman(imp, seq)
ter = _tercile_diff(imp, seq, aid)
rng = np.random.default_rng(seed)
masks = [aid == a for a in np.unique(aid)]
cnt_r = cnt_t = 0
n_t = 0
for _ in range(n_perm):
sp = seq.copy()
for m in masks:
sp[m] = rng.permutation(sp[m])
if abs(_spearman(imp, sp)) >= abs(rho):
cnt_r += 1
if np.isfinite(ter):
tp = _tercile_diff(imp, sp, aid)
if np.isfinite(tp):
n_t += 1
if abs(tp) >= abs(ter):
cnt_t += 1
p = (1 + cnt_r) / (n_perm + 1)
p_ter = (1 + cnt_t) / (n_t + 1) if (np.isfinite(ter) and n_t > 0) else np.nan
return dict(rho=round(rho, 3), p=p, ter=(round(ter, 3) if np.isfinite(ter) else np.nan),
p_ter=p_ter, n=n)
# ---------------------------------------------------------------------------
# Celle
# ---------------------------------------------------------------------------
def build_frames(tf: str, anchor: int):
out = {}
for a in ("BTC", "ETH"):
d = daily(a)
out[a] = d if tf == "1d" else weekly_from_daily(d, anchor)
return out
def segments_for(df, sdef: str, tf: str):
if sdef == "tsmom":
return tsmom_segments(df["close"].values.astype(float), TSMOM_L[tf])
return zigzag_segments(df, ZZ_K[tf], ZZ_ATRWIN[tf])
def cell_rows(frames, sdef: str, tf: str, N: int) -> pd.DataFrame:
parts = []
for i, (a, df) in enumerate(frames.items()):
m = measure(df, segments_for(df, sdef, tf), N, ZZ_ATRWIN[tf])
if len(m):
m["aid"] = i
m["asset"] = a
parts.append(m)
return pd.concat(parts, ignore_index=True) if parts else pd.DataFrame()
def run_cell(rows: pd.DataFrame, claim: str, pre2025: bool, norm: str = "pre"):
if rows.empty:
return dict(rho=np.nan, p=np.nan, ter=np.nan, p_ter=np.nan, n=0)
r = rows
if pre2025:
r = r[pd.to_datetime(r["t0"], utc=True) < HOLDOUT]
col = {"A": {"pre": "seqA", "post": "seqA_post"},
"B": {"pre": "seqB", "post": "seqB_post"}}[claim][norm]
return perm_test(r["imp"].values.astype(float), r[col].values.astype(float),
r["aid"].values)
def main():
print(__doc__.split("\n")[0])
print(f"seed={SEED} n_perm={N_PERM} Bonferroni m={BONF_M} (celle primarie)")
results = []
frames_1d = build_frames("1d", 0)
frames_1w = {k: build_frames("1w", k) for k in range(7)}
for a, df in frames_1d.items():
print(f" {a} 1d: {len(df)} barre {df['datetime'].iloc[0]} -> {df['datetime'].iloc[-1]}"
f" | 1w(lun): {len(frames_1w[0][a])} barre")
for sdef in ("tsmom", "zigzag"):
for tf in ("1d", "1w"):
fr = frames_1d if tf == "1d" else frames_1w[0]
# conteggio segmenti grezzi (informativo)
nseg = {a: len(segments_for(df, sdef, tf)) for a, df in fr.items()}
print(f"\n[{sdef} {tf}] segmenti grezzi: {nseg}")
for N in N_LIST:
rows = cell_rows(fr, sdef, tf, N)
for claim in ("A", "B"):
full = run_cell(rows, claim, pre2025=False)
ins = run_cell(rows, claim, pre2025=True)
post = run_cell(rows, claim, pre2025=False, norm="post")
rec = dict(sdef=sdef, tf=tf, N=N, claim=claim,
n=full["n"], rho=full["rho"], p=full["p"],
p_bonf=min(1.0, full["p"] * BONF_M) if np.isfinite(full["p"]) else np.nan,
ter=full["ter"], p_ter=full["p_ter"],
rho_is=ins["rho"], p_is=ins["p"], n_is=ins["n"],
rho_postnorm=post["rho"])
# banda ancore weekly (tutte e 7, stessa cella)
if tf == "1w":
rhos, ps = [], []
for k in range(7):
rr = run_cell(cell_rows(frames_1w[k], sdef, tf, N), claim, False)
rhos.append(float(rr["rho"]) if np.isfinite(rr["rho"]) else np.nan)
ps.append(float(rr["p"]) if np.isfinite(rr["p"]) else np.nan)
if np.any(np.isfinite(rhos)):
med = float(np.nanmedian(rhos))
rec["anchor_rho_band"] = (round(float(np.nanmin(rhos)), 3),
round(med, 3),
round(float(np.nanmax(rhos)), 3))
rec["anchor_p_band"] = (round(float(np.nanmin(ps)), 4),
round(float(np.nanmax(ps)), 4))
rec["anchor_sign_flips"] = int(np.nansum(
np.sign(rhos) != np.sign(med)))
else:
rec["anchor_rho_band"] = rec["anchor_p_band"] = None
rec["anchor_sign_flips"] = None
results.append(rec)
R = pd.DataFrame(results)
R.to_csv(f"{SCRATCH}/ell_a_cells.csv", index=False)
print("\n" + "=" * 118)
print("TABELLA CELLE (norm primaria = ATR pre-segmento; claim A/B predicono rho<0; p permutato two-sided)")
print("=" * 118)
hdr = (f"{'def':7}{'tf':4}{'N':3}{'cl':3}{'n':5}{'rho':>7}{'p':>8}{'p_bonf':>8}"
f"{'ter(c-a)':>9}{'p_ter':>8}{'rho_IS':>8}{'p_IS':>8}{'n_IS':>5}{'rho_post':>9} anchors(1w)")
print(hdr)
for _, r in R.iterrows():
anch = ""
if r["tf"] == "1w" and r["anchor_rho_band"] is not None:
anch = (f"rho[min/med/max]={r['anchor_rho_band']} "
f"p[{r['anchor_p_band'][0]}..{r['anchor_p_band'][1]}] "
f"sign_flips={r['anchor_sign_flips']}")
print(f"{r['sdef']:7}{r['tf']:4}{r['N']:<3}{r['claim']:3}{r['n']:<5}"
f"{r['rho']:>7}{r['p']:>8.4f}{r['p_bonf']:>8.3f}"
f"{str(r['ter']):>9}{r['p_ter']:>8.4f}{str(r['rho_is']):>8}{r['p_is']:>8.4f}{r['n_is']:>5}"
f"{str(r['rho_postnorm']):>9} {anch}")
# ------------------------------------------------------------------
# Verdetti
# ------------------------------------------------------------------
print("\n" + "=" * 118)
for claim in ("A", "B"):
sub = R[R["claim"] == claim]
sig = sub[(sub["p_bonf"] < 0.05) & (sub["rho"] < 0)]
sig_is = sub[(sub["p_is"] * BONF_M < 0.05) & (sub["rho_is"] < 0)]
neg = int((sub["rho"] < 0).sum())
print(f"CLAIM {claim}: celle={len(sub)} rho<0 (segno del claim)={neg}/{len(sub)} "
f"pass Bonferroni FULL={len(sig)} pass Bonferroni pre-2025={len(sig_is)}")
if len(sig) == 0 and len(sig_is) == 0:
print(f" -> NESSUNA cella regge Bonferroni. Claim {claim} NON supportato: "
f"si strategizza solo con evidenza statistica -> STOP qui per questo claim.")
else:
print(f" -> celle sopravvissute:\n{sig.to_string()}")
print(" -> procedere a strategizzazione SOLO su selezione pre-2025 "
"(study_family_honest).")
n_med = int(R["n"].median())
print(f"\nCAVEAT CAMPIONE: n mediano per cella = {n_med} segmenti "
f"(weekly ~{int(R[R['tf'] == '1w']['n'].median())}). Con decine di osservazioni "
"un p marginale (0.01-0.05 raw) è indistinguibile dal multiple-testing: "
"24 celle primarie + 2 claim correlati + 7 ancore weekly.")
print("NOTE: (1) zigzag = associazione ex-post (pivot noto solo a conferma); "
"(2) norm 'post' (ATR post-impulso) è ANTI-conservativa (divisore contaminato "
"dall'impulso) -> se rho<0 appare solo lì, è artefatto di normalizzazione; "
"(3) daily = ancora 00:00 UTC fissa (feed certificato).")
print(f"CSV: {SCRATCH}/ell_a_cells.csv")
if __name__ == "__main__":
main()