76120b59c2
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>
563 lines
27 KiB
Python
563 lines
27 KiB
Python
"""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()
|