research(wave-0822): ledger degli esiti — ADAPTIVE-HORIZON scartato
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0822_adaptive_horizon.py — ADAPTIVE-HORIZON TSMOM: stimare *quanto indietro guardare*
|
||||
dai dati (change-point / persistenza) invece di fissarlo a 30/90/180 giorni.
|
||||
|
||||
DOMANDA
|
||||
-------
|
||||
Un TSMOM a lookback ADATTIVO batte il TSMOM a orizzonti FISSI 30/90/180 (TP01), in modo
|
||||
MARGINALE e ONESTO?
|
||||
|
||||
COSA NON E' (i due vicini gia' bocciati, per non rifarli)
|
||||
---------------------------------------------------------
|
||||
* 02/07 "velocita' trend regime-condizionata": pesava i TRE orizzonti fissi in funzione del
|
||||
percentile di vol -> risulto' un tilt-30d statico travestito (pctl 0.71 vs il null dei pesi
|
||||
statici casuali). Li' gli orizzonti restavano 30/90/180: cambiavano solo i PESI.
|
||||
* 29/06 "meta-allocazione dinamica" fra sleeve: peggiore dei pesi fissi. Li' l'oggetto adattato
|
||||
erano i pesi di PORTAFOGLIO, non un parametro della strategia.
|
||||
Qui l'oggetto adattato e' **l'orizzonte stesso**: L_t stimato da un rilevatore di change-point
|
||||
(CUSUM) o di persistenza (variance-ratio, Hurst). Mai provato nel progetto.
|
||||
|
||||
FORMA FUNZIONALE (scelta perche' rende il confronto strutturalmente pulito)
|
||||
--------------------------------------------------------------------------
|
||||
TP01 canonico e' il blend di sign(c[i]/c[i-h]-1) su h = (30, 90, 180) giorni, long-flat,
|
||||
vol-target 20%, leva cap 2x. Ma (30, 90, 180) == (L/3, L, 2L) con **L = 90 giorni**.
|
||||
Quindi il candidato e' LA STESSA FUNZIONE con L che varia nel tempo:
|
||||
blend='triple' -> horizons_t = (L_t/3, L_t, 2*L_t) (TP01 = L_t costante a 90g)
|
||||
blend='single' -> horizons_t = (L_t,) (un solo orizzonte adattivo)
|
||||
Tutto il resto (long-flat, target_vol 20%, vol_win 30g, leva 2x, fee 0.05%/lato, cadenza,
|
||||
50/50 BTC+ETH) e' IDENTICO al baseline: **l'unico grado di liberta' e' l'orizzonte**.
|
||||
=> il null non e' "meglio di zero", e' "meglio del TSMOM fisso a PARI GEOMETRIA".
|
||||
|
||||
FAMIGLIA DICHIARATA **PRIMA** DI GUARDARE (conteggio al RIALZO)
|
||||
--------------------------------------------------------------
|
||||
rilevatore (3): cusum | vratio | hurst
|
||||
parametro (3): cusum k in {2,4,8} sigma-cumulate; vratio/hurst finestra W in {60,120,250}g
|
||||
mappatura (2): direct (piu' persistenza / piu' tempo dall'ultimo change-point -> L piu' LUNGO)
|
||||
inverse (specchiata) [il verso NON e' ovvio a priori => e' un asse, non una scelta]
|
||||
blend (2): single | triple
|
||||
timeframe (2): 1d | 12h [>= 12h: sotto, costi+overfit dominano (19/06)]
|
||||
------------------------------------------------------------------
|
||||
= 3 x 3 x 2 x 2 = 36 celle x 2 TF = **72 valutazioni**.
|
||||
Costanti FISSATE A PRIORI e non cercate (dichiarate perche' sono gradi di liberta' non contati
|
||||
nella griglia): Lmin=20g, Lmax=200g (contengono 30..180 del canonico); q=5g del variance-ratio;
|
||||
scale del Hurst k in {1,2,4,8}g; finestra della percentile causale M=250g. Per questo il
|
||||
deflated-Sharpe e' riportato ANCHE a N=144 e N=216 trial (sensibilita' del verdetto al conteggio,
|
||||
regola del 30/07): un verdetto che si ribalta col conteggio si cita come tale.
|
||||
|
||||
GATE (tutti dell'harness del progetto, nessuno riscritto)
|
||||
---------------------------------------------------------
|
||||
A.study_family_honest -> cella scelta IN-SAMPLE-ONLY + deflated-Sharpe (NO selezione su hold-out)
|
||||
A.marginal_vs_tp01 -> ADDS/HEDGE/NOISE/REDUNDANT/DILUTES + corr al baseline
|
||||
A.causality_ok -> ricalcolo su prefisso (nessun look-ahead nella costruzione del segnale)
|
||||
A.anchor_luck_band -> stima onesta = MEDIANA delle 24 ancore, non l'ancora 00:00
|
||||
A.anchor_luck_delta -> confronto adattivo-vs-fisso: MEDIANA delle DIFFERENZE APPAIATE
|
||||
A.implausible_sharpe -> Sharpe troppo bello = rischio fuori dal campione
|
||||
A.eval_weights_smallcap-> haircut reale a $600 (min-order $5)
|
||||
null del de-levering -> esiste k<1 che da' al FISSO lo stesso maxDD con Sharpe >=?
|
||||
differenza APPAIATA PER ANNO -> un vantaggio concentrato in un anno e' un evento, non un meccanismo
|
||||
|
||||
Causalita': ogni rolling ha min_periods e finestra all'indietro; la CUSUM e' un loop in avanti
|
||||
con reset (prefix-stable); la percentile e' rolling causale; nessuna statistica full-sample.
|
||||
Runtime atteso ~3-6 min su 2 core. Nessun file toccato fuori da questo.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import altlib as A # noqa: E402
|
||||
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
|
||||
|
||||
ASSETS = A.CERTIFIED
|
||||
HOLDOUT = A.HOLDOUT
|
||||
|
||||
# --- costanti FISSATE A PRIORI (dichiarate sopra, non cercate) --------------
|
||||
LMIN_D, LMAX_D = 20, 200 # banda dell'orizzonte adattivo, in giorni
|
||||
FIXED_H = (30, 90, 180) # == (L/3, L, 2L) con L=90 -> il baseline
|
||||
TGT_VOL, LEV, VOLWIN = 0.20, 2.0, 30
|
||||
VR_Q_DAYS = 5 # passo del variance-ratio
|
||||
HURST_K_DAYS = (1, 2, 4, 8) # scale del Hurst
|
||||
PCTL_M_DAYS = 250 # finestra della percentile causale
|
||||
|
||||
TFS = ("1d", "12h")
|
||||
DETECTORS = ("cusum", "vratio", "hurst")
|
||||
PARAMS = {"cusum": (2.0, 4.0, 8.0), "vratio": (60, 120, 250), "hurst": (60, 120, 250)}
|
||||
MAPPINGS = ("direct", "inverse")
|
||||
BLENDS = ("single", "triple")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# RILEVATORI -> lookback adattivo L_t (in barre). Tutti causali.
|
||||
# ===========================================================================
|
||||
def _logret(c: np.ndarray) -> np.ndarray:
|
||||
r = np.zeros(len(c))
|
||||
r[1:] = np.log(c[1:] / c[:-1])
|
||||
return r
|
||||
|
||||
|
||||
def _roll_pctl(x: np.ndarray, m: int) -> np.ndarray:
|
||||
"""Percentile CAUSALE del valore corrente nella sua finestra all'indietro (rolling rank).
|
||||
Warmup -> 0.5 (neutro): non si inventa un regime dove non c'e' campione."""
|
||||
s = pd.Series(x)
|
||||
u = s.rolling(m, min_periods=max(20, m // 4)).rank(pct=True).values
|
||||
return np.where(np.isfinite(u), u, 0.5)
|
||||
|
||||
|
||||
def _cusum_age(c: np.ndarray, k: float, bpd: int) -> np.ndarray:
|
||||
"""Eta' (in barre) dall'ultimo change-point CUSUM a due code su rendimenti standardizzati.
|
||||
Loop in avanti con reset: dipende solo dal passato -> stabile su prefisso."""
|
||||
lr = _logret(c)
|
||||
sig = pd.Series(lr).rolling(30 * bpd, min_periods=max(5, 15 * bpd)).std().values
|
||||
s = np.where((sig > 0) & np.isfinite(sig), lr / np.where(sig > 0, sig, 1.0), 0.0)
|
||||
s = np.nan_to_num(s)
|
||||
n = len(c)
|
||||
age = np.zeros(n)
|
||||
sp = sm = 0.0
|
||||
last = 0
|
||||
for i in range(n):
|
||||
sp = max(0.0, sp + s[i])
|
||||
sm = min(0.0, sm + s[i])
|
||||
if max(sp, -sm) > k:
|
||||
sp = sm = 0.0
|
||||
last = i
|
||||
age[i] = i - last
|
||||
return age
|
||||
|
||||
|
||||
def _vratio(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray:
|
||||
"""Variance ratio VR = Var(r_q) / (q * Var(r_1)) su finestra w_days. >1 = persistente."""
|
||||
lr = _logret(c)
|
||||
qb = max(2, VR_Q_DAYS * bpd)
|
||||
lg = np.log(c)
|
||||
lrq = np.zeros(len(c))
|
||||
lrq[qb:] = lg[qb:] - lg[:-qb]
|
||||
lrq[:qb] = np.nan
|
||||
wb = max(20, w_days * bpd)
|
||||
v1 = pd.Series(lr).rolling(wb, min_periods=wb // 2).var().values
|
||||
vq = pd.Series(lrq).rolling(wb, min_periods=wb // 2).var().values
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
vr = vq / (qb * v1)
|
||||
return vr
|
||||
|
||||
|
||||
def _hurst(c: np.ndarray, w_days: int, bpd: int) -> np.ndarray:
|
||||
"""Hurst da scaling della volatilita': sd(r_k) ~ k^H. Regressione di log sd su log k
|
||||
(x fisso -> forma chiusa). Tutto rolling causale."""
|
||||
lg = np.log(c)
|
||||
wb = max(20, w_days * bpd)
|
||||
xs, ys = [], []
|
||||
for kd in HURST_K_DAYS:
|
||||
kb = max(1, kd * bpd)
|
||||
d = np.full(len(c), np.nan)
|
||||
d[kb:] = lg[kb:] - lg[:-kb]
|
||||
sd = pd.Series(d).rolling(wb, min_periods=wb // 2).std().values
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
ys.append(np.log(np.where(sd > 0, sd, np.nan)))
|
||||
xs.append(np.log(kb))
|
||||
X = np.asarray(xs, float)
|
||||
Y = np.vstack(ys) # (n_scale, n_bar)
|
||||
xm = X.mean()
|
||||
denom = float(((X - xm) ** 2).sum())
|
||||
with warnings.catch_warnings(): # warmup = colonne tutte-NaN
|
||||
warnings.simplefilter("ignore", RuntimeWarning)
|
||||
ym = np.nanmean(Y, axis=0)
|
||||
H = ((X - xm)[:, None] * (Y - ym[None, :])).sum(axis=0) / denom
|
||||
return H
|
||||
|
||||
|
||||
_STAT_CACHE: dict = {}
|
||||
|
||||
|
||||
def _stat_key(df: pd.DataFrame, det: str, par) -> tuple:
|
||||
ts = df["timestamp"].values
|
||||
return (det, par, len(df), int(ts[0]), int(ts[-1]))
|
||||
|
||||
|
||||
def lookback_bars(df: pd.DataFrame, det: str, par, mapping: str) -> np.ndarray:
|
||||
"""L_t in BARRE, dentro [Lmin, Lmax] giorni. Cache chiusa sul CONTENUTO del frame
|
||||
(lunghezza + primo/ultimo timestamp) -> un prefisso NON prende la cache del pieno,
|
||||
altrimenti causality_ok verrebbe ingannata dalla cache stessa."""
|
||||
bpd = A.bars_per_day(df)
|
||||
lo, hi = max(2, LMIN_D * bpd), max(3, LMAX_D * bpd)
|
||||
c = df["close"].values.astype(float)
|
||||
key = _stat_key(df, det, par)
|
||||
if key not in _STAT_CACHE:
|
||||
if det == "cusum":
|
||||
_STAT_CACHE[key] = _cusum_age(c, float(par), bpd)
|
||||
elif det == "vratio":
|
||||
_STAT_CACHE[key] = _roll_pctl(_vratio(c, int(par), bpd), PCTL_M_DAYS * bpd)
|
||||
elif det == "hurst":
|
||||
_STAT_CACHE[key] = _roll_pctl(_hurst(c, int(par), bpd), PCTL_M_DAYS * bpd)
|
||||
else:
|
||||
raise ValueError(det)
|
||||
stat = _STAT_CACHE[key]
|
||||
if det == "cusum":
|
||||
base = np.clip(stat, lo, hi)
|
||||
L = base if mapping == "direct" else (lo + hi - base)
|
||||
else:
|
||||
u = np.clip(stat, 0.0, 1.0)
|
||||
L = lo + u * (hi - lo) if mapping == "direct" else hi - u * (hi - lo)
|
||||
return np.clip(np.round(L), 2, hi).astype(int)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# DIREZIONE TSMOM (orizzonte costante o variabile) + vol-target
|
||||
# ===========================================================================
|
||||
def _sign_at(c: np.ndarray, h: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
n = len(c)
|
||||
i = np.arange(n)
|
||||
j = i - h
|
||||
ok = j >= 0
|
||||
s = np.zeros(n)
|
||||
jj = np.where(ok, j, 0)
|
||||
s[ok] = np.sign(c[i[ok]] / c[jj[ok]] - 1.0)
|
||||
return s, ok
|
||||
|
||||
|
||||
def _dir_from_L(c: np.ndarray, L: np.ndarray, blend: str) -> np.ndarray:
|
||||
hs = [L] if blend == "single" else [
|
||||
np.maximum(2, np.round(L / 3.0).astype(int)), L, np.round(2.0 * L).astype(int)]
|
||||
acc = np.zeros(len(c))
|
||||
cnt = np.zeros(len(c))
|
||||
for h in hs:
|
||||
s, ok = _sign_at(c, np.asarray(h, int))
|
||||
acc[ok] += s[ok]
|
||||
cnt[ok] += 1
|
||||
out = np.zeros(len(c))
|
||||
nz = cnt > 0
|
||||
out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def fixed_target(df: pd.DataFrame) -> np.ndarray:
|
||||
"""BASELINE: TSMOM fisso 30/90/180, long-flat, vol-target 20%, leva 2x = TP01 canonico."""
|
||||
c = df["close"].values.astype(float)
|
||||
bpd = A.bars_per_day(df)
|
||||
acc = np.zeros(len(c))
|
||||
cnt = np.zeros(len(c))
|
||||
for hd in FIXED_H:
|
||||
s, ok = _sign_at(c, np.full(len(c), hd * bpd, int))
|
||||
acc[ok] += s[ok]
|
||||
cnt[ok] += 1
|
||||
d = np.zeros(len(c))
|
||||
nz = cnt > 0
|
||||
d[nz] = acc[nz] / cnt[nz]
|
||||
return A.vol_target(np.clip(d, 0, None), df, TGT_VOL, VOLWIN, LEV)
|
||||
|
||||
|
||||
def const_factory(tf: str, L: int, blend: str):
|
||||
"""TSMOM a orizzonte COSTANTE L giorni, STESSA geometria del candidato adattivo.
|
||||
blend='triple' -> (L/3, L, 2L): con L=90 e' ESATTAMENTE TP01 canonico 30/90/180."""
|
||||
def fn(df: pd.DataFrame) -> np.ndarray:
|
||||
c = df["close"].values.astype(float)
|
||||
bpd = A.bars_per_day(df)
|
||||
Lb = np.full(len(c), max(2, int(round(L * bpd))), int)
|
||||
d = np.clip(_dir_from_L(c, Lb, blend), 0, None)
|
||||
return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV)
|
||||
fn.__name__ = f"const_L{L}_{blend}"
|
||||
return fn
|
||||
|
||||
|
||||
def factory(tf: str, det: str, par, mapping: str, blend: str):
|
||||
"""target_fn(df) -> posizione per barra, decisa con dati <= close[i]."""
|
||||
def fn(df: pd.DataFrame) -> np.ndarray:
|
||||
c = df["close"].values.astype(float)
|
||||
L = lookback_bars(df, det, par, mapping)
|
||||
d = np.clip(_dir_from_L(c, L, blend), 0, None)
|
||||
return A.vol_target(d, df, TGT_VOL, VOLWIN, LEV)
|
||||
fn.__name__ = f"adapt_{det}{par}_{mapping}_{blend}"
|
||||
return fn
|
||||
|
||||
|
||||
GRID = [dict(det=d, par=p, mapping=m, blend=b)
|
||||
for d in DETECTORS for p in PARAMS[d] for m in MAPPINGS for b in BLENDS]
|
||||
|
||||
# CONTROLLO dichiarato DOPO aver visto che il vincitore ha L_t incollato al bordo (sezione 3):
|
||||
# il null "TSMOM fisso 30/90/180" NON basta piu', perche' confonde due cose diverse —
|
||||
# "adattare l'orizzonte paga" e "un orizzonte piu' CORTO paga su questo campione".
|
||||
# Il controllo giusto e' il migliore fra gli orizzonti COSTANTI della stessa banda, scelto
|
||||
# in-sample-only. Trial aggiuntivi, contati al rialzo.
|
||||
CONST_L = (20, 30, 45, 60, 90, 120, 150, 200)
|
||||
CONST_GRID = [dict(L=L, blend=b) for L in CONST_L for b in BLENDS]
|
||||
|
||||
# ESTENSIONE dichiarata DOPO aver visto che l'ottimo costante cade sul BORDO BASSO della banda
|
||||
# (L=20g = LMIN_D): un ottimo al bordo non e' un ottimo, e' il punto in cui si e' smesso di
|
||||
# guardare. Si estende SOTTO e si guarda se e' un plateau o una scivolata verso il rumore.
|
||||
# +4 valori x 1 blend x 2 TF = 8 trial in piu' (contati al rialzo).
|
||||
CONST_L_EXT = (5, 7, 10, 15)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# UTILITA' DI MISURA
|
||||
# ===========================================================================
|
||||
def _dd(s: pd.Series) -> float:
|
||||
eq = np.cumprod(1.0 + np.asarray(s.dropna().values, float))
|
||||
pk = np.maximum.accumulate(eq)
|
||||
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
|
||||
|
||||
|
||||
def _cagr(s: pd.Series) -> float:
|
||||
s = s.dropna()
|
||||
if len(s) < 5:
|
||||
return float("nan")
|
||||
eq = float(np.prod(1.0 + s.values))
|
||||
yrs = (s.index[-1] - s.index[0]).total_seconds() / (86400 * 365.25)
|
||||
return eq ** (1 / max(yrs, 1e-6)) - 1.0
|
||||
|
||||
|
||||
def resample_offset(df_1h: pd.DataFrame, hours: int, off: int) -> pd.DataFrame:
|
||||
"""Stessa convenzione di trend_portfolio.resample_tf, con ancora spostata di `off` ore
|
||||
(r0702_tp01_offset.py). NB: si usa `offset=`, non `origin=` (pandas ignora origin su 7D)."""
|
||||
g = df_1h.copy()
|
||||
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||
idx.name = "dt"
|
||||
g.index = idx
|
||||
out = g.resample(f"{hours}h", offset=pd.Timedelta(hours=off), label="left", closed="left").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"]]
|
||||
|
||||
|
||||
def daily_from_target(fn, tf: str) -> pd.Series:
|
||||
return A.candidate_daily(fn, tf=tf)
|
||||
|
||||
|
||||
def paired_by_year(cand: pd.Series, base: pd.Series) -> list[dict]:
|
||||
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
|
||||
rows = []
|
||||
for y, g in J.groupby(J.index.year):
|
||||
if len(g) < 40:
|
||||
continue
|
||||
rows.append(dict(year=int(y), n=len(g),
|
||||
sh_cand=round(A._sh(g["C"]), 2), sh_base=round(A._sh(g["B"]), 2),
|
||||
d_sh=round(A._sh(g["C"]) - A._sh(g["B"]), 2),
|
||||
ret_cand=round(float(np.prod(1 + g["C"].values) - 1) * 100, 1),
|
||||
ret_base=round(float(np.prod(1 + g["B"].values) - 1) * 100, 1)))
|
||||
return rows
|
||||
|
||||
|
||||
def delever_null(cand: pd.Series, base: pd.Series) -> dict:
|
||||
"""Il fisso, DE-LEVERATO a pari maxDD del candidato, che Sharpe fa?
|
||||
Scalare le POSIZIONI per k scala gross e fee insieme -> il netto scala per k e lo Sharpe
|
||||
e' INVARIANTE: percio' un candidato che compra solo meno DD non e' un miglioramento."""
|
||||
J = pd.concat({"C": cand, "B": base}, axis=1, join="inner").dropna()
|
||||
dd_c, dd_b = _dd(J["C"]), _dd(J["B"])
|
||||
if dd_c >= dd_b or dd_b <= 0:
|
||||
return dict(applicabile=False, dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
|
||||
nota="il candidato NON riduce il maxDD: il null non si applica")
|
||||
lo, hi = 0.0, 1.0
|
||||
for _ in range(60):
|
||||
k = 0.5 * (lo + hi)
|
||||
if _dd(k * J["B"]) < dd_c:
|
||||
lo = k
|
||||
else:
|
||||
hi = k
|
||||
k = 0.5 * (lo + hi)
|
||||
return dict(applicabile=True, k=round(k, 4), dd_cand=round(dd_c, 4), dd_base=round(dd_b, 4),
|
||||
dd_base_delev=round(_dd(k * J["B"]), 4),
|
||||
sharpe_cand=round(A._sh(J["C"]), 3),
|
||||
sharpe_base_delev=round(A._sh(k * J["B"]), 3),
|
||||
superato=bool(A._sh(J["C"]) > A._sh(k * J["B"])))
|
||||
|
||||
|
||||
_ANCHOR_CACHE: dict = {}
|
||||
|
||||
|
||||
def series_at_anchor(fn, name: str, hours: int, off: int) -> pd.Series:
|
||||
"""Serie giornaliera 50/50 BTC+ETH della strategia `fn` ribilanciata sull'ancora `off`
|
||||
(resample del 1h certificato). Cache per (name, hours, off)."""
|
||||
key = (name, hours, off)
|
||||
if key not in _ANCHOR_CACHE:
|
||||
ser = {}
|
||||
for a in ASSETS:
|
||||
dfo = resample_offset(A.get(a, "1h"), hours, off)
|
||||
ev = A.eval_weights(dfo, A._call_target(fn, dfo, a))
|
||||
ser[a] = pd.Series(ev["net"], index=ev["idx"])
|
||||
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||
_ANCHOR_CACHE[key] = A._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
|
||||
return _ANCHOR_CACHE[key]
|
||||
|
||||
|
||||
def adaptivity(tf: str, det: str, par, mapping: str) -> dict:
|
||||
"""Quanto si MUOVE davvero L_t (diagnostica di MECCANISMO, non di rendimento).
|
||||
Una cella con L_t incollato a un bordo NON e' adattiva: e' un TSMOM a orizzonte
|
||||
COSTANTE travestito, e non appartiene alla famiglia che si sta testando."""
|
||||
fr_lo, fr_hi, iqr, med = [], [], [], []
|
||||
for a in ASSETS:
|
||||
df = A.get(a, tf)
|
||||
bpd = A.bars_per_day(df)
|
||||
L = lookback_bars(df, det, par, mapping) / bpd
|
||||
fr_lo.append(float(np.mean(L <= LMIN_D + 1)))
|
||||
fr_hi.append(float(np.mean(L >= LMAX_D - 1)))
|
||||
iqr.append(float(np.percentile(L, 75) - np.percentile(L, 25)))
|
||||
med.append(float(np.median(L)))
|
||||
return dict(frac_lo=float(np.mean(fr_lo)), frac_hi=float(np.mean(fr_hi)),
|
||||
iqr_days=float(np.mean(iqr)), med_days=float(np.mean(med)),
|
||||
adattiva=bool(np.mean(fr_lo) < 0.5 and np.mean(fr_hi) < 0.5
|
||||
and np.mean(iqr) >= 10.0))
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "=" * 78)
|
||||
if t:
|
||||
print(t)
|
||||
print("=" * 78)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
def main() -> None:
|
||||
print(__doc__.split("Runtime atteso")[0])
|
||||
|
||||
# -- 0. SANITY: il mio baseline DEVE essere TP01 canonico, bit-per-bit ----------------
|
||||
hr("0. SANITY — il baseline 'fisso' e' TP01 canonico (se non lo e', ogni Δ e' inventato)")
|
||||
tp = TrendPortfolio(**CANONICAL)
|
||||
worst = 0.0
|
||||
for a in ASSETS:
|
||||
df = A.get(a, "1d")
|
||||
worst = max(worst, float(np.max(np.abs(fixed_target(df) - tp.target_series(df)))))
|
||||
base_1d = daily_from_target(fixed_target, "1d")
|
||||
tp01 = A.tp01_baseline_daily()
|
||||
JJ = pd.concat({"a": base_1d, "b": tp01}, axis=1, join="inner").dropna()
|
||||
print(f" max|target_mio - TrendPortfolio.target_series| (1d, BTC+ETH) = {worst:.3e}")
|
||||
print(f" max|serie_giornaliera_mia - al.tp01_baseline_daily()| = "
|
||||
f"{float(np.max(np.abs(JJ['a'] - JJ['b']))):.3e} (n={len(JJ)})")
|
||||
assert worst < 1e-12, "il baseline NON riproduce TP01: fermarsi qui"
|
||||
|
||||
# -- 1. GRIGLIA ONESTA: cella scelta IN-SAMPLE + deflated-Sharpe ----------------------
|
||||
hr(f"1. study_family_honest — {len(GRID)} celle x {len(TFS)} TF = {len(GRID)*len(TFS)} trial")
|
||||
rep = A.study_family_honest("ADAPT-H", factory, GRID, TFS)
|
||||
ch = rep["chosen"]
|
||||
print(f" celle valutate : {rep['n_cells']}")
|
||||
print(f" cella scelta IN-SAMPLE : tf={ch['tf']} {ch['params']} "
|
||||
f"Sharpe IS {ch['insample_sharpe']} FULL {ch['full_sharpe']}")
|
||||
print(f" deflated-Sharpe (N={rep['n_cells']}) : {rep['deflated_sharpe']} "
|
||||
f"(max atteso sotto il null {rep['expected_null_max']}) PASS={rep['dsr_pass']}")
|
||||
print(f" earns_slot_marginal : {rep['earns_slot_marginal']}")
|
||||
print(f" EARNS_SLOT_HONEST : {rep['earns_slot_honest']}")
|
||||
print("\n top-8 IN-SAMPLE (come si sceglie onestamente):")
|
||||
for r in rep["rows"][:8]:
|
||||
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} "
|
||||
f"{r['tf']:>3} {r['params']}")
|
||||
|
||||
# sensibilita' del verdetto al CONTEGGIO dei trial (regola 30/07)
|
||||
fn_ch = factory(tf=ch["tf"], **ch["params"])
|
||||
d_ch = daily_from_target(fn_ch, ch["tf"])
|
||||
allsr = [r["full_sharpe"] for r in rep["rows"]]
|
||||
print("\n sensibilita' del DSR al numero di trial dichiarati:")
|
||||
for n_decl in (len(GRID) * len(TFS), 144, 216):
|
||||
pad = allsr + [float(np.mean(allsr))] * max(0, n_decl - len(allsr))
|
||||
dsr, _ = A.deflated_sharpe(A._sh(d_ch), pad, d_ch)
|
||||
print(f" N={n_decl:>4} DSR={dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}")
|
||||
|
||||
# cosa avrebbe scelto un disonesto (diagnostica, NON una decisione)
|
||||
diag = []
|
||||
for r in rep["rows"][:12]:
|
||||
s_all = daily_from_target(factory(tf=r["tf"], **r["params"]), r["tf"])
|
||||
diag.append({**r, "hold": round(A._sh(s_all[s_all.index >= HOLDOUT]), 3)})
|
||||
diag.sort(key=lambda r: -r["hold"])
|
||||
print("\n (diagnostica) le stesse 12 celle riordinate per HOLD-OUT — cio' che NON si fa:")
|
||||
for r in diag[:4]:
|
||||
print(f" HOLD {r['hold']:+.3f} IS {r['insample_sharpe']:+.3f} {r['tf']:>3} {r['params']}")
|
||||
|
||||
# -- 2. NUMERI del candidato vs il FISSO a pari geometria -----------------------------
|
||||
hr("2. CANDIDATO vs TSMOM FISSO 30/90/180 — stessa geometria, stesso TF, stesse fee")
|
||||
base_ch = daily_from_target(fixed_target, ch["tf"])
|
||||
J = pd.concat({"C": d_ch, "B": base_ch}, axis=1, join="inner").dropna()
|
||||
for lab, sl in (("FULL", J), ("HOLD-OUT", J[J.index >= HOLDOUT])):
|
||||
if len(sl) < 30:
|
||||
continue
|
||||
print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% "
|
||||
f"CAGR {_cagr(sl['C'])*100:6.2f}% | fisso Sh {A._sh(sl['B']):+.3f} "
|
||||
f"DD {_dd(sl['B'])*100:5.2f}% CAGR {_cagr(sl['B'])*100:6.2f}% "
|
||||
f"| dSh {A._sh(sl['C'])-A._sh(sl['B']):+.3f}")
|
||||
print(f" corr(adattivo, fisso) = {J['C'].corr(J['B']):.3f}")
|
||||
|
||||
print("\n DIFFERENZA APPAIATA PER ANNO (un vantaggio in un anno solo e' un evento):")
|
||||
rows = paired_by_year(d_ch, base_ch)
|
||||
for r in rows:
|
||||
print(f" {r['year']} adatt Sh {r['sh_cand']:+.2f} ({r['ret_cand']:+7.1f}%) "
|
||||
f"fisso Sh {r['sh_base']:+.2f} ({r['ret_base']:+7.1f}%) dSh {r['d_sh']:+.2f}")
|
||||
ds = [r["d_sh"] for r in rows]
|
||||
print(f" -> anni con dSh>0: {sum(1 for x in ds if x > 0)}/{len(ds)} "
|
||||
f"mediana dSh {np.median(ds):+.2f} media {np.mean(ds):+.2f}")
|
||||
|
||||
# -- 3. LOOKBACK: si muove davvero? ---------------------------------------------------
|
||||
hr("3. IL LOOKBACK ADATTIVO SI MUOVE? (se sta al bordo e' un TSMOM COSTANTE travestito)")
|
||||
for a in ASSETS:
|
||||
df = A.get(a, ch["tf"])
|
||||
bpd = A.bars_per_day(df)
|
||||
L = lookback_bars(df, ch["params"]["det"], ch["params"]["par"],
|
||||
ch["params"]["mapping"]) / bpd
|
||||
lo, hi = LMIN_D, LMAX_D
|
||||
print(f" {a}: L_t giorni min {L.min():.0f} p25 {np.percentile(L,25):.0f} "
|
||||
f"mediana {np.median(L):.0f} p75 {np.percentile(L,75):.0f} max {L.max():.0f} "
|
||||
f"| al bordo basso {np.mean(L<=lo+1)*100:.1f}% al bordo alto {np.mean(L>=hi-1)*100:.1f}%"
|
||||
f" | |dL|/giorno mediano {np.median(np.abs(np.diff(L))):.1f}g")
|
||||
|
||||
# -- 4. GATE ---------------------------------------------------------------------------
|
||||
hr("4. GATE")
|
||||
print(A.fmt_marginal(rep["marginal"]))
|
||||
|
||||
cz = A.causality_ok(fn_ch, tf=ch["tf"])
|
||||
print(f"\n causality_ok : ok={cz['ok']} max_tail_diff={cz['max_tail_diff']} "
|
||||
f"(prefissi controllati: {cz['checked']})")
|
||||
|
||||
imp = A.implausible_sharpe(d_ch)
|
||||
print(f" implausible_sharpe : implausible={imp['implausible']} Sh {imp.get('sharpe')} "
|
||||
f"maxDD {imp.get('maxdd')} perdite/attive {imp.get('loss_frac')} "
|
||||
f"attive {imp.get('active_frac')}")
|
||||
for rr in imp["reasons"]:
|
||||
print(f" - {rr}")
|
||||
|
||||
dl = delever_null(d_ch, base_ch)
|
||||
print(f" null del de-levering : {dl}")
|
||||
|
||||
# small-cap a $600
|
||||
print(" haircut a $600 (min-order $5):")
|
||||
for a in ASSETS:
|
||||
df = A.get(a, ch["tf"])
|
||||
sc_c = A.eval_weights_smallcap(df, A._call_target(fn_ch, df, a), capital=600.0)
|
||||
sc_b = A.eval_weights_smallcap(df, fixed_target(df), capital=600.0)
|
||||
print(f" {a}: adattivo modellato {sc_c['modeled']['sharpe']:+.3f} -> reale "
|
||||
f"{sc_c['realistic']['sharpe']:+.3f} (haircut {sc_c['sharpe_haircut']:+.3f}, "
|
||||
f"{sc_c['n_executed_trades']} trade) | fisso {sc_b['modeled']['sharpe']:+.3f} -> "
|
||||
f"{sc_b['realistic']['sharpe']:+.3f} (haircut {sc_b['sharpe_haircut']:+.3f}, "
|
||||
f"{sc_b['n_executed_trades']} trade)")
|
||||
|
||||
# -- 5. BANDA D'ANCORA -----------------------------------------------------------------
|
||||
hours = 24 if ch["tf"] == "1d" else 12
|
||||
offs = list(range(hours))
|
||||
hr(f"5. BANDA D'ANCORA — {len(offs)} ancore, stima onesta = MEDIANA (mai l'ancora 00:00)")
|
||||
|
||||
def _series_at(off: int, which: str) -> pd.Series:
|
||||
return series_at_anchor(fn_ch if which == "cand" else fixed_target, which, hours, off)
|
||||
|
||||
# sanity: l'ancora 0 riproduce la serie canonica
|
||||
s0 = _series_at(0, "fixed")
|
||||
K = pd.concat({"a": s0, "b": base_ch}, axis=1, join="inner").dropna()
|
||||
print(f" sanity ancora 0 vs serie canonica del fisso: max|Δ| = "
|
||||
f"{float(np.max(np.abs(K['a']-K['b']))):.3e} (n={len(K)})")
|
||||
|
||||
for lab, mtr in (("Sharpe FULL", A._sh),
|
||||
("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))):
|
||||
b_c = A.anchor_luck_band(lambda o: _series_at(o, "cand"), offs, metric=mtr)
|
||||
b_f = A.anchor_luck_band(lambda o: _series_at(o, "fixed"), offs, metric=mtr)
|
||||
print(f" {lab:<12} adattivo: canonica {b_c['canonical']:+.3f} (pctl {b_c['canonical_pctl']:.2f}) "
|
||||
f"MEDIANA {b_c['median']:+.3f} banda [{b_c['lo']:+.3f},{b_c['hi']:+.3f}]")
|
||||
print(f" {'':<12} fisso : canonica {b_f['canonical']:+.3f} (pctl {b_f['canonical_pctl']:.2f}) "
|
||||
f"MEDIANA {b_f['median']:+.3f} banda [{b_f['lo']:+.3f},{b_f['hi']:+.3f}]")
|
||||
dlt = A.anchor_luck_delta(lambda o: _series_at(o, "cand"),
|
||||
lambda o: _series_at(o, "fixed"), offs, metric=mtr)
|
||||
print(f" {'':<12} DELTA APPAIATO: mediana {dlt['median_paired']:+.3f} "
|
||||
f"positivo in {dlt['n_positive']}/{dlt['n_anchors']} ancore "
|
||||
f"banda [{dlt['lo']:+.3f},{dlt['hi']:+.3f}] gate_pass={dlt['gate_pass']}")
|
||||
|
||||
# -- 6. ADATTIVITA' EFFETTIVA DI TUTTA LA FAMIGLIA ------------------------------------
|
||||
hr("6. QUANTE DELLE 72 CELLE SONO DAVVERO ADATTIVE? (filtro di MECCANISMO, non di resa)")
|
||||
print(" criterio: L_t sta a un bordo <50% del tempo E l'IQR di L_t e' >= 10 giorni")
|
||||
seen, adatt_rows = {}, []
|
||||
for r in rep["rows"]:
|
||||
k = (r["tf"], r["params"]["det"], r["params"]["par"], r["params"]["mapping"])
|
||||
if k not in seen:
|
||||
seen[k] = adaptivity(*k)
|
||||
r["_adapt"] = seen[k]
|
||||
if seen[k]["adattiva"]:
|
||||
adatt_rows.append(r)
|
||||
n_ad_cells = sum(1 for r in rep["rows"] if r["_adapt"]["adattiva"])
|
||||
print(f" celle DAVVERO adattive: {n_ad_cells}/{len(rep['rows'])} "
|
||||
f"(le altre sono TSMOM a orizzonte COSTANTE travestito)")
|
||||
for k, v in sorted(seen.items()):
|
||||
print(f" {k[0]:>3} {k[1]:>6} {str(k[2]):>5} {k[3]:<7} bordo-basso {v['frac_lo']*100:5.1f}% "
|
||||
f"bordo-alto {v['frac_hi']*100:5.1f}% IQR {v['iqr_days']:5.1f}g "
|
||||
f"mediana {v['med_days']:5.1f}g -> {'ADATTIVA' if v['adattiva'] else 'costante'}")
|
||||
|
||||
# -- 7. IL CONTROLLO CHE MANCAVA: il MIGLIOR ORIZZONTE COSTANTE ------------------------
|
||||
hr(f"7. CONTROLLO L COSTANTE — {len(CONST_GRID)} celle x {len(TFS)} TF = "
|
||||
f"{len(CONST_GRID)*len(TFS)} trial IN PIU'")
|
||||
print(" la domanda diventa: il guadagno viene dall'ADATTARE, o dal fatto che su questo")
|
||||
print(" campione un orizzonte piu' CORTO batte 30/90/180 anche se lo si tiene fisso?")
|
||||
csel = A.select_cell_insample(lambda tf, L, blend: const_factory(tf, L, blend),
|
||||
CONST_GRID, TFS)
|
||||
cch = csel["chosen"]
|
||||
print(f"\n miglior COSTANTE scelto in-sample: tf={cch['tf']} {cch['params']} "
|
||||
f"Sharpe IS {cch['insample_sharpe']} FULL {cch['full_sharpe']}")
|
||||
print(" griglia costante, in-sample (L=90 triple == TP01 canonico):")
|
||||
for r in csel["rows"]:
|
||||
tag = " <== TP01 canonico" if (r["params"]["L"] == 90 and r["params"]["blend"] == "triple"
|
||||
and r["tf"] == "1d") else ""
|
||||
print(f" IS {r['insample_sharpe']:+.3f} FULL {r['full_sharpe']:+.3f} {r['tf']:>3} "
|
||||
f"L={r['params']['L']:>3}g {r['params']['blend']}{tag}")
|
||||
|
||||
fn_const = const_factory(**cch["params"], tf=cch["tf"])
|
||||
d_const = daily_from_target(fn_const, cch["tf"])
|
||||
JC = pd.concat({"C": d_ch, "K": d_const}, axis=1, join="inner").dropna()
|
||||
print(f"\n ADATTIVO vs MIGLIOR COSTANTE (entrambi scelti in-sample):")
|
||||
for lab, sl in (("FULL", JC), ("HOLD-OUT", JC[JC.index >= HOLDOUT])):
|
||||
if len(sl) < 30:
|
||||
continue
|
||||
print(f" {lab:<9} adattivo Sh {A._sh(sl['C']):+.3f} DD {_dd(sl['C'])*100:5.2f}% | "
|
||||
f"costante Sh {A._sh(sl['K']):+.3f} DD {_dd(sl['K'])*100:5.2f}% | "
|
||||
f"dSh {A._sh(sl['C'])-A._sh(sl['K']):+.3f}")
|
||||
print(f" corr(adattivo, miglior costante) = {JC['C'].corr(JC['K']):.3f}")
|
||||
print("\n differenza APPAIATA per anno vs il miglior COSTANTE:")
|
||||
rows2 = paired_by_year(d_ch, d_const)
|
||||
for r in rows2:
|
||||
print(f" {r['year']} adatt {r['sh_cand']:+.2f} costante {r['sh_base']:+.2f} "
|
||||
f"dSh {r['d_sh']:+.2f}")
|
||||
d2 = [r["d_sh"] for r in rows2]
|
||||
print(f" -> anni con dSh>0: {sum(1 for x in d2 if x > 0)}/{len(d2)} "
|
||||
f"mediana {np.median(d2):+.2f}")
|
||||
dl2 = delever_null(d_ch, d_const)
|
||||
print(f" null del de-levering vs costante: {dl2}")
|
||||
|
||||
# ancora appaiata adattivo vs miglior costante (se stesso TF, altrimenti dichiarato)
|
||||
if cch["tf"] == ch["tf"]:
|
||||
for lab, mtr in (("Sharpe FULL", A._sh),
|
||||
("Sharpe HOLD", lambda s: A._sh(s[s.index >= HOLDOUT]))):
|
||||
dd2 = A.anchor_luck_delta(lambda o: series_at_anchor(fn_ch, "cand", hours, o),
|
||||
lambda o: series_at_anchor(fn_const, "const", hours, o),
|
||||
offs, metric=mtr)
|
||||
print(f" ancora {lab}: DELTA APPAIATO mediana {dd2['median_paired']:+.3f} "
|
||||
f"positivo in {dd2['n_positive']}/{dd2['n_anchors']} "
|
||||
f"banda [{dd2['lo']:+.3f},{dd2['hi']:+.3f}] gate_pass={dd2['gate_pass']}")
|
||||
else:
|
||||
print(f" (ancora appaiata non girata: TF diversi, adattivo {ch['tf']} vs "
|
||||
f"costante {cch['tf']} -> le ancore non sono coppie)")
|
||||
|
||||
# fee sweep: l'adattivo tradà di piu', il conto va fatto
|
||||
print("\n FEE SWEEP (il candidato ha piu' turnover: e' li' che muore o no)")
|
||||
for nm, f_ in (("adattivo", fn_ch), ("costante", fn_const), ("TP01 30/90/180", fixed_target)):
|
||||
tfx = ch["tf"] if nm != "costante" else cch["tf"]
|
||||
line = []
|
||||
for fee in (0.0, 0.0005, 0.001, 0.0015):
|
||||
dser = A.candidate_daily(f_, tf=tfx, fee_side=fee)
|
||||
line.append(f"{2*fee*100:.2f}%RT {A._sh(dser):+.3f}")
|
||||
tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(f_, A.get(a, tfx), a))
|
||||
["turnover_per_year"] for a in ASSETS])
|
||||
print(f" {nm:<16} " + " ".join(line) + f" | turnover {tt:.0f}x/anno")
|
||||
|
||||
# -- 8. LA FAMIGLIA RISTRETTA ALLE CELLE DAVVERO ADATTIVE -----------------------------
|
||||
hr("8. GATE ONESTO SULLA SOLA FAMIGLIA ADATTIVA (la domanda della missione, isolata)")
|
||||
if not adatt_rows:
|
||||
print(" NESSUNA cella della famiglia e' davvero adattiva -> la domanda non e'")
|
||||
print(" misurabile su questa famiglia: ogni 'vincitore' e' un orizzonte costante.")
|
||||
else:
|
||||
best_ad = max(adatt_rows, key=lambda r: r["insample_sharpe"])
|
||||
print(f" celle adattive: {len(adatt_rows)} miglior IN-SAMPLE: tf={best_ad['tf']} "
|
||||
f"{best_ad['params']} IS {best_ad['insample_sharpe']} FULL {best_ad['full_sharpe']}")
|
||||
fn_ad = factory(tf=best_ad["tf"], **best_ad["params"])
|
||||
d_ad = daily_from_target(fn_ad, best_ad["tf"])
|
||||
sm_ad = A.study_marginal("ADAPT-H(solo adattive)", fn_ad, tf=best_ad["tf"])
|
||||
dsr_ad, sr0_ad = A.deflated_sharpe(A._sh(d_ad),
|
||||
[r["full_sharpe"] for r in adatt_rows], d_ad)
|
||||
print(f" marginal={sm_ad['marginal_verdict']} earns_slot={sm_ad['earns_slot']} "
|
||||
f"corr->TP01 {sm_ad['marginal'].get('corr_full')}")
|
||||
print(f" deflated-Sharpe (N={len(adatt_rows)}) = {dsr_ad:.3f} "
|
||||
f"{'PASS' if dsr_ad >= 0.95 else 'FAIL'} (null max atteso {sr0_ad:.3f})")
|
||||
JA = pd.concat({"C": d_ad, "K": d_const, "B": base_1d}, axis=1, join="inner").dropna()
|
||||
for lab, sl in (("FULL", JA), ("HOLD-OUT", JA[JA.index >= HOLDOUT])):
|
||||
if len(sl) < 30:
|
||||
continue
|
||||
print(f" {lab:<9} adattiva-vera Sh {A._sh(sl['C']):+.3f} | miglior costante "
|
||||
f"{A._sh(sl['K']):+.3f} | TP01 {A._sh(sl['B']):+.3f}")
|
||||
rows3 = paired_by_year(d_ad, d_const)
|
||||
d3 = [r["d_sh"] for r in rows3]
|
||||
print(f" vs miglior costante, per anno: dSh>0 in {sum(1 for x in d3 if x>0)}/{len(d3)}"
|
||||
f" mediana {np.median(d3):+.2f}")
|
||||
|
||||
# -- 9. L'OTTIMO COSTANTE E' AL BORDO: si estende la banda ----------------------------
|
||||
hr("9. L'OTTIMO COSTANTE CADE SUL BORDO — plateau o scivolata verso il rumore?")
|
||||
print(" un argmax al bordo della griglia non e' un ottimo: e' il punto in cui si e' smesso")
|
||||
print(" di guardare. Si estende SOTTO i 20 giorni (8 trial in piu', contati al rialzo).")
|
||||
ext = sorted(set(CONST_L) | set(CONST_L_EXT))
|
||||
tab: dict = {}
|
||||
for tfx in TFS:
|
||||
print(f"\n --- TF {tfx}, blend triple (L/3, L, 2L) ---")
|
||||
print(f" {'L(g)':>5} {'orizzonti':>16} {'IS':>7} {'FULL':>7} {'HOLD':>7} "
|
||||
f"{'maxDD':>7} {'CAGR':>7} {'turn/a':>7} {'Sh@0.30%RT':>11}")
|
||||
for L in ext:
|
||||
fnx = const_factory(tfx, L, "triple")
|
||||
dx = daily_from_target(fnx, tfx)
|
||||
dx_h = dx[dx.index >= HOLDOUT]
|
||||
dx_i = dx[dx.index < HOLDOUT]
|
||||
d030 = A.candidate_daily(fnx, tf=tfx, fee_side=0.0015)
|
||||
tt = np.mean([A.eval_weights(A.get(a, tfx), A._call_target(fnx, A.get(a, tfx), a))
|
||||
["turnover_per_year"] for a in ASSETS])
|
||||
tag = " <== TP01" if L == 90 else ""
|
||||
tab[(tfx, L)] = dict(IS=A._sh(dx_i), FULL=A._sh(dx), HOLD=A._sh(dx_h))
|
||||
print(f" {L:>5} {f'{L//3}/{L}/{2*L}':>16} {A._sh(dx_i):>7.3f} {A._sh(dx):>7.3f} "
|
||||
f"{A._sh(dx_h):>7.3f} {_dd(dx)*100:>6.2f}% {_cagr(dx)*100:>6.2f}% "
|
||||
f"{tt:>7.0f} {A._sh(d030):>11.3f}{tag}")
|
||||
|
||||
# --- e' una CURVA con un ottimo, o un crinale di RUMORE? --------------------------
|
||||
print("\n RISOLUZIONE della griglia (regola 07/08: un criterio che decide su una frazione")
|
||||
print(" dello spread della griglia e' rumore anche quando 'passa'):")
|
||||
for tfx in TFS:
|
||||
v = [tab[(tfx, L)]["IS"] for L in ext]
|
||||
jump = float(np.median(np.abs(np.diff(v))))
|
||||
arg = ext[int(np.argmax(v))]
|
||||
arg_orig = max(CONST_L, key=lambda L: tab[(tfx, L)]["IS"])
|
||||
print(f" {tfx:>3}: argmax IS con la griglia ORIGINALE L={arg_orig}g -> con la griglia "
|
||||
f"ESTESA L={arg}g ({'SPOSTATO' if arg != arg_orig else 'stabile'})")
|
||||
print(f" salto IS mediano fra L ADIACENTI {jump:+.3f} contro il vantaggio del "
|
||||
f"vincitore su TP01 {max(v) - tab[(tfx, 90)]['IS']:+.3f} "
|
||||
f"(rapporto {jump / max(1e-9, max(v) - tab[(tfx, 90)]['IS']):.2f})")
|
||||
allc = [(tf, L) for tf in TFS for L in ext]
|
||||
for met in ("IS", "FULL", "HOLD"):
|
||||
vals = sorted((tab[k][met] for k in allc), reverse=True)
|
||||
r1d = 1 + sum(1 for x in vals if x > tab[("1d", 90)][met])
|
||||
print(f" rango di TP01 (1d, L=90g) fra le {len(allc)} celle su {met}: "
|
||||
f"{r1d}/{len(allc)} (suo {tab[('1d',90)][met]:+.3f}, migliore {vals[0]:+.3f})")
|
||||
|
||||
hr("FINE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user