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:
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user