638 lines
34 KiB
Python
638 lines
34 KiB
Python
"""r0822b_prop_recal — PROP-ALLOC RIFATTO con XS01 preso dalla STORIA LUNGA (2026-08-22, ondata 2).
|
|
|
|
IL BUCO CHE CHIUDE
|
|
------------------
|
|
Il risultato di testa dell'ondata (RESULTS-0822 §1, PROP-ALLOC) e': su un conto FUNDED da $100k
|
|
l'allocazione ottima sotto barriera di drawdown mette **XS01 al 50%** e porta
|
|
J = P(pass) x P(vivo 12m) da 0,335 (libro live 75/25) a **0,738**, con
|
|
**P(>=50 EUR/g) da 600 EUR in 36 mesi = 42%**. Quel filone ha dichiarato da solo il proprio limite:
|
|
|
|
"tutto il vantaggio poggia sul drift di XS01 misurato sulla sua finestra di scoperta"
|
|
|
|
e ha proposto un gate che era un'ATTESA: *non aprire un funded prima che XS01 abbia una finestra
|
|
fuori dal 2024-2026*. Il filone XS01-OOS (RESULTS-0822 §21) ha trovato quella finestra lo stesso
|
|
giorno — stessi 19 ticker su Binance spot USDT 1d, **2021-01 -> 2023-12, con LUNA e FTX dentro** —
|
|
e ha misurato che l'edge di RENDIMENTO regge (mediana di fase +1,12 fuori contro +0,37 dentro,
|
|
differenza appaiata +0,668 in 10/10 fasi). Ma ha anche misurato TRE correzioni nel verso opposto:
|
|
(i) il maxDD standalone di XS01 RADDOPPIA: 10,8% -> 20,9-22,0%;
|
|
(ii) il peso 0,50 non sopravvive: fuori campione a w=0,50 il maxDD di libro risale a 8,9%
|
|
contro 6,0% a w=0,25 — cioe' restituisce la protezione di DD che e' l'UNICA ragione per
|
|
mettere XS01 su un conto a barriera;
|
|
(iii) il 32% dell'universo non e' testabile (ARB/OP/SUI/APT/SEI/TIA non quotati nel 2021-23) e
|
|
su di esso vive gran parte del numero di scoperta (19 gambe 1,31 vs 13 gambe 0,54).
|
|
|
|
Questo script rifa' il conto di PROP-ALLOC con la serie LUNGA al posto di quella di scoperta e
|
|
risponde a UNA domanda: **J regge?**
|
|
|
|
LA PREVISIONE, REGISTRATA PRIMA DI MISURARE
|
|
-------------------------------------------
|
|
Mi aspetto che J SCENDA ma resti sopra il libro live, per tre ragioni che tirano in versi diversi:
|
|
(a) il drift di XS01 fuori campione e' PIU' ALTO -> spinge J su;
|
|
(b) il suo maxDD raddoppia e la barriera e' STATICA (una soglia assoluta non e' scale-invariante)
|
|
-> spinge J giu', e su un conto a barriera questa e' la forza che conta;
|
|
(c) la finestra lunga contiene il 2022, quindi TP01 (lo sleeve DIFENSIVO, sotto-pesato da
|
|
PROP-ALLOC su un campione senza sinistro) ci arriva con un curriculum diverso -> mi aspetto
|
|
che l'argmax RIALZI TP01 e ABBASSI XS01 rispetto al 25/25/50.
|
|
Previsione numerica dichiarata prima: argmax con w_XS in [0,25 ; 0,38], J fra 0,50 e 0,70,
|
|
P(>=50/g) fra il 10% e il 35%. Se J restasse >= 0,70 con w_XS = 0,50 la previsione e' sbagliata e
|
|
lo dico; se J crollasse sotto il livello del libro live, il fronte funded va ripensato ADESSO.
|
|
|
|
DISCIPLINA DI RIPRODUZIONE (non negoziabile)
|
|
--------------------------------------------
|
|
Prima di pubblicare un numero nuovo la macchina deve riprodurre quello vecchio. §0 rigira
|
|
PROP-ALLOC con la sua stessa macchineria (`r0822_prop_alloc` IMPORTATO, non riscritto), stessi
|
|
semi, stessa griglia, stesso de-luck, e confronta con i valori PUBBLICATI, cablati qui sotto in
|
|
`PUB`. Se la riproduzione fallisce, quello E' il risultato e viene detto.
|
|
|
|
CONVENZIONI DICHIARATE
|
|
----------------------
|
|
* Lente **ACCOPPIATA obbligatoria**. Su una regola di DD a UN GIORNO la lente close-only e'
|
|
esattamente CIECA (misurato oggi: 0,00 breach contro 0,40-1,21 veri, rapporto INF). La
|
|
macchineria del wick e' quella di `r0725_prop_coupled` (crypto: minimo ESATTO sul path orario
|
|
condiviso TP01+SKH01; XS01: 4 checkpoint O/estremo/estremo/C con ordinamento CONDIVISO e ordine
|
|
AVVERSO dagli OHLC giornalieri). Per la finestra 2021-23 gli OHLC sono quelli **Binance** della
|
|
cache di `r0822b_xs01_oos` — il costruttore di tuple e' lo STESSO codice e viene validato in §1
|
|
contro `pc.xsec_daily_tuples()` sui dati HL, dove deve dare max|diff| = 0.
|
|
* **Universo dichiarato: U13** (i 13 dei 19 quotati prima del 2020-11) come serie PRIMARIA, perche'
|
|
e' l'unica testabile fuori campione e perche' e' la versione **PIU' DEBOLE** di quella che sta nel
|
|
libro (13 gambe invece di 19: XS01-OOS misura 0,54 contro 1,31 sulla stessa finestra e venue).
|
|
Scegliere la piu' debole e' conservativo per la decisione. U19-EXP (universo che si allarga da 10
|
|
a 19 gambe) e' riportato come **altro capo della banda**, mai come numero di testa.
|
|
* Meccanismo **CONGELATO** (XS_CFG: blend [30,90], H=10, k=5, gate p30, vol-target 20%, 5 bps/lato).
|
|
Nessun parametro toccato, nessuna cella cercata: qui non si seleziona, si RI-MISURA.
|
|
* de-luck sul drift **x0,89** (misurato 26/07), identico a PROP-ALLOC.
|
|
* **Distorsioni portate avanti DICHIARATE, non rimosse in silenzio:**
|
|
(1) la banda d'ancora NON e' girata sotto questa lente (fuori budget il 22/08 e fuori budget
|
|
qui): l'ottimo gira sull'ancora canonica e si appoggia a SKH01, che il progetto misura come
|
|
lo sleeve con la fortuna d'ancora piu' grande da restituire -> la distorsione e' A FAVORE
|
|
del risultato. Lo stress SKH x0,50 / x0,33 e' li' apposta.
|
|
(2) l'intero risultato poggia sull'assunto — **MAI VERIFICATO CON UNA FIRM** — che una firm
|
|
crypto listi i 19 (qui 13) alt Hyperliquid **con short abilitato**. Il critico dell'ondata
|
|
lo chiama "la ripetizione esatta dell'errore GTAA01/PRIIPs". Non e' verificabile da qui
|
|
(serve leggere un listino), quindi §6 misura quanto del risultato MUORE se e' falso.
|
|
* Monte Carlo: 2500 percorsi per J (come PROP-ALLOC), 1500 per la scala di conti. **Un Monte Carlo
|
|
ha una risoluzione e va detta**: SE su un livello di J ~ +-0,010, sulle differenze APPAIATE
|
|
(indici di bootstrap condivisi) molto meno, ~+-0,007.
|
|
|
|
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_prop_recal.py`
|
|
Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance di r0822b_xs01_oos).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
|
|
|
import r0725_prop_coupled as pc # noqa: E402 macchineria del wick accoppiato (25/07)
|
|
import r0725_prop_ladder as pl # noqa: E402 regole delle firm + scala di conti
|
|
import r0822_prop_alloc as PA # noqa: E402 IL filone da riprodurre (22/08)
|
|
import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08)
|
|
from src.portfolio.sleeves import XS_UNIVERSE, _HL_DIR # noqa: E402
|
|
|
|
RAW = ROOT / "data" / "raw"
|
|
FEE_SIDE = 0.001 / 2.0
|
|
HYRO = "HYRO"
|
|
|
|
# ---------------------------------------------------------------- numeri PUBBLICATI da riprodurre
|
|
# (RESULTS-0822 §1, tabella "1 — PROP-ALLOC"; log della corsa nello scratchpad)
|
|
PUB = dict(
|
|
argmaxJ=dict(w="25/25/50", lev=0.750, J=0.738, p_pass=0.822, p_alive=0.898, sharpe=1.69),
|
|
argmaxS=dict(w="38/25/38", lev=0.750, J=0.721, p_pass=0.773, p_alive=0.933, sharpe=1.73),
|
|
live=dict(w="75/25/0", lev=0.500, J=0.335, p_pass=0.386, p_alive=0.868, sharpe=1.09),
|
|
dJ_vs_sharpe=0.0164, dJ_vs_live=0.3996,
|
|
ladder_med=39.28, ladder_p50=0.417, ladder_zero=0.139,
|
|
)
|
|
|
|
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
|
|
STEP = 8
|
|
N_PATHS = PA.N_PATHS # 2500
|
|
EV_H = FU_H = 365
|
|
SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC
|
|
W3 = PA.CRYPTO_PROP # ("TP01","SKH01","XS01")
|
|
|
|
TRIALS = 0 # contatore della griglia dichiarata (al RIALZO)
|
|
|
|
|
|
def bump(n: int) -> None:
|
|
global TRIALS
|
|
TRIALS += n
|
|
|
|
|
|
def hr(t: str = "") -> None:
|
|
print("\n" + "-" * 104)
|
|
if t:
|
|
print(f" {t}")
|
|
print("-" * 104)
|
|
|
|
|
|
# =============================================================== 1. XS01 ACCOPPIATO, QUALSIASI DATO
|
|
|
|
def ohlc_panels(loader, syms: list[str]) -> tuple[pd.DataFrame, ...]:
|
|
"""(C, O, H, L) allineati. `loader(sym) -> DataFrame con timestamp/open/high/low/close`."""
|
|
o, h, lo, c = {}, {}, {}, {}
|
|
for s in syms:
|
|
d = loader(s)
|
|
i = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
|
o[s] = pd.Series(d["open"].astype(float).values, index=i)
|
|
h[s] = pd.Series(d["high"].astype(float).values, index=i)
|
|
lo[s] = pd.Series(d["low"].astype(float).values, index=i)
|
|
c[s] = pd.Series(d["close"].astype(float).values, index=i)
|
|
C = pd.concat(c, axis=1).sort_index()
|
|
return C, pd.concat(o, axis=1).reindex(C.index)[C.columns], \
|
|
pd.concat(h, axis=1).reindex(C.index)[C.columns], \
|
|
pd.concat(lo, axis=1).reindex(C.index)[C.columns]
|
|
|
|
|
|
def xs_tuples(C: pd.DataFrame, O: pd.DataFrame, H: pd.DataFrame, L: pd.DataFrame,
|
|
phase: int = 0, wick_mode: str = "shared", **kw) -> pd.DataFrame:
|
|
"""(R, m) giornalieri di XS01 su un pannello QUALSIASI, con la stessa convenzione di wick di
|
|
`r0725_prop_coupled.xsec_daily_tuples`. Le posizioni vengono dal motore CONGELATO di
|
|
`r0822b_xs01_oos.xsec_engine` (che regge i NaN = asset non ancora quotato).
|
|
|
|
Validato in §1 contro `pc.xsec_daily_tuples()` sui dati HL: deve dare max|diff| = 0."""
|
|
det: dict = {}
|
|
r = X.xsec_engine(C, phase=phase, detail=det, **kw)
|
|
W, scale, turn = det["W"], det["scale"], det["turn"]
|
|
px = C.values.astype(float)
|
|
n, A = px.shape
|
|
prev_c = np.vstack([np.full((1, A), np.nan), px[:-1]])
|
|
held = np.vstack([np.zeros((1, A)), W[:-1]]) # gambe TENUTE nel giorno i
|
|
with np.errstate(invalid="ignore"):
|
|
rO = O.values / prev_c - 1.0
|
|
rH = H.values / prev_c - 1.0
|
|
rL = L.values / prev_c - 1.0
|
|
rC = px / prev_c - 1.0
|
|
vO = np.nansum(held * rO, axis=1)
|
|
vH = np.nansum(held * rH, axis=1)
|
|
vL = np.nansum(held * rL, axis=1)
|
|
vC = np.nansum(held * rC, axis=1)
|
|
if wick_mode == "perleg": # bound severo: ogni gamba al suo peggio
|
|
worst = np.where(held >= 0, rL, rH)
|
|
m_gross = np.minimum(np.nansum(held * worst, axis=1), vC)
|
|
else: # 'shared': ordine AVVERSO
|
|
m_gross = np.minimum(np.minimum.reduce([vO, vL, vH, vC]),
|
|
np.minimum.reduce([vO, vH, vL, vC]))
|
|
m_net = (m_gross - turn * FEE_SIDE) * scale
|
|
R_net = r.values
|
|
out = pd.DataFrame({"R": R_net, "m": np.minimum(m_net, R_net)}, index=C.index)
|
|
return out
|
|
|
|
|
|
def bin_loader(sym: str) -> pd.DataFrame:
|
|
"""Legge dalla cache di r0822b (nessuna rete) e SCARTA la barra del giorno in corso: una barra
|
|
giornaliera parziale in coda e' esattamente il difetto trovato oggi nei monitor forward."""
|
|
d = X.fetch_binance_daily(sym)
|
|
today = pd.Timestamp.utcnow().normalize().value // 10**6
|
|
return d[d["timestamp"] < today].reset_index(drop=True)
|
|
|
|
|
|
def hl_loader(sym: str) -> pd.DataFrame:
|
|
return pd.read_parquet(_HL_DIR / f"hl_{sym.lower()}_1d.parquet")
|
|
|
|
|
|
# =============================================================== 2. LIBRO A 3 GAMBE, PANNELLO LIBERO
|
|
|
|
def rg(w: dict, idx: pd.DatetimeIndex, xs: pd.DataFrame, drop_xs: bool = False,
|
|
dm: tuple = ()) -> tuple[np.ndarray, np.ndarray]:
|
|
"""(R, gap) del libro TP01/SKH01/XS01 su un pannello arbitrario e con una serie XS01
|
|
arbitraria. E' `PA.book_RG` con il pannello e la serie XS01 come argomenti invece che
|
|
cablati: crypto col minimo ESATTO sul path orario condiviso (`PA._crypto`, che per uno
|
|
stress taglia il drift sulle GAMBE ORARIE prima di comporre — il minimo giornaliero non e'
|
|
separabile per gamba, quindi non si puo' tagliare a valle); il minimo di XS01 si SOMMA
|
|
(worst simultaneo, convenzione severa del 25/07). gap = min_intraday - chiusura (<= 0).
|
|
Validato bit-exact contro `PA.book_RG` in §1.
|
|
|
|
`dm` = (("XS01", 0.5), ...) taglia il DRIFT di quello sleeve lasciando la forma della coda.
|
|
⚠ Il taglio crypto usa la media sull'INTERA storia oraria (2019+), non sulla finestra: e'
|
|
la convenzione di PROP-ALLOC, tenuta identica perche' le due tabelle vanno confrontate."""
|
|
dmd = dict(dm)
|
|
w_tp, w_sk = float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0))
|
|
w_xs = 0.0 if drop_xs else float(w.get("XS01", 0.0))
|
|
R = np.zeros(len(idx))
|
|
M = np.zeros(len(idx))
|
|
if w_tp > 0 or w_sk > 0:
|
|
c = PA._crypto(w_tp, w_sk, dmd.get("TP01", 1.0), dmd.get("SKH01", 1.0)).reindex(idx)
|
|
R += np.nan_to_num(c["R"].values)
|
|
M += np.nan_to_num(c["m"].values)
|
|
if w_xs > 0:
|
|
f = xs.reindex(idx)
|
|
rv = np.nan_to_num(f["R"].values)
|
|
mv = np.nan_to_num(f["m"].values)
|
|
mult = dmd.get("XS01", 1.0)
|
|
if mult != 1.0:
|
|
cutv = (1.0 - mult) * rv.mean() # media PRIMA del taglio (come PA)
|
|
rv, mv = rv - cutv, mv - cutv
|
|
R += w_xs * rv
|
|
M += w_xs * mv
|
|
return R, np.minimum(M, R) - R
|
|
|
|
|
|
def obj_rg(R: np.ndarray, G: np.ndarray, lev: float, ev_idx, fu_idx,
|
|
firm: str = HYRO, factor: float = PA.DELUCK) -> dict:
|
|
"""`PA.objective` senza la costruzione del libro (che qui e' su un pannello diverso)."""
|
|
Rd = PA.deluck(R, factor)
|
|
e = PA.eval_sim(Rd, G, ev_idx, lev, firm)
|
|
f = PA.funded_sim(Rd, G, fu_idx, lev, firm, 100_000.0)
|
|
ann = float(Rd.mean() * 365.0)
|
|
vol = float(Rd.std() * np.sqrt(365.0))
|
|
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
|
|
e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0),
|
|
vol=vol, drift=ann, jvec=(e["passed"] & f["alive"]).astype(float))
|
|
|
|
|
|
def maxdd(R: np.ndarray) -> float:
|
|
eq = np.cumprod(1.0 + R)
|
|
return float((eq / np.maximum.accumulate(eq) - 1.0).min())
|
|
|
|
|
|
def scan(grid: list[dict], idx, xs, ev_idx, fu_idx, drop_xs: bool = False,
|
|
dm: tuple = ()) -> pd.DataFrame:
|
|
rows = []
|
|
for w in grid:
|
|
R, G = rg(w, idx, xs, drop_xs=drop_xs, dm=dm)
|
|
dd = maxdd(PA.deluck(R))
|
|
for lev in LEVS:
|
|
o = obj_rg(R, G, lev, ev_idx, fu_idx)
|
|
rows.append(dict(w=PA.wkey(w), lev=lev, maxdd=dd,
|
|
**{k: v for k, v in o.items() if k != "jvec"}))
|
|
bump(len(grid) * len(LEVS))
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def best_row(df: pd.DataFrame, key: str = "J") -> pd.Series:
|
|
return df.loc[df[key].idxmax()]
|
|
|
|
|
|
def wdict(key: str) -> dict:
|
|
return dict(zip(W3, [int(x) / 100 for x in key.split("/")]))
|
|
|
|
|
|
# =============================================================== main
|
|
def main() -> None:
|
|
try:
|
|
sys.stdout.reconfigure(line_buffering=True)
|
|
except Exception: # pragma: no cover
|
|
pass
|
|
t0 = time.time()
|
|
|
|
def tick(lab: str) -> None:
|
|
print(f" [{time.time()-t0:6.0f}s] {lab}")
|
|
|
|
print("=" * 104)
|
|
print(" PROP-RECAL — J regge se XS01 viene dalla STORIA LUNGA invece che dalla sua finestra"
|
|
" di scoperta?")
|
|
print("=" * 104)
|
|
print(" PREVISIONE REGISTRATA PRIMA: argmax con w_XS in [0.25, 0.38], J in [0.50, 0.70],")
|
|
print(" P(>=50 EUR/g) in [10%, 35%]. Il verso atteso: drift XS piu' alto (spinge su) contro")
|
|
print(" maxDD raddoppiato + barriera STATICA + 2022 dentro (spingono giu'; su un conto a")
|
|
print(" barriera la seconda forza pesa di piu').")
|
|
|
|
# ================================================== §0. RIPRODUZIONE
|
|
hr("§0. RIPRODUZIONE — la macchina deve dare i numeri PUBBLICATI prima di darne di nuovi")
|
|
idx_pub = PA._panel_index(PA.START_WIN)
|
|
ev_pub = PA.boot_idx(len(idx_pub), EV_H, N_PATHS, seed=SEED_EV)
|
|
fu_pub = PA.boot_idx(len(idx_pub), FU_H, N_PATHS, seed=SEED_FU)
|
|
print(f" pannello 5-sleeve di PROP-ALLOC: {len(idx_pub)} giorni "
|
|
f"{idx_pub[0].date()} -> {idx_pub[-1].date()} | de-luck x{PA.DELUCK} | "
|
|
f"{N_PATHS} percorsi | semi {SEED_EV}/{SEED_FU}")
|
|
grid = PA.simplex_grid(W3, STEP)
|
|
t = time.time()
|
|
Gpub = PA.grid_scan(grid, LEVS, HYRO, ev_pub, fu_pub)
|
|
bump(len(grid) * len(LEVS))
|
|
print(f" griglia {len(grid)} pesi x {len(LEVS)} leve = {len(grid)*len(LEVS)} celle "
|
|
f"({time.time()-t:.0f}s)")
|
|
b_pub = best_row(Gpub)
|
|
ws = Gpub.loc[Gpub["sharpe"].idxmax(), "w"]
|
|
bs_pub = best_row(Gpub[Gpub["w"] == ws])
|
|
bl_pub = best_row(Gpub[Gpub["w"] == "75/25/0"])
|
|
print(f"\n {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'PUB':>7} {'d':>7} | "
|
|
f"{'P(pass)':>8} {'PUB':>7} | {'P(vivo)':>8} {'PUB':>7} | {'Sharpe':>7} {'PUB':>6}")
|
|
ok = True
|
|
for lab, row, p in (("argmax J", b_pub, PUB["argmaxJ"]), ("argmax Sharpe", bs_pub, PUB["argmaxS"]),
|
|
("libro LIVE 75/25", bl_pub, PUB["live"])):
|
|
same_w = (row["w"] == p["w"]) and abs(row["lev"] - p["lev"]) < 1e-9
|
|
dj = row["J"] - p["J"]
|
|
ok &= same_w and abs(dj) < 0.02
|
|
print(f" {lab:>18} {row['w']:>10} {row['lev']:>6.3f} {row['J']:>7.3f} {p['J']:>7.3f} "
|
|
f"{dj:>+7.3f} | {row['p_pass']:>8.1%} {p['p_pass']:>7.1%} | "
|
|
f"{row['p_alive']:>8.1%} {p['p_alive']:>7.1%} | {row['sharpe']:>7.2f} {p['sharpe']:>6.2f}"
|
|
f"{'' if same_w else ' <-- CELLA DIVERSA'}")
|
|
dA, seA = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]),
|
|
wdict(bs_pub["w"]), float(bs_pub["lev"]), HYRO, ev_pub, fu_pub)
|
|
dB, seB = PA.paired_delta(wdict(b_pub["w"]), float(b_pub["lev"]),
|
|
{"TP01": 0.75, "SKH01": 0.25}, float(bl_pub["lev"]),
|
|
HYRO, ev_pub, fu_pub)
|
|
bump(4)
|
|
print(f"\n differenze APPAIATE argmaxJ - argmaxSharpe {dA:+.4f} +-{seA:.4f} "
|
|
f"(PUB {PUB['dJ_vs_sharpe']:+.4f})")
|
|
print(f" argmaxJ - LIVE {dB:+.4f} +-{seB:.4f} "
|
|
f"(PUB {PUB['dJ_vs_live']:+.4f})")
|
|
ok &= abs(dA - PUB["dJ_vs_sharpe"]) < 0.02 and abs(dB - PUB["dJ_vs_live"]) < 0.03
|
|
pc.DELUCK = PA.DELUCK
|
|
r_lad = pc.simulate([tuple(wdict(b_pub["w"]).get(n, 0.0) for n in W3)],
|
|
[0] * pl.MAX_CONCURRENT, n_paths=1500, lev=0.75, seed=20260822,
|
|
lens="coupled", start=PA.START_WIN)
|
|
print(f" scala di conti (I-bis) EUR/g med {r_lad['med']:.2f} (PUB {PUB['ladder_med']:.2f}) | "
|
|
f"P(>=50/g) {r_lad['p_target']:.1%} (PUB {PUB['ladder_p50']:.1%}) | "
|
|
f"P(zero) {r_lad['p_zero']:.1%} (PUB {PUB['ladder_zero']:.1%})")
|
|
ok &= abs(r_lad["p_target"] - PUB["ladder_p50"]) < 0.03
|
|
verdetto = "OK — si procede" if ok else "FALLITA — ogni numero sotto va letto con questa riserva"
|
|
print(f"\n >>> RIPRODUZIONE: {verdetto}")
|
|
tick("§0 riproduzione")
|
|
|
|
# ================================================== §1. LA SERIE LUNGA, ACCOPPIATA
|
|
hr("§1. LA SERIE LUNGA DI XS01, CON IL WICK — e la validazione del costruttore di tuple")
|
|
Chl, Ohl, Hhl, Lhl = ohlc_panels(hl_loader, [s for s in XS_UNIVERSE
|
|
if (_HL_DIR / f"hl_{s.lower()}_1d.parquet").exists()])
|
|
Chl = Chl.dropna()
|
|
Ohl, Hhl, Lhl = Ohl.reindex(Chl.index), Hhl.reindex(Chl.index), Lhl.reindex(Chl.index)
|
|
mine = xs_tuples(Chl, Ohl, Hhl, Lhl, phase=0)
|
|
ref = pc.xsec_daily_tuples("shared")
|
|
j = mine.join(ref, lsuffix="_m", rsuffix="_r", how="inner").dropna()
|
|
dR = float(np.abs(j["R_m"] - j["R_r"]).max())
|
|
dM = float(np.abs(j["m_m"] - j["m_r"]).max())
|
|
print(f" CONTROLLO: il mio costruttore di tuple, alimentato con gli OHLC HL, contro")
|
|
print(f" `pc.xsec_daily_tuples()` (la macchineria del 25/07): n={len(j)} "
|
|
f"max|dR| = {dR:.2e} max|dm| = {dM:.2e} -> "
|
|
f"{'IDENTICO' if max(dR, dM) < 1e-12 else 'DIVERGE — STOP'}")
|
|
if max(dR, dM) >= 1e-12:
|
|
print(" !! il costruttore non replica: i numeri sulla storia lunga sarebbero di un'altra")
|
|
print(" macchina. Mi fermo qui.")
|
|
return
|
|
|
|
for wtest in ({"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50}, {"TP01": 0.75, "SKH01": 0.25}):
|
|
Ra, Ga = rg(wtest, idx_pub, ref)
|
|
Rb_, Gb_ = PA.book_RG(wtest)
|
|
dd1 = float(np.abs(Ra - Rb_).max()); dd2 = float(np.abs(Ga - Gb_).max())
|
|
print(f" CONTROLLO: il mio costruttore di LIBRO contro `PA.book_RG` "
|
|
f"({PA.wkey(wtest)}): max|dR| {dd1:.2e} max|dgap| {dd2:.2e} -> "
|
|
f"{'IDENTICO' if max(dd1, dd2) < 1e-12 else 'DIVERGE'}")
|
|
|
|
Cb, Ob, Hb, Lb = ohlc_panels(bin_loader, XS_UNIVERSE)
|
|
p13 = Cb[X.U13].dropna()
|
|
o13, h13, l13 = Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13], Lb.reindex(p13.index)[X.U13]
|
|
xs13 = xs_tuples(p13, o13, h13, l13, phase=0)
|
|
xsEXP = xs_tuples(Cb, Ob, Hb, Lb, phase=0)
|
|
bump(2)
|
|
first13 = xs13.index[np.flatnonzero(xs13["R"].values != 0.0)[0]]
|
|
firstEX = xsEXP.index[np.flatnonzero(xsEXP["R"].values != 0.0)[0]]
|
|
print(f"\n XS01 U13 (13 gambe fisse, Binance) prima barra ATTIVA {first13.date()} "
|
|
f"-> {xs13.index[-1].date()}")
|
|
print(f" XS01 U19-EXP (10 -> 19 gambe, Binance) prima barra ATTIVA {firstEX.date()} "
|
|
f"-> {xsEXP.index[-1].date()}")
|
|
print(" (barra del giorno in corso SCARTATA: una giornaliera parziale in coda e' il difetto")
|
|
print(" trovato oggi nei monitor forward)")
|
|
|
|
# finestre dichiarate
|
|
cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index
|
|
base_idx = cry_idx.intersection(xs13.index)
|
|
S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC"))
|
|
S_DISC = pd.Timestamp(PA.START_WIN, tz="UTC")
|
|
I_LONG = base_idx[base_idx >= S_LONG]
|
|
I_OOS = I_LONG[I_LONG < S_DISC]
|
|
I_DISC = base_idx[base_idx >= S_DISC]
|
|
WINDOWS = (("W-LONG 2021+ (OOS + scoperta)", I_LONG),
|
|
("W-OOS 2021-2023 (puro OOS)", I_OOS),
|
|
("W-DISC 2024+ (scoperta)", I_DISC))
|
|
print(f"\n {'finestra':>32} {'giorni':>7} {'da':>12} {'a':>12}")
|
|
for lab, ii in WINDOWS:
|
|
print(f" {lab:>32} {len(ii):>7d} {str(ii[0].date()):>12} {str(ii[-1].date()):>12}")
|
|
tick("§1 serie lunga")
|
|
|
|
# ================================================== §2. PROFILO SOTTO BARRIERA
|
|
hr("§2. IL PROFILO CHE CAMBIA — cio' che una barriera STATICA vede: drift, vol, coda, gap")
|
|
print(" (de-luckato x0.89; `gap peggiore` = l'escursione intraday che la close-only NON vede)")
|
|
print(f" {'sleeve / finestra':>34} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'maxDD':>7} "
|
|
f"{'g.peggiore':>11} {'gap p90':>9} {'gap peggiore':>13}")
|
|
prof_rows = [
|
|
("TP01 W-LONG", {"TP01": 1.0}, I_LONG, xs13),
|
|
("TP01 W-DISC", {"TP01": 1.0}, I_DISC, xs13),
|
|
("SKH01 W-LONG", {"SKH01": 1.0}, I_LONG, xs13),
|
|
("SKH01 W-DISC", {"SKH01": 1.0}, I_DISC, xs13),
|
|
("XS01 U13 Binance W-OOS", {"XS01": 1.0}, I_OOS, xs13),
|
|
("XS01 U13 Binance W-DISC", {"XS01": 1.0}, I_DISC, xs13),
|
|
("XS01 U19EXP Binance W-OOS", {"XS01": 1.0}, I_OOS, xsEXP),
|
|
("XS01 UFFICIALE HL W-DISC", {"XS01": 1.0}, I_DISC, ref),
|
|
]
|
|
for lab, w, ii, xf in prof_rows:
|
|
R, G = rg(w, ii, xf)
|
|
Rd = PA.deluck(R)
|
|
vol = Rd.std() * np.sqrt(365.0)
|
|
print(f" {lab:>34} {Rd.mean()*365/vol if vol > 0 else 0:>7.2f} {vol:>6.1%} "
|
|
f"{Rd.mean()*365:>8.1%} {maxdd(Rd)*100:>6.1f}% {Rd.min():>11.2%} "
|
|
f"{-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp")
|
|
print("\n Le righe da leggere insieme sono XS01 W-OOS e XS01 UFFICIALE: il RENDIMENTO regge")
|
|
print(" (anzi sale), ma il maxDD e la coda peggiorano — e la barriera guarda la coda.")
|
|
tick("§2 profilo")
|
|
|
|
# ================================================== §3. LA GRIGLIA RIFATTA
|
|
hr("§3. LA GRIGLIA RIFATTA SULLA STORIA LUNGA — stesso simplesso, stesse leve, stessa lente")
|
|
res = {}
|
|
for lab, ii in WINDOWS:
|
|
ev = PA.boot_idx(len(ii), EV_H, N_PATHS, seed=SEED_EV)
|
|
fu = PA.boot_idx(len(ii), FU_H, N_PATHS, seed=SEED_FU)
|
|
t = time.time()
|
|
Gx = scan(grid, ii, xs13, ev, fu)
|
|
res[lab] = dict(G=Gx, ev=ev, fu=fu, idx=ii)
|
|
b, bl = best_row(Gx), best_row(Gx[Gx["w"] == "75/25/0"])
|
|
print(f"\n {lab} ({time.time()-t:.0f}s)")
|
|
print(f" {'':>18} {'pesi':>10} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
|
|
f"{'Sharpe':>7} {'maxDD':>7} {'E[pay/a]':>10}")
|
|
for l2, r2 in (("argmax J", b), ("libro LIVE 75/25", bl)):
|
|
print(f" {l2:>18} {r2['w']:>10} {r2['lev']:>6.3f} {r2['J']:>7.3f} "
|
|
f"{r2['p_pass']:>8.1%} {r2['p_alive']:>8.1%} {r2['sharpe']:>7.2f} "
|
|
f"{r2['maxdd']*100:>6.1f}% {r2['e_payout']:>9,.0f}$")
|
|
d, se = None, None
|
|
try:
|
|
Rb, Gb = rg(wdict(b["w"]), ii, xs13)
|
|
Rl, Gl = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13)
|
|
a1 = obj_rg(Rb, Gb, float(b["lev"]), ev, fu)["jvec"]
|
|
b1 = obj_rg(Rl, Gl, float(bl["lev"]), ev, fu)["jvec"]
|
|
dd_ = a1 - b1
|
|
d, se = float(dd_.mean()), float(dd_.std(ddof=1) / np.sqrt(len(dd_)))
|
|
bump(2)
|
|
except Exception as exc: # pragma: no cover
|
|
print(f" delta appaiato non calcolato: {exc}")
|
|
if d is not None:
|
|
print(f" delta APPAIATO argmax - LIVE : {d:+.4f} +-{se:.4f}"
|
|
f" (PROP-ALLOC pubblicava {PUB['dJ_vs_live']:+.4f} su W-DISC con XS01 HL)")
|
|
tick("§3 griglie")
|
|
|
|
# ================================================== §4. LA REGIONE, NON IL PUNTO
|
|
hr("§4. IL PESO DI XS01 — c'e' una REGIONE robusta su ENTRAMBE le finestre?")
|
|
print(" PROP-ALLOC ha gia' misurato che il PUNTO non conta (il massimo di 200 pesi casuali")
|
|
print(" eguagliava l'argmax): la domanda giusta e' se esiste una regione comune. Per ogni")
|
|
print(" livello di w_XS si riporta il MEGLIO su TP/SKH e su tutte le leve.")
|
|
xs_levels = sorted({int(round(100 * (i / STEP))) for i in range(STEP + 1)})
|
|
print(f"\n {'w_XS':>6} | " + " | ".join(f"{lab.split()[0]:>26}" for lab, _ in WINDOWS))
|
|
print(f" {'':>6} | " + " | ".join(f"{'J':>7} {'maxDD':>7} {'pesi':>10}" for _ in WINDOWS))
|
|
prof_by_w = {lab: {} for lab, _ in WINDOWS}
|
|
for wx in xs_levels:
|
|
cells = []
|
|
for lab, _ in WINDOWS:
|
|
Gx = res[lab]["G"]
|
|
sub = Gx[Gx["w"].str.endswith(f"/{wx}")]
|
|
if len(sub) == 0:
|
|
cells.append(f"{'—':>7} {'—':>7} {'—':>10}")
|
|
continue
|
|
r2 = best_row(sub)
|
|
prof_by_w[lab][wx] = float(r2["J"])
|
|
cells.append(f"{r2['J']:>7.3f} {r2['maxdd']*100:>6.1f}% {r2['w']:>10}")
|
|
print(f" {wx:>5}% | " + " | ".join(cells))
|
|
print("\n Regione robusta = livelli di w_XS entro il 3% del meglio di finestra, su TUTTE e tre:")
|
|
robust = []
|
|
for wx in xs_levels:
|
|
okall = True
|
|
for lab, _ in WINDOWS:
|
|
d = prof_by_w[lab]
|
|
if wx not in d or d[wx] < 0.97 * max(d.values()):
|
|
okall = False
|
|
if okall:
|
|
robust.append(wx)
|
|
print(f" {robust if robust else 'NESSUN livello comune'}"
|
|
f" (entro il 5%: "
|
|
f"{[wx for wx in xs_levels if all(wx in prof_by_w[l] and prof_by_w[l][wx] >= 0.95*max(prof_by_w[l].values()) for l, _ in WINDOWS)]})")
|
|
tick("§4 regione")
|
|
|
|
# ================================================== §5. LA BANDA ANCORATA AL MISURATO
|
|
hr("§5. LA BANDA — con gli stress ANCORATI a cio' che e' misurato, non a cio' che era assunto")
|
|
print(" PROP-ALLOC pubblicava 42% / 7,7% / 0,7% dove il caso severo era 'XS x0,00' = il drift")
|
|
print(" di XS01 assunto NULLO perche' non misurabile fuori campione. Quel drift ORA E'")
|
|
print(" MISURATO, e fuori campione e' PIU' ALTO: lo scenario cambia stato, da assunzione a")
|
|
print(" misura. Cio' che resta assunto e' (a) la fortuna d'ancora di SKH01 (misurata altrove,")
|
|
print(" non sotto questa lente) e (b) il LISTINO della firm (§6). Gli stress qui sotto sono")
|
|
print(" quelli, non un pessimismo arbitrario.")
|
|
st_rows = []
|
|
STRESS = ((), (("SKH01", 0.50),), (("SKH01", 0.33),))
|
|
SLAB = {(): "nessuno (drift pieno)",
|
|
(("SKH01", 0.50),): "SKH x0.50 (ancora 26/07)",
|
|
(("SKH01", 0.33),): "SKH x0.33 (ancora severa)"}
|
|
for lab, ii in (WINDOWS[0], WINDOWS[1]):
|
|
Gw, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"]
|
|
top_keys = list(Gw.sort_values("J", ascending=False).drop_duplicates("w")["w"].head(10))
|
|
print(f"\n {lab} (argmax ri-cercato sulle {len(top_keys)} celle-peso migliori: e' un")
|
|
print(f" risparmio di calcolo, ed e' CONSERVATIVO per la stabilita' dell'ottimo)")
|
|
print(f" {'stress':>28} {'argmax pesi':>12} {'leva':>6} {'J':>7} {'J del LIVE':>11} "
|
|
f"{'delta appaiato':>16}")
|
|
for dmt in STRESS:
|
|
best_j, best_k, best_l, best_v = -1.0, None, None, None
|
|
for k in top_keys:
|
|
R, G = rg(wdict(k), ii, xs13, dm=dmt)
|
|
for lv in LEVS:
|
|
o = obj_rg(R, G, lv, ev, fu)
|
|
if o["J"] > best_j:
|
|
best_j, best_k, best_l, best_v = o["J"], k, lv, o["jvec"]
|
|
bump(len(top_keys) * len(LEVS))
|
|
RL, GL = rg({"TP01": 0.75, "SKH01": 0.25}, ii, xs13, dm=dmt)
|
|
oL, lvL = None, None
|
|
for lv in LEVS:
|
|
o = obj_rg(RL, GL, lv, ev, fu)
|
|
if oL is None or o["J"] > oL["J"]:
|
|
oL, lvL = o, lv
|
|
bump(len(LEVS))
|
|
d_ = best_v - oL["jvec"]
|
|
# guardia contro l'errore del 22/08: un delta appaiato DEVE ereditare lo stress
|
|
# della riga in cui compare, o confronta due configurazioni non stressate.
|
|
assert abs(float(d_.mean()) - (best_j - oL["J"])) < 0.05, "delta appaiato incoerente"
|
|
print(f" {SLAB[dmt]:>28} {best_k:>12} {best_l:>6.3f} {best_j:>7.3f} "
|
|
f"{oL['J']:>11.3f} {d_.mean():>+11.4f} "
|
|
f"+-{d_.std(ddof=1)/np.sqrt(len(d_)):.4f}")
|
|
st_rows.append((lab, SLAB[dmt], best_k, best_l, best_j, oL["J"]))
|
|
tick("§5 banda")
|
|
|
|
# ================================================== §6. XS01 ASSENTE
|
|
hr("§6. LO SCENARIO CHE UCCIDE TUTTO — la firm NON lista gli alt (assunto mai verificato)")
|
|
print(" Non e' uno stress sul drift: e' la RIMOZIONE dello sleeve. Il libro torna a due gambe")
|
|
print(" e il conto funded diventa il libro live a leva. E' l'unico scenario che non si misura")
|
|
print(" con un backtest — si verifica leggendo un listino — quindi qui se ne misura il DANNO.")
|
|
print(f"\n {'finestra':>28} {'con XS01: pesi':>15} {'lev':>5} {'J':>7} | "
|
|
f"{'SENZA XS01: pesi':>17} {'lev':>5} {'J':>7} | {'J perso':>8} {'quota':>7}")
|
|
absent = {}
|
|
for lab, ii in WINDOWS:
|
|
Gx, ev, fu = res[lab]["G"], res[lab]["ev"], res[lab]["fu"]
|
|
b = best_row(Gx)
|
|
sub2 = Gx[Gx["w"].str.endswith("/0")]
|
|
b2 = best_row(sub2)
|
|
absent[lab] = b2
|
|
lost = float(b["J"]) - float(b2["J"])
|
|
print(f" {lab:>28} {b['w']:>15} {b['lev']:>5.2f} {b['J']:>7.3f} | "
|
|
f"{b2['w']:>17} {b2['lev']:>5.2f} {b2['J']:>7.3f} | {lost:>+8.3f} "
|
|
f"{lost/max(b['J'],1e-9):>6.0%}")
|
|
print("\n NB: 'SENZA XS01' e' gia' ri-ottimizzato su TP/SKH e su tutte le leve — e' il MEGLIO")
|
|
print(" che il libro a due gambe puo' fare, non il 75/25 cablato.")
|
|
tick("§6 assenza")
|
|
|
|
# ================================================== §7. LA SCALA DI CONTI
|
|
hr("§7. LA SCALA DI CONTI — P(>=50 EUR/g) da EUR 600 in 36 mesi, lente ACCOPPIATA")
|
|
print(f" macchineria del 25/07 (`pc.simulate`), de-luck x{PA.DELUCK}, {pl.MAX_CONCURRENT} conti"
|
|
f" max, morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, 1500 percorsi")
|
|
print(" (le tuple del libro vengono INIETTATE nella cache di `pc.book_tuples`, cosi' la")
|
|
print(" simulazione e' letteralmente la stessa, con un input diverso)")
|
|
print(f"\n {'finestra':>28} {'allocazione':>26} {'leva':>6} {'EUR/g med':>10} "
|
|
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
|
|
|
|
def ladder(ii, w: dict, lev: float, tag: str, wlab: str, drop_xs: bool = False) -> dict:
|
|
R, G = rg(w, ii, xs13, drop_xs=drop_xs)
|
|
cfg = (float(w.get("TP01", 0.0)), float(w.get("SKH01", 0.0)),
|
|
0.0 if drop_xs else float(w.get("XS01", 0.0)))
|
|
key = (cfg[0], cfg[1], cfg[2], "shared")
|
|
saved = pc._TUP_CACHE.pop(key, None)
|
|
pc._TUP_CACHE[key] = (R, G, ii)
|
|
r = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=1500, lev=lev,
|
|
seed=20260822, lens="coupled", start=str(ii[0].date()))
|
|
pc._TUP_CACHE.pop(key, None)
|
|
if saved is not None:
|
|
pc._TUP_CACHE[key] = saved
|
|
print(f" {tag:>28} {wlab:>26} {lev:>6.2f} {r['med']:>10.2f} {r['p_ge10']:>10.1%} "
|
|
f"{r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
|
|
return r
|
|
|
|
lad = {}
|
|
for lab, ii in WINDOWS:
|
|
Gx = res[lab]["G"]
|
|
b = best_row(Gx)
|
|
b2 = absent[lab]
|
|
short = lab.split()[0]
|
|
lad[(short, "argmax")] = ladder(ii, wdict(b["w"]), float(b["lev"]), short,
|
|
f"argmax {b['w']}")
|
|
lad[(short, "2550")] = ladder(ii, wdict("25/25/50"), 0.75, short,
|
|
"25/25/50 (PROP-ALLOC)")
|
|
lad[(short, "noxs")] = ladder(ii, wdict(b2["w"]), float(b2["lev"]), short,
|
|
f"SENZA XS01 {b2['w']}", drop_xs=True)
|
|
lad[(short, "live")] = ladder(ii, {"TP01": 0.75, "SKH01": 0.25}, 0.75, short,
|
|
"LIVE 75/25 @0.75x")
|
|
bump(12)
|
|
tick("§7 scala di conti")
|
|
|
|
# ================================================== §8. SINTESI + GATE
|
|
hr("§8. SINTESI — la banda da citare, e il gate da RI-REGISTRARE")
|
|
bL = best_row(res[WINDOWS[0][0]]["G"])
|
|
bO = best_row(res[WINDOWS[1][0]]["G"])
|
|
bD = best_row(res[WINDOWS[2][0]]["G"])
|
|
print(f" J e argmax per finestra (XS01 = U13 Binance in tutte e tre, quindi la SOLA cosa che")
|
|
print(f" cambia fra le righe e' la FINESTRA):")
|
|
print(f" W-DISC 2024+ argmax {bD['w']:>10} @{bD['lev']:.2f}x J {bD['J']:.3f}")
|
|
print(f" W-OOS 2021-2023 argmax {bO['w']:>10} @{bO['lev']:.2f}x J {bO['J']:.3f}")
|
|
print(f" W-LONG 2021+ argmax {bL['w']:>10} @{bL['lev']:.2f}x J {bL['J']:.3f}")
|
|
print(f" (PROP-ALLOC pubblicato, XS01 HL 19 gambe, W-DISC: 25/25/50 @0.75x J 0.738)")
|
|
print(f"\n P(>=50 EUR/g) in 36 mesi, banda onesta:")
|
|
for short in ("W-LONG", "W-OOS", "W-DISC"):
|
|
row = " | ".join(f"{k}: {lad[(short, k)]['p_target']:.1%}"
|
|
for k in ("argmax", "2550", "noxs", "live") if (short, k) in lad)
|
|
print(f" {short:>8} {row}")
|
|
print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}")
|
|
print(f"\n fatto in {time.time()-t0:.0f}s")
|
|
print("=" * 104)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|