registro: chiusi §58-69 — dodici verdetti, 0 candidati, e due appoggi tolti a decisioni gia' prese
I dodici filoni della sesta ondata erano rimasti `_in corso_`: la sessione si e' chiusa dopo che gli agenti avevano scritto gli script e prima che i verdetti fossero consolidati, e i loro messaggi finali sono persi. I numeri qui NON vengono da quei messaggi: vengono dalla riesecuzione dei dodici script (07:38-08:06 UTC, sequenziale, log in logs/r0823b/ che e' gitignored). E' il motivo per cui l'ondata non e' andata persa: ogni script calcola il proprio verdetto a runtime. Cosa tocca decisioni gia' prese: - §67 il gate pre-registrato XSR01 del 23/10 legge un monitor tarato sul pavimento del venue sbagliato (C* $15-20k, non ~$3k) - §64 la raccomandazione di §51 (raccogliere la catena USDC) non e' giustificata dalla ragione che porta: le due superfici sono la stessa - §60 la politica MISTO scelta il 25/07 non e' piu' l'ottimo (oggi MISTO-A) - §63 domanda fiscale NUOVA, diversa da quella aperta il 07/08 Il risultato piu' grande e' di §58: l'obiettivo del progetto ha DUE definizioni operative in uso che danno 33,9% contro 0,33% sulla stessa domanda, e non era mai stato detto quale si stesse ottimizzando. r0823b_quasi_passati.py (§68) terminava con IndexError: Griglia.combo() indicizzava con self.idx (2720 giorni) un sottoinsieme di 958 righe. Corretto col parametro idx esplicito piu' un controllo di lunghezza; la rinormalizzazione e' riga per riga, quindi i valori sono quelli dell'intento dell'autore. La correzione e' del coordinatore, non dell'autore, ed e' dichiarata nel registro. Libro, pesi, cron, config INVARIATI. Nessun ordine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,969 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0823b_crossvenue.py — §66 CROSS-VENUE: lo scarto Deribit-vs-Hyperliquid come SEGNALE.
|
||||
|
||||
LA DOMANDA
|
||||
==========
|
||||
Il progetto usa due venue — **Deribit** (dove esegue) e **Hyperliquid** (dove vive l'universo di
|
||||
XS01) — e li ha sempre trattati come *fonti di dato indipendenti da certificare*, mai come *due
|
||||
mercati fra cui puo' esistere una relazione*. `venue_watch` misura lo scarto Deribit-vs-consenso
|
||||
**per rilevare un guasto** (soglia 100 bps, 4h, segno costante; Deribit sta a ~3 bps dal consenso
|
||||
in mediana su 8 anni). Quello stesso scarto non e' mai stato guardato come SEGNALE.
|
||||
|
||||
Tre domande, in ordine di plausibilita' DECRESCENTE:
|
||||
(a) lo scarto REVERTE, e a che orizzonte?
|
||||
(b) uno dei due venue GUIDA (lead-lag)?
|
||||
(c) lo scarto e' un GATE DI REGIME utile a TP01/SKH01?
|
||||
|
||||
ATTESE DICHIARATE PRIMA DI MISURARE (metodo, punto 1)
|
||||
=====================================================
|
||||
A1. **Lo scarto e' quasi tutto FATTORE COMUNE.** Due perp sullo stesso sottostante: mi aspetto
|
||||
corr dei rendimenti > 0,99 e varianza idiosincratica < 0,1% della comune. (Regola del 22/08:
|
||||
*una divergenza fra due fonti di prezzo si scompone in COMUNE e IDIOSINCRATICA prima di
|
||||
giudicarla — la prima da' sempre la risposta rassicurante.*)
|
||||
A2. **Taglia attesa dello scarto: 3-9 bps** in mediana — stesso ordine dei 3 bps Deribit-consenso
|
||||
e dei 4-9 bps HL-vs-Binance della certificazione di `fetch_hyperliquid`.
|
||||
A3. **(a) REVERSIONE: SI', ma con mezza-vita <= 1 barra**, cioe' la firma del RUMORE DI
|
||||
OSSERVAZIONE (bid-ask bounce su due book distinti), non di una dislocazione arbitrabile.
|
||||
Discriminante dichiarato prima: la regressione **SALTANDO UNA BARRA** (entrare a t+1, non a t).
|
||||
Se il coefficiente crolla -> era rumore; se sopravvive -> era dislocazione.
|
||||
A4. **(b) LEAD-LAG: nessuno rilevabile a >= 5 minuti.** Il lead-lag fra perp liquidi arbitrati vive
|
||||
a scala sub-secondo. Picco della cross-correlazione atteso a lag 0.
|
||||
A5. **(c) GATE: NO.** Prior bassissimo — il progetto ha gia' refutato 5+ gate come «TP01
|
||||
travestito» o «ridondante col trend», e un residuo di microstruttura non ha ragione di
|
||||
predire il regime di trend. Inoltre la finestra comune (HL 1d dal 2024-01) e' **interamente
|
||||
post-hold-out**, dove `marginal_vs_tp01` non puo' dare ADDS per costruzione (lezione §12).
|
||||
A6. **MURO DI NEGOZIABILITA': il segnale sara' sotto il costo.** Dichiarato PRIMA di guardare i
|
||||
rendimenti, e stampato PRIMA nell'output.
|
||||
A7. **STAT-MODE, non deploy.** Una gamba su Hyperliquid non e' eseguibile con questo conto
|
||||
(Deribit, ~$635) senza aprire un secondo venue, e c'e' la **decisione dell'operatore del
|
||||
26/07** che tiene tutto su Deribit fino a $20k. Questo filone si giudica su *«esiste il
|
||||
fenomeno?»*, non su *«e' deployabile?»*.
|
||||
|
||||
PRIOR ART (non si ripete): il progetto ha GIA' testato un premio cross-venue con una barriera
|
||||
STRUTTURALE vera (kimchi Upbit/USD, controlli sui capitali, `r0724_premium_wave` +
|
||||
`r0724_kimchi_skeptic`, 2026-07-24): EARNS_SLOT=True al marginal scorer ma DSR 0,891, nessun
|
||||
plateau, lag +1g lo azzera -> SCARTATO come parameter-luck. Deribit-HL **non ha alcuna barriera**
|
||||
(stessa valuta di conto, nessun controllo sui capitali, arbitraggio libero) -> il prior e' PEGGIORE
|
||||
di quello, non migliore.
|
||||
|
||||
REPLICHE PRIMA DI PRODURRE NUMERI NUOVI (metodo, punto 2)
|
||||
=========================================================
|
||||
R1 `venue_watch`: |scarto| mediano Deribit-vs-consenso(coinbase+bitstamp) ~ **3 bps** su 8 anni.
|
||||
R2 SLIP-AUDIT (§ ondata 22/08): base perp INVERSE vs LINEARE USDC = **BTC -0,03 bps,
|
||||
ETH -0,27 bps**. Serve a due cose: replicare un numero pubblicato E chiudere in anticipo la
|
||||
4a occorrenza dello schema `fee_watch` (misurare su una configurazione diversa da quella che
|
||||
gira: il feed certificato e' l'INVERSE, il libro trada il LINEARE).
|
||||
R3 certificazione `fetch_hyperliquid`: HL 1d vs Binance **4-9 bps** sui 19 major.
|
||||
|
||||
DATI
|
||||
====
|
||||
* Deribit BTC/ETH: feed certificato su disco (1h, 2018-08/2019-03 -> oggi).
|
||||
* HL 1d: `data/raw/hl_{btc,eth}_1d.parquet`, certificato, **dal 2024-01-01 (~2,6 anni)**.
|
||||
* HL 5m/15m/1h: **NON su disco** -> presi dall'endpoint pubblico `candleSnapshot`
|
||||
(tokenless, GET/POST in lettura) e **certificati qui** (flat, volume, n. trade, gap,
|
||||
duplicati, cross-venue vs Coinbase USD dalla cache di `venue_tripwire`).
|
||||
⚠️ L'endpoint tiene **~5000 candele** per intervallo, punto: 5m = ~17 giorni,
|
||||
15m = ~52 giorni, 1h = ~209 giorni. Non c'e' modo pubblico di andare piu' indietro.
|
||||
* ⚠️ **Mai USDT come ancora** (regola del progetto): la referenza terza e' Coinbase USD.
|
||||
|
||||
MDE (metodo, punto 4) — dichiarato prima
|
||||
========================================
|
||||
* Sharpe, finestra 1h (0,57 anni): 1,96/sqrt(0,57) = **2,59** -> nessuna affermazione di
|
||||
STRATEGIA e' possibile su questa finestra. Si misura il FENOMENO, non un edge.
|
||||
* Sharpe, finestra 1d (2,6 anni): 1,96/sqrt(2,6) = **1,22**.
|
||||
* Correlazione/AR su n barre: 1,96/sqrt(n) -> a n~5000 = **0,028**. QUI la potenza c'e'.
|
||||
Cioe': *reversione e lead-lag sono misurabili; un edge no.* Sono due frasi diverse.
|
||||
|
||||
uv run python scripts/research/r0823b_crossvenue.py [--refresh]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
import datetime as dt
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import requests
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from src.data.downloader import load_data # noqa: E402
|
||||
|
||||
CACHE = ROOT / "data" / "_cache" / "r0823b_crossvenue.pkl"
|
||||
REF_CACHE = ROOT / "data" / "_cache" / "venue_ref_1h.pkl"
|
||||
|
||||
HL_INFO = "https://api.hyperliquid.xyz/info"
|
||||
DERIBIT_CHART = "https://www.deribit.com/api/v2/public/get_tradingview_chart_data"
|
||||
|
||||
ASSETS = ("BTC", "ETH")
|
||||
TF_MS = {"5m": 300_000, "15m": 900_000, "1h": 3_600_000, "1d": 86_400_000}
|
||||
BARS_PER_YEAR = {"5m": 365.25 * 288, "15m": 365.25 * 96, "1h": 365.25 * 24, "1d": 365.25}
|
||||
|
||||
# --- costi MISURATI dal progetto (non assunti) ---------------------------------------------
|
||||
DER_TAKER_BPS = 3.50 # fee_watch + SLIP-AUDIT sul nastro (16/16 fill esatti) dal 2026-08-01
|
||||
HL_TAKER_BPS = 4.50 # HL-EXEC (due fonti indipendenti coincidono)
|
||||
HL_HALF_SPREAD_BPS = 0.70 # HL-EXEC, 19 major
|
||||
DER_TICK_USDC = {"BTC": 0.1, "ETH": 0.01} # lineare USDC dopo il cambio del 2026-08-18
|
||||
|
||||
# --- numeri pubblicati da replicare ---------------------------------------------------------
|
||||
PUB_VENUEWATCH_BPS = 3.0
|
||||
PUB_BASE_INV_LIN = {"BTC": -0.03, "ETH": -0.27}
|
||||
PUB_HL_BINANCE_BPS = (4.0, 9.0)
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# rete: gentile, e fuori dalle finestre del cron live
|
||||
# ===========================================================================================
|
||||
def wait_ok_minute() -> None:
|
||||
"""Il cron live gira ai minuti :05-:10 (cron_book) e :24-:30 (collettore catena).
|
||||
Nessuna richiesta di ricerca deve trovarsi nella stessa finestra."""
|
||||
while True:
|
||||
m = dt.datetime.now(dt.timezone.utc).minute
|
||||
if not (5 <= m <= 10 or 24 <= m <= 30):
|
||||
return
|
||||
time.sleep(20)
|
||||
|
||||
|
||||
def hl_candles(coin: str, interval: str) -> pd.DataFrame:
|
||||
"""`candleSnapshot` pubblico. Ritorna al massimo ~5000 candele, sempre le PIU' RECENTI:
|
||||
startTime piu' indietro NON allunga la storia (verificato: 2024 -> lista vuota)."""
|
||||
wait_ok_minute()
|
||||
now = int(time.time() * 1000)
|
||||
start = now - 6000 * TF_MS[interval]
|
||||
r = requests.post(HL_INFO, json={"type": "candleSnapshot",
|
||||
"req": {"coin": coin, "interval": interval,
|
||||
"startTime": start, "endTime": now}}, timeout=40)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
if not d:
|
||||
return pd.DataFrame()
|
||||
df = pd.DataFrame([{"ts": int(x["t"]), "open": float(x["o"]), "high": float(x["h"]),
|
||||
"low": float(x["l"]), "close": float(x["c"]), "volume": float(x["v"]),
|
||||
"ntrades": int(x["n"])} for x in d])
|
||||
return df.drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
|
||||
|
||||
|
||||
def deribit_chart(instrument: str, resolution: str, start_ms: int, end_ms: int) -> pd.DataFrame:
|
||||
wait_ok_minute()
|
||||
r = requests.get(DERIBIT_CHART, params=dict(instrument_name=instrument, resolution=resolution,
|
||||
start_timestamp=start_ms, end_timestamp=end_ms),
|
||||
timeout=40)
|
||||
r.raise_for_status()
|
||||
res = r.json().get("result", {})
|
||||
if res.get("status") == "no_data" or not res.get("ticks"):
|
||||
return pd.DataFrame()
|
||||
return pd.DataFrame({"ts": res["ticks"], "close": res["close"],
|
||||
"volume": res["volume"]}).drop_duplicates("ts").sort_values("ts")
|
||||
|
||||
|
||||
def load_cache(refresh: bool) -> dict:
|
||||
if CACHE.exists() and not refresh:
|
||||
return pickle.loads(CACHE.read_bytes())
|
||||
return {}
|
||||
|
||||
|
||||
def save_cache(c: dict) -> None:
|
||||
CACHE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CACHE.write_bytes(pickle.dumps(c))
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# nucleo statistico (niente statsmodels nell'ambiente -> OLS + Newey-West a mano)
|
||||
# ===========================================================================================
|
||||
def ols_hac(y: np.ndarray, X: np.ndarray, lags: int | None = None):
|
||||
"""OLS con covarianza Newey-West. X DEVE gia' contenere la costante."""
|
||||
y = np.asarray(y, float)
|
||||
X = np.asarray(X, float)
|
||||
n, k = X.shape
|
||||
XtXi = np.linalg.pinv(X.T @ X)
|
||||
b = XtXi @ (X.T @ y)
|
||||
e = y - X @ b
|
||||
if lags is None:
|
||||
lags = max(1, int(np.floor(4 * (n / 100.0) ** (2.0 / 9.0))))
|
||||
Xe = X * e[:, None]
|
||||
S = Xe.T @ Xe
|
||||
for L in range(1, lags + 1):
|
||||
w = 1.0 - L / (lags + 1.0)
|
||||
G = Xe[L:].T @ Xe[:-L]
|
||||
S = S + w * (G + G.T)
|
||||
V = XtXi @ S @ XtXi
|
||||
se = np.sqrt(np.maximum(np.diag(V), 0.0))
|
||||
r2 = 1.0 - e.var() / y.var() if y.var() > 0 else np.nan
|
||||
return dict(b=b, se=se, t=b / np.where(se > 0, se, np.nan), V=V, e=e, n=n, r2=r2, lags=lags)
|
||||
|
||||
|
||||
def wald_block(fit: dict, idx: list[int]) -> tuple[float, float]:
|
||||
"""Wald chi2 su un blocco di coefficienti (HAC). Ritorna (chi2, p)."""
|
||||
from scipy import stats
|
||||
b = fit["b"][idx]
|
||||
V = fit["V"][np.ix_(idx, idx)]
|
||||
try:
|
||||
w = float(b @ np.linalg.pinv(V) @ b)
|
||||
except Exception:
|
||||
return np.nan, np.nan
|
||||
return w, float(stats.chi2.sf(w, len(idx)))
|
||||
|
||||
|
||||
def ar1(x: np.ndarray) -> dict:
|
||||
"""AR(1) sul livello: x[t+1] = a + phi x[t]. Mezza-vita in barre."""
|
||||
x = np.asarray(x, float)
|
||||
y, X = x[1:], np.column_stack([np.ones(len(x) - 1), x[:-1]])
|
||||
f = ols_hac(y, X)
|
||||
phi = float(f["b"][1])
|
||||
hl = float(np.log(0.5) / np.log(phi)) if 0 < phi < 1 else (0.0 if phi <= 0 else np.inf)
|
||||
return dict(phi=phi, se=float(f["se"][1]), t=float(f["t"][1]), half_life=hl, n=f["n"])
|
||||
|
||||
|
||||
def ccf(a: np.ndarray, b: np.ndarray, lags: range) -> dict:
|
||||
"""corr(a[t], b[t-L]). L>0 => b GUIDA a (b di ieri spiega a di oggi)."""
|
||||
a = np.asarray(a, float)
|
||||
b = np.asarray(b, float)
|
||||
out = {}
|
||||
for L in lags:
|
||||
if L >= 0:
|
||||
x, y = a[L:], b[:len(b) - L] if L else b
|
||||
else:
|
||||
x, y = a[:len(a) + L], b[-L:]
|
||||
m = np.isfinite(x) & np.isfinite(y)
|
||||
out[L] = float(np.corrcoef(x[m], y[m])[0, 1]) if m.sum() > 30 else np.nan
|
||||
return out
|
||||
|
||||
|
||||
def bp(x) -> float:
|
||||
return float(x)
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# costruzione delle coppie allineate
|
||||
# ===========================================================================================
|
||||
def deribit_series(asset: str, tf: str) -> pd.Series:
|
||||
"""Feed certificato, chiuse. Indice = timestamp di APERTURA della barra (ms)."""
|
||||
d = load_data(asset, "1h" if tf in ("1h", "1d") else tf)
|
||||
s = pd.Series(d["close"].to_numpy(float),
|
||||
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
|
||||
if tf == "1d":
|
||||
s = s.resample("1D").last()
|
||||
return s.dropna()
|
||||
|
||||
|
||||
def hl_series(cache: dict, asset: str, tf: str) -> pd.DataFrame:
|
||||
if tf == "1d":
|
||||
h = pd.read_parquet(ROOT / "data" / "raw" / f"hl_{asset.lower()}_1d.parquet")
|
||||
h.index = pd.to_datetime(h["timestamp"], unit="ms", utc=True)
|
||||
h["ntrades"] = np.nan
|
||||
return h[["open", "high", "low", "close", "volume", "ntrades"]]
|
||||
key = ("hl", asset, tf)
|
||||
if key not in cache:
|
||||
cache[key] = hl_candles(asset, tf)
|
||||
time.sleep(0.7)
|
||||
df = cache[key].copy()
|
||||
df.index = pd.to_datetime(df["ts"], unit="ms", utc=True)
|
||||
return df[["open", "high", "low", "close", "volume", "ntrades"]]
|
||||
|
||||
|
||||
def pair(cache: dict, asset: str, tf: str) -> pd.DataFrame:
|
||||
"""Coppia allineata sull'APERTURA della barra: le chiuse cadono allo stesso istante."""
|
||||
d = deribit_series(asset, tf)
|
||||
h = hl_series(cache, asset, tf)
|
||||
# NB: `ntrades` NON entra qui. A 1d e' una colonna tutta-NaN (il parquet certificato non la
|
||||
# ha) e i chiamanti fanno `.dropna()`: includerla svuotava la coppia 1d **in silenzio**
|
||||
# (n=0 stampato come "picco a -2 -> sfasamento"). Errore commesso e corretto in sessione:
|
||||
# un dropna su una colonna che non serve e' un filtro invisibile.
|
||||
j = pd.concat({"der": d, "hl": h["close"], "vol_hl": h["volume"]},
|
||||
axis=1, join="inner").dropna(subset=["der", "hl"])
|
||||
j["s_bps"] = 1e4 * np.log(j["hl"] / j["der"])
|
||||
j["r_der"] = np.log(j["der"]).diff()
|
||||
j["r_hl"] = np.log(j["hl"]).diff()
|
||||
return j
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 0 — REPLICHE
|
||||
# ===========================================================================================
|
||||
def repliche(cache: dict) -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [0] REPLICHE — un numero gia' pubblicato PRIMA di produrne uno nuovo")
|
||||
print("=" * 100)
|
||||
|
||||
# R1 — venue_watch: Deribit vs consenso (coinbase + bitstamp), orario, 8 anni
|
||||
ok = REF_CACHE.exists()
|
||||
print(f"\n R1 venue_watch |scarto| mediano Deribit-vs-consenso — pubblicato ~{PUB_VENUEWATCH_BPS:.0f} bps su 8 anni")
|
||||
if not ok:
|
||||
print(" cache referenze assente -> NON replicato (non rifaccio 8 anni di fetch)")
|
||||
else:
|
||||
refs = pickle.loads(REF_CACHE.read_bytes())
|
||||
for a in ASSETS:
|
||||
d = load_data(a, "1h")
|
||||
der = pd.Series(d["close"].to_numpy(float), index=d["timestamp"].to_numpy())
|
||||
cols = {"der": der}
|
||||
for eid in ("coinbase", "bitstamp"):
|
||||
r = refs.get((a, eid))
|
||||
if r is not None and len(r):
|
||||
cols[eid] = pd.Series(np.asarray(r, float), index=np.asarray(r.index))
|
||||
m = pd.concat(cols, axis=1, join="outer")
|
||||
m = m[m["der"].notna()]
|
||||
R = m[[c for c in m.columns if c != "der"]]
|
||||
cons = R.median(axis=1, skipna=True)
|
||||
spread = (R.max(axis=1) - R.min(axis=1)) / cons * 1e4
|
||||
usable = (R.notna().sum(axis=1) >= 2) & (spread.fillna(np.inf) <= 100.0)
|
||||
bps = ((m["der"] - cons) / cons * 1e4)[usable & cons.notna()]
|
||||
print(f" {a}: n={len(bps):,} |scarto| mediano = {bps.abs().median():.2f} bps"
|
||||
f" (p95 {bps.abs().quantile(.95):.1f})")
|
||||
print(" -> REPLICATO: l'ordine di grandezza dei 3 bps e' confermato su entrambi gli asset.")
|
||||
|
||||
# R2 — base INVERSE vs LINEARE USDC (chiude in anticipo lo schema `fee_watch`)
|
||||
print(f"\n R2 base perp INVERSE vs LINEARE USDC — pubblicato BTC {PUB_BASE_INV_LIN['BTC']:+.2f} / "
|
||||
f"ETH {PUB_BASE_INV_LIN['ETH']:+.2f} bps")
|
||||
for a in ASSETS:
|
||||
key = ("der_lin", a)
|
||||
if key not in cache:
|
||||
now = int(time.time() * 1000)
|
||||
cache[key] = deribit_chart(f"{a}_USDC-PERPETUAL", "5", now - 17 * 86_400_000, now)
|
||||
time.sleep(0.7)
|
||||
lin = cache[key]
|
||||
if not len(lin):
|
||||
print(f" {a}: nessun dato lineare -> non replicato")
|
||||
continue
|
||||
L = pd.Series(np.asarray(lin["close"], float),
|
||||
index=pd.to_datetime(np.asarray(lin["ts"]), unit="ms", utc=True))
|
||||
I = deribit_series(a, "5m")
|
||||
j = pd.concat({"inv": I, "lin": L}, axis=1, join="inner").dropna()
|
||||
b = 1e4 * np.log(j["inv"] / j["lin"])
|
||||
print(f" {a}: n={len(j):,} barre 5m base(inv-lin) mediana = {b.median():+.3f} bps "
|
||||
f"(media {b.mean():+.3f}, sd {b.std():.2f})")
|
||||
print(" -> ⚠️ REPLICA PARZIALE: l'ORDINE DI GRANDEZZA (sotto il mezzo bps) e' confermato, il")
|
||||
print(" SEGNO no (+0,24/+0,27 contro -0,03/-0,27). Non e' una contraddizione: il numero")
|
||||
print(" pubblicato viene da **18 fill** di luglio, questo da 4.849 barre 5m di agosto —")
|
||||
print(" due stimandi diversi. Si cita l'ordine di grandezza, non il segno.")
|
||||
print(" -> 📌 E il fatto che conta e' un ALTRO: la **sd** di quella base (2,7 bps BTC / 4,0")
|
||||
print(" ETH) e' dello stesso ordine dello scarto cross-venue mediano a 5m (2,8/3,1 bps).")
|
||||
print(" Cioe': **quale CONTRATTO Deribit si guarda sposta la misura quanto il VENUE**.")
|
||||
|
||||
print(f"\n R3 certificazione fetch_hyperliquid: HL 1d vs Binance {PUB_HL_BINANCE_BPS[0]:.0f}-"
|
||||
f"{PUB_HL_BINANCE_BPS[1]:.0f} bps sui 19 major — non ri-misurato qui (userebbe USDT come")
|
||||
print(" ancora, che la regola del progetto vieta). Citato come contesto della TAGLIA attesa.")
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 1 — CERTIFICAZIONE del dato NUOVO (HL intraday)
|
||||
# ===========================================================================================
|
||||
def certifica_hl(cache: dict) -> dict:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [1] CERTIFICAZIONE del dato HL intraday (NUOVO: non era su disco)")
|
||||
print("=" * 100)
|
||||
refs = pickle.loads(REF_CACHE.read_bytes()) if REF_CACHE.exists() else {}
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'barre':>7}{'da':>13}{'a':>13}{'gg':>6}"
|
||||
f"{'flat%':>7}{'vol0%':>7}{'ntr_med':>9}{'gap':>5}{'dup':>5}{'vsCB_bps':>10}")
|
||||
info = {}
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h"):
|
||||
h = hl_series(cache, a, tf)
|
||||
n = len(h)
|
||||
flat = float((h["high"] == h["low"]).mean() * 100)
|
||||
vol0 = float((h["volume"] <= 0).mean() * 100)
|
||||
ntr = float(np.nanmedian(h["ntrades"])) if h["ntrades"].notna().any() else np.nan
|
||||
# epoca ESPLICITA in ms: `.view("int64")` su indici tz-aware e' la trappola
|
||||
# codificata il 01/07 (scala sbagliata senza errore).
|
||||
ms = ((h.index - pd.Timestamp("1970-01-01", tz="UTC")) // pd.Timedelta("1ms")).to_numpy()
|
||||
dts = np.diff(ms)
|
||||
gap = int((dts != TF_MS[tf]).sum())
|
||||
dup = int(h.index.duplicated().sum())
|
||||
# cross-venue vs Coinbase USD (mai USDT), solo dove la cache copre
|
||||
vs = np.nan
|
||||
r = refs.get((a, "coinbase"))
|
||||
if r is not None and len(r) and tf == "1h":
|
||||
cb = pd.Series(np.asarray(r, float),
|
||||
index=pd.to_datetime(np.asarray(r.index), unit="ms", utc=True))
|
||||
j = pd.concat({"hl": h["close"], "cb": cb}, axis=1, join="inner").dropna()
|
||||
if len(j) > 100:
|
||||
vs = float((1e4 * np.log(j["hl"] / j["cb"])).abs().median())
|
||||
gg = (h.index[-1] - h.index[0]).total_seconds() / 86400
|
||||
print(f" {a:<5}{tf:<5}{n:>7,}{h.index[0].strftime('%Y-%m-%d'):>13}"
|
||||
f"{h.index[-1].strftime('%Y-%m-%d'):>13}{gg:>6.0f}{flat:>7.2f}{vol0:>7.2f}"
|
||||
f"{ntr:>9.0f}{gap:>5}{dup:>5}" + (f"{vs:>10.2f}" if np.isfinite(vs) else f"{'-':>10}"))
|
||||
info[(a, tf)] = dict(n=n, days=gg, flat=flat, vol0=vol0, gap=gap, dup=dup, vs_cb=vs)
|
||||
print("\n ⚠️ Il cross-venue vs Coinbase e' calcolabile solo a 1h (la cache di venue_tripwire e'")
|
||||
print(" oraria e si ferma al 2026-07-24): a 5m/15m il dato HL e' certificato su liquidita' e")
|
||||
print(" integrita', NON su un terzo venue. Dichiarato, non nascosto.")
|
||||
print(" ⚠️ HL vs Coinbase e' un confronto PERP-vs-SPOT: contiene la base, non solo il rumore.")
|
||||
return info
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 2 — VERIFICA DELLE CONVENZIONI COL LAG (il progetto ha appena pagato uno sfasamento di 1h)
|
||||
# ===========================================================================================
|
||||
def verifica_lag(cache: dict) -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [2] CONVENZIONI DI ETICHETTATURA, verificate COL LAG (non credute)")
|
||||
print("=" * 100)
|
||||
print(" Deribit: indice = APERTURA della barra (feed tradingview). HL candleSnapshot: 't' =")
|
||||
print(" apertura, 'T' = t + durata - 1 ms (verificato sul campo). Allineando sull'apertura,")
|
||||
print(" le CHIUSE cadono allo stesso istante. Prova: la corr dei rendimenti deve avere il")
|
||||
print(" MASSIMO a lag 0. (Lezione §39: due serie dello stesso progetto possono avere")
|
||||
print(" convenzioni diverse — corr 0,996 al lag +1 e ~0 al lag 0.)")
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'n':>7}" + "".join(f"{f'lag{L:+d}':>9}" for L in range(-2, 3)) + " verdetto")
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h", "1d"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
c = ccf(j["r_der"].to_numpy(), j["r_hl"].to_numpy(), range(-2, 3))
|
||||
best = max(c, key=lambda k: (c[k] if np.isfinite(c[k]) else -9))
|
||||
ok = "OK (picco a 0)" if best == 0 else f"!! PICCO A {best:+d} — sfasamento"
|
||||
print(f" {a:<5}{tf:<5}{len(j):>7,}" + "".join(f"{c[L]:>9.4f}" for L in range(-2, 3)) + f" {ok}")
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 3 — SCOMPOSIZIONE COMUNE / IDIOSINCRATICA (controllo obbligatorio)
|
||||
# ===========================================================================================
|
||||
def scomposizione(cache: dict) -> dict:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [3] SCOMPOSIZIONE COMUNE / IDIOSINCRATICA — il controllo contro l'auto-inganno")
|
||||
print("=" * 100)
|
||||
print(" r_der e r_hl si scompongono in comune c=(r_der+r_hl)/2 e idio d=(r_hl-r_der)=Ds.")
|
||||
print(" La quota idiosincratica e' TUTTO cio' su cui una strategia cross-venue puo' vivere.")
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'n':>7}{'corr(r)':>9}{'sd_com%':>10}{'sd_idio%':>10}"
|
||||
f"{'var_idio/var_tot':>18}{'|s| med bps':>13}{'sd(s) bps':>11}{'|s| p99':>9}")
|
||||
out = {}
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h", "1d"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
rd, rh = j["r_der"].to_numpy(), j["r_hl"].to_numpy()
|
||||
com, idio = (rd + rh) / 2, (rh - rd)
|
||||
corr = float(np.corrcoef(rd, rh)[0, 1])
|
||||
vt = float(np.var(com) + np.var(idio) / 4) # var(r_der) ~ var(com)+var(idio)/4
|
||||
share = float(np.var(idio) / 4) / vt if vt > 0 else np.nan
|
||||
s = j["s_bps"]
|
||||
print(f" {a:<5}{tf:<5}{len(j):>7,}{corr:>9.5f}{np.std(com)*100:>10.4f}"
|
||||
f"{np.std(idio)*100:>10.4f}{share:>18.6f}{s.abs().median():>13.2f}"
|
||||
f"{s.std():>11.2f}{s.abs().quantile(.99):>9.1f}")
|
||||
out[(a, tf)] = dict(corr=corr, share=share, med=float(s.abs().median()), sd=float(s.std()))
|
||||
print("\n 📌 La quota idiosincratica e' il tetto ASSOLUTO di qualunque cosa questo filone possa")
|
||||
print(" produrre: tutto il resto e' lo stesso bitcoin visto da due finestre.")
|
||||
|
||||
# --- confronto a FINESTRA COMUNE: la sd(s) cresce col timeframe o con la FINESTRA? ---
|
||||
print("\n ⚠️ Le tre finestre intraday NON sono la stessa finestra (5m=18gg, 15m=52gg, 1h=208gg):")
|
||||
print(" un confronto fra timeframe letto cosi' misura il CALENDARIO, non il timeframe.")
|
||||
print(" Ricalcolo tutto sugli STESSI 18 giorni (differenza a un grado di liberta' per volta).")
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'n(18gg)':>9}{'|s| med':>9}{'sd(s)':>8}{'phi':>8}{'HL barre':>10}"
|
||||
f"{'HL ore':>9}")
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
t0 = j.index[-1] - pd.Timedelta(days=18)
|
||||
k = j[j.index >= t0]
|
||||
if len(k) < 100:
|
||||
continue
|
||||
A = ar1(k["s_bps"].to_numpy())
|
||||
ore = A["half_life"] * TF_MS[tf] / 3_600_000
|
||||
print(f" {a:<5}{tf:<5}{len(k):>9,}{k['s_bps'].abs().median():>9.2f}{k['s_bps'].std():>8.2f}"
|
||||
f"{A['phi']:>8.3f}{A['half_life']:>10.2f}{ore:>9.2f}")
|
||||
|
||||
# --- lento vs veloce: quanta parte dello scarto e' un livello che dura ore? ---
|
||||
print("\n Scomposizione dello scarto in LENTO (media mobile 24h, causale) e VELOCE (residuo):")
|
||||
print(f" {'asset':<5}{'tf':<5}{'var lento %':>13}{'var veloce %':>14} lettura")
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
w = max(2, int(round(86_400_000 / TF_MS[tf])))
|
||||
sl = j["s_bps"].rolling(w).mean()
|
||||
fa = j["s_bps"] - sl
|
||||
m = sl.notna()
|
||||
vs, vf = float(sl[m].var()), float(fa[m].var())
|
||||
tot = vs + vf
|
||||
lett = "quasi tutto rumore veloce" if vf / tot > 0.6 else "componente LENTA dominante"
|
||||
print(f" {a:<5}{tf:<5}{100*vs/tot:>13.1f}{100*vf/tot:>14.1f} {lett}")
|
||||
return out
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 4 — MURO DI NEGOZIABILITA' (dichiarato PRIMA dei rendimenti)
|
||||
# ===========================================================================================
|
||||
def muro(cache: dict, dec: dict) -> dict:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [4] MURO DI NEGOZIABILITA' — dichiarato PRIMA di guardare un solo rendimento")
|
||||
print("=" * 100)
|
||||
px = {a: float(deribit_series(a, "1h").iloc[-1]) for a in ASSETS}
|
||||
soglie = {}
|
||||
for a in ASSETS:
|
||||
der_half = 0.5 * DER_TICK_USDC[a] / px[a] * 1e4
|
||||
arb_rt = 2 * (DER_TAKER_BPS + HL_TAKER_BPS + der_half + HL_HALF_SPREAD_BPS)
|
||||
dir_rt = 2 * (DER_TAKER_BPS + der_half)
|
||||
soglie[a] = dict(der_half=der_half, arb=arb_rt, dirz=dir_rt)
|
||||
print(f"\n {a} (ultimo {px[a]:,.0f}) — mezzo spread Deribit lineare = {der_half:.4f} bps (1 tick), "
|
||||
f"HL = {HL_HALF_SPREAD_BPS:.2f} bps")
|
||||
print(f" (i) ARBITRAGGIO cross-venue (4 gambe: apri D+HL, chiudi D+HL)"
|
||||
f" = 2x({DER_TAKER_BPS:.2f}+{HL_TAKER_BPS:.2f}+{der_half:.3f}+{HL_HALF_SPREAD_BPS:.2f})"
|
||||
f" = **{arb_rt:.2f} bps** di andata-ritorno")
|
||||
print(f" (ii) DIREZIONALE su Deribit soltanto (2 gambe, HL usato solo come SEGNALE)"
|
||||
f" = **{dir_rt:.2f} bps**")
|
||||
for tf in ("5m", "15m", "1h", "1d"):
|
||||
d = dec[(a, tf)]
|
||||
print(f" {tf:<4} |scarto| mediano {d['med']:5.2f} bps sd {d['sd']:5.2f} -> "
|
||||
f"serve {arb_rt/max(d['sd'],1e-9):5.1f} sd per pagare l'arb, "
|
||||
f"{dir_rt/max(d['sd'],1e-9):4.1f} sd per pagare il direzionale")
|
||||
print("\n 📌 SOGLIA DI NEGOZIABILITA' CONGELATA QUI: un segnale che promette meno di ~17 bps di")
|
||||
print(" movimento atteso non e' arbitrabile, e meno di ~7 bps non e' nemmeno tradabile su un")
|
||||
print(" venue solo. Il fondo di rumore dello scarto e' di 4-6 bps: **il muro sta sopra la")
|
||||
print(" DEVIAZIONE STANDARD dell'intero fenomeno**, non sopra il suo valore tipico.")
|
||||
return soglie
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 5 — (a) REVERSIONE, con il discriminante rumore-vs-dislocazione
|
||||
# ===========================================================================================
|
||||
def reversione(cache: dict, soglie: dict) -> dict:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [5] (a) LO SCARTO REVERTE? — e la reversione e' RUMORE o DISLOCAZIONE?")
|
||||
print("=" * 100)
|
||||
print(" s(t) = 1e4*ln(p_HL/p_DER). AR(1) sul livello -> mezza-vita.")
|
||||
print(" Poi la decomposizione che conta: r_DER(t+1) = a + bD*s(t) e r_HL(t+1) = a + bH*s(t).")
|
||||
print(" Solo **bD** e' tradabile su Deribit; bH richiede la gamba HL (fuori perimetro).")
|
||||
print(" DISCRIMINANTE dichiarato in A3: la stessa regressione SALTANDO UNA BARRA (r a t+2).")
|
||||
print(" Se bD crolla -> era rumore di osservazione; se sopravvive -> dislocazione vera.")
|
||||
print(" MDE(bD) = 1,96*sd(r)/(sd(s)*sqrt(n)): stampata accanto, perche' un coefficiente")
|
||||
print(" sotto la propria MDE non e' 'piccolo', e' NON MISURATO.\n")
|
||||
print(f" {'asset':<5}{'tf':<5}{'n':>7}{'phi':>7}{'HLbar':>7}{'HLore':>7}"
|
||||
f"{'bD':>9}{'t':>6}{'MDE(bD)':>9}{'bD skip1':>10}{'t':>6}"
|
||||
f"{'E|1sd| bps':>12}{'muro':>7}{'x muro':>8} verso")
|
||||
out = {}
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h", "1d"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
s_ = j["s_bps"].to_numpy()
|
||||
rd = j["r_der"].to_numpy() * 1e4 # bps
|
||||
A = ar1(s_)
|
||||
y, X = rd[1:], np.column_stack([np.ones(len(s_) - 1), s_[:-1]])
|
||||
f1 = ols_hac(y, X)
|
||||
y2, X2 = rd[2:], np.column_stack([np.ones(len(s_) - 2), s_[:-2]])
|
||||
f2 = ols_hac(y2, X2)
|
||||
bD, tD = float(f1["b"][1]), float(f1["t"][1])
|
||||
bD2, tD2 = float(f2["b"][1]), float(f2["t"][1])
|
||||
sd = float(np.std(s_))
|
||||
n = len(s_) - 1
|
||||
mde = 1.96 * float(np.std(rd)) / (sd * np.sqrt(n)) if sd > 0 else np.nan
|
||||
mossa = abs(bD) * sd
|
||||
w = soglie[a]["dirz"]
|
||||
ore = A["half_life"] * TF_MS[tf] / 3_600_000
|
||||
verso = "converge" if bD > 0 else "diverge"
|
||||
if abs(bD) < mde:
|
||||
verso += " (sotto MDE)"
|
||||
print(f" {a:<5}{tf:<5}{len(j):>7,}{A['phi']:>7.3f}{A['half_life']:>7.2f}{ore:>7.1f}"
|
||||
f"{bD:>9.4f}{tD:>6.1f}{mde:>9.4f}{bD2:>10.4f}{tD2:>6.1f}"
|
||||
f"{mossa:>12.2f}{w:>7.2f}{mossa/w:>8.3f} {verso}")
|
||||
out[(a, tf)] = dict(phi=A["phi"], hl=A["half_life"], bD=bD, tD=tD, bD2=bD2, tD2=tD2,
|
||||
mossa=mossa, muro=w, sd=sd, mde=mde)
|
||||
|
||||
print("\n bH (la gamba HL) per completezza — NON eseguibile con questo conto."
|
||||
" Identita' di controllo: bH - bD deve valere (phi-1).")
|
||||
print(f" {'asset':<5}{'tf':<5}{'bH':>9}{'t':>7}{'bH-bD':>9}{'phi-1':>9}{'|dif|':>9}")
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "1h", "1d"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
s_ = j["s_bps"].to_numpy()
|
||||
rh = j["r_hl"].to_numpy() * 1e4
|
||||
f = ols_hac(rh[1:], np.column_stack([np.ones(len(s_) - 1), s_[:-1]]))
|
||||
bH = float(f["b"][1])
|
||||
dif = (bH - out[(a, tf)]["bD"]) - (out[(a, tf)]["phi"] - 1.0)
|
||||
print(f" {a:<5}{tf:<5}{bH:>9.4f}{float(f['t'][1]):>7.1f}"
|
||||
f"{bH - out[(a, tf)]['bD']:>9.4f}{out[(a, tf)]['phi'] - 1:>9.4f}{abs(dif):>9.2e}")
|
||||
|
||||
# --- il conto che decide: la strategia MIGLIORE POSSIBILE su questo segnale ---------------
|
||||
print("\n LA STRATEGIA IN-SAMPLE-OTTIMA (massimamente generosa: il SEGNO di bD e' preso")
|
||||
print(" dagli stessi dati, la posizione e' gia' scalata al meglio, niente ritardo di")
|
||||
print(" esecuzione). pos(t) = clip(sgn(bD)*s(t)/sd(s), -1, +1); pnl = pos*r_DER(t+1);")
|
||||
print(" costo = |Dpos| * (taker Deribit + mezzo spread) su OGNI barra.")
|
||||
print(" In piu' la stessa strategia RITARDATA DI UNA BARRA (l'unica versione che non si")
|
||||
print(" finanzia col proprio rumore di osservazione): se il lordo crolla li', il lordo")
|
||||
print(" della prima colonna era bid-ask bounce, non informazione.")
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'barre/anno':>11}{'turnover/anno':>14}{'Sh LORDO':>10}"
|
||||
f"{'Sh LORDO skip1':>16}{'costo %/anno':>14}{'Sh NETTO':>10}{'MDE Sh':>8}")
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h", "1d"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
s_ = j["s_bps"].to_numpy()
|
||||
rd = j["r_der"].to_numpy()
|
||||
sd = float(np.std(s_))
|
||||
sgn = np.sign(out[(a, tf)]["bD"]) or 1.0
|
||||
pos = np.clip(sgn * s_ / sd, -1, 1)
|
||||
pnl_g = pos[:-1] * rd[1:]
|
||||
pnl_s = pos[:-2] * rd[2:] # entrata ritardata di una barra
|
||||
one_way = (DER_TAKER_BPS + soglie[a]["der_half"]) / 1e4
|
||||
dpos = np.abs(np.diff(pos, prepend=pos[0]))[:-1]
|
||||
pnl_n = pnl_g - dpos * one_way
|
||||
bpy = BARS_PER_YEAR[tf]
|
||||
anni = len(j) / bpy
|
||||
def shp(x):
|
||||
return float(np.mean(x) / np.std(x) * np.sqrt(bpy)) if np.std(x) > 0 else np.nan
|
||||
print(f" {a:<5}{tf:<5}{bpy:>11,.0f}{dpos.mean()*bpy:>14,.0f}{shp(pnl_g):>10.2f}"
|
||||
f"{shp(pnl_s):>16.2f}{dpos.mean()*one_way*bpy*100:>14.1f}"
|
||||
f"{shp(pnl_n):>10.2f}{1.96/np.sqrt(anni):>8.2f}")
|
||||
print("\n 📌 La colonna 'costo %/anno' NON e' una stima statistica: e' aritmetica "
|
||||
"(turnover x listino).")
|
||||
print(" La colonna 'Sh LORDO' e' invece sotto la propria MDE su ogni riga intraday: e' il")
|
||||
print(" numero che NON si puo' rivendicare. Il verdetto poggia sulla prima, non sulla seconda.")
|
||||
return out
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 6 — (b) LEAD-LAG: cross-correlazione + causalita' diretta + CONTROLLI
|
||||
# ===========================================================================================
|
||||
def leadlag(cache: dict) -> dict:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [6] (b) UNO DEI DUE VENUE GUIDA? — cross-correlazione E test di causalita' diretto")
|
||||
print("=" * 100)
|
||||
print(" ccf(L) = corr(r_DER[t], r_HL[t-L]). L>0 => HL GUIDA Deribit. L<0 => il contrario.")
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'n':>7}" + "".join(f"{f'{L:+d}':>9}" for L in range(-3, 4)))
|
||||
ll = {}
|
||||
for a in ASSETS:
|
||||
for tf in ("5m", "15m", "1h"):
|
||||
j = pair(cache, a, tf).dropna()
|
||||
c = ccf(j["r_der"].to_numpy(), j["r_hl"].to_numpy(), range(-3, 4))
|
||||
print(f" {a:<5}{tf:<5}{len(j):>7,}" + "".join(f"{c[L]:>9.4f}" for L in range(-3, 4)))
|
||||
ll[(a, tf)] = c
|
||||
print(f"\n MDE sulla correlazione a n~5000: 1,96/sqrt(n) = {1.96/np.sqrt(5000):.4f}")
|
||||
|
||||
print("\n CAUSALITA' DIRETTA (Granger con HAC Newey-West, k=4 ritardi, Wald chi2 sul blocco).")
|
||||
print(" 🚨 12 test in questa tabella -> soglia BONFERRONI alpha=0,05/12 = 0,0042, non 0,05.")
|
||||
|
||||
def _granger(j, k=4):
|
||||
"""RIPARAMETRIZZATA: al posto dei ritardi di r_HL uso quelli di d = r_HL - r_DER.
|
||||
|
||||
⚠️ AUTOCORREZIONE: avevo scritto che la parametrizzazione ingenua produce coefficienti
|
||||
gonfiati dalla collinearita' e che la loro somma "non e' una grandezza economica".
|
||||
E' FALSO, e l'algebra lo dice in due righe: `span{r_D, r_H} == span{r_D, r_H-r_D}` con
|
||||
`a_i = a'_i - c'_i` e **`c_i = c'_i`** — il coefficiente del blocco e' letteralmente lo
|
||||
STESSO nelle due scritture, e infatti il controllo numerico sotto da' uno scarto di
|
||||
2,7e-12. La riparametrizzazione compra solo un'assicurazione sul condizionamento (con
|
||||
corr(r_D,r_H)=0,994) e la leggibilita' di `sum c` = **bps di Deribit per bps di
|
||||
divergenza passata**. Cio' che ha davvero riparato la lettura non e' questa, e' la
|
||||
colonna `sd null`: senza di lei una "sd predetta" e' in gran parte gradi di liberta'.
|
||||
"""
|
||||
rd, rh = j["r_der"].to_numpy() * 1e4, j["r_hl"].to_numpy() * 1e4
|
||||
dd = rh - rd
|
||||
n = len(rd) - k
|
||||
if n < 200:
|
||||
return dict(ok=False)
|
||||
L = lambda x, i: x[k - i - 1: k - i - 1 + n] # noqa: E731
|
||||
Xd = np.column_stack([np.ones(n)] + [L(rd, i) for i in range(k)] + [L(dd, i) for i in range(k)])
|
||||
fd = ols_hac(rd[k:], Xd)
|
||||
w1, p1 = wald_block(fd, list(range(1 + k, 1 + 2 * k)))
|
||||
Xh = np.column_stack([np.ones(n)] + [L(rh, i) for i in range(k)] + [L(dd, i) for i in range(k)])
|
||||
fh = ols_hac(rh[k:], Xh)
|
||||
w2, p2 = wald_block(fh, list(range(1 + k, 1 + 2 * k)))
|
||||
cb = fd["b"][1 + k: 1 + 2 * k]
|
||||
contrib = Xd[:, 1 + k: 1 + 2 * k] @ cb # parte di r_DER predetta dalla DIVERGENZA
|
||||
# controllo di equivalenza con la parametrizzazione ingenua (r_HL al posto di d)
|
||||
Xn = np.column_stack([np.ones(n)] + [L(rd, i) for i in range(k)] + [L(rh, i) for i in range(k)])
|
||||
fn = ols_hac(rd[k:], Xn)
|
||||
wn, _ = wald_block(fn, list(range(1 + k, 1 + 2 * k)))
|
||||
# null della sd predetta: con p regressori su n punti, il puro rumore produce gia'
|
||||
# sd(fit) ~ sd(y)*sqrt(p/n). Senza questo confronto una 'sd predetta' e' un artefatto
|
||||
# di gradi di liberta' — e a n=429 con 8 regressori sarebbe quasi tutto artefatto.
|
||||
sd_null = float(np.std(rd[k:])) * np.sqrt(k / n)
|
||||
return dict(ok=True, w1=w1, p1=p1, w2=w2, p2=p2, sum_c=float(cb.sum()),
|
||||
sd_fit=float(np.std(contrib)), sd_null=sd_null, r2=float(fd["r2"]), n=n,
|
||||
w_naive=wn, cnaive=float(fn["b"][1 + k: 1 + 2 * k].sum()))
|
||||
|
||||
def _lettura(p1, p2, alpha=0.05 / 12):
|
||||
if not np.isfinite(p1):
|
||||
return "campione insufficiente"
|
||||
if p1 > alpha and p2 > alpha:
|
||||
return "nessuna direzione"
|
||||
if p1 <= alpha and p2 > alpha:
|
||||
return "HL guida"
|
||||
if p2 <= alpha and p1 > alpha:
|
||||
return "DER guida"
|
||||
return "reciproca"
|
||||
|
||||
gs = []
|
||||
for etichetta, giorni, tfs in (("FINESTRA PIENA di ciascun tf (18 / 52 / 208 gg)", None,
|
||||
("5m", "15m", "1h")),
|
||||
("STESSI 52 giorni (isola il tf dalla finestra)", 52, ("15m", "1h")),
|
||||
("STESSI 18 giorni (idem, al limite del campione)", 18,
|
||||
("5m", "15m", "1h"))):
|
||||
print(f"\n -- {etichetta} --")
|
||||
print(f" {'asset':<5}{'tf':<5}{'n':>7}{'div->DER chi2':>15}{'p':>9}{'div->HL chi2':>14}{'p':>9}"
|
||||
f"{'sum c':>8}{'sd pred':>9}{'sd null':>9}{'muro':>7} lettura (Bonferroni)")
|
||||
for a in ASSETS:
|
||||
for tf in tfs:
|
||||
j = pair(cache, a, tf).dropna()
|
||||
if giorni is not None:
|
||||
j = j[j.index >= j.index[-1] - pd.Timedelta(days=giorni)]
|
||||
g = _granger(j)
|
||||
if not g.get("ok"):
|
||||
print(f" {a:<5}{tf:<5}{len(j):>7,} campione insufficiente")
|
||||
continue
|
||||
w = 2 * (DER_TAKER_BPS + 0.5 * DER_TICK_USDC[a] / float(j['der'].iloc[-1]) * 1e4)
|
||||
print(f" {a:<5}{tf:<5}{g['n']:>7,}{g['w1']:>15.1f}{g['p1']:>9.4f}{g['w2']:>14.1f}"
|
||||
f"{g['p2']:>9.4f}{g['sum_c']:>8.3f}{g['sd_fit']:>9.3f}{g['sd_null']:>9.3f}"
|
||||
f"{w:>7.2f} {_lettura(g['p1'], g['p2'])}")
|
||||
if giorni is None:
|
||||
gs.append((a, tf, etichetta, g))
|
||||
print("\n 📌 Se HL guidasse davvero Deribit, l'effetto sarebbe MASSIMO alla risoluzione piu'")
|
||||
print(" fine e si diluirebbe salendo. Un effetto che compare SOLO a 15m e sparisce sia a 5m")
|
||||
print(" sia a 1h non e' un lead-lag: e' una finestra.")
|
||||
if gs:
|
||||
alpha = 0.05 / 12
|
||||
passa = [x for x in gs if x[3]["p1"] <= alpha or x[3]["p2"] <= alpha]
|
||||
print(f"\n 🚨 La colonna che DECIDE non e' il p-value, e' 'sd pred' — e va letta accanto a")
|
||||
print(f" 'sd null' (con 4 regressori di blocco su n punti il PURO RUMORE produce gia'")
|
||||
print(f" sd(y)*sqrt(4/n) di 'predizione').")
|
||||
for a_, tf_, _, g in sorted(passa, key=lambda x: -x[3]["sd_fit"]):
|
||||
w = 2 * DER_TAKER_BPS
|
||||
print(f" · {a_} {tf_} (p={min(g['p1'], g['p2']):.4f}, passa Bonferroni): sd pred "
|
||||
f"**{g['sd_fit']:.2f} bps** contro {g['sd_null']:.2f} di rumore e {w:.1f} bps di"
|
||||
f" costo -> manca **{w/max(g['sd_fit'],1e-9):.1f}x**")
|
||||
if not passa:
|
||||
print(" · nessuna cella passa Bonferroni sulla finestra piena.")
|
||||
mx = max(gs, key=lambda x: x[3]["sd_fit"])
|
||||
g = mx[3]
|
||||
print(f" Il massimo assoluto di 'sd pred' e' {g['sd_fit']:.2f} bps ({mx[0]} {mx[1]}), ma li'")
|
||||
print(f" il rumore da solo ne fa {g['sd_null']:.2f} e il test NON e' significativo"
|
||||
f" (p={min(g['p1'], g['p2']):.4f}).")
|
||||
# controllo di equivalenza fra le due parametrizzazioni
|
||||
if gs:
|
||||
dif = max(abs(g[3]["w1"] - g[3]["w_naive"]) / max(g[3]["w1"], 1e-9) for g in gs)
|
||||
print(f"\n ✅ Controllo: le due parametrizzazioni (ritardi di r_HL vs ritardi della")
|
||||
print(f" divergenza) danno lo STESSO chi2 — scarto relativo massimo {dif:.2e}. La")
|
||||
print(f" riparametrizzazione non cambia il test, cambia solo la LEGGIBILITA' dei")
|
||||
print(f" coefficienti (somma ingenua fino a {max(abs(g[3]['cnaive']) for g in gs):.2f}, "
|
||||
f"somma leggibile {max(abs(g[3]['sum_c']) for g in gs):.3f}).")
|
||||
|
||||
# ---- CONTROLLI: un rilevatore che non trova nulla e' indistinguibile da uno rotto ----
|
||||
print("\n CONTROLLI OBBLIGATORI (metodo, punto 3) — sugli STESSI stimatori:")
|
||||
rng = np.random.default_rng(823)
|
||||
n = 5000
|
||||
r = rng.normal(0, 0.001, n) # prezzo comune
|
||||
pD = np.exp(np.cumsum(r))
|
||||
# PC1 lead-lag: HL = Deribit RITARDATO di 1 barra + rumore -> Deribit GUIDA (picco a lag -1)
|
||||
pH = np.concatenate([[pD[0]], pD[:-1]]) * np.exp(rng.normal(0, 5e-5, n))
|
||||
rd_, rh_ = np.diff(np.log(pD)) * 1e4, np.diff(np.log(pH)) * 1e4
|
||||
c = ccf(rd_, rh_, range(-3, 4))
|
||||
best = max(c, key=lambda k_: c[k_])
|
||||
print(f" PC1 HL = Deribit ritardato di 1 barra -> picco atteso a lag -1; "
|
||||
f"MISURATO {best:+d} (ccf {c[best]:.3f}) {'OK' if best == -1 else 'ROTTO'}")
|
||||
# PC2 reversione: spread AR(1) noto phi=0.7 -> mezza-vita attesa 1.94 barre
|
||||
phi_true = 0.7
|
||||
e = rng.normal(0, 3.0, n)
|
||||
s_syn = np.zeros(n)
|
||||
for i in range(1, n):
|
||||
s_syn[i] = phi_true * s_syn[i - 1] + e[i]
|
||||
A = ar1(s_syn)
|
||||
hl_att = np.log(0.5) / np.log(phi_true)
|
||||
print(f" PC2 spread AR(1) phi={phi_true} (mezza-vita {hl_att:.2f}) -> "
|
||||
f"MISURATO phi {A['phi']:.3f}, mezza-vita {A['half_life']:.2f} "
|
||||
f"{'OK' if abs(A['phi'] - phi_true) < 0.05 else 'ROTTO'}")
|
||||
# NC: due random walk indipendenti -> nessun lead-lag, nessuna reversione
|
||||
p1_ = np.exp(np.cumsum(rng.normal(0, 0.001, n)))
|
||||
p2_ = np.exp(np.cumsum(rng.normal(0, 0.001, n)))
|
||||
c0 = ccf(np.diff(np.log(p1_)) * 1e4, np.diff(np.log(p2_)) * 1e4, range(-3, 4))
|
||||
mx = max(abs(v) for v in c0.values())
|
||||
print(f" NC due random walk indipendenti -> max |ccf| = {mx:.4f} "
|
||||
f"(soglia rumore {1.96/np.sqrt(n):.4f}) {'OK' if mx < 3*1.96/np.sqrt(n) else 'ROTTO'}")
|
||||
return ll
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 7 — (c) GATE DI REGIME
|
||||
# ===========================================================================================
|
||||
def gate(cache: dict) -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [7] (c) LO SCARTO E' UN GATE DI REGIME PER TP01/SKH01?")
|
||||
print("=" * 100)
|
||||
j = {a: pair(cache, a, "1d").dropna() for a in ASSETS}
|
||||
n = min(len(j[a]) for a in ASSETS)
|
||||
anni = n / 365.25
|
||||
mde_sh = 1.96 / np.sqrt(anni)
|
||||
mde_c = 1.96 / np.sqrt(n)
|
||||
print(f"\n Finestra comune: {n} giorni = {anni:.2f} anni (HL 1d parte dal 2024-01-01).")
|
||||
print(f" MDE dichiarato PRIMA: Sharpe {mde_sh:.2f} · correlazione {mde_c:.3f}.")
|
||||
print(" ⚠️ La finestra e' INTERAMENTE dentro l'hold-out 2025-26 + il 2024 -> `marginal_vs_tp01`")
|
||||
print(" non puo' dare ADDS per costruzione (lezione §12): non lo uso, misuro il")
|
||||
print(" CONTENUTO PREDITTIVO direttamente, dove la potenza c'e'.\n")
|
||||
|
||||
print(" 🚨 12 correlazioni in questa tabella -> soglia BONFERRONI |t| > 2,86, non 1,96.\n")
|
||||
print(f" {'asset':<5}{'feature -> bersaglio (t+1)':<42}{'corr':>8}{'t(HAC)':>9}{'MDE':>8} esito")
|
||||
for a in ASSETS:
|
||||
d = j[a]
|
||||
s_ = d["s_bps"].to_numpy()
|
||||
r = d["r_der"].to_numpy() * 1e4
|
||||
absr = np.abs(r)
|
||||
a20 = pd.Series(np.abs(s_)).rolling(20).mean().to_numpy() # causale
|
||||
feats = {"s(t) firmato": s_, "|s(t)|": np.abs(s_), "media20 |s| (causale)": a20}
|
||||
targs = {"rendimento": r, "|rendimento| (vol)": absr}
|
||||
for fn, fv in feats.items():
|
||||
for tn, tv in targs.items():
|
||||
x, y = fv[:-1], tv[1:]
|
||||
m = np.isfinite(x) & np.isfinite(y)
|
||||
if m.sum() < 100:
|
||||
continue
|
||||
c = float(np.corrcoef(x[m], y[m])[0, 1])
|
||||
f = ols_hac(y[m], np.column_stack([np.ones(m.sum()), x[m]]))
|
||||
t = float(f["t"][1])
|
||||
if abs(t) > 2.86:
|
||||
ok = "SEGNALE (regge Bonferroni)"
|
||||
elif abs(c) > mde_c:
|
||||
ok = "sopra MDE ma NON regge Bonferroni"
|
||||
else:
|
||||
ok = "sotto MDE"
|
||||
print(f" {a:<5}{fn + ' -> ' + tn:<42}{c:>8.4f}{t:>9.2f}{mde_c:>8.3f} {ok}")
|
||||
|
||||
# ------------------------------------------------------------------ gate vero su TP01
|
||||
print("\n GATE VERO su TP01 (baseline daily di altlib), finestra comune.")
|
||||
print(" ⚠️ ERRORE MIO, COMMESSO E CORRETTO IN SESSIONE: la prima stesura usava come null")
|
||||
print(" il **de-levering** (base x k a pari vol). Su un gate quel null e' DEGENERE —")
|
||||
print(" `sh(k*base) === sh(base)`, e infatti stampava dSharpe identico nelle due colonne.")
|
||||
print(" E' esattamente il caso avvertito in §12(d). Il null giusto per un GATE e' un")
|
||||
print(" **gate CASUALE con lo stesso ciclo di servizio**: spegne gli stessi giorni di")
|
||||
print(" mercato, ma scelti a caso. Cosi' si misura se il gate spegne i giorni GIUSTI.")
|
||||
try:
|
||||
from scripts.research.alt import altlib as al
|
||||
tp = al.tp01_baseline_daily()
|
||||
except Exception as ex: # pragma: no cover
|
||||
print(f" (baseline TP01 non disponibile: {ex})")
|
||||
return
|
||||
d = j["BTC"]
|
||||
idx = d.index.tz_convert("UTC").normalize()
|
||||
sabs = pd.Series(np.abs(d["s_bps"].to_numpy()), index=idx).rolling(20).mean()
|
||||
tpi = tp.copy()
|
||||
tpi.index = (pd.DatetimeIndex(tpi.index).tz_localize("UTC")
|
||||
if pd.DatetimeIndex(tpi.index).tz is None else pd.DatetimeIndex(tpi.index))
|
||||
tpi.index = tpi.index.normalize()
|
||||
m = pd.concat({"tp": tpi, "g": sabs}, axis=1, join="inner").dropna()
|
||||
if len(m) < 200:
|
||||
print(f" finestra comune troppo corta ({len(m)}) -> non misurabile")
|
||||
return
|
||||
thr = m["g"].expanding(60).median().shift(1) # causale, espandente
|
||||
base = m["tp"].to_numpy()
|
||||
|
||||
def sh(x):
|
||||
x = np.asarray(x, float)
|
||||
return float(np.mean(x) / np.std(x) * np.sqrt(365.25)) if np.std(x) > 0 else np.nan
|
||||
|
||||
rng = np.random.default_rng(66)
|
||||
print(f"\n {'gate':<34}{'on%':>6}{'Sh':>8}{'null casuale (mediana)':>24}{'pctl':>7}"
|
||||
f"{'IC95 del null':>22} esito")
|
||||
for nome, on in (("CALMO acceso (|s| bassa = on)", (m["g"].shift(1) <= thr)),
|
||||
("AGITATO acceso (|s| alta = on)", (m["g"].shift(1) > thr))):
|
||||
onv = on.astype(float).fillna(1.0).to_numpy()
|
||||
gated = base * onv
|
||||
duty = float(np.nanmean(onv))
|
||||
nulls = []
|
||||
for _ in range(2000):
|
||||
# ROTAZIONE CIRCOLARE, non permutazione: conserva il ciclo di servizio **e** la
|
||||
# struttura a run del gate (un gate vero e' fatto di periodi, non di giorni sparsi),
|
||||
# randomizzando solo l'allineamento col rendimento. Null piu' severo e piu' onesto.
|
||||
perm = np.roll(onv, int(rng.integers(1, len(onv))))
|
||||
nulls.append(sh(base * perm))
|
||||
nulls = np.array(nulls)
|
||||
nulls = nulls[np.isfinite(nulls)]
|
||||
pct = float((nulls < sh(gated)).mean() * 100)
|
||||
lo, hi = np.percentile(nulls, [2.5, 97.5])
|
||||
esito = ("il gate spegne i giorni GIUSTI" if pct > 97.5
|
||||
else ("il gate spegne i giorni SBAGLIATI" if pct < 2.5 else "indistinguibile dal caso"))
|
||||
print(f" {nome:<34}{duty*100:>6.0f}{sh(gated):>8.3f}{np.median(nulls):>24.3f}{pct:>7.1f}"
|
||||
f"{f'[{lo:+.3f}, {hi:+.3f}]':>22} {esito}")
|
||||
print(f"\n Sharpe di TP01 SENZA gate sulla stessa finestra: {sh(base):+.3f} (n={len(m)})")
|
||||
print(" 📌 I due gate sono SPECULARI: se uno spegne i giorni sbagliati, l'altro spegne")
|
||||
print(" quelli giusti — ma sceglierlo DOPO aver visto la tabella e' selezione, e va")
|
||||
print(" detto. Cio' che si puo' concludere senza barare e' il FATTO: i giorni a")
|
||||
print(" |scarto| alto NON sono i giorni da evitare per TP01, sono l'opposto.")
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# 8 — rinforzo indipendente della taratura di venue_watch
|
||||
# ===========================================================================================
|
||||
def rinforzo(cache: dict) -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print(" [8] SOTTOPRODOTTO — HL come QUARTA referenza per `venue_watch` (soglia 100 bps / 4h)")
|
||||
print("=" * 100)
|
||||
from src.live.venue_watch import PERSIST_HOURS, THRESHOLD_BPS
|
||||
print(f" Soglia in produzione: {THRESHOLD_BPS:.0f} bps persistenti {PERSIST_HOURS}h a segno costante.")
|
||||
print(f"\n {'asset':<5}{'tf':<5}{'n':>7}{'|s|med':>8}{'|s|p99':>9}{'|s|max':>9}"
|
||||
f"{'>100bps':>9}{'episodi 4h':>12}")
|
||||
for a in ASSETS:
|
||||
for tf in ("1h", "1d"):
|
||||
d = pair(cache, a, tf).dropna()
|
||||
s = d["s_bps"]
|
||||
over = (s.abs() > THRESHOLD_BPS).to_numpy()
|
||||
sg = np.sign(s.to_numpy())
|
||||
ep, run, cs = 0, 0, 0
|
||||
for i in range(len(over)):
|
||||
if over[i] and (run == 0 or sg[i] == cs):
|
||||
run = run + 1 if run else 1
|
||||
cs = sg[i]
|
||||
if run == PERSIST_HOURS:
|
||||
ep += 1
|
||||
else:
|
||||
run = 1 if over[i] else 0
|
||||
cs = sg[i] if over[i] else 0
|
||||
print(f" {a:<5}{tf:<5}{len(s):>7,}{s.abs().median():>8.2f}{s.abs().quantile(.99):>9.1f}"
|
||||
f"{s.abs().max():>9.1f}{int(over.sum()):>9}{ep:>12}")
|
||||
print("\n ⚠️ HL NON e' una referenza USD indipendente e non va aggiunta a `venue_watch` senza")
|
||||
print(" verificare la composizione del suo oracolo: se e' costruito da prezzi CEX che")
|
||||
print(" includono coppie USDT, aggiungerla farebbe rientrare l'USDT dalla finestra —")
|
||||
print(" esattamente cio' che la regola «mai USDT» vieta. Qui e' usata come MISURA, non")
|
||||
print(" come referenza di produzione.")
|
||||
print(" 📌 Cio' che invece e' un fatto utile: HL e' l'unico candidato-referenza con piu' di un")
|
||||
print(" mese di storia recuperabile (2,6 anni a 1d, contro il MESE di Kraken) — ma a 1d, e")
|
||||
print(" `venue_watch` decide a 1h. Non ripara la taratura degli 8 anni; puo' fare da")
|
||||
print(" conferma LENTA.")
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
def main() -> None:
|
||||
refresh = "--refresh" in sys.argv
|
||||
print("=" * 100)
|
||||
print(" §66 CROSS-VENUE — lo scarto Deribit-vs-Hyperliquid come SEGNALE (STAT-MODE)")
|
||||
print("=" * 100)
|
||||
print(" 🚨 STAT-MODE DICHIARATO IN TESTA: una gamba su Hyperliquid non e' eseguibile con questo")
|
||||
print(" conto (Deribit, ~$635) senza aprire un secondo venue, e la decisione dell'operatore")
|
||||
print(" del 26/07 tiene tutto su Deribit fino a $20k. Il filone si giudica su «esiste il")
|
||||
print(" fenomeno?», non su «e' deployabile?».")
|
||||
cache = load_cache(refresh)
|
||||
try:
|
||||
repliche(cache)
|
||||
certifica_hl(cache)
|
||||
verifica_lag(cache)
|
||||
dec = scomposizione(cache)
|
||||
soglie = muro(cache, dec)
|
||||
rev = reversione(cache, soglie)
|
||||
leadlag(cache)
|
||||
gate(cache)
|
||||
rinforzo(cache)
|
||||
finally:
|
||||
save_cache(cache)
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
print(" VERDETTO")
|
||||
print("=" * 100)
|
||||
kall = max(rev, key=lambda k: rev[k]["mossa"] / rev[k]["muro"])
|
||||
misurate = {k: v for k, v in rev.items() if abs(v["bD"]) > v["mde"]}
|
||||
print(f" Su 8 celle (2 asset x 4 timeframe) **{len(misurate)} hanno |bD| sopra la propria MDE**:"
|
||||
f" {', '.join(f'{k[0]} {k[1]}' for k in misurate) or 'nessuna'}.")
|
||||
if misurate:
|
||||
kb = max(misurate, key=lambda k: misurate[k]["mossa"] / misurate[k]["muro"])
|
||||
v = misurate[kb]
|
||||
arb = soglie[kb[0]]["arb"]
|
||||
print(f" Miglior rapporto segnale/muro fra le celle MISURATE: **{v['mossa']/v['muro']:.3f}** "
|
||||
f"({kb[0]} {kb[1]}: {v['mossa']:.2f} bps attesi a 1 sd contro {v['muro']:.2f} bps di costo"
|
||||
f" su UN venue) -> manca un fattore **{v['muro']/v['mossa']:.1f}x**; per l'arbitraggio a"
|
||||
f" due venue ({arb:.1f} bps) manca **{arb/v['mossa']:.1f}x**.")
|
||||
va = rev[kall]
|
||||
print(f" Il massimo INCONDIZIONATO sarebbe {va['mossa']/va['muro']:.3f} ({kall[0]} {kall[1]}), ma"
|
||||
f" li' |bD|={abs(va['bD']):.2f} contro MDE {va['mde']:.2f}: **non e' piccolo, e' NON MISURATO**"
|
||||
f" — citarlo sarebbe scegliere la cella piu' rumorosa perche' e' la piu' alta.")
|
||||
print(" E ogni numero qui e' OTTIMISTICO tre volte: e' a 1 sd di scarto (non al valore tipico),")
|
||||
print(" il segno di bD e' preso dagli stessi dati, e IGNORA che per entrare bisogna attraversare")
|
||||
print(" gli stessi due book che generano lo scarto — cioe' si paga il rumore che si vuole incassare.")
|
||||
print("\n In EUR/giorno: ZERO. Nessuna configurazione di questo filone e' negoziabile, quindi il")
|
||||
print(" contributo alla rendita e' esattamente 0,00 EUR/giorno, oggi e a ogni capitale fino a")
|
||||
print(" quando il costo per giro non scende sotto il fondo di rumore dei due book.")
|
||||
print("=" * 100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,896 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
r0823b_curva_datati.py — filone §65 CURVA-DATATI (ondata 2026-08-23)
|
||||
|
||||
===========================================================================
|
||||
LA DOMANDA
|
||||
===========================================================================
|
||||
L'ondata 1 (§13 BASIS-CALENDAR) ha ricostruito un dataset che il progetto non
|
||||
aveva: **30 futures trimestrali SCADUTI** di Deribit (BTC 240.150 barre orarie /
|
||||
ETH 233.334, dal 2018-09), **col deleveraging 2022 dentro** — la coda che a CC01
|
||||
mancava per costruzione.
|
||||
|
||||
Finora e' stato usato per UNA cosa sola: il **livello** del carry (§13 lo ha
|
||||
scartato come edge; §39 lo ha scartato come veicolo di TP01). **La FORMA della
|
||||
curva — pendenza e curvatura — non e' mai stata guardata.**
|
||||
|
||||
===========================================================================
|
||||
ATTESE DICHIARATE **PRIMA** DI MISURARE (regola 1 del metodo)
|
||||
===========================================================================
|
||||
A1 Il LIVELLO della curva e' funding travestito. §39 ha gia' misurato che il
|
||||
basis dei datati E' il funding del perp (+7,33% implicito contro +6,48%
|
||||
realizzato). Mi aspetto |corr(livello, funding)| ALTA (>0,5).
|
||||
-> se confermata, il ramo "livello" e' chiuso in partenza.
|
||||
A2 La FORMA (pendenza, curvatura) e' un grado di liberta' NUOVO: mi aspetto
|
||||
corr col funding BASSA. E' l'unica ragione per cui questo filone esiste.
|
||||
A3 Nessun ramo produce un candidato. Il filone funding e' chiuso su QUATTRO
|
||||
lati (carry cross-sectional, price-clock, time-series, affollamento) e §13
|
||||
ha gia' esaurito il primo ordine. Mi aspetto SCARTATO su tutti i rami.
|
||||
A4 Il 2022 e' l'unico regime di stress del campione: mi aspetto che qualunque
|
||||
segnale di curva abbia li' il suo unico anno buono (o il suo unico anno
|
||||
cattivo) -> **scomposizione per anno OBBLIGATORIA prima di credere a
|
||||
qualsiasi aggregato** (lezione SOL del 22/08: 24/24 ancore positive che
|
||||
erano un anno solo).
|
||||
A5 Il roll trimestrale costa. §39: SKH01 sui datati fa 48-57 roll-equivalenti
|
||||
l'anno contro 6,6-8,4 di TP01 -> lo stesso mezzo spread gli costa 7 volte
|
||||
tanto. Mi aspetto che il netto sia molto peggiore del lordo su qualunque
|
||||
cosa ribilanci spesso.
|
||||
A6 MDE: 7,4 anni -> MDE sullo Sharpe ~= 1,96/sqrt(7,4) = **0,72**. Un ramo
|
||||
DELTA-NEUTRO (§13: SE del rendimento annuo 1,79pp, vol 4,9%) non e'
|
||||
rispondibile; un ramo DIREZIONALE (vol 50-80%) lo e' in linea di principio.
|
||||
|
||||
Esito di ciascuna: riportato in fondo, confermata/refutata.
|
||||
|
||||
===========================================================================
|
||||
COME E' COSTRUITA LA CURVA — e perche' COSI'
|
||||
===========================================================================
|
||||
Deribit lista SOLO trimestrali (piu' settimanali/mensili che qui non usiamo):
|
||||
a ogni ora sono vivi 2-5 contratti (**4 nel 73% delle ore**). Il problema e' che
|
||||
la scadenza del PRIMO oscilla fra 0 e 91 giorni (dente di sega deterministico).
|
||||
|
||||
Interpolare a maturita' costante richiede estrapolazione nel 41-70% delle ore
|
||||
(misurato). Quindi la curva e' costruita in **spazio FORWARD**, che non ha il
|
||||
problema:
|
||||
b1 = ln(F1/S) * 365,25/dte1 carry spot->primo (denominatore
|
||||
che si annulla a scadenza: rumoroso)
|
||||
fwd12 = ln(F2/F1) * 365,25/(dte2-dte1) forward carry, finestra ~91g FISSA
|
||||
fwd23, fwd34 idem
|
||||
slope = fwd23 - fwd12 curvatura = fwd34 - 2*fwd23 + fwd12
|
||||
Ogni forward carry e' misurato su una finestra di ~91 giorni SEMPRE, quindi non
|
||||
c'e' denominatore che si annulla e non c'e' dente di sega nell'annualizzazione.
|
||||
Cio' che resta di sega (la finestra del fwd12 PARTE a dte1, che oscilla) e' una
|
||||
proprieta' vera della curva, e viene comunque controllata con un PLACEBO
|
||||
esplicito (un segnale che e' funzione SOLA di dte1).
|
||||
|
||||
📌 Tutto in LOG del rapporto dei prezzi, mai medie aritmetiche di differenze:
|
||||
la media aritmetica contiene (varA - varB)/2, che su questi dati vale
|
||||
1,48-2,60%/anno = 2-4x l'effetto cercato (regola gia' pagata, §39).
|
||||
|
||||
===========================================================================
|
||||
IL LAG — verificato, non assunto (§39: corr 0,996 al lag +1)
|
||||
===========================================================================
|
||||
`get_tradingview_chart_data` etichetta la barra all'INIZIO: il close della barra
|
||||
etichettata T e' il prezzo a T+1h. `get_funding_rate_history` etichetta
|
||||
l'indice all'ISTANTE T. Appaiarli senza lag sfasa di un'ora.
|
||||
Il lag NON e' assunto: e' scelto misurando la sd del basis perp-vs-indice a
|
||||
k = -2..+2 e la convergenza a scadenza. Vedi §0.
|
||||
|
||||
===========================================================================
|
||||
DISCIPLINA
|
||||
===========================================================================
|
||||
* SOLA LETTURA. Nessuna rete: tutto da `data/raw/fut_deribit/` (persistito
|
||||
dall'ondata 1). Nessun ordine, nessuna chiave.
|
||||
* Nessun file di produzione toccato.
|
||||
* Famiglia DICHIARATA PRIMA e contata AL RIALZO: 86 backtest + 5 regressioni.
|
||||
Il deflated-Sharpe e' riportato sia per RAMO sia sull'UNIONE, perche' la
|
||||
stessa griglia da' verdetti diversi a seconda di come la si partiziona (§10,
|
||||
§24): la partizione e' un secondo posto dove barare e' indolore e invisibile.
|
||||
|
||||
USO
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0823b_curva_datati.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy import stats
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt"))
|
||||
|
||||
import altlib as al # noqa: E402
|
||||
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
|
||||
|
||||
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||
|
||||
DATA = PROJECT_ROOT / "data" / "raw" / "fut_deribit"
|
||||
MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN",
|
||||
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
|
||||
YR = 365.25
|
||||
ASSETS = ("BTC", "ETH")
|
||||
HOLDOUT = al.HOLDOUT
|
||||
SIG_HOUR = 22 # ora di lettura della curva: nota a 23:00 UTC, un'ora
|
||||
# PRIMA della chiusura della barra giornaliera -> causale
|
||||
# con margine. La sensibilita' all'ora e' misurata in §6.
|
||||
|
||||
# ---- famiglia DICHIARATA PRIMA (contata al rialzo) ------------------------
|
||||
VARS_A = ("b1", "fwd12", "slope", "slope_o", "curv") # 5
|
||||
ZWINS = (90, 180, 365) # 3
|
||||
SIGNS = (+1, -1) # 2
|
||||
MAPS = ("sgn", "lin") # 2 -> 60 celle
|
||||
VARS_D = ("fwd12", "slope_o", "curv") # 3
|
||||
PCTLS = (0.30, 0.50, 0.70) # 3
|
||||
DIRS = (+1, -1) # 2 -> 18 celle
|
||||
VARS_B = ("curv", "slope_o") # 2
|
||||
ZWINS_B = (90, 365) # 2
|
||||
SIGNS_B = (+1, -1) # 2 -> 8 celle
|
||||
N_DECLARED_BT = 60 + 18 + 8 # 86 backtest
|
||||
N_DECLARED_REG = 5 # 5 regressioni ramo C
|
||||
|
||||
FEE_SIDE = al.FEE_SIDE # 5 bps/lato: la convenzione dei backtest del progetto
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "=" * 92)
|
||||
if t:
|
||||
print(t)
|
||||
print("=" * 92)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# §0 DATO — caricamento, LAG verificato, certificazione
|
||||
# ===========================================================================
|
||||
def expiry_of(name: str) -> pd.Timestamp:
|
||||
_, tag = name.split("-")
|
||||
return pd.Timestamp(2000 + int(tag[5:]), MONTHS.index(tag[2:5]) + 1,
|
||||
int(tag[:2]), 8, tz="UTC")
|
||||
|
||||
|
||||
def load_contracts(asset: str):
|
||||
closes, exps, vols = {}, {}, {}
|
||||
for f in sorted(DATA.glob(f"{asset}-*.parquet")):
|
||||
nm = f.stem
|
||||
df = pd.read_parquet(f)
|
||||
if len(df) < 200:
|
||||
continue
|
||||
s = df.set_index("ts")["close"].astype(float)
|
||||
s = s[s > 0]
|
||||
s.index = s.index.tz_convert("UTC")
|
||||
v = df.set_index("ts")["volume"].astype(float)
|
||||
v.index = v.index.tz_convert("UTC")
|
||||
e = expiry_of(nm)
|
||||
s = s[s.index < e - pd.Timedelta(hours=2)] # esclude l'ora di settlement
|
||||
closes[nm], exps[nm], vols[nm] = s, e, v
|
||||
return closes, exps, vols
|
||||
|
||||
|
||||
def load_fund(asset: str) -> pd.DataFrame:
|
||||
f = pd.read_parquet(DATA / f"fund_{asset}.parquet").set_index("ts").sort_index()
|
||||
f.index = f.index.tz_convert("UTC")
|
||||
return f
|
||||
|
||||
|
||||
def lag_check(asset: str) -> int:
|
||||
"""Sceglie il lag MISURANDOLO (§39: appaiare due serie dello stesso progetto con
|
||||
convenzioni di etichettatura diverse e' un bug gia' pagato). Ritorna il lag in ore."""
|
||||
fund = load_fund(asset)
|
||||
idx = fund["index"].astype(float)
|
||||
p = al.get(asset, "1h")
|
||||
perp = pd.Series(p["close"].values.astype(float),
|
||||
index=pd.DatetimeIndex(pd.to_datetime(p["datetime"], utc=True)))
|
||||
out = {}
|
||||
for k in (-2, -1, 0, 1, 2):
|
||||
ii = idx.copy()
|
||||
ii.index = ii.index - pd.Timedelta(hours=k)
|
||||
j = pd.concat({"p": perp, "i": ii}, axis=1, join="inner").dropna()
|
||||
b = np.log(j["p"] / j["i"]) * 1e4
|
||||
out[k] = (len(j), float(b.median()), float(b.std()))
|
||||
best = min(out, key=lambda k: out[k][2])
|
||||
print(f" {asset}: basis perp-vs-indice per lag (sd = il criterio)")
|
||||
for k, (n, m, s) in out.items():
|
||||
mark = " <- SCELTO (sd minima)" if k == best else ""
|
||||
print(f" lag {k:+d}h n={n:,} mediana {m:+7.2f} bps sd {s:8.2f} bps{mark}")
|
||||
return best
|
||||
|
||||
|
||||
def build_curve(asset: str, lag_h: int) -> pd.DataFrame:
|
||||
closes, exps, vols = load_contracts(asset)
|
||||
fund = load_fund(asset)
|
||||
idx = fund["index"].astype(float)
|
||||
f1h = fund["f1h"].astype(float)
|
||||
wide = pd.DataFrame(closes).sort_index()
|
||||
S = idx.reindex(wide.index + pd.Timedelta(hours=lag_h))
|
||||
S.index = wide.index
|
||||
keep = S.notna()
|
||||
wide, S = wide[keep], S[keep]
|
||||
DTE = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0
|
||||
for nm in wide.columns}, index=wide.index).where(wide.notna())
|
||||
dt_v, px_v, sv = DTE.values, wide.values, S.values
|
||||
n = len(wide)
|
||||
out = np.full((n, 6), np.nan)
|
||||
for i in range(n):
|
||||
m = np.isfinite(dt_v[i]) & np.isfinite(px_v[i]) & (dt_v[i] > 0.5)
|
||||
d, p = dt_v[i][m], px_v[i][m]
|
||||
o = np.argsort(d)
|
||||
d, p = d[o], p[o]
|
||||
k = len(d)
|
||||
out[i, 5] = k
|
||||
if k >= 1:
|
||||
out[i, 0] = d[0]
|
||||
out[i, 1] = np.log(p[0] / sv[i]) * YR / d[0]
|
||||
if k >= 2:
|
||||
out[i, 2] = np.log(p[1] / p[0]) * YR / (d[1] - d[0])
|
||||
if k >= 3:
|
||||
out[i, 3] = np.log(p[2] / p[1]) * YR / (d[2] - d[1])
|
||||
if k >= 4:
|
||||
out[i, 4] = np.log(p[3] / p[2]) * YR / (d[3] - d[2])
|
||||
C = pd.DataFrame(out, index=wide.index,
|
||||
columns=["dte1", "b1", "fwd12", "fwd23", "fwd34", "nlive"])
|
||||
C["slope"] = C["fwd23"] - C["fwd12"]
|
||||
C["curv"] = C["fwd34"] - 2 * C["fwd23"] + C["fwd12"]
|
||||
C["fund24"] = f1h.reindex(C.index).rolling(24).mean() * 24 * YR
|
||||
C["S"] = S
|
||||
return C
|
||||
|
||||
|
||||
def causal_orth(y: pd.Series, x: pd.Series, min_obs: int = 180) -> pd.Series:
|
||||
"""Residuo di y su x con beta stimato SOLO sul passato (media espandente, shift 1).
|
||||
Se il beta usasse tutto il campione sarebbe look-ahead che `causality_ok` non vede."""
|
||||
j = pd.concat({"y": y, "x": x}, axis=1)
|
||||
v = j.notna().all(axis=1)
|
||||
yy = j["y"].where(v)
|
||||
xx = j["x"].where(v)
|
||||
n = v.astype(float).cumsum().shift(1)
|
||||
sx = xx.fillna(0).cumsum().shift(1)
|
||||
sy = yy.fillna(0).cumsum().shift(1)
|
||||
sxx = (xx.fillna(0) ** 2).cumsum().shift(1)
|
||||
sxy = (xx.fillna(0) * yy.fillna(0)).cumsum().shift(1)
|
||||
cov = sxy / n - (sx / n) * (sy / n)
|
||||
var = sxx / n - (sx / n) ** 2
|
||||
beta = (cov / var).where(n >= min_obs)
|
||||
alpha = (sy / n) - beta * (sx / n)
|
||||
return (yy - (alpha + beta * xx)).where(n >= min_obs)
|
||||
|
||||
|
||||
def daily_curve(asset: str, C: pd.DataFrame, hour: int = SIG_HOUR) -> pd.DataFrame:
|
||||
d = C[C.index.hour == hour].copy()
|
||||
d.index = d.index.normalize()
|
||||
d = d[~d.index.duplicated(keep="last")]
|
||||
d["slope_o"] = causal_orth(d["slope"], d["fwd12"])
|
||||
return d
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# SEGNALI E BACKTEST
|
||||
# ===========================================================================
|
||||
def zscore(s: pd.Series, w: int) -> pd.Series:
|
||||
m = s.rolling(w, min_periods=max(30, w // 2)).mean()
|
||||
sd = s.rolling(w, min_periods=max(30, w // 2)).std()
|
||||
return ((s - m) / sd).replace([np.inf, -np.inf], np.nan)
|
||||
|
||||
|
||||
def direction_from(z: pd.Series, sign: int, mp: str) -> pd.Series:
|
||||
if mp == "sgn":
|
||||
d = np.sign(z)
|
||||
else:
|
||||
d = np.clip(z / 2.0, -1.0, 1.0)
|
||||
return (sign * d).fillna(0.0)
|
||||
|
||||
|
||||
def _align_to_bars(df: pd.DataFrame, s: pd.Series, fill: float = 0.0) -> np.ndarray:
|
||||
idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True))
|
||||
return s.reindex(idx).fillna(fill).values.astype(float)
|
||||
|
||||
|
||||
def _ms(idx: pd.DatetimeIndex) -> np.ndarray:
|
||||
"""Epoca in MILLISECONDI, esplicita.
|
||||
⚠️ `idx.view("int64")` NON e' sicuro: i parquet dei datati hanno risoluzione
|
||||
`datetime64[ms]` e `al.get` `[ns]` -> la stessa riga di codice ritorna ms in un
|
||||
posto e ns nell'altro, senza eccezione e senza NaN. E' la famiglia di bug
|
||||
codificata il 01/07 (`.view("int64")` su indici tz-aware non-ns) e ripagata in
|
||||
§35. Qui e' stata ripagata una TERZA volta, da me, e l'ha resa visibile un
|
||||
`n=0` — non un numero sbagliato."""
|
||||
return (pd.DatetimeIndex(idx).tz_convert("UTC").as_unit("ns")
|
||||
.astype("int64") // 10 ** 6)
|
||||
|
||||
|
||||
def expanding_pctl(s: pd.Series, min_obs: int = 180) -> pd.Series:
|
||||
"""Percentile ESPANDENTE causale: quota delle osservazioni STRETTAMENTE
|
||||
precedenti che stanno sotto quella corrente. O(n log n) con bisect."""
|
||||
import bisect
|
||||
vals, seen, out = s.values.astype(float), [], np.full(len(s), np.nan)
|
||||
for i, v in enumerate(vals):
|
||||
if np.isfinite(v):
|
||||
if len(seen) >= min_obs:
|
||||
out[i] = bisect.bisect_right(seen, v) / len(seen)
|
||||
bisect.insort(seen, v)
|
||||
return pd.Series(out, index=s.index)
|
||||
|
||||
|
||||
def cell_daily_A(dcur: dict, var: str, w: int, sign: int, mp: str,
|
||||
fee_side: float = FEE_SIDE) -> pd.Series:
|
||||
"""Serie giornaliera netta 50/50 BTC+ETH del ramo A (stessa convenzione di
|
||||
`altlib.candidate_daily`: eval_weights fa lo shift, non si puo' fare leak)."""
|
||||
ser = {}
|
||||
for a in ASSETS:
|
||||
df = al.get(a, "1d")
|
||||
d = dcur[a]
|
||||
dirn = direction_from(zscore(d[var], w), sign, mp)
|
||||
tgt = al.vol_target(_align_to_bars(df, dirn), df, target_vol=0.20,
|
||||
vol_win_days=30, leverage_cap=2.0)
|
||||
ev = al.eval_weights(df, tgt, fee_side=fee_side)
|
||||
ser[a] = pd.Series(ev["net"], index=ev["idx"])
|
||||
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||
return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
|
||||
|
||||
|
||||
def tp01_targets() -> dict:
|
||||
tp = TrendPortfolio(**CANONICAL)
|
||||
return {a: tp.target_series(al.get(a, "1d")) for a in ASSETS}
|
||||
|
||||
|
||||
def cell_daily_D(dcur: dict, tgts: dict, var: str, pctl: float, dirn: int,
|
||||
g_off: float = 0.0, fee_side: float = FEE_SIDE,
|
||||
t0: pd.Timestamp | None = None) -> pd.Series:
|
||||
"""TP01 moltiplicato per un gate di curva. Il percentile e' ESPANDENTE e causale."""
|
||||
ser = {}
|
||||
for a in ASSETS:
|
||||
df = al.get(a, "1d")
|
||||
d = dcur[a]
|
||||
rk = expanding_pctl(d[var])
|
||||
on = (rk >= pctl) if dirn > 0 else (rk <= pctl)
|
||||
g = pd.Series(np.where(on.fillna(False), 1.0, g_off), index=d.index)
|
||||
g[rk.isna()] = 1.0 # prima del warm-up: gate INATTIVO
|
||||
# ⚠️ fill=1.0, NON 0.0: fuori dalla finestra della curva il gate non esiste,
|
||||
# e riempire a zero spegnerebbe TP01 su meta' della sua storia. E' il difetto
|
||||
# che il controllo d'identita' ha catturato (max|diff| 2,4e-02, non 0).
|
||||
tgt = tgts[a] * _align_to_bars(df, g, fill=1.0)
|
||||
ev = al.eval_weights(df, tgt, fee_side=fee_side)
|
||||
ser[a] = pd.Series(ev["net"], index=ev["idx"])
|
||||
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||
out = al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
|
||||
return out[out.index >= t0] if t0 is not None else out
|
||||
|
||||
|
||||
def beta_baseline(fee_side: float = FEE_SIDE) -> pd.Series:
|
||||
"""LONG SEMPRE, vol-targeted 20% con lo stesso motore: e' il benchmark che il
|
||||
ramo A deve battere. Senza di lui una cella 'quasi sempre lunga' su un campione
|
||||
2019-2026 con due tori sembra un segnale e non lo e'."""
|
||||
ser = {}
|
||||
for a in ASSETS:
|
||||
df = al.get(a, "1d")
|
||||
tgt = al.vol_target(np.ones(len(df)), df, target_vol=0.20,
|
||||
vol_win_days=30, leverage_cap=2.0)
|
||||
ev = al.eval_weights(df, tgt, fee_side=fee_side)
|
||||
ser[a] = pd.Series(ev["net"], index=ev["idx"])
|
||||
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||
return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]])
|
||||
|
||||
|
||||
def mean_pos(dcur: dict, var: str, w: int, sign: int, mp: str) -> float:
|
||||
"""Esposizione media della cella: se e' ~1 la cella e' un LONG travestito."""
|
||||
v = []
|
||||
for a in ASSETS:
|
||||
df = al.get(a, "1d")
|
||||
dirn = direction_from(zscore(dcur[a][var], w), sign, mp)
|
||||
v.append(float(np.mean(_align_to_bars(df, dirn))))
|
||||
return float(np.mean(v))
|
||||
|
||||
|
||||
def sh(s) -> float:
|
||||
return al._sh(s)
|
||||
|
||||
|
||||
def dd(s) -> float:
|
||||
return al._dd_ret(s)
|
||||
|
||||
|
||||
def split(s: pd.Series):
|
||||
return s[s.index < HOLDOUT], s[s.index >= HOLDOUT]
|
||||
|
||||
|
||||
def per_year(s: pd.Series) -> dict:
|
||||
return {int(y): round(sh(g), 2) for y, g in s.groupby(s.index.year) if len(g) > 60}
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
def main() -> None:
|
||||
print(__doc__.split("USO")[0])
|
||||
|
||||
# ---------------------------------------------------------------- §0
|
||||
hr("§0 IL DATO — lag MISURATO (non assunto), curva, certificazione")
|
||||
lags = {}
|
||||
for a in ASSETS:
|
||||
lags[a] = lag_check(a)
|
||||
lag = lags["BTC"]
|
||||
print(f"\n -> lag adottato: +{lag}h su ENTRAMBI gli asset "
|
||||
f"(BTC {lags['BTC']:+d}, ETH {lags['ETH']:+d})")
|
||||
|
||||
curves, dcur = {}, {}
|
||||
for a in ASSETS:
|
||||
C = build_curve(a, lag)
|
||||
curves[a] = C
|
||||
dcur[a] = daily_curve(a, C)
|
||||
nl = C["nlive"].value_counts().sort_index()
|
||||
print(f"\n {a}: {len(C):,} ore {C.index.min().date()} .. {C.index.max().date()}"
|
||||
f" ({(C.index.max()-C.index.min()).days/365.25:.1f} anni)")
|
||||
print(f" contratti vivi/ora: " +
|
||||
" ".join(f"{int(k)}:{v/len(C)*100:.0f}%" for k, v in nl.items()))
|
||||
print(f" giorni con curva (ora {SIG_HOUR:02d}): {len(dcur[a]):,}")
|
||||
|
||||
print("\n CONVERGENZA A SCADENZA |ln(F/indice)| ultime 4h — col lag e senza")
|
||||
for a in ASSETS:
|
||||
closes, exps, _ = load_contracts(a)
|
||||
idx = load_fund(a)["index"].astype(float)
|
||||
for k in (0, lag):
|
||||
v = []
|
||||
for nm, s in closes.items():
|
||||
tail = s[s.index >= exps[nm] - pd.Timedelta(hours=6)]
|
||||
if not len(tail):
|
||||
continue
|
||||
ii = idx.reindex(tail.index + pd.Timedelta(hours=k))
|
||||
x = np.abs(np.log(tail.values / ii.values)) * 1e4
|
||||
x = x[np.isfinite(x)]
|
||||
if len(x):
|
||||
v.append(float(np.median(x)))
|
||||
print(f" {a} lag +{k}h: mediana {np.median(v):5.1f} bps (n={len(v)})")
|
||||
print(" ⚠️ §13 pubblico' 33-47 bps: era calcolato SENZA il lag (merge_asof 'nearest').")
|
||||
print(" Correzione a un numero pubblicato: il dato e' migliore di come e' stato descritto.")
|
||||
|
||||
# ---------------------------------------------------------------- §1
|
||||
hr("§1 LA FORMA DELLA CURVA — livello, pendenza, curvatura (mai guardate prima)")
|
||||
print(" Tutte le grandezze sono carry ANNUALIZZATI in log; fwdXY = forward carry")
|
||||
print(" su finestra ~91g FISSA (niente denominatore che si annulla).\n")
|
||||
for a in ASSETS:
|
||||
C = curves[a]
|
||||
print(f" --- {a} ---")
|
||||
print(" var n mediana p05 p95 sd")
|
||||
for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "curv", "fund24"):
|
||||
s = C[c].dropna()
|
||||
print(f" {c:9s} {len(s):7,} {s.median()*100:8.2f}% {s.quantile(.05)*100:9.2f}% "
|
||||
f"{s.quantile(.95)*100:9.2f}% {s.std()*100:8.2f}%")
|
||||
|
||||
# ---------------------------------------------------------------- §2
|
||||
hr("§2 PRIOR A1/A2 — la curva e' funding travestito? (si misura PRIMA di backtestare)")
|
||||
print(" Attesa dichiarata: LIVELLO si (|corr|>0,5), FORMA no.\n")
|
||||
corr_tab = {}
|
||||
for a in ASSETS:
|
||||
d = dcur[a]
|
||||
print(f" --- {a} (giornaliero, n={len(d):,}) ---")
|
||||
for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "slope_o", "curv"):
|
||||
j = d[[c, "fund24"]].dropna()
|
||||
r = float(j[c].corr(j["fund24"]))
|
||||
corr_tab[(a, c)] = r
|
||||
tag = " <- LIVELLO: funding travestito" if abs(r) > 0.5 else ""
|
||||
print(f" corr({c:8s}, funding24h) = {r:+.3f} n={len(j):,}{tag}")
|
||||
j = d[["slope", "fwd12"]].dropna()
|
||||
print(f" [contorno] corr(slope, livello) = {j['slope'].corr(j['fwd12']):+.3f}"
|
||||
f" -> la pendenza GREZZA e' in gran parte il livello col segno rovesciato")
|
||||
print("\n CONTROLLO POSITIVO del rilevatore di correlazione (un rilevatore che non trova")
|
||||
print(" nulla e' indistinguibile da uno rotto): rumore bianco vs funding, e funding vs se stesso")
|
||||
rng = np.random.default_rng(20260823)
|
||||
d = dcur["BTC"]
|
||||
noise = pd.Series(rng.normal(size=len(d)), index=d.index)
|
||||
j = pd.concat({"n": noise, "f": d["fund24"]}, axis=1).dropna()
|
||||
print(f" corr(rumore, funding) = {j['n'].corr(j['f']):+.3f} (atteso ~0)")
|
||||
print(f" corr(funding, funding) = {1.0:+.3f} (atteso +1)")
|
||||
print("\n 📌 ESITO: A1 CONFERMATA (b1 +0,6 su entrambi), A2 CONFERMATA — e in modo piu'")
|
||||
print(" forte dell'atteso: la pendenza ORTOGONALIZZATA al livello e la curvatura")
|
||||
print(" sono praticamente scorrelate dal funding. **La forma e' un grado di")
|
||||
print(" liberta' nuovo: il ramo non e' chiuso in partenza.**")
|
||||
|
||||
# ---------------------------------------------------------------- §3
|
||||
hr("§3 PRE-TEST DI REGRESSIONE — e il suo CONTROLLO POSITIVO, che FALLISCE")
|
||||
print(" Regressione dei rendimenti spot futuri sulla variabile di curva (z-unit).")
|
||||
print(" t corretto grossolanamente per sovrapposizione: t_naive/sqrt(h).\n")
|
||||
reg_rows = []
|
||||
for a in ASSETS:
|
||||
d = dcur[a]
|
||||
lp = np.log(d["S"])
|
||||
print(f" --- {a} ---")
|
||||
print(" var h=5g h=21g h=63g")
|
||||
for v in list(VARS_A) + ["mom90(CTRL+)"]:
|
||||
if v.startswith("mom90"):
|
||||
x0 = lp - lp.shift(90)
|
||||
else:
|
||||
x0 = d[v]
|
||||
row = f" {v:11s}"
|
||||
for h in (5, 21, 63):
|
||||
fw = lp.shift(-h) - lp
|
||||
jj = pd.concat([x0, fw], axis=1).dropna()
|
||||
jj.columns = ["x", "y"]
|
||||
if len(jj) < 100 or jj["x"].std() == 0:
|
||||
row += " n/a "
|
||||
continue
|
||||
xz = (jj["x"] - jj["x"].mean()) / jj["x"].std()
|
||||
sl, ic, rr, pv, se = stats.linregress(xz, jj["y"])
|
||||
t = sl / se / np.sqrt(h)
|
||||
row += f" {sl*100:+6.2f}%(t{t:+5.2f})"
|
||||
if not v.startswith("mom90"):
|
||||
reg_rows.append(dict(asset=a, var=v, h=h, beta=sl, t=t))
|
||||
print(row)
|
||||
print("\n 🚨 IL CONTROLLO POSITIVO NON PASSA: il momentum a 90 giorni — il segnale che")
|
||||
print(" REGGE il 75% del libro live (TP01, Sharpe FULL 1,31 = t~3,6 su 7,4 anni) —")
|
||||
print(" in questa lente da |t| 0,4-1,4. **La regressione predittiva univariata su")
|
||||
print(" rendimenti crypto NON HA POTENZA a questo campione**: un t basso qui non e'")
|
||||
print(" evidenza di assenza. -> il ramo si giudica sui BACKTEST (§4-§5), non su questi t.")
|
||||
print(" (Le regressioni restano a registro: 5 variabili x 3 orizzonti x 2 asset.)")
|
||||
|
||||
# ---------------------------------------------------------------- §4
|
||||
hr("§4 RAMO A — la curva come segnale DIREZIONALE (60 celle dichiarate)")
|
||||
B = al.tp01_baseline_daily()
|
||||
print(f" ✅ REPLICA prima di produrre numeri nuovi: TP01 baseline 50/50")
|
||||
print(f" Sharpe FULL {sh(B):.3f} (pubblicato ~1,30) HOLD {sh(B[B.index>=HOLDOUT]):.3f}")
|
||||
print(f" ⚠️ l'hold-out pubblicato e' ~0,31-0,44 a seconda del VINTAGE di data/raw/")
|
||||
print(f" (gitignored, riscritto ogni notte): la terza cifra non e' confrontabile.\n")
|
||||
|
||||
BB = beta_baseline()
|
||||
bi, bh = split(BB)
|
||||
print(f" BENCHMARK BETA (long sempre, vol-target 20%, stesso motore):")
|
||||
print(f" Sharpe IS {sh(bi):+.3f} FULL {sh(BB):+.3f} HOLD {sh(bh):+.3f}"
|
||||
f" maxDD {dd(BB)*100:.1f}%")
|
||||
print(" ⚠️ e' IL benchmark del ramo A: su 2019-2026 (due tori) una cella che sta")
|
||||
print(" long quasi sempre eredita questo Sharpe senza contenere informazione.\n")
|
||||
|
||||
rowsA = []
|
||||
for var in VARS_A:
|
||||
for w in ZWINS:
|
||||
for sg in SIGNS:
|
||||
for mp in MAPS:
|
||||
s = cell_daily_A(dcur, var, w, sg, mp)
|
||||
ins, hol = split(s)
|
||||
rowsA.append(dict(var=var, w=w, sign=sg, map=mp, daily=s,
|
||||
is_sh=sh(ins) if len(ins) > 60 else np.nan,
|
||||
full=sh(s), hold=sh(hol) if len(hol) > 60 else np.nan,
|
||||
dd=dd(s)))
|
||||
assert len(rowsA) == 60, len(rowsA)
|
||||
print(f" celle valutate: {len(rowsA)} (dichiarate 60)")
|
||||
validA = [r for r in rowsA if np.isfinite(r["is_sh"])]
|
||||
chosenA = max(validA, key=lambda r: r["is_sh"])
|
||||
print(f" distribuzione dei 60 Sharpe FULL: mediana {np.median([r['full'] for r in rowsA]):+.3f}"
|
||||
f" min {min(r['full'] for r in rowsA):+.3f} max {max(r['full'] for r in rowsA):+.3f}")
|
||||
print(f" frazione di celle con FULL>0: {np.mean([r['full']>0 for r in rowsA]):.0%}"
|
||||
f" con HOLD>0: {np.mean([r['hold']>0 for r in rowsA]):.0%}")
|
||||
print("\n CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out):")
|
||||
print(f" {chosenA['var']} w={chosenA['w']} sign={chosenA['sign']:+d} map={chosenA['map']}"
|
||||
f" Sh IS {chosenA['is_sh']:+.3f} FULL {chosenA['full']:+.3f}"
|
||||
f" HOLD {chosenA['hold']:+.3f} maxDD {chosenA['dd']*100:.1f}%")
|
||||
best_hold = max(validA, key=lambda r: r["hold"])
|
||||
print(f" (per contrasto, la cella a MASSIMO hold-out — che NON e' selezionabile —:")
|
||||
print(f" {best_hold['var']} w={best_hold['w']} sign={best_hold['sign']:+d} "
|
||||
f"map={best_hold['map']} IS {best_hold['is_sh']:+.3f} HOLD {best_hold['hold']:+.3f})")
|
||||
print("\n top-8 per Sharpe IN-SAMPLE (pos = esposizione media: ~1 => LONG travestito)")
|
||||
print(" var w sgn map ShIS ShFULL ShHOLD maxDD pos")
|
||||
for r in sorted(validA, key=lambda r: r["is_sh"], reverse=True)[:8]:
|
||||
mp_ = mean_pos(dcur, r["var"], r["w"], r["sign"], r["map"])
|
||||
print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+2d} {r['map']:4s}"
|
||||
f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}%"
|
||||
f" {mp_:+6.2f}")
|
||||
print(f" 🚨 NESSUNA delle 60 celle batte il benchmark beta ({sh(BB):+.3f} FULL): la")
|
||||
print(f" migliore in-sample fa {chosenA['full']:+.3f}, la migliore in assoluto "
|
||||
f"{max(r['full'] for r in rowsA):+.3f}.")
|
||||
|
||||
allA = [r["full"] for r in rowsA]
|
||||
dsrA, sr0A = al.deflated_sharpe(chosenA["full"], allA, chosenA["daily"])
|
||||
print(f"\n deflated-Sharpe (ramo A, N=60): {dsrA:.3f} "
|
||||
f"(massimo atteso dal PURO RUMORE {sr0A:+.3f}) "
|
||||
f"{'PASS' if dsrA >= 0.95 else 'FAIL'}")
|
||||
|
||||
mgA = al.marginal_vs_tp01(chosenA["daily"])
|
||||
print(f" marginal_vs_tp01: {mgA['marginal_verdict']} corr {mgA['corr_full']:+.3f} "
|
||||
f"beta {mgA['beta_to_tp01']:+.3f} alpha {mgA['alpha_ann']*100:+.2f}%/anno")
|
||||
print(f" uplift w25 FULL {mgA['blends']['w25']['uplift_full']:+.3f} "
|
||||
f"HOLD {mgA['blends']['w25']['uplift_hold']:+.3f} "
|
||||
f"robust_oos {mgA['robust_oos']} has_insample_edge {mgA['has_insample_edge']} "
|
||||
f"is_hedge {mgA['is_hedge']}")
|
||||
earnsA = bool(mgA["marginal_verdict"] == "ADDS" and dsrA >= 0.95)
|
||||
print(f" -> earns_slot_honest (ramo A) = {earnsA}")
|
||||
|
||||
print("\n PER ANNO della cella scelta (A4: un aggregato si scompone prima di crederci):")
|
||||
py = per_year(chosenA["daily"])
|
||||
print(" " + " ".join(f"{y}:{v:+.2f}" for y, v in py.items()))
|
||||
pos = sum(1 for v in py.values() if v > 0)
|
||||
print(f" anni positivi {pos}/{len(py)} 2022 (l'unico stress del campione): "
|
||||
f"{py.get(2022, float('nan')):+.2f}")
|
||||
|
||||
print("\n PLACEBO DEL DENTE DI SEGA (il rischio strutturale di questo dataset):")
|
||||
print(" un segnale che e' funzione SOLA di dte1 (giorni alla prima scadenza) non")
|
||||
print(" contiene informazione di curva. Se battesse la cella scelta, il ramo A")
|
||||
print(" starebbe misurando il calendario dei roll, non la forma della curva.")
|
||||
dsaw = {a: dcur[a].assign(**{"__saw": dcur[a]["dte1"]}) for a in ASSETS}
|
||||
for w in ZWINS:
|
||||
s = cell_daily_A(dsaw, "__saw", w, +1, "lin")
|
||||
s2 = cell_daily_A(dsaw, "__saw", w, -1, "lin")
|
||||
print(f" dte1 w={w:3d}: Sh FULL {sh(s):+.3f} / {sh(s2):+.3f} (segno +/-)"
|
||||
f" pos {mean_pos(dsaw, '__saw', w, +1, 'lin'):+.2f}")
|
||||
print(" 🚨 IL PLACEBO BATTE OGNI CELLA DI CURVA — e la diagnosi e' il contrario di")
|
||||
print(" quella che sembra. I due segni sono SPECCHI quasi esatti e l'esposizione")
|
||||
print(" media e' ~+1: il placebo non sta misurando il dente di sega, sta stando")
|
||||
print(f" LONG, e riproduce il benchmark beta ({sh(BB):+.3f}). Cio' che dimostra e'")
|
||||
print(" piu' severo: **nessun segnale di curva batte 'stai lungo e basta'** su")
|
||||
print(" questo campione. Un placebo va letto sulla sua ESPOSIZIONE prima che sul")
|
||||
print(" suo Sharpe, o si conclude l'opposto.")
|
||||
|
||||
# ---------------------------------------------------------------- §5
|
||||
hr("§5 RAMO D — la curva come GATE DI REGIME su TP01 (18 celle dichiarate)")
|
||||
tgts = tp01_targets()
|
||||
T0 = max(dcur[a].index.min() for a in ASSETS)
|
||||
base_gate = cell_daily_D(dcur, tgts, "fwd12", 0.0, +1) # gate sempre acceso
|
||||
print(f" ✅ CONTROLLO DI IDENTITA': gate sempre acceso vs TP01 baseline sulla finestra comune")
|
||||
j = pd.concat({"g": base_gate, "b": B}, axis=1, join="inner").dropna()
|
||||
print(f" max|diff| = {float((j['g']-j['b']).abs().max()):.2e} "
|
||||
f"Sh {sh(j['g']):.4f} vs {sh(j['b']):.4f}")
|
||||
ref = base_gate[base_gate.index >= T0]
|
||||
rowsD = []
|
||||
for var in VARS_D:
|
||||
for p in PCTLS:
|
||||
for dr in DIRS:
|
||||
s = cell_daily_D(dcur, tgts, var, p, dr, t0=T0)
|
||||
ins, hol = split(s)
|
||||
rowsD.append(dict(var=var, p=p, dir=dr, daily=s,
|
||||
is_sh=sh(ins), full=sh(s), hold=sh(hol), dd=dd(s)))
|
||||
assert len(rowsD) == 18, len(rowsD)
|
||||
chosenD = max(rowsD, key=lambda r: r["is_sh"])
|
||||
ri, rh = split(ref)
|
||||
print(f"\n riferimento (TP01 sulla finestra della curva): IS {sh(ri):+.3f} "
|
||||
f"FULL {sh(ref):+.3f} HOLD {sh(rh):+.3f} maxDD {dd(ref)*100:.1f}%")
|
||||
print(" celle (18): var pctl dir ShIS ShFULL ShHOLD maxDD dShFULL")
|
||||
for r in sorted(rowsD, key=lambda r: r["is_sh"], reverse=True):
|
||||
mark = " <- scelta IS" if r is chosenD else ""
|
||||
print(f" {r['var']:9s} {r['p']:.2f} {r['dir']:+d} {r['is_sh']:+7.3f}"
|
||||
f" {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}% "
|
||||
f"{r['full']-sh(ref):+8.3f}{mark}")
|
||||
allD = [r["full"] for r in rowsD]
|
||||
dsrD, sr0D = al.deflated_sharpe(chosenD["full"], allD, chosenD["daily"])
|
||||
print(f"\n deflated-Sharpe (ramo D, N=18): {dsrD:.3f} (rumore max atteso {sr0D:+.3f}) "
|
||||
f"{'PASS' if dsrD >= 0.95 else 'FAIL'}")
|
||||
|
||||
print("\n 🚨 NULL DEL DE-LEVERING (7 occorrenze pagate dal progetto): un gate SPEGNE")
|
||||
print(" esposizione, quindi va confrontato con un semplice k*TP01 a PARI maxDD.")
|
||||
tgt_dd = chosenD["dd"]
|
||||
lo, hi = 0.05, 1.0
|
||||
for _ in range(40):
|
||||
k = (lo + hi) / 2
|
||||
if dd(k * ref) > tgt_dd:
|
||||
hi = k
|
||||
else:
|
||||
lo = k
|
||||
k = (lo + hi) / 2
|
||||
print(f" gate scelto: maxDD {tgt_dd*100:.2f}% ShFULL {chosenD['full']:+.3f} "
|
||||
f"ShHOLD {chosenD['hold']:+.3f}")
|
||||
print(f" k*TP01 a pari maxDD (k={k:.3f}): maxDD {dd(k*ref)*100:.2f}% "
|
||||
f"ShFULL {sh(k*ref):+.3f} ShHOLD {sh((k*ref)[(k*ref).index>=HOLDOUT]):+.3f}")
|
||||
beats = chosenD["full"] > sh(k * ref) + 1e-9
|
||||
print(f" -> il gate batte il de-levering puro? {beats}")
|
||||
print(" (nota: lo Sharpe e' invariante alla scala, quindi il de-levering NON puo'")
|
||||
print(" cambiare lo Sharpe: il confronto giusto e' 'a pari DD lo Sharpe migliora?'")
|
||||
print(" -> qui la domanda si riduce a dShFULL > 0, e il null e' DEGENERE (§24 (d)).")
|
||||
print(" Il fatto che conta e' che il gate cambia lo Sharpe, non che riduca il DD.")
|
||||
print("\n PER ANNO della cella D scelta, contro TP01:")
|
||||
pyD, pyR = per_year(chosenD["daily"]), per_year(ref)
|
||||
for y in sorted(set(pyD) | set(pyR)):
|
||||
print(f" {y}: gate {pyD.get(y, float('nan')):+6.2f} TP01 {pyR.get(y, float('nan')):+6.2f}"
|
||||
f" d {pyD.get(y,np.nan)-pyR.get(y,np.nan):+6.2f}")
|
||||
|
||||
# ---------------------------------------------------------------- §6
|
||||
hr("§6 SENSIBILITA' ALL'ORA DI LETTURA (robustezza alla SCELTA, non un IC — §57)")
|
||||
print(" 24 ore di lettura della curva, stessa griglia giornaliera 00:00. Tutte causali.")
|
||||
bandA, bandD = [], []
|
||||
for h in range(24):
|
||||
dc = {a: daily_curve(a, curves[a], hour=h) for a in ASSETS}
|
||||
bandA.append(sh(cell_daily_A(dc, chosenA["var"], chosenA["w"],
|
||||
chosenA["sign"], chosenA["map"])))
|
||||
bandD.append(sh(cell_daily_D(dc, tgts, chosenD["var"], chosenD["p"],
|
||||
chosenD["dir"], t0=T0)))
|
||||
for nm, band, canon in (("A", bandA, chosenA["full"]), ("D", bandD, chosenD["full"])):
|
||||
b = np.array(band)
|
||||
print(f" ramo {nm}: canonica(h={SIG_HOUR}) {canon:+.3f} mediana {np.median(b):+.3f}"
|
||||
f" p10 {np.percentile(b,10):+.3f} p90 {np.percentile(b,90):+.3f}"
|
||||
f" pctl della canonica {float((b<=canon).mean())*100:.0f}°")
|
||||
bD = np.array(bandD) - sh(ref)
|
||||
print(f" ramo D, dShFULL vs TP01 su 24 ore: mediana {np.median(bD):+.3f}, "
|
||||
f"positivo in {int((bD>0).sum())}/24 (⚠️ ~2 osservazioni indipendenti, §57)")
|
||||
|
||||
# ---------------------------------------------------------------- §7
|
||||
hr("§7 RAMO B — la BUTTERFLY delta-neutra (8 celle) e il muro dell'MDE")
|
||||
print(" F1 - 2*F2 + F3 a nozionale netto ZERO: e' esattamente la CURVATURA, quotata.")
|
||||
print(" §13 aveva testato il calendar spread (F1 vs F2, 108 celle); la butterfly e'")
|
||||
print(" il secondo ordine e non era mai stata guardata.\n")
|
||||
fly = {}
|
||||
for a in ASSETS:
|
||||
C = curves[a]
|
||||
closes, exps, _ = load_contracts(a)
|
||||
wide = pd.DataFrame(closes).sort_index().reindex(C.index)
|
||||
DTEa = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0
|
||||
for nm in wide.columns}, index=wide.index).where(wide.notna())
|
||||
lr = np.log(wide).diff()
|
||||
dv, lv = DTEa.values, lr.values
|
||||
pv = wide.values
|
||||
n = len(wide)
|
||||
out = np.full(n, np.nan)
|
||||
turn = np.zeros(n)
|
||||
prev = None
|
||||
for i in range(n):
|
||||
m = np.isfinite(dv[i]) & np.isfinite(pv[i]) & (dv[i] > 0.5)
|
||||
ordr = np.argsort(np.where(m, dv[i], np.inf))
|
||||
sel = ordr[:3]
|
||||
if not np.all(m[sel]) or len(sel) < 3:
|
||||
prev = None
|
||||
continue
|
||||
wts = np.array([1.0, -2.0, 1.0])
|
||||
if prev is not None and np.array_equal(prev, sel):
|
||||
r = np.nansum(wts * lv[i][sel])
|
||||
out[i] = r
|
||||
else:
|
||||
turn[i] = 4.0 # 4 gambe di nozionale unitario cambiate (1+2+1)
|
||||
prev = sel
|
||||
fly[a] = pd.Series(out, index=wide.index), pd.Series(turn, index=wide.index)
|
||||
print(" rendimento LORDO della butterfly (nozionale lordo 4x, hold continuo):")
|
||||
for a in ASSETS:
|
||||
r, t = fly[a]
|
||||
rd = r.resample("1D").sum().dropna()
|
||||
yrs = (rd.index[-1] - rd.index[0]).days / YR
|
||||
mu = float(rd.mean() * YR)
|
||||
sd = float(rd.std() * np.sqrt(YR))
|
||||
se = sd / np.sqrt(yrs)
|
||||
n_roll = int((t > 0).sum())
|
||||
print(f" {a}: drift {mu*100:+6.2f}%/anno vol {sd*100:5.2f}% Sharpe {mu/sd:+.2f}"
|
||||
f" SE(drift) {se*100:.2f}pp roll {n_roll} in {yrs:.1f} anni")
|
||||
print(f" 🚨 MDE: serve |drift| > {1.96*se*100:.2f}%/anno per distinguerlo da zero.")
|
||||
rowsB = []
|
||||
for var in VARS_B:
|
||||
for w in ZWINS_B:
|
||||
for sg in SIGNS_B:
|
||||
ser, ser0 = {}, {}
|
||||
for a in ASSETS:
|
||||
r, t = fly[a]
|
||||
d = dcur[a]
|
||||
z = zscore(d[var], w)
|
||||
pos = (sg * np.clip(z / 2, -1, 1)).fillna(0.0)
|
||||
rd = r.resample("1D").sum()
|
||||
td = t.resample("1D").sum()
|
||||
idx = rd.index.intersection(pos.index)
|
||||
p = pos.reindex(idx).shift(1).fillna(0.0)
|
||||
gross = p * rd.reindex(idx)
|
||||
cost = (p.abs() * td.reindex(idx).fillna(0.0) +
|
||||
4.0 * p.diff().abs().fillna(0.0))
|
||||
ser[a] = gross - FEE_SIDE * cost
|
||||
ser0[a] = gross
|
||||
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||
J0 = pd.concat(ser0, axis=1, join="inner").fillna(0.0)
|
||||
s = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]
|
||||
s0 = 0.5 * J0[ASSETS[0]] + 0.5 * J0[ASSETS[1]]
|
||||
ins, hol = split(s)
|
||||
rowsB.append(dict(var=var, w=w, sign=sg, daily=s, is_sh=sh(ins),
|
||||
full=sh(s), hold=sh(hol), gross=sh(s0)))
|
||||
assert len(rowsB) == 8, len(rowsB)
|
||||
chosenB = max(rowsB, key=lambda r: r["is_sh"])
|
||||
print("\n celle (8): var w sgn Sh LORDO ShIS ShFULL ShHOLD")
|
||||
for r in sorted(rowsB, key=lambda r: r["is_sh"], reverse=True):
|
||||
mark = " <- scelta IS" if r is chosenB else ""
|
||||
print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+d} {r['gross']:+9.3f}"
|
||||
f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f}{mark}")
|
||||
gmax = max(r["gross"] for r in rowsB)
|
||||
print(f"\n 📌 il MIGLIOR lordo della famiglia e' {gmax:+.3f}: sotto l'MDE (Sharpe 0,72)")
|
||||
print(" ANCHE a fee zero. Quindi NON e' morte-per-fee — la fee la seppellisce, ma")
|
||||
print(" sotto non c'e' un edge lordo da salvare. (Distinzione che il progetto fa")
|
||||
print(" sistematicamente: §42 TP01-LS, §7 CRT.)")
|
||||
|
||||
# ---------------------------------------------------------------- §8
|
||||
hr("§8 RAMO C — la curva predice la VOLATILITA' futura? (5 regressioni)")
|
||||
print(" RV30 futura su curva, CONTROLLANDO RV30 corrente e DVOL (che e' la previsione")
|
||||
print(" di mercato). Senza i controlli si misura solo 'la vol e' persistente'.\n")
|
||||
for a in ASSETS:
|
||||
d = dcur[a]
|
||||
lp = np.log(d["S"])
|
||||
r = lp.diff()
|
||||
rv = r.rolling(30).std() * np.sqrt(YR)
|
||||
fut = rv.shift(-30)
|
||||
ts = pd.DataFrame({"timestamp": _ms(d.index)})
|
||||
dv = pd.Series(al.dvol(ts, a), index=d.index) / 100.0
|
||||
assert dv.notna().sum() > 500 and 0.2 < float(dv.median()) < 1.5, (
|
||||
"controllo positivo DVOL fallito: merge vuoto o fuori scala "
|
||||
f"(n={int(dv.notna().sum())}, mediana={float(dv.median()):.3f})")
|
||||
base = pd.concat({"y": fut, "rv": rv, "dv": dv}, axis=1).dropna()
|
||||
print(f" --- {a} (n={len(base):,}, dal {base.index.min().date()}: DVOL parte 2021-03) ---")
|
||||
for v in VARS_A:
|
||||
jj = pd.concat([base, d[v].rename("x")], axis=1).dropna()
|
||||
if len(jj) < 200:
|
||||
print(f" {v:9s} n troppo piccolo")
|
||||
continue
|
||||
X = np.column_stack([np.ones(len(jj)), jj["rv"], jj["dv"],
|
||||
(jj["x"] - jj["x"].mean()) / jj["x"].std()])
|
||||
bb, *_ = np.linalg.lstsq(X, jj["y"].values, rcond=None)
|
||||
res = jj["y"].values - X @ bb
|
||||
s2 = res @ res / (len(jj) - 4)
|
||||
se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X)))[3]
|
||||
print(f" {v:9s} beta {bb[3]*100:+6.2f}pp di vol per 1sd "
|
||||
f"t {bb[3]/se:+6.2f} (naive) t/sqrt(30) {bb[3]/se/np.sqrt(30):+5.2f}")
|
||||
print("\n ⚠️ stesso limite di §3: nessun controllo positivo passa a questa risoluzione,")
|
||||
print(" quindi queste righe sono descrittive, non decisive.")
|
||||
|
||||
# ---------------------------------------------------------------- §9
|
||||
hr("§9 DEFLATED-SHARPE SULL'UNIONE, e la sua dipendenza dalla PARTIZIONE (§10/§24)")
|
||||
allBT = allA + allD + [r["full"] for r in rowsB]
|
||||
print(f" backtest dichiarati: {N_DECLARED_BT}, valutati: {len(allBT)} "
|
||||
f"(+{N_DECLARED_REG} regressioni ramo C)")
|
||||
for nm, pool, ch in (("ramo A (60)", allA, chosenA), ("ramo D (18)", allD, chosenD),
|
||||
("ramo B (8)", [r["full"] for r in rowsB], chosenB),
|
||||
("UNIONE (86)", allBT, chosenA)):
|
||||
dsr, sr0 = al.deflated_sharpe(ch["full"], pool, ch["daily"])
|
||||
print(f" {nm:12s} N={len(pool):3d} sd(Sharpe di griglia) {np.std(pool, ddof=1):.3f}"
|
||||
f" max atteso dal rumore {sr0:+.3f} DSR {dsr:.3f} "
|
||||
f"{'PASS' if dsr >= 0.95 else 'FAIL'}")
|
||||
print(" 📌 La stessa griglia da' verdetti diversi a seconda di come la si partiziona:")
|
||||
print(" e' il secondo posto dove barare e' indolore e invisibile (dopo il conteggio).")
|
||||
|
||||
# ---------------------------------------------------------------- §10
|
||||
hr("§10 ESEGUIBILITA' — il roll trimestrale, e il muro che nessun backtest supera")
|
||||
lb = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/basis_cache/live_books.parquet")
|
||||
if lb.exists():
|
||||
d = pd.read_parquet(lb)
|
||||
print(" spread bid-ask VIVI (fotografia del 22/08, non storia — va detto):")
|
||||
for _, r in d.iterrows():
|
||||
print(f" {r['instrument']:16s} spread {r['spread_bps']:6.2f} bps"
|
||||
f" mezzo-spread {r['spread_bps']/2:5.2f} bps")
|
||||
q = d[d.instrument.str.contains("-2")]
|
||||
hs = float(q["spread_bps"].median()) / 2
|
||||
else:
|
||||
hs = 3.0
|
||||
print(" (cache dei book non presente: mezzo-spread assunto 3,0 bps)")
|
||||
print(f"\n costo per gamba = fee taker 3,50 bps + mezzo-spread mediano {hs:.2f} bps"
|
||||
f" = {3.5+hs:.2f} bps")
|
||||
print(f" un ROLL trimestrale = 2 gambe (chiudi F1, apri F2) = {2*(3.5+hs):.1f} bps,")
|
||||
print(f" 4 roll/anno = {4*2*(3.5+hs):.0f} bps/anno = {4*2*(3.5+hs)/100:.2f}%/anno di drag")
|
||||
print(" -> contro un premio a termine misurato da §13 a +0,85%/anno con IC95 che")
|
||||
print(" CONTIENE LO ZERO. Il costo del roll da solo e' dello stesso ordine.")
|
||||
print(f" ⚠️ A5: una butterfly ribilanciata ogni giorno paga 4 gambe per ribilancio.")
|
||||
print(" Il ramo B sopra e' gia' al NETTO di questo, ed e' il motivo per cui il")
|
||||
print(" suo lordo e il suo netto non si somigliano.")
|
||||
|
||||
# ---------------------------------------------------------------- §11
|
||||
hr("§11 VERDETTO")
|
||||
print(f" A1 (livello = funding travestito) -> CONFERMATA "
|
||||
f"(corr b1/funding {corr_tab[('BTC','b1')]:+.2f} BTC, {corr_tab[('ETH','b1')]:+.2f} ETH)")
|
||||
print(f" A2 (la FORMA e' un grado di liberta' nuovo) -> CONFERMATA e piu' forte "
|
||||
f"dell'atteso (slope_o {corr_tab[('BTC','slope_o')]:+.2f}/"
|
||||
f"{corr_tab[('ETH','slope_o')]:+.2f}, curv {corr_tab[('BTC','curv')]:+.2f}/"
|
||||
f"{corr_tab[('ETH','curv')]:+.2f})")
|
||||
print(f" A3 (nessun candidato) -> "
|
||||
f"{'CONFERMATA' if not earnsA else 'REFUTATA'}")
|
||||
print(f" A4 (2022 domina) -> vedi la scomposizione per anno")
|
||||
print(f" A5 (il roll costa) -> {4*2*(3.5+hs)/100:.2f}%/anno, "
|
||||
f"stesso ordine del premio")
|
||||
print(f" A6 (MDE) -> ramo B non rispondibile, "
|
||||
f"rami A/D si'")
|
||||
print()
|
||||
print(f" RAMO A direzionale : earns_slot_honest = {earnsA} "
|
||||
f"(DSR {dsrA:.3f}, marginale {mgA['marginal_verdict']})")
|
||||
print(f" RAMO D gate TP01 : dShFULL mediano su 24 ore {np.median(bD):+.3f}, "
|
||||
f"DSR {dsrD:.3f}")
|
||||
print(f" RAMO B butterfly : sotto l'MDE per costruzione")
|
||||
print(f" RAMO C volatilita' : nessun controllo positivo passa -> non decisivo")
|
||||
print()
|
||||
isp = al.implausible_sharpe(chosenA["daily"])
|
||||
print(f" implausible_sharpe sulla cella A scelta: implausible={isp['implausible']}"
|
||||
f" motivi={isp['reasons'] or 'nessuno'} barre attive {isp['active_frac']:.0%}"
|
||||
f" barre attive in perdita {isp['loss_frac']:.0%}")
|
||||
print()
|
||||
verdetto = ("LA FORMA DELLA CURVA E' UN GRADO DI LIBERTA' NUOVO (scorrelato dal funding) "
|
||||
"E NON PREDICE NULLA: 0 candidati su 4 rami")
|
||||
print(f" `{verdetto}`")
|
||||
print()
|
||||
print(" In EUR/giorno: questo filone NON avvicina i 50 EUR/giorno. Il miglior effetto")
|
||||
print(" misurato (ramo D, dShFULL mediano di banda) e' dentro il rumore d'ancora del")
|
||||
print(" libro stesso; a $635 di conto vale **0,00 EUR/giorno**. Versare 500 EUR/mese")
|
||||
print(" resta la leva dominante misurata dal progetto.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,890 @@
|
||||
"""r0823b_fisco_payout — §63 FISCO-PAYOUT: l'aliquota sui payout funded e' un FORFAIT mai
|
||||
giustificato, e il canale funded non e' mai stato misurato contro di essa (2026-08-23).
|
||||
|
||||
IL BUCO CHE CHIUDE
|
||||
------------------
|
||||
Il progetto ha modellato con cura il fisco sull'accumulo del LIBRO PROPRIO (33% sulle plusvalenze
|
||||
cripto, minusvalenze in carry 4 anni, 2 per mille patrimoniale) e il 07/08 ha misurato che
|
||||
ignorarlo costava -30% a 10 anni. Sul canale FUNDED non ha modellato niente: `r0725_prop_ladder.py`
|
||||
importa `TAX_RATE = 0.33` da `r0725_capcurve` e lo applica ai payout con una riga
|
||||
|
||||
net = gross * (1.0 - TAX_RATE) # r0725_prop_ladder:269
|
||||
|
||||
senza una giustificazione. Ed e' l'aliquota **sbagliata di CATEGORIA**, non solo di livello: su un
|
||||
conto funded il trader **non possiede le posizioni** e **non possiede il conto**; cio' che riceve e'
|
||||
un **compenso contrattuale** (profit share su un contratto di servizi), non una plusvalenza da
|
||||
cripto-attivita' ex art. 67 c.1 lett. c-sexies TUIR. Il 33% e' l'aliquota di una cosa che qui non
|
||||
succede.
|
||||
|
||||
Importa perche' §37 conclude che **il biglietto batte il versare** con EUR 500/mese (mediana
|
||||
$39.820 contro $22.444). Se l'aliquota vera fosse molto piu' alta di 33%, quella conclusione
|
||||
potrebbe ribaltarsi; se fosse piu' bassa (regime forfettario), il canale varrebbe di piu'.
|
||||
|
||||
QUESTO FILONE NON CONCLUDE QUALE SIA L'ALIQUOTA. Il progetto ha gia' una regola: una domanda
|
||||
fiscale si **pone al commercialista**, non si risolve backtestando (07/08). Qui si fa l'unica cosa
|
||||
che uno script puo' fare: **misurare quanto vale la risposta**, cioe' la sensibilita' delle
|
||||
conclusioni del canale funded all'aliquota, e **formulare la domanda in modo che sia rispondibile**.
|
||||
NON e' un parere fiscale.
|
||||
|
||||
LE ATTESE, REGISTRATE PRIMA DI MISURARE
|
||||
---------------------------------------
|
||||
A1. **L'EV del biglietto e' ESATTAMENTE lineare in (1-tau)** a livello di conto singolo: la soglia
|
||||
di prelievo ($100 di quota trader) e' sul LORDO, quindi l'imposta non cambia quali eventi
|
||||
accadono, solo quanto valgono. Previsione forte e falsificabile: misurato == analitico
|
||||
**al bit**, non "entro il rumore MC".
|
||||
A2. **A livello di SCALA DI CONTI la linearita' si ROMPE**, e nel verso sfavorevole: i payout netti
|
||||
tornano in cassa e **ricomprano biglietti**, quindi l'imposta si compone. Mi aspetto che la
|
||||
ricchezza terminale scenda **piu' che proporzionalmente** a (1-tau), e che il divario cresca
|
||||
col deposito mensile (piu' cassa = piu' biglietti = piu' composizione).
|
||||
A3. **L'EV del singolo biglietto resta positivo fino a un'aliquota assurda** (>70%), perche' il
|
||||
prezzo ($579) e' piccolo rispetto a E[payout]. Quindi la gamba (c1) del `GATE PROP-01`
|
||||
**non e' il criterio che morde**: mordera' il confronto con la strada-libro.
|
||||
A4. **La soglia di ribaltamento cade DENTRO la banda plausibile** delle qualificazioni fiscali
|
||||
(che va dal ~21% del forfettario al ~55-60% di IRPEF+addizionali+Gestione Separata) →
|
||||
la domanda al commercialista diventa **BLOCCANTE**. Questa e' l'attesa che conta: se invece
|
||||
la soglia cadesse fuori dalla banda, la domanda resterebbe interessante e non decisiva.
|
||||
A5. **L'aliquota EFFETTIVA di uno scaglione progressivo, al livello di reddito che questo canale
|
||||
produce, e' molto piu' bassa dell'aliquota MARGINALE del suo scaglione.** EUR 50/giorno sono
|
||||
~EUR 18.250/anno: un'aliquota "43%" citata a mente sarebbe la marginale del terzo scaglione,
|
||||
che a quel reddito non si tocca nemmeno. Mi aspetto che il modello progressivo, a reddito
|
||||
preesistente ZERO, dia un'effettiva **sotto** il 33% che il progetto usa oggi — cioe' che il
|
||||
forfait attuale sia **conservativo** proprio nello scenario che sembra il piu' minaccioso.
|
||||
Ma con un reddito preesistente da lavoro dipendente il payout si somma **al margine** e
|
||||
l'effettiva sale: **la stessa qualificazione da' due risposte diverse a seconda di chi la
|
||||
riceve.** Questo, se confermato, e' il risultato trasferibile del filone.
|
||||
|
||||
CONVENZIONI DICHIARATE (ereditate e congelate da §37, nessuna scelta nuova)
|
||||
---------------------------------------------------------------------------
|
||||
* Cella d'ancora **MEDIANA** dei 23 offset x 3 fasi (non la canonica, che sta al 91 pctl), scelta
|
||||
con lo stesso criterio di `r0822d_gate_c` §2 — **ricalcolata qui**, non copiata, e confrontata
|
||||
con quella pubblicata.
|
||||
* Conto funded: **W_A = TP01 38 / SKH01 38 / XS01 25**, XS01 = U13 Binance, finestra W-LONG 2021+,
|
||||
de-luck sul drift x0,89, **funding DENTRO**, lente **ACCOPPIATA**.
|
||||
* Conto proprio: libro **LIVE 75/25** a $654 con min-order $5, funding dentro, fisco sul modello
|
||||
di `r0807_piano_netto` (aliquota sulla variazione ANNUA di valore, carry delle minusvalenze).
|
||||
* Morte-firm 10%/anno, cap $200k/trader, split 80%, soglia di prelievo $100: assunzioni del
|
||||
progetto, dichiarate.
|
||||
* Bootstrap **CONDIVISO** fra tutte le aliquote e fra le due strade -> ogni confronto e' APPAIATO
|
||||
e la differenza fra due aliquote e' molto meglio risolta del livello.
|
||||
|
||||
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_fisco_payout.py`
|
||||
Rete: NESSUNA. Scritture: NESSUNA.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
T0 = time.time()
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
if "--no-net" not in sys.argv:
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata
|
||||
import r0725_prop_ladder as pl # noqa: E402 regole firm
|
||||
import r0822_prop_alloc as PA # noqa: E402 boot_idx, DELUCK
|
||||
import r0822b_prop_recal as PR # noqa: E402 pannelli, obj_rg
|
||||
import r0822b_xs01_oos as X # noqa: E402 U13
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01
|
||||
import r0822d_funding as FD # noqa: E402 funding
|
||||
import r0822d_gate_c as GC # noqa: E402 LA MACCHINERIA DI §37
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
EURUSD = pl.EURUSD
|
||||
SPLIT = pl.SPLIT
|
||||
DELUCK = PA.DELUCK
|
||||
W_A = GC.W_A
|
||||
W_LIVE = GC.W_LIVE
|
||||
OFFSETS = AP.OFFSETS
|
||||
PHASES_SUB = AP.PHASES_SUB
|
||||
N_LAD = GC.N_PATHS_LAD
|
||||
N_TIX = GC.N_PATHS_TIX
|
||||
SEED_LAD = GC.SEED_LAD
|
||||
MIN_PAYOUT = GC.MIN_PAYOUT_TRADER
|
||||
HORIZON_DAYS = GC.HORIZON_DAYS
|
||||
PRICE_100K = 579.0
|
||||
# Cella d'ancora MEDIANA dei 23 offset x 3 fasi: **stampata da `r0822d_gate_c` sullo stesso
|
||||
# vintage di feed** (corsa del 23/08: "offset 30 min, fase 7, leva 0.75x"). E' un INPUT
|
||||
# ereditato e congelato, come tutte le altre convenzioni di §37, non una scelta di questo
|
||||
# filone. Ri-derivarla costa ~70 valutazioni della scala di conti: `--band` la ricalcola e la
|
||||
# confronta con questa costante (fatto una volta; su una macchina scarica sono ~4 minuti).
|
||||
CELL_M = (30, 7, 0.75)
|
||||
BAND = "--band" in sys.argv
|
||||
|
||||
# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi (§37 / §32)
|
||||
PUB = dict(ev_sunk=1615.0, ev_at_pass=1919.0, ev_at_payout=1835.0,
|
||||
p50g_fund=0.026, pzero_fund=0.403,
|
||||
med_big_500=39820.0, med_lib_500=22444.0,
|
||||
med_big_0=132.0, med_lib_0=787.0,
|
||||
mean_big_500=46298.0, mean_lib_500=22694.0,
|
||||
mean_big_0=9592.0, mean_lib_0=803.0)
|
||||
|
||||
|
||||
# =============================================================================== 0. IL FISCO
|
||||
# ATTENZIONE — QUESTA SEZIONE NON E' UN PARERE FISCALE.
|
||||
# Ogni riga qui sotto e' una IPOTESI DI QUALIFICAZIONE con la sua fonte e il suo grado di
|
||||
# affidabilita'. Il progetto ha gia' pagato una citazione normativa sbagliata cablata in 5 file
|
||||
# (la "L.199/2025" che non era l'origine del 33%, corretta il 07/08): percio' cio' che NON e'
|
||||
# stato verificato su fonte primaria e' etichettato come tale, e non c'e' un default nascosto.
|
||||
#
|
||||
# IL FATTO STRUTTURALE, che vale piu' di qualunque aliquota:
|
||||
# su un conto funded il trader NON possiede il conto, NON possiede le posizioni, e non c'e'
|
||||
# nessun "realizzo di cripto-attivita'". Riceve una quota di un risultato SIMULATO in base a un
|
||||
# contratto. Percio' l'art. 67 c.1 lett. c-sexies (33%, cripto) e la lett. c-quater (26%,
|
||||
# derivati) — le due caselle su cui il progetto sta discutendo per il LIBRO PROPRIO — sono
|
||||
# entrambe **fuori categoria** per il canale funded. La discussione vera e' fra reddito
|
||||
# diverso da "obbligazione di fare" (art. 67 c.1 lett. l), lavoro autonomo (abituale o
|
||||
# occasionale) e reddito d'impresa. Cambia la CATEGORIA, non solo il numero.
|
||||
|
||||
# IRPEF 2026 — VERIFICATA in sessione (WebSearch 23/08): tre scaglioni 23% / 33% / 43%, con il
|
||||
# SECONDO sceso da 35% a 33% per effetto della **L. 199/2025** (Legge di Bilancio 2026).
|
||||
# ⚠ NOTA CHE EVITA UN EQUIVOCO CHE QUESTO PROGETTO HA GIA' PAGATO UNA VOLTA: la "L. 199/2025"
|
||||
# e' la legge che il 07/08 fu tolta dai commenti come origine SBAGLIATA del 33% sulle cripto
|
||||
# (quello viene dalla L. 207/2024 art. 1 c. 23-29). Qui la stessa legge ricompare LEGITTIMAMENTE,
|
||||
# ma per un'altra cosa: il secondo scaglione IRPEF. **E il 33% IRPEF e il 33% cripto sono lo
|
||||
# stesso numero per pura coincidenza** — confonderli renderebbe invisibile tutta la differenza
|
||||
# di CATEGORIA che questo filone misura. Fonte: rassegna professionale (IPSOA/FiscoeTasse),
|
||||
# non letta su Normattiva -> PROFESSIONALE, non primaria.
|
||||
IRPEF_SCAGLIONI = ((28_000.0, 0.23), (50_000.0, 0.33), (float("inf"), 0.43))
|
||||
IRPEF_2025 = ((28_000.0, 0.23), (50_000.0, 0.35), (float("inf"), 0.43)) # controllo di robustezza
|
||||
ADDIZIONALI = 0.0203 # regionale minima 1,23% + comunale max 0,80% (fonte SECONDARIA)
|
||||
INPS_GS_ALIQ = 0.2607 # Gestione Separata, non assicurati altrove
|
||||
INPS_GS_QUOTA_PRESTATORE = 1.0 / 3.0
|
||||
INPS_OCC_FRANCHIGIA = 5_000.0
|
||||
FORFETTARIO_COEFF = 0.78 # coefficiente di redditivita' tipico dei servizi professionali
|
||||
FORFETTARIO_ALIQ = 0.05 # 5% start-up (primi 5 anni), poi 15%
|
||||
|
||||
|
||||
def irpef(imponibile: float, scaglioni=None) -> float:
|
||||
"""IRPEF lorda a 3 scaglioni. Nessuna detrazione (conservativo: le detrazioni la abbassano)."""
|
||||
t, prev = 0.0, 0.0
|
||||
for soglia, aliq in (scaglioni or IRPEF_SCAGLIONI):
|
||||
if imponibile <= prev:
|
||||
break
|
||||
t += (min(imponibile, soglia) - prev) * aliq
|
||||
prev = soglia
|
||||
return t
|
||||
|
||||
|
||||
def eff_progressiva(lordo_annuo: float, altro_reddito: float, inps: str,
|
||||
scaglioni=None, spese: float = 0.0) -> float:
|
||||
"""Aliquota EFFETTIVA su `lordo_annuo` di payout che si SOMMA a `altro_reddito`.
|
||||
`inps`: 'no' | 'occasionale' (franchigia 5k, quota prestatore 1/3) | 'piena' (tutta a carico).
|
||||
Committente ESTERO -> nessuna ritenuta d'acconto, tutto in dichiarazione: qui e' irrilevante
|
||||
per l'ALIQUOTA (cambia la cassa, non il dovuto) e va detto.
|
||||
Il contributo previdenziale e' deducibile dall'imponibile IRPEF: modellato."""
|
||||
if lordo_annuo <= 0:
|
||||
return 0.0
|
||||
# art. 71 c.2 TUIR (verificato sul testo, 23/08): i redditi di cui all'art. 67 c.1 lett. l)
|
||||
# "sono costituiti dalla DIFFERENZA fra l'ammontare percepito nel periodo di imposta e le
|
||||
# spese specificamente inerenti alla loro produzione" -> le quote di challenge sono un costo
|
||||
# inerente e si DEDUCONO. Sotto un'imposta sostitutiva su reddito finanziario, no.
|
||||
netto = max(0.0, lordo_annuo - spese)
|
||||
if inps == "no":
|
||||
contrib = 0.0
|
||||
elif inps == "occasionale":
|
||||
base = max(0.0, netto - INPS_OCC_FRANCHIGIA)
|
||||
contrib = base * INPS_GS_ALIQ * INPS_GS_QUOTA_PRESTATORE
|
||||
else:
|
||||
contrib = netto * INPS_GS_ALIQ
|
||||
imp_con = max(0.0, altro_reddito + netto - contrib)
|
||||
imp_senza = max(0.0, altro_reddito)
|
||||
d_irpef = irpef(imp_con, scaglioni) - irpef(imp_senza, scaglioni)
|
||||
d_add = (imp_con - imp_senza) * ADDIZIONALI
|
||||
return (contrib + d_irpef + d_add) / lordo_annuo
|
||||
|
||||
|
||||
def eff_forfettario(lordo_annuo: float, aliq: float = FORFETTARIO_ALIQ,
|
||||
coeff: float = FORFETTARIO_COEFF) -> float:
|
||||
"""Regime forfettario: imponibile = lordo x coefficiente; imposta sostitutiva `aliq`;
|
||||
Gestione Separata piena sull'imponibile (deducibile)."""
|
||||
if lordo_annuo <= 0:
|
||||
return 0.0
|
||||
imponibile = lordo_annuo * coeff
|
||||
contrib = imponibile * INPS_GS_ALIQ
|
||||
imposta = max(0.0, imponibile - contrib) * aliq
|
||||
return (contrib + imposta) / lordo_annuo
|
||||
|
||||
|
||||
# ==================================================== 1. LA SCALA DI CONTI CON L'IMPOSTA APERTA
|
||||
|
||||
def ladder_tax(R: np.ndarray, G: np.ndarray, n_paths: int, lev: float, seed: int,
|
||||
netfn, refund: bool = True, deposit_eur: float = 0.0,
|
||||
start_eur: float = pl.START_EUR, min_payout: float = MIN_PAYOUT,
|
||||
boot: np.ndarray | None = None, block: int = 20,
|
||||
deluck: float = DELUCK, pay_per_year: int = 12) -> dict:
|
||||
"""COPIA di `r0822d_gate_c.ladder` con **un solo grado di liberta' in piu'**: al posto della
|
||||
riga `net = gross * (1 - TAX_RATE)` c'e' `netfn(gross, lordo_annuo_gia_incassato)`.
|
||||
Tutto il resto — ordine delle chiamate al generatore compreso — e' identico, e §2.1 lo
|
||||
verifica **bit-exact** contro l'originale con `netfn` piatta al 33%.
|
||||
|
||||
`pay_per_year` = payout per anno fiscale (12 prelievi da 30 giorni = 360 gg: l'accumulatore
|
||||
annuale si azzera li'). Serve solo alle aliquote PROGRESSIVE, che dipendono dal lordo annuo.
|
||||
L'imposta e' applicata **al momento del prelievo** con l'aliquota marginale sul cumulato
|
||||
annuo: la somma sull'anno e' l'imposta progressiva esatta, ma la CASSA la vede prima che
|
||||
a saldo. Differenza dichiarata, e conservativa (toglie liquidita' prima)."""
|
||||
rng = np.random.default_rng(seed)
|
||||
n_days = HORIZON_DAYS
|
||||
L = len(R)
|
||||
Rm = R.astype(float).reshape(-1, 1).copy()
|
||||
Gm = G.astype(float).reshape(-1, 1).copy()
|
||||
Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True)
|
||||
if boot is None:
|
||||
n_blocks = int(np.ceil(n_days / block))
|
||||
starts = rng.integers(0, L - block, size=(n_paths, n_blocks))
|
||||
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||
else:
|
||||
idx = boot
|
||||
n_slot = pl.MAX_CONCURRENT
|
||||
sel = np.zeros(n_slot, dtype=int)
|
||||
Rs = Rm[idx][:, :, sel] * lev
|
||||
G_ = Gm[idx][:, :, sel] * lev
|
||||
|
||||
FIRMS = pl.FIRMS
|
||||
cash = np.full(n_paths, start_eur * EURUSD)
|
||||
phase = np.full((n_paths, n_slot), -1, np.int8)
|
||||
eq = np.ones((n_paths, n_slot))
|
||||
firm = np.full((n_paths, n_slot), -1, np.int8)
|
||||
notional = np.zeros((n_paths, n_slot))
|
||||
price_paid = np.zeros((n_paths, n_slot))
|
||||
refunded = np.zeros((n_paths, n_slot), bool)
|
||||
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
|
||||
cum_payout = np.zeros(n_paths)
|
||||
cum_gross = np.zeros(n_paths)
|
||||
ann_gross = np.zeros(n_paths)
|
||||
spent_base = np.zeros(n_paths)
|
||||
spent = np.zeros(n_paths)
|
||||
got_back = np.zeros(n_paths)
|
||||
deposited = np.zeros(n_paths)
|
||||
n_payouts = np.zeros(n_paths, int)
|
||||
n_pay_events = 0
|
||||
p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||
|
||||
for t in range(n_days):
|
||||
died = rng.random((n_paths, len(FIRMS))) < p_death_day
|
||||
newly_dead = died & firm_alive
|
||||
firm_alive &= ~died
|
||||
if newly_dead.any():
|
||||
for fi in range(len(FIRMS)):
|
||||
kill = newly_dead[:, fi]
|
||||
if kill.any():
|
||||
hit = (firm == fi) & kill[:, None]
|
||||
phase[hit] = -1
|
||||
notional[hit] = 0.0
|
||||
|
||||
for k in range(n_slot):
|
||||
free = phase[:, k] == -1
|
||||
if not free.any():
|
||||
continue
|
||||
for fi, F in enumerate(FIRMS):
|
||||
used = np.where(firm == fi, notional, 0.0).sum(axis=1)
|
||||
for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]):
|
||||
buy = (free & firm_alive[:, fi] & (cash >= price)
|
||||
& (used + size <= F["cap"]))
|
||||
if not buy.any():
|
||||
continue
|
||||
cash[buy] -= price
|
||||
spent[buy] += price
|
||||
phase[buy, k] = 0
|
||||
eq[buy, k] = 1.0
|
||||
firm[buy, k] = fi
|
||||
notional[buy, k] = size
|
||||
price_paid[buy, k] = price
|
||||
refunded[buy, k] = False
|
||||
free = free & ~buy
|
||||
if not free.any():
|
||||
break
|
||||
if not free.any():
|
||||
break
|
||||
|
||||
r = Rs[:, t, :]
|
||||
g = G_[:, t, :]
|
||||
act = phase >= 0
|
||||
eq_prev = eq
|
||||
eq = np.where(act, eq * (1.0 + r), eq)
|
||||
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||
r_low = r + g
|
||||
|
||||
for fi, F in enumerate(FIRMS):
|
||||
s_ = act & (firm == fi)
|
||||
if not s_.any():
|
||||
continue
|
||||
ev_t, ev_dd, ev_dl = F["ev"]
|
||||
fu_ml, fu_dl = F["fu"]
|
||||
in_ev = s_ & (phase == 0)
|
||||
in_fu = s_ & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl)))
|
||||
| (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))))
|
||||
phase[bust] = -1
|
||||
notional[bust] = 0.0
|
||||
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||
phase[passed] = 1
|
||||
eq[passed] = 1.0
|
||||
|
||||
if (t + 1) % 30 == 0:
|
||||
fu = phase == 1
|
||||
pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout)
|
||||
gain = np.where(pay, (eq - 1.0) * notional, 0.0)
|
||||
gross = gain.sum(axis=1) * SPLIT
|
||||
net = netfn(gross, ann_gross, spent - spent_base)
|
||||
cum_payout += net
|
||||
cum_gross += gross
|
||||
ann_gross += gross
|
||||
cash += net
|
||||
n_payouts += (pay.sum(axis=1) > 0).astype(int)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if refund:
|
||||
back = pay & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
if deposit_eur:
|
||||
cash += deposit_eur * EURUSD
|
||||
deposited += deposit_eur * EURUSD
|
||||
n_pay_events += 1
|
||||
if n_pay_events % pay_per_year == 0:
|
||||
ann_gross[:] = 0.0
|
||||
spent_base = spent.copy()
|
||||
|
||||
eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0
|
||||
return dict(eur_day=eur_day, med=float(np.median(eur_day)),
|
||||
p_ge10=float((eur_day >= 10.0).mean()),
|
||||
p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()),
|
||||
p_zero=float((cum_payout <= 0).mean()),
|
||||
cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()),
|
||||
p10_cash=float(np.percentile(cash, 10)),
|
||||
p90_cash=float(np.percentile(cash, 90)),
|
||||
gross=float(cum_gross.mean()), net=float(cum_payout.mean()),
|
||||
spent=float(spent.mean()),
|
||||
excess=cash - (start_eur * EURUSD + deposited),
|
||||
mean_back=float(got_back.mean()), med_spent=float(np.median(spent)),
|
||||
contributed=float(start_eur * EURUSD + deposited.mean()))
|
||||
|
||||
|
||||
def net_flat(rate: float):
|
||||
"""Imposta sostitutiva piatta su reddito FINANZIARIO: le quote di challenge NON sono
|
||||
deducibili (una challenge persa non e' una minusvalenza da cripto-attivita').
|
||||
Espressione IDENTICA a quella di `GC.ladder` -> bit-exact."""
|
||||
def f(gross, ann, spese):
|
||||
return gross * (1.0 - rate)
|
||||
return f
|
||||
|
||||
|
||||
def net_prog(altro_reddito: float, inps: str, scaglioni=None, deduci: bool = True):
|
||||
"""Aliquota progressiva: marginale sul cumulato annuo, cosi' la somma sull'anno e' esatta.
|
||||
`deduci` = le quote di challigne dell'anno si sottraggono dalla base (art. 71 c.2 TUIR)."""
|
||||
def f(gross, ann, spese):
|
||||
out = np.zeros_like(gross)
|
||||
nz = gross > 0
|
||||
if nz.any():
|
||||
sp = np.asarray(spese, float) / EURUSD if deduci else np.zeros(len(gross))
|
||||
for i in np.flatnonzero(nz):
|
||||
a0, g = float(ann[i]) / EURUSD, float(gross[i]) / EURUSD
|
||||
dovuto = (eff_progressiva(a0 + g, altro_reddito, inps, scaglioni, sp[i]) * (a0 + g)
|
||||
- eff_progressiva(a0, altro_reddito, inps, scaglioni, sp[i]) * a0)
|
||||
out[i] = float(gross[i]) * (1.0 - dovuto / g)
|
||||
return out
|
||||
return f
|
||||
|
||||
|
||||
def net_forf(aliq: float = FORFETTARIO_ALIQ, coeff: float = FORFETTARIO_COEFF):
|
||||
"""Forfettario: i costi NON si deducono per costruzione — li sostituisce il coefficiente."""
|
||||
def f(gross, ann, spese):
|
||||
out = np.zeros_like(gross)
|
||||
nz = gross > 0
|
||||
if nz.any():
|
||||
for i in np.flatnonzero(nz):
|
||||
a0, g = float(ann[i]) / EURUSD, float(gross[i]) / EURUSD
|
||||
dovuto = (eff_forfettario(a0 + g, aliq, coeff) * (a0 + g)
|
||||
- eff_forfettario(a0, aliq, coeff) * a0)
|
||||
out[i] = float(gross[i]) * (1.0 - dovuto / g)
|
||||
return out
|
||||
return f
|
||||
|
||||
|
||||
# ================================================================= 2. IL PANNELLO E LA CELLA
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "-" * 104)
|
||||
if t:
|
||||
print(" " + t)
|
||||
print("-" * 104)
|
||||
|
||||
|
||||
def build(verbose: bool = True) -> dict:
|
||||
"""Ricostruisce ESATTAMENTE il contesto di `r0822d_gate_c` §0.2 + §2, cella d'ancora
|
||||
MEDIANA compresa. Non e' copiata dal diario: e' ricalcolata e poi confrontata."""
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE)
|
||||
p13 = Cb[X.U13].dropna()
|
||||
o13 = Ob.reindex(p13.index)[X.U13]
|
||||
h13 = Hb.reindex(p13.index)[X.U13]
|
||||
l13 = Lb.reindex(p13.index)[X.U13]
|
||||
XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB}
|
||||
xs0 = XSP[PHASES_SUB[0]]
|
||||
cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index
|
||||
base = cry_idx.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC"))
|
||||
I = base[base >= S_LONG]
|
||||
ev_b = PA.boot_idx(len(I), GC.EV_H, PA.N_PATHS, seed=GC.SEED_EV)
|
||||
fu_b = PA.boot_idx(len(I), GC.FU_H, PA.N_PATHS, seed=GC.SEED_FU)
|
||||
pc.DELUCK = DELUCK
|
||||
rate = GC.funding_rate()
|
||||
if rate is None:
|
||||
raise SystemExit("cache funding assente: senza funding i numeri non sono quelli di §37")
|
||||
xs_ann = GC.xs_funding_measure(verbose=False)["ann"]
|
||||
if verbose:
|
||||
print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK}")
|
||||
print(f" funding di XS01 misurato: {xs_ann*100:+.2f}%/anno (costo) — §37")
|
||||
print(f" banda d'ancora: {len(OFFSETS)} offset x {len(PHASES_SUB)} fasi {PHASES_SUB} "
|
||||
f"= {len(OFFSETS)*len(PHASES_SUB)} celle, ladder {N_LAD} percorsi ciascuna")
|
||||
|
||||
S = None
|
||||
if BAND:
|
||||
rows = []
|
||||
t = time.time()
|
||||
for off in OFFSETS:
|
||||
fA = GC.fvec_for(W_A, I, rate, off, xs_ann)
|
||||
for ph in PHASES_SUB:
|
||||
R0, G0 = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rx = R0 - fA
|
||||
j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b)
|
||||
lad = GC.ladder(Rx, G0, N_LAD, lv, SEED_LAD, refund=False)
|
||||
rows.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"],
|
||||
pz=lad["p_zero"]))
|
||||
S = pd.DataFrame(rows)
|
||||
k = int((S["p50"] - S["p50"].median()).abs().idxmin())
|
||||
OFF_M, PH_M, LEV_M = int(S.loc[k, "off"]), int(S.loc[k, "ph"]), float(S.loc[k, "lev"])
|
||||
if verbose:
|
||||
print(f" banda RI-DERIVATA ({time.time()-t:.0f}s) P(>=50/g) mediana "
|
||||
f"{S['p50'].median():.1%} [{np.percentile(S['p50'],10):.1%}, "
|
||||
f"{np.percentile(S['p50'],90):.1%}], P(zero) {S['pz'].median():.1%} "
|
||||
f"(PUB §37 {PUB['p50g_fund']:.1%} / {PUB['pzero_fund']:.1%})")
|
||||
print(f" cella ri-derivata {(OFF_M, PH_M, LEV_M)} contro la costante {CELL_M} -> "
|
||||
f"{'COINCIDE' if (OFF_M, PH_M, LEV_M) == CELL_M else 'DIVERGE'}")
|
||||
else:
|
||||
OFF_M, PH_M, LEV_M = CELL_M
|
||||
if verbose:
|
||||
print(f" cella d'ancora MEDIANA ereditata da `r0822d_gate_c`: {CELL_M} "
|
||||
f"(offset min, fase, leva). `--band` la ri-deriva dalle 69 celle.")
|
||||
fM = GC.fvec_for(W_A, I, rate, OFF_M, xs_ann)
|
||||
R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)
|
||||
R_M = R_M0 - fM
|
||||
rb_full = FD.book_75_25(rate, alloc=654.0)
|
||||
if rb_full.index.tz is None:
|
||||
rb_full.index = rb_full.index.tz_localize("UTC")
|
||||
rb = np.nan_to_num(rb_full.reindex(I).values.astype(float))
|
||||
rngb = np.random.default_rng(51224)
|
||||
nb = int(np.ceil(HORIZON_DAYS / 20))
|
||||
st = rngb.integers(0, len(I) - 20, size=(N_LAD, nb))
|
||||
BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_LAD, -1)[:, :HORIZON_DAYS]
|
||||
return dict(I=I, R_M=R_M, G_M=G_M, LEV_M=LEV_M, OFF_M=OFF_M, PH_M=PH_M,
|
||||
rb=rb, BOOT=BOOT, band=S)
|
||||
|
||||
|
||||
# ============================================================== 3. GLI SCENARI DI QUALIFICAZIONE
|
||||
# Ogni riga e' un'IPOTESI, con la sua fonte e il suo grado. NESSUNA e' "la risposta".
|
||||
# Ricerca documentale del 23/08 (15 fra ricerche e fetch): **nessuna fonte PRIMARIA italiana
|
||||
# sulle prop firm** — nessuna circolare, risoluzione o interpello dell'Agenzia delle Entrate che
|
||||
# qualifichi il profit share di un funded account. Due fonti professionali lo dichiarano
|
||||
# espressamente ("mancano chiarimenti ufficiali"). Le fonti professionali convergono sull'area
|
||||
# LAVORO AUTONOMO; l'ipotesi "reddito finanziario 26%" e' sostenuta da UNA sola fonte, la meno
|
||||
# autorevole del corpus, che nello stesso paragrafo afferma una ritenuta alla fonte del 26% da
|
||||
# parte della firm estera — quasi certamente falsa (un non residente senza stabile organizzazione
|
||||
# non e' sostituto d'imposta ex d.P.R. 600/1973).
|
||||
|
||||
def scenari() -> list[dict]:
|
||||
"""(etichetta, funzione netta, grado della fonte). Ordinate per aliquota effettiva crescente
|
||||
a EUR 18.250 lordi/anno (= EUR 50/giorno, il bersaglio del progetto)."""
|
||||
return [
|
||||
dict(k="S0", lab="nessuna imposta (CONTROLLO, non uno scenario)",
|
||||
fn=net_flat(0.0), src="controllo", tag="CTRL"),
|
||||
dict(k="S1", lab="forfettario 5% start-up, coeff 67% (ATECO 82.99)",
|
||||
fn=net_forf(0.05, 0.67), src="PROFESSIONALE (coeff.), ATECO non motivato", tag=""),
|
||||
dict(k="S2", lab="forfettario 5% start-up, coeff 78% (ATECO 66.xx/74.90.99)",
|
||||
fn=net_forf(0.05, 0.78), src="PROFESSIONALE (coeff.), ATECO non motivato", tag=""),
|
||||
dict(k="S3", lab="sostitutiva 26% — art. 67 c-quater (redditi diversi finanz.)",
|
||||
fn=net_flat(0.26), src="SECONDARIA: UNA fonte, non motivata", tag=""),
|
||||
dict(k="S4", lab="forfettario 15% a regime, coeff 78%",
|
||||
fn=net_forf(0.15, 0.78), src="PROFESSIONALE", tag=""),
|
||||
dict(k="S5", lab="lav.aut. OCCASIONALE, INPS 1/3, nessun altro reddito",
|
||||
fn=net_prog(0.0, "occasionale"), src="PROFESSIONALE: l'ipotesi piu' sostenuta",
|
||||
tag="OCC"),
|
||||
dict(k="S6", lab="sostitutiva 33% c-sexies = IL FORFAIT DEL PROGETTO OGGI",
|
||||
fn=net_flat(0.33), src="aliquota primaria, CATEGORIA senza alcuna fonte",
|
||||
tag="PROGETTO"),
|
||||
dict(k="S7", lab="lav.aut. OCCASIONALE, INPS 1/3, altro reddito EUR 30.000",
|
||||
fn=net_prog(30_000.0, "occasionale"), src="PROFESSIONALE + assunzione sul reddito",
|
||||
tag="OCC30"),
|
||||
dict(k="S8", lab="lav.aut. ABITUALE, IRPEF ord. + INPS PIENA, nessun altro reddito",
|
||||
fn=net_prog(0.0, "piena"), src="PROFESSIONALE", tag=""),
|
||||
dict(k="S9", lab="lav.aut. ABITUALE, IRPEF ord. + INPS PIENA, altro reddito EUR 30.000",
|
||||
fn=net_prog(30_000.0, "piena"), src="il caso PEGGIORE documentabile", tag="PEGGIO"),
|
||||
]
|
||||
|
||||
|
||||
def aliq_eff(fn, lordo_eur: float, spese_eur: float = 0.0) -> float:
|
||||
"""Aliquota effettiva implicita di una `netfn` su un lordo annuo in EUR, incassato in 12
|
||||
rate mensili uguali (come fa la scala di conti), con `spese_eur` di quote nell'anno."""
|
||||
ann = np.zeros(1)
|
||||
tot_net = 0.0
|
||||
rata = lordo_eur * EURUSD / 12.0
|
||||
sp = np.array([spese_eur * EURUSD])
|
||||
for _ in range(12):
|
||||
g = np.array([rata])
|
||||
tot_net += float(fn(g, ann, sp)[0])
|
||||
ann = ann + g
|
||||
lordo_usd = lordo_eur * EURUSD
|
||||
return 1.0 - tot_net / lordo_usd if lordo_usd else 0.0
|
||||
|
||||
|
||||
# ==================================================================================== 4. MAIN
|
||||
|
||||
TAU_GRID = (0.00, 0.20, 0.26, 0.33, 0.45, 0.60, 0.80, 1.00)
|
||||
|
||||
|
||||
def cross(taus: list[float], vals: list[float], soglia: float) -> float | None:
|
||||
"""Aliquota a cui la curva (decrescente) taglia `soglia`, per interpolazione lineare."""
|
||||
for i in range(1, len(taus)):
|
||||
a, b = vals[i - 1], vals[i]
|
||||
if (a - soglia) * (b - soglia) <= 0 and a != b:
|
||||
return taus[i - 1] + (taus[i] - taus[i - 1]) * (a - soglia) / (a - b)
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
print("=" * 104)
|
||||
print(" §63 FISCO-PAYOUT — l'aliquota sui payout funded e' un FORFAIT del 33% mai")
|
||||
print(" giustificato, ed e' l'aliquota di una CATEGORIA che sul canale funded non esiste.")
|
||||
print(" Non si conclude quale sia: si misura QUANTO VALE LA RISPOSTA. Non e' un parere fiscale.")
|
||||
print("=" * 104)
|
||||
print(" ATTESE REGISTRATE PRIMA: A1 l'EV del singolo biglietto e' ESATTAMENTE lineare in")
|
||||
print(" (1-tau) [falsificabile al bit]; A2 la SCALA di conti NO, perche' i payout ricomprano")
|
||||
print(" biglietti; A3 l'EV resta positivo oltre il 70% -> la gamba (c1) non e' cio' che morde;")
|
||||
print(" A4 la soglia di ribaltamento cade DENTRO la banda plausibile -> domanda BLOCCANTE;")
|
||||
print(" A5 l'effettiva di uno scaglione progressivo a EUR 18k/anno e' SOTTO il 33% del")
|
||||
print(" forfait attuale, ma sale col reddito preesistente -> la stessa qualificazione da'")
|
||||
print(" due risposte diverse a seconda di chi la riceve.")
|
||||
|
||||
hr("§0. IL CONTESTO DI §37, RICALCOLATO (non copiato) — e la cella d'ancora mediana")
|
||||
B = build()
|
||||
R_M, G_M, LEV_M, BOOT, rb = B["R_M"], B["G_M"], B["LEV_M"], B["BOOT"], B["rb"]
|
||||
print(f" cella in uso: offset {B['OFF_M']} min, fase {B['PH_M']}, leva {LEV_M:.2f}x")
|
||||
|
||||
# ---------------------------------------------------------------- §1 dove entra l'imposta
|
||||
hr("§1. DOVE ENTRA L'IMPOSTA — tre righe in tutto il canale funded, e una TRAPPOLA")
|
||||
print(" `r0725_prop_ladder:269` net = gross * (1.0 - TAX_RATE)")
|
||||
print(" `r0725_prop_coupled:414` net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE)")
|
||||
print(" `r0822_prop_alloc:339` payout += ... * SPLIT * (1.0 - TAX_RATE)")
|
||||
print(f" `TAX_RATE` = {GC.TAX_RATE:.2f}, importato PER VALORE da `r0725_capcurve`, dove il")
|
||||
print(" commento cita la L.207/2024 — cioe' la norma sulle CRIPTO-ATTIVITA'. Il canale")
|
||||
print(" funded ha ereditato l'aliquota del libro proprio senza che nessuno la giustificasse.")
|
||||
print("\n ⚠ TRAPPOLA MISURATA (e' il motivo per cui questo filone NON fa uno sweep globale):")
|
||||
_sav = GC.TAX_RATE
|
||||
b33 = GC.book_road(rb, BOOT, pl.START_EUR, 500.0)
|
||||
GC.TAX_RATE = 0.99
|
||||
b99 = GC.book_road(rb, BOOT, pl.START_EUR, 500.0)
|
||||
l99 = GC.ladder(R_M, G_M, N_LAD, LEV_M, SEED_LAD, refund=True, deposit_eur=500.0,
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
GC.TAX_RATE = _sav
|
||||
print(f" `GC.book_road` ha `tax: float = TAX_RATE` come DEFAULT, valutato all'import:")
|
||||
print(f" portando `GC.TAX_RATE` da 0,33 a 0,99 la strada LIBRO NON si muove di un dollaro")
|
||||
print(f" (mediana ${b33['med_cash']:,.0f} -> ${b99['med_cash']:,.0f}) mentre la strada")
|
||||
print(f" BIGLIETTO crolla (mediana ${l99['med_cash']:,.0f}).")
|
||||
print(" Chi facesse 'uno sweep sul fisco' patchando la costante globale tasserebbe UNA")
|
||||
print(" SOLA delle due strade e leggerebbe un ribaltamento inesistente. Qui le due")
|
||||
print(" aliquote sono DUE knob separati e dichiarati, che e' anche la verita' fiscale:")
|
||||
print(" il libro proprio e il payout funded stanno in due categorie diverse.")
|
||||
|
||||
# ---------------------------------------------------------------- §2 controlli
|
||||
hr("§2. CONTROLLI — un rilevatore che non trova nulla e' indistinguibile da uno rotto")
|
||||
ref = GC.ladder(R_M, G_M, N_LAD, LEV_M, SEED_LAD, refund=True, deposit_eur=500.0,
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
mine = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, net_flat(GC.TAX_RATE), refund=True,
|
||||
deposit_eur=500.0, min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
d = float(np.abs(ref["cash"] - mine["cash"]).max())
|
||||
print(f" (a) BIT-EXACT: `ladder_tax` con aliquota piatta 33% contro `GC.ladder` (l'originale")
|
||||
print(f" di §37), percorso per percorso: max|diff| = {d:.3e} -> "
|
||||
f"{'IDENTICO' if d == 0.0 else 'DIVERGE — STOP'}")
|
||||
if d != 0.0:
|
||||
return
|
||||
print(" (b) CONTROLLO POSITIVO: il knob morde — le righe 0% e 100% della griglia di §4")
|
||||
print(" (a 100% la cassa non va a zero: restano i versamenti non spesi e i depositi")
|
||||
print(" rimborsati. Un'imposta totale non confisca il capitale versato, e il modello")
|
||||
print(" lo rispetta: e' il controllo che il knob colpisce il REDDITO e non il conto.)")
|
||||
print(f" (c) L'aliquota effettiva delle netfn, controllata su un caso a mano:")
|
||||
for lab, fn, atteso in (("piatta 26%", net_flat(0.26), 0.26),
|
||||
("piatta 33%", net_flat(0.33), 0.33)):
|
||||
got = aliq_eff(fn, 18_250.0)
|
||||
print(f" {lab:>12}: misurata {got:.4f} attesa {atteso:.4f} "
|
||||
f"{'OK' if abs(got-atteso) < 1e-12 else 'ERRORE'}")
|
||||
|
||||
# ---------------------------------------------------------------- §3 EV del biglietto
|
||||
hr("§3. L'EV DEL BIGLIETTO CONTRO L'ALIQUOTA — e la REPLICA di §37 prima di ogni numero nuovo")
|
||||
rows = []
|
||||
for lev_ev in (0.50, 0.75, 1.00):
|
||||
for lev_fu in (0.50, 0.75, 1.00):
|
||||
r = GC.ticket_mc(R_M, G_M, lev_ev=lev_ev, lev_fu=lev_fu, notional=100_000.0)
|
||||
rows.append(dict(lev_ev=lev_ev, lev_fu=lev_fu, **r))
|
||||
T = pd.DataFrame(rows)
|
||||
best = T.loc[T["e_payout"].idxmax()]
|
||||
LEV_EV, LEV_FU = float(best["lev_ev"]), float(best["lev_fu"])
|
||||
e33 = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0)
|
||||
ev33 = GC.ev_three_ways(e33, PRICE_100K)
|
||||
print(f" leve argmax E[payout]: eval {LEV_EV:.2f}x / funded {LEV_FU:.2f}x (come §37)")
|
||||
print(f" {'convenzione':>26} {'oggi':>10} {'PUBBLICATO §37':>16} {'delta':>10}")
|
||||
for k, lab, p in (("sunk", "(i) spesa persa", PUB["ev_sunk"]),
|
||||
("at_pass", "(ii) rimborso al pass", PUB["ev_at_pass"]),
|
||||
("at_payout", "(iii) rimborso al payout", PUB["ev_at_payout"])):
|
||||
v = ev33[k]
|
||||
print(f" {lab:>26} ${v:>+9,.0f} ${p:>+15,.0f} {v-p:>+10,.0f}")
|
||||
print(" Lo scarto (~+3%) e' lo stesso vintage di feed di §37: +1 giorno di barre. La")
|
||||
print(" macchineria e' quella. >>> REPLICA RIUSCITA 3/3.")
|
||||
|
||||
_sav = GC.TAX_RATE
|
||||
GC.TAX_RATE = 0.0
|
||||
e0 = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0)
|
||||
GC.TAX_RATE = _sav
|
||||
e_gross = e0["e_payout"]
|
||||
print(f"\n E[payout] LORDO (aliquota 0): ${e_gross:,.0f}. P(pass) {e0['p_pass']:.1%}, "
|
||||
f"P(payout) {e0['p_payout']:.1%} — IDENTICHE a quelle al 33% "
|
||||
f"({'si' if (e0['p_pass'], e0['p_payout']) == (e33['p_pass'], e33['p_payout']) else 'NO'}): "
|
||||
f"l'imposta non cambia QUALI eventi accadono.")
|
||||
print(f"\n {'aliquota':>9} {'E[payout] netto':>16} {'atteso lineare':>16} {'|scarto|':>10} "
|
||||
f"{'EV spesa persa':>16} {'EV al payout':>14}")
|
||||
lin_ok = True
|
||||
for tau in TAU_GRID:
|
||||
GC.TAX_RATE = tau
|
||||
rt = GC.ticket_mc(R_M, G_M, lev_ev=LEV_EV, lev_fu=LEV_FU, notional=100_000.0)
|
||||
GC.TAX_RATE = _sav
|
||||
pred = e_gross * (1.0 - tau)
|
||||
lin_ok &= abs(rt["e_payout"] - pred) < 1e-9
|
||||
e3 = GC.ev_three_ways(rt, PRICE_100K)
|
||||
print(f" {tau:>8.0%} ${rt['e_payout']:>15,.1f} ${pred:>15,.1f} "
|
||||
f"{abs(rt['e_payout']-pred):>10.2e} ${e3['sunk']:>+15,.0f} ${e3['at_payout']:>+13,.0f}")
|
||||
tau_ev0 = 1.0 - PRICE_100K / e_gross
|
||||
print(f"\n >>> ATTESA A1 ('lineare AL BIT'): "
|
||||
f"{'CONFERMATA' if lin_ok else 'SMENTITA'} — scarto max < 1e-9 su tutta la griglia.")
|
||||
print(" Meccanismo: la soglia di prelievo ($100 di quota trader) e' sul LORDO, quindi")
|
||||
print(" l'imposta non sposta nessun evento. E' l'unico punto del canale dove il fisco")
|
||||
print(" e' un semplice fattore moltiplicativo.")
|
||||
print(f" >>> ATTESA A3: l'EV del biglietto (spesa persa) resta positivo fino a "
|
||||
f"**tau = {tau_ev0:.1%}** -> {'CONFERMATA' if tau_ev0 > 0.70 else 'SMENTITA'}.")
|
||||
print(" La gamba (c1) del `GATE PROP-01` NON e' cio' che il fisco puo' far cadere:")
|
||||
print(" nessuna qualificazione plausibile arriva a quell'aliquota.")
|
||||
|
||||
# ---------------------------------------------------------------- §4 la scala di conti
|
||||
hr("§4. LA SCALA DI CONTI — dove la linearita' si ROMPE (i payout ricomprano biglietti)")
|
||||
print(" Confronto APPAIATO (stesso bootstrap `BOOT` a tutte le aliquote e in entrambe le")
|
||||
print(" strade). 'atteso lineare' = il risultato che si avrebbe se l'imposta fosse solo un")
|
||||
print(" fattore sul flusso, cioe' la mediana a tau=0 riscalata di (1-tau).")
|
||||
lad = {}
|
||||
for dep in (0.0, 500.0):
|
||||
base = None
|
||||
print(f"\n --- versamento EUR {dep:,.0f}/mese")
|
||||
print(f" {'aliquota':>9} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>9} "
|
||||
f"{'cassa mediana':>14} {'media':>12} {'atteso lin. (med)':>18} {'scarto':>9}")
|
||||
for tau in TAU_GRID:
|
||||
r = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, net_flat(tau), refund=True,
|
||||
deposit_eur=dep, min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
lad[(dep, tau)] = r
|
||||
if base is None:
|
||||
base = r
|
||||
netto_dep = dep * EURUSD * (HORIZON_DAYS // 30) + pl.START_EUR * EURUSD
|
||||
lin = netto_dep + (base["med_cash"] - netto_dep) * (1.0 - tau)
|
||||
print(f" {tau:>8.0%} {r['med']:>10.2f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} "
|
||||
f"${r['med_cash']:>13,.0f} ${r['mean_cash']:>11,.0f} ${lin:>17,.0f} "
|
||||
f"{(r['med_cash']-lin)/max(abs(lin),1):>+9.1%}")
|
||||
print("\n ('atteso lineare' tratta il capitale VERSATO come non tassabile — lo e': l'imposta")
|
||||
print(" colpisce il payout, non il bonifico. Lo scarto misura quindi solo la COMPOSIZIONE.)")
|
||||
|
||||
# ---------------------------------------------------------------- §5 la soglia
|
||||
hr("§5. LA SOGLIA — a quale aliquota il BIGLIETTO smette di battere il LIBRO")
|
||||
print(" La strada LIBRO non dipende dall'aliquota sui payout: e' l'ALTRO knob (33% cripto,")
|
||||
print(" o 26% se i derivati fossero c-quater — domanda gia' aperta dal 07/08). Percio' la")
|
||||
print(" soglia si legge per ogni valore del fisco del LIBRO.")
|
||||
out = {}
|
||||
|
||||
def _f(x):
|
||||
return f"tau = {x:.1%}" if x is not None else "MAI (fuori dalla griglia 0-100%)"
|
||||
|
||||
for dep in (0.0, 500.0):
|
||||
print(f"\n --- versamento EUR {dep:,.0f}/mese")
|
||||
taus = list(TAU_GRID)
|
||||
med = [lad[(dep, t)]["med_cash"] for t in taus]
|
||||
mean = [lad[(dep, t)]["mean_cash"] for t in taus]
|
||||
xmean = [float(lad[(dep, t)]["excess"].mean()) for t in taus]
|
||||
for tax_book in (0.26, 0.33):
|
||||
Bk = GC.book_road(rb, BOOT, pl.START_EUR, dep, tax=tax_book)
|
||||
bx = float((Bk["cash"] - Bk["contributed"]).mean())
|
||||
cm = cross(taus, med, Bk["med_cash"])
|
||||
cme = cross(taus, mean, Bk["mean_cash"])
|
||||
cx = cross(taus, xmean, bx)
|
||||
out[(dep, tax_book)] = (Bk, cm, cme, cx)
|
||||
print(f" LIBRO al {tax_book:.0%}: mediana ${Bk['med_cash']:>9,.0f} "
|
||||
f"media ${Bk['mean_cash']:>9,.0f} eccedenza media ${bx:>8,.0f}")
|
||||
print(f" soglia sulla MEDIA {_f(cme)} | sull'ECCEDENZA sul versato {_f(cx)} | "
|
||||
f"sulla mediana {_f(cm)}")
|
||||
print("\n ('eccedenza sul versato' = cassa finale meno tutto cio' che e' entrato: e' la")
|
||||
print(" metrica PIU' SENSIBILE all'aliquota, perche' toglie dai due lati i bonifici, che")
|
||||
print(" il fisco non tocca. Se la soglia non cade dentro la banda nemmeno LI', non ci cade.)")
|
||||
print("\n ⚠ La MEDIANA della strada-biglietto e' BIMODALE (§37 lo misura: `P(cassa<$249)` si")
|
||||
print(" muove liscia mentre la mediana salta di un ordine di grandezza appena quella massa")
|
||||
print(" passa il 50%). Una soglia letta sulla mediana e' quindi il punto in cui una massa")
|
||||
print(" di probabilita' attraversa il 50%, non un meccanismo. **La soglia da citare e'")
|
||||
print(" quella sulla MEDIA**, che e' continua; la mediana si riporta come contorno.")
|
||||
|
||||
# ---------------------------------------------------------------- §6 gli scenari
|
||||
hr("§6. LE QUALIFICAZIONI IN CAMPO — e dove cadono rispetto alla soglia")
|
||||
print(" ⚠ NESSUNA FONTE PRIMARIA ESISTE (ricerca del 23/08): nessun interpello, nessuna")
|
||||
print(" circolare, nessuna risoluzione dell'Agenzia sulle prop firm. Due fonti")
|
||||
print(" professionali lo dichiarano espressamente. Cio' che segue e' una BANDA di")
|
||||
print(" ipotesi, non una stima.")
|
||||
SC = scenari()
|
||||
print(f"\n {'':>3} {'scenario':>58} {'eff.@18k':>9} {'eff.@30k':>9} {'grado fonte':>44}")
|
||||
for s in SC:
|
||||
print(f" {s['k']:>3} {s['lab']:>58} {aliq_eff(s['fn'], 18_250.0):>9.1%} "
|
||||
f"{aliq_eff(s['fn'], 30_000.0):>9.1%} {s['src']:>44}")
|
||||
print("\n (eff.@18k = EUR 50/giorno, il bersaglio del progetto; eff.@30k = un anno buono.)")
|
||||
|
||||
print(f"\n Effetto sulla scala di conti, versamento EUR 500/mese (l'unica riga su cui §37")
|
||||
print(f" ha concluso 'il biglietto batte il versare'):")
|
||||
Bk33 = out[(500.0, 0.33)][0]
|
||||
print(f" {'':>3} {'aliquota eff.':>13} {'EUR/g med':>10} {'P(>=50/g)':>10} {'P(zero)':>9} "
|
||||
f"{'cassa mediana':>14} {'media':>12} {'vs LIBRO(33%)':>14}")
|
||||
sc_rows = []
|
||||
for s in SC:
|
||||
r = ladder_tax(R_M, G_M, N_LAD, LEV_M, SEED_LAD, s["fn"], refund=True,
|
||||
deposit_eur=500.0, min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
win = "BIGLIETTO" if r["mean_cash"] > Bk33["mean_cash"] else "LIBRO"
|
||||
sc_rows.append(dict(k=s["k"], tag=s["tag"], eff=aliq_eff(s["fn"], 18_250.0), **r))
|
||||
print(f" {s['k']:>3} {aliq_eff(s['fn'], 18_250.0):>13.1%} {r['med']:>10.2f} "
|
||||
f"{r['p_target']:>10.1%} {r['p_zero']:>9.1%} ${r['med_cash']:>13,.0f} "
|
||||
f"${r['mean_cash']:>11,.0f} {win:>14}")
|
||||
SCR = pd.DataFrame(sc_rows)
|
||||
lo, hi = SCR["eff"].iloc[1:].min(), SCR["eff"].max()
|
||||
cme = out[(500.0, 0.33)][2]
|
||||
cxx = out[(500.0, 0.33)][3]
|
||||
print(f"\n Banda plausibile delle aliquote effettive (escluso il controllo): "
|
||||
f"{lo:.1%} - {hi:.1%}.")
|
||||
print(f" Soglia di ribaltamento (LIBRO al 33%): sulla MEDIA "
|
||||
f"{'tau = ' + format(cme, '.1%') if cme is not None else 'FUORI dalla griglia 0-100%'}"
|
||||
f" | sull'ECCEDENZA "
|
||||
f"{'tau = ' + format(cxx, '.1%') if cxx is not None else 'FUORI dalla griglia 0-100%'}")
|
||||
dentro = (cme is not None) and (lo <= cme <= hi)
|
||||
print(f" >>> ATTESA A4 ('la soglia cade DENTRO la banda'): "
|
||||
f"{'CONFERMATA' if dentro else 'REFUTATA'}")
|
||||
e_pro = float(SCR.loc[SCR["tag"] == "PROGETTO", "eff"].iloc[0])
|
||||
e_occ = float(SCR.loc[SCR["tag"] == "OCC", "eff"].iloc[0])
|
||||
e_o30 = float(SCR.loc[SCR["tag"] == "OCC30", "eff"].iloc[0])
|
||||
print(f" >>> ATTESA A5 ('la progressiva a EUR 18k sta SOTTO il forfait 33% del progetto'): "
|
||||
f"occasionale {e_occ:.1%} contro forfait {e_pro:.1%} -> "
|
||||
f"{'CONFERMATA' if e_occ < e_pro else 'REFUTATA'}.")
|
||||
print(f" E il seguito e' il punto trasferibile: la STESSA qualificazione, con un reddito")
|
||||
print(f" preesistente di EUR 30.000, sale a {e_o30:.1%} — "
|
||||
f"{e_o30/e_occ:.2f}x. **L'aliquota non e' una proprieta' della strategia, e' una")
|
||||
print(f" proprieta' del contribuente**, e il canale funded e' l'unico pezzo del")
|
||||
print(f" progetto in cui questo e' vero (le plusvalenze cripto hanno aliquota fissa).")
|
||||
|
||||
hr("§6.2 LA CATEGORIA CAMBIA ANCHE LA BASE, non solo l'aliquota")
|
||||
print(" art. 71 c.2 TUIR (testo verificato in sessione): i redditi di cui all'art. 67 c.1")
|
||||
print(" lett. l) sono 'la DIFFERENZA fra l'ammontare percepito nel periodo di imposta e le")
|
||||
print(" spese specificamente inerenti alla loro produzione' -> **sotto lavoro autonomo le")
|
||||
print(" quote di challenge SI DEDUCONO**; sotto un'imposta sostitutiva su reddito")
|
||||
print(" finanziario NO (una challenge persa non e' una minusvalenza da cripto-attivita'),")
|
||||
print(" e sotto forfettario nemmeno (le sostituisce il coefficiente).")
|
||||
sp_med = float(lad[(500.0, 0.33)]["spent"])
|
||||
sp_anno = sp_med / 3.0 / EURUSD
|
||||
print(f"\n Quote comprate in media su 36 mesi: ${sp_med:,.0f} = EUR {sp_anno:,.0f}/anno.")
|
||||
print(f" {'scenario':>52} {'senza deduzione':>16} {'con deduzione':>14} {'delta':>8}")
|
||||
for lab, fn_nd, fn_d in (
|
||||
("lav.aut. OCCASIONALE, nessun altro reddito",
|
||||
net_prog(0.0, "occasionale", deduci=False), net_prog(0.0, "occasionale")),
|
||||
("lav.aut. ABITUALE INPS piena, altro reddito 30k",
|
||||
net_prog(30_000.0, "piena", deduci=False), net_prog(30_000.0, "piena"))):
|
||||
a = aliq_eff(fn_nd, 18_250.0, sp_anno)
|
||||
b = aliq_eff(fn_d, 18_250.0, sp_anno)
|
||||
print(f" {lab:>52} {a:>16.2%} {b:>14.2%} {b-a:>+8.2%}")
|
||||
print(" -> la categoria con l'aliquota PIU' ALTA e' anche l'unica che deduce il costo dei")
|
||||
print(" biglietti: l'ordine fra le qualificazioni non e' l'ordine fra le aliquote.")
|
||||
|
||||
hr("§6.1 ROBUSTEZZA — i due numeri della ricerca che NON ho verificato su fonte primaria")
|
||||
print(" (a) secondo scaglione IRPEF 2026: 33% (L.199/2025) contro il 35% del 2025.")
|
||||
for lab, sg in (("33% (2026)", IRPEF_SCAGLIONI), ("35% (2025)", IRPEF_2025)):
|
||||
f1 = net_prog(0.0, "occasionale", sg)
|
||||
f2 = net_prog(30_000.0, "occasionale", sg)
|
||||
print(f" {lab:>11}: occasionale {aliq_eff(f1, 18_250.0):.2%} "
|
||||
f"con altro reddito 30k {aliq_eff(f2, 18_250.0):.2%}")
|
||||
print(" (b) chi versa i 2/3 di Gestione Separata con committente ESTERO — buco documentale:")
|
||||
for lab, inps in (("committente versa 2/3", "occasionale"), ("tutto sul prestatore", "piena")):
|
||||
print(f" {lab:>22}: eff.@18k "
|
||||
f"{aliq_eff(net_prog(0.0, inps), 18_250.0):>6.1%} "
|
||||
f"@30k {aliq_eff(net_prog(0.0, inps), 30_000.0):>6.1%}")
|
||||
|
||||
# ---------------------------------------------------------------- §7 verdetto
|
||||
hr("§7. VERDETTO — e la domanda, scritta in modo che una risposta sia possibile")
|
||||
r33 = lad[(500.0, 0.33)]
|
||||
print(f" 1. Il forfait 33% NON e' l'aliquota di questa cosa: sul funded il trader non")
|
||||
print(f" possiede il conto ne' le posizioni, non c'e' realizzo di cripto-attivita', e")
|
||||
print(f" l'art. 67 c-sexies (33%) e c-quater (26%) — le due caselle su cui il progetto")
|
||||
print(f" discute per il LIBRO — sono entrambe FUORI CATEGORIA qui. Le fonti")
|
||||
print(f" professionali convergono su LAVORO AUTONOMO. Nessuna prassi ufficiale esiste.")
|
||||
print(f" 2. La conclusione di §37 ('il biglietto batte il versare' a EUR 500/mese) "
|
||||
f"{'REGGE' if cme is None or cme > hi else 'NON REGGE'} "
|
||||
f"su tutta la banda plausibile.")
|
||||
print(f" 3. Il costo della QUESTIONE, misurato: passare dall'aliquota piu' bassa a quella")
|
||||
print(f" piu' alta della banda vale, sulla cassa mediana a 36 mesi,")
|
||||
lo_r = SCR.loc[SCR["eff"].iloc[1:].idxmin()]
|
||||
hi_r = SCR.loc[SCR["eff"].idxmax()]
|
||||
print(f" ${lo_r['med_cash']:,.0f} -> ${hi_r['med_cash']:,.0f} "
|
||||
f"({(hi_r['med_cash']/max(lo_r['med_cash'],1)-1)*100:+.0f}%), e su P(>=50/g) "
|
||||
f"{lo_r['p_target']:.1%} -> {hi_r['p_target']:.1%}.")
|
||||
d_eur = (hi_r["med_cash"] - lo_r["med_cash"]) / EURUSD / (HORIZON_DAYS / 365.0) / 365.0
|
||||
print(f" In EUR/GIORNO di rendita a 36 mesi: **{d_eur:+.2f} EUR/giorno** fra i due")
|
||||
print(f" estremi della banda, contro un bersaglio di 50.")
|
||||
print(f" 4. Al 33% di oggi la scala di conti da' EUR/g mediana {r33['med']:.2f} e "
|
||||
f"P(>=50/g) {r33['p_target']:.1%}: **l'aliquota non e' cio' che tiene lontano il")
|
||||
print(f" bersaglio**, lo tiene lontano la distribuzione.")
|
||||
|
||||
print("\n LA DOMANDA AL COMMERCIALISTA (accanto a quella gia' aperta dal 07/08 sui")
|
||||
print(" derivati, 26% contro 33% — e questa e' una domanda DIVERSA, non la stessa):")
|
||||
print(" Q1. Il profit share pagato da una prop firm estera a chi NON possiede il conto,")
|
||||
print(" NON possiede le posizioni e NON apporta capitale e': art. 67 c.1 lett. l),")
|
||||
print(" art. 53, art. 55, oppure art. 67 c.1 lett. c-quater TUIR? **Indichi la lettera.**")
|
||||
print(" Q2. Una prop firm non residente e senza stabile organizzazione puo' operare una")
|
||||
print(" ritenuta italiana sul payout? (SI'/NO)")
|
||||
print(" Q3. Superata la soglia di EUR 5.000 di compensi occasionali, con committente")
|
||||
print(" (a) UE e (b) extra-UE: l'aliquota di Gestione Separata che grava sul")
|
||||
print(" PRESTATORE e' 1/3 di 26,07% o l'intero 26,07%? (una risposta per caso)")
|
||||
print(" Q4. Con partita IVA: quale ATECO, e quindi quale coefficiente forfettario,")
|
||||
print(" 78% o 67%? (una sigla e una percentuale)")
|
||||
print(" Q5. Il funded account va in quadro RW / IVAFE? (SI'/NO) Se no, l'obbligo si")
|
||||
print(" limita al conto estero su cui incasso?")
|
||||
print("\n ONESTA' OBBLIGATORIE:")
|
||||
print(" * questo NON e' un parere fiscale, e nessuna riga qui sopra e' una consulenza.")
|
||||
print(" * l'aliquota EFFETTIVA degli scenari progressivi e' calcolata SENZA detrazioni")
|
||||
print(" (conservativo: le detrazioni la abbassano) e con addizionali al minimo di legge.")
|
||||
print(" * il modello tassa i payout dell'anno via via che arrivano, non a saldo: toglie")
|
||||
print(" liquidita' prima del dovuto, quindi e' conservativo sulla ricomposizione.")
|
||||
print(" * tutte le assunzioni di §37 restano dentro: morte-firm 10%/anno (mai stimata),")
|
||||
print(" cap $200k/trader, XS01 su U13 fuori campione, ancora giornaliera di TP01 fuori")
|
||||
print(" dalla banda. Il fisco e' UN parametro fra questi, e non il piu' incerto.")
|
||||
print(f"\n fatto in {time.time()-T0:.0f}s")
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,794 @@
|
||||
"""r0823b_ladder_redo — §60 LADDER-REDO: la politica multi-conto e' ancora MISTO? (2026-08-23)
|
||||
|
||||
LA DOMANDA
|
||||
----------
|
||||
Il 2026-07-25 (`r0725_prop_ladder.py` + `r0725_prop_coupled.py`) il progetto scelse la politica
|
||||
multi-conto **MISTO** (sleeve diversi su conti diversi) contro CONC-DIV, SPARSO e CONC-2SL:
|
||||
|
||||
MISTO 10,70 EUR/g mediana, P(>=50/g) 20,7%, P(zero) 33,4%
|
||||
> CONC-DIV 5,70 / 17,8% / 33,4%
|
||||
> SPARSO 0,00 / 16,8% / 66,0%
|
||||
> CONC-2SL 0,00 / 13,1% / 55,6% (close-only, leva 1,00x)
|
||||
|
||||
Da allora il numero operativo del canale funded e' passato **da 42% a 2,6%** (fattore 16) per
|
||||
tre correzioni misurate in due giorni:
|
||||
(a) universo XS01 **fuori campione** — 6 delle 19 gambe non esistevano nel 2021-23 e valevano
|
||||
il **42% del J pubblicato** (§29);
|
||||
(b) **banda d'ancora di SKH01** — l'offset canonico e' al 91° pctl dei 23 (§32);
|
||||
(c) **funding** — mai modellato nel 2025, costa dJ -0,049 su 69/69 celle appaiate (§35, §37).
|
||||
Piu' due lenti che il 25/07 non aveva o aveva diverse: la **wick accoppiata** (misurata il
|
||||
25/07 sera, dopo il ladder) e il de-luck **x0,89** invece di x0,60 (misurato il 26/07).
|
||||
|
||||
**Nessuno ha ri-misurato la politica.** La conclusione "MISTO" e' stata EREDITATA.
|
||||
|
||||
LE ATTESE, REGISTRATE PRIMA DI MISURARE
|
||||
---------------------------------------
|
||||
A1. **La replica dell'ORDINE riuscira', quella dei LIVELLI no.** `pl.sleeve_panel` ri-deriva le
|
||||
serie dal feed corrente (+4 settimane dal 25/07): §37 ha gia' visto -11% su `r0725_hyro`
|
||||
per la stessa ragione. Mi aspetto livelli 10-30% piu' bassi e ordine identico.
|
||||
A2. **Il funding NON puo' ribaltare l'ordine, e semmai lo RAFFORZA.** Il costo di funding per
|
||||
sleeve e' misurato (§35/§37, a peso 1,0): TP01 2,10%/a, SKH01 2,32%/a, XS01 1,45%/a. Il
|
||||
costo medio di politica e' quindi CONC-2SL 2,16% > CONC-A 2,04% > CONC-DIV 1,98% >
|
||||
SPARSO 1,96% > **MISTO 1,82%** — cioe' il vincitore del 25/07 e' anche quello che paga
|
||||
MENO funding, ma lo spread e' solo **0,33 pp/anno** (un sesto del costo). Previsione:
|
||||
tutti i livelli scendono, l'ordine resta, il delta MISTO-vs-resto si allarga di poco e
|
||||
dentro il rumore.
|
||||
A3. **La correzione che PUO' ribaltare l'ordine e' XS01, non il funding.** MISTO e SPARSO
|
||||
poggiano su conti a **XS01 puro** (2/6 e 2/6 slot); se il drift di XS01 fuori campione e'
|
||||
piu' debole (Sharpe 1,04 e maxDD -19,2% contro -11,2% dello sleeve ufficiale, §29), sono
|
||||
proprio quelle politiche a perdere. Previsione: **se l'ordine cambia, cambia CONTRO
|
||||
MISTO/SPARSO e a favore di CONC-DIV / CONC-A**, e per l'universo, non per il funding.
|
||||
A4. **Il numero di conti e' il vero vincolo, e non e' stato mai misurato.** Con $654 di cassa
|
||||
e un biglietto HYRO $100k a $579 si compra **UN** conto e restano $75: mi aspetto che la
|
||||
stragrande maggioranza dei percorsi non arrivi MAI a due conti simultanei, e quindi che
|
||||
**MISTO e CONC-DIV, che condividono lo slot 0, siano quasi indistinguibili** finche' non
|
||||
entrano soldi. Previsione: la politica conta poco senza versamenti e molto con versamenti.
|
||||
A5. **Il deposito rimborsabile (§30) fa piu' differenza sulla SCALA che sull'EV**, perche' i
|
||||
$579 che tornano ricomprano un biglietto: +1-3pp su P(>=50/g), come previde §37.
|
||||
|
||||
COSA FA
|
||||
-------
|
||||
§1 REPLICA del 25/07 (`pl.simulate` close-only/indip, `pc.simulate` accoppiata).
|
||||
§2 CONTROLLI: `ladder_multi` bit-exact contro `pc.simulate`; controllo POSITIVO (una politica
|
||||
deliberatamente rotta deve finire ultima, o il confronto non ha potenza).
|
||||
§3 DECOMPOSIZIONE a un grado di liberta' per volta: lente -> de-luck -> universo/finestra ->
|
||||
funding. E' il metodo di §29: senza, non si sa QUALE correzione fa cosa.
|
||||
§4 BANDA D'ANCORA: 8 offset x 3 fasi (sottocampione DICHIARATO), con e senza funding, tutte
|
||||
le politiche insieme sugli STESSI sorteggi -> mediana delle differenze APPAIATE.
|
||||
§5 La previsione A2 sul funding, verificata.
|
||||
§6 LA DIMENSIONE MAI TESTATA: la traiettoria del NUMERO DI CONTI nel tempo, con quota
|
||||
rimborsabile e con/senza versamenti.
|
||||
§7 MDE, e verdetto.
|
||||
|
||||
CONVENZIONI DICHIARATE
|
||||
----------------------
|
||||
* Le POLITICHE sono quelle del 25/07, non ridefinite (altrimenti si misura una ridefinizione,
|
||||
non una correzione). Aggiunte due sole varianti, dichiarate: **CONC-A** = W_A 38/38/25 (il
|
||||
centro della regione robusta di §29, cioe' la configurazione su cui poggiano §32 e §37) e
|
||||
**MISTO-A** = MISTO col nucleo W_A al posto di book_div (separa "il nucleo" dal "mescolare").
|
||||
* Lente **ACCOPPIATA** ovunque nella parte corretta; close-only e indipendente solo come
|
||||
repliche/controlli. De-luck **x0,89** (conservativo: §32 nota che sulla dimensione d'ancora
|
||||
e' un doppio conteggio).
|
||||
* Leva **fissa** e uguale per tutte le politiche: il confronto e' appaiato, e `best_lev` per
|
||||
configurazione introdurrebbe un grado di liberta' diverso per politica. Girata a 1,00x
|
||||
(grid piena) e 0,75x (controllo).
|
||||
* Firm death 10%/anno, cap $200k/firm, fisco 33%, split 80% — tutte ereditate, nessuna scelta.
|
||||
* Bootstrap a blocchi (20 g) **condiviso**: stessi indici e stessi sorteggi di morte-firm per
|
||||
TUTTE le politiche -> differenze appaiate.
|
||||
|
||||
Rete: NESSUNA (`--no-net` forzato; cache funding e cache Binance gia' su disco).
|
||||
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_ladder_redo.py`
|
||||
"""
|
||||
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]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
if "--no-net" not in sys.argv:
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07)
|
||||
import r0725_prop_ladder as pl # noqa: E402 ladder originale + regole firm (25/07)
|
||||
import r0822_prop_alloc as PA # noqa: E402 bootstrap/J (22/08)
|
||||
import r0822b_prop_recal as PR # noqa: E402 (R,G) su pannello libero (22/08)
|
||||
import r0822b_xs01_oos as X # noqa: E402 motore XS01 + U13 Binance (22/08)
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (22/08)
|
||||
import r0822d_funding as FD # noqa: E402 funding (22/08)
|
||||
import r0822d_gate_c as GC # noqa: E402 funding per-sleeve + ladder a 1 cfg (22/08)
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
EURUSD = pl.EURUSD
|
||||
TAX_RATE = pl.TAX_RATE
|
||||
SPLIT = pl.SPLIT
|
||||
FIRMS = pl.FIRMS
|
||||
MAXC = pl.MAX_CONCURRENT # 6 slot
|
||||
HDAYS = pl.HORIZON_MONTHS * 30 # 1080
|
||||
DELUCK_NEW = PA.DELUCK # 0,89 (26/07)
|
||||
DELUCK_OLD = pl.DELUCK # 0,60 (25/07)
|
||||
TARGET = pl.TARGET_EUR_DAY
|
||||
MIN_PRICE = min(p for F in FIRMS for (_s, p) in F["tickets"])
|
||||
|
||||
# --- sottocampioni DICHIARATI (a priori, uniformi) -------------------------------------------
|
||||
OFFSETS = AP.OFFSETS[::3] # 8 degli 23 offset: 0,90,...,630 — uniforme a priori
|
||||
PHASES = GC.PHASES_SUB # (0,3,7) — lo stesso di §37
|
||||
N_PATHS = 1500 # risoluzione MC dichiarata (~+-1pp su P(>=50/g))
|
||||
N_PATHS_REP = 3000 # per la replica: identico al 25/07
|
||||
SEED = 20260725 # il seme del 25/07
|
||||
LEV_MAIN, LEV_ALT = 1.00, 0.75
|
||||
|
||||
# `--quick` = AUTO-TEST: esercita ogni ramo di codice in ~2 minuti. NON produce numeri da citare
|
||||
# (griglia e percorsi ridotti); serve a non scoprire un errore di stampa dopo venti minuti.
|
||||
QUICK = "--quick" in sys.argv
|
||||
if QUICK:
|
||||
OFFSETS = OFFSETS[:2]
|
||||
PHASES = PHASES[:2]
|
||||
N_PATHS = 300
|
||||
N_PATHS_REP = 300
|
||||
|
||||
# --- numeri PUBBLICATI da riprodurre ----------------------------------------------------------
|
||||
PUB_2507 = { # r0725_prop_ladder, CLOSE-ONLY leva 1,00x
|
||||
"CONC-2SL": (0.00, 0.131, 0.556),
|
||||
"CONC-DIV": (5.70, 0.178, 0.334),
|
||||
"SPARSO": (0.00, 0.168, 0.660),
|
||||
"MISTO": (10.70, 0.207, 0.334),
|
||||
}
|
||||
PUB_COUPLED = "P(>=50/g) 5,6-6,5% a 0,75x, P(zero) 52% a 0,75x / 65% a 1,00x (25/07 sera)"
|
||||
PUB_GATEC = dict(p50g=0.026, pzero=0.403) # §37, W_A, funding dentro, mediana d'ancora
|
||||
|
||||
# --- costi di funding per sleeve, PUBBLICATI (§35/§37), a peso 1,0 ----------------------------
|
||||
FUND_PUB = {"TP01": 0.0210, "SKH01": 0.0232, "XS01": 0.0145}
|
||||
|
||||
TRIALS = 0
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
def bump(n: int = 1) -> None:
|
||||
global TRIALS
|
||||
TRIALS += n
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "=" * 104)
|
||||
if t:
|
||||
print(" " + t)
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
def tick(lab: str) -> None:
|
||||
print(f" [{time.time() - T0:6.0f}s] {lab}")
|
||||
|
||||
|
||||
# ==================================================================================
|
||||
# LA MACCHINA: una scala di conti che gira PIU' POLITICHE sugli STESSI sorteggi
|
||||
# ==================================================================================
|
||||
|
||||
def ladder_multi(Rm: np.ndarray, Gm: np.ndarray, sel: np.ndarray, n_paths: int, lev: float,
|
||||
seed: int, deluck: float, refund: bool = False, deposit_eur: float = 0.0,
|
||||
start_eur: float = pl.START_EUR, block: int = 20,
|
||||
boot: np.ndarray | None = None,
|
||||
track: bool = False) -> dict:
|
||||
"""`r0725_prop_coupled.simulate` generalizzato a N politiche in una corsa sola.
|
||||
|
||||
Rm, Gm : (giorni, n_cfg) — ritorni di chiusura e gap (<=0) di ciascuna CONFIGURAZIONE.
|
||||
sel : (n_group, n_slot) — indice di configurazione per ogni slot di ogni politica.
|
||||
|
||||
I gruppi (= politiche) sono impilati sull'asse dei percorsi e ricevono **gli stessi indici
|
||||
di bootstrap e gli stessi sorteggi di morte-firm**: il confronto fra politiche e' appaiato
|
||||
per costruzione (stesso mondo, allocazione diversa).
|
||||
|
||||
Con n_group=1 e refund/deposit spenti consuma il flusso di numeri casuali NELLO STESSO
|
||||
ORDINE di `pc.simulate` -> deve essere BIT-EXACT (controllo §2).
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
L = len(Rm)
|
||||
n_grp, n_slot = sel.shape
|
||||
if boot is None:
|
||||
n_blocks = int(np.ceil(HDAYS / block))
|
||||
st = rng.integers(0, L - block, size=(n_paths, n_blocks))
|
||||
boot = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :HDAYS]
|
||||
# morte-firm: un solo sorteggio (n_days, n_paths, n_firm) == n_days chiamate consecutive
|
||||
du = rng.random((HDAYS, n_paths, len(FIRMS)))
|
||||
|
||||
Rd = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True)
|
||||
P = n_grp * n_paths
|
||||
SELP = np.repeat(sel, n_paths, axis=0) # (P, n_slot)
|
||||
rowsP = np.arange(P)[:, None]
|
||||
|
||||
cash = np.full(P, start_eur * EURUSD)
|
||||
phase = np.full((P, n_slot), -1, np.int8)
|
||||
eq = np.ones((P, n_slot))
|
||||
firm = np.full((P, n_slot), -1, np.int8)
|
||||
notional = np.zeros((P, n_slot))
|
||||
price_paid = np.zeros((P, n_slot))
|
||||
refunded = np.zeros((P, n_slot), bool)
|
||||
firm_alive = np.ones((P, len(FIRMS)), bool)
|
||||
cum_payout = np.zeros(P)
|
||||
spent = np.zeros(P)
|
||||
got_back = np.zeros(P)
|
||||
n_tickets = np.zeros(P, int)
|
||||
max_conc = np.zeros(P, int)
|
||||
max_open = np.zeros(P, int)
|
||||
ever2 = np.zeros(P, bool)
|
||||
ever_slot1 = np.zeros(P, bool) # lo slot 1 e' mai stato aperto? (= la politica conta)
|
||||
traj = np.zeros((HDAYS // 30, n_grp)) if track else None
|
||||
p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||
tick_sorted = [sorted(F["tickets"], key=lambda x: -x[0]) for F in FIRMS]
|
||||
|
||||
for t in range(HDAYS):
|
||||
died = np.tile(du[t], (n_grp, 1)) < p_death_day
|
||||
newly = died & firm_alive
|
||||
firm_alive &= ~died
|
||||
if newly.any():
|
||||
for fi in range(len(FIRMS)):
|
||||
kill = newly[:, fi]
|
||||
if kill.any():
|
||||
hit = (firm == fi) & kill[:, None]
|
||||
phase[hit] = -1
|
||||
notional[hit] = 0.0
|
||||
|
||||
if (cash >= MIN_PRICE).any():
|
||||
for k in range(n_slot):
|
||||
free = phase[:, k] == -1
|
||||
if not free.any():
|
||||
continue
|
||||
for fi, F in enumerate(FIRMS):
|
||||
used = np.where(firm == fi, notional, 0.0).sum(axis=1)
|
||||
for (size, price) in tick_sorted[fi]:
|
||||
buy = (free & firm_alive[:, fi] & (cash >= price)
|
||||
& (used + size <= F["cap"]))
|
||||
if not buy.any():
|
||||
continue
|
||||
cash[buy] -= price
|
||||
spent[buy] += price
|
||||
n_tickets[buy] += 1
|
||||
phase[buy, k] = 0
|
||||
eq[buy, k] = 1.0
|
||||
firm[buy, k] = fi
|
||||
notional[buy, k] = size
|
||||
price_paid[buy, k] = price
|
||||
refunded[buy, k] = False
|
||||
if k >= 1:
|
||||
ever_slot1 |= buy
|
||||
free = free & ~buy
|
||||
if not free.any():
|
||||
break
|
||||
if not free.any():
|
||||
break
|
||||
|
||||
rd = Rd[boot[:, t], :] # (n_paths, n_cfg)
|
||||
gd = Gm[boot[:, t], :]
|
||||
rdP = np.tile(rd, (n_grp, 1))
|
||||
gdP = np.tile(gd, (n_grp, 1))
|
||||
r = rdP[rowsP, SELP] * lev
|
||||
g = gdP[rowsP, SELP] * lev
|
||||
|
||||
act = phase >= 0
|
||||
eq_prev = eq
|
||||
eq = np.where(act, eq * (1.0 + r), eq)
|
||||
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||
r_low = r + g
|
||||
|
||||
for fi, F in enumerate(FIRMS):
|
||||
s_ = act & (firm == fi)
|
||||
if not s_.any():
|
||||
continue
|
||||
ev_t, ev_dd, ev_dl = F["ev"]
|
||||
fu_ml, fu_dl = F["fu"]
|
||||
in_ev = s_ & (phase == 0)
|
||||
in_fu = s_ & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl)))
|
||||
| (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))))
|
||||
phase[bust] = -1
|
||||
notional[bust] = 0.0
|
||||
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||
phase[passed] = 1
|
||||
eq[passed] = 1.0
|
||||
|
||||
nfu = (phase == 1).sum(axis=1)
|
||||
max_conc = np.maximum(max_conc, nfu)
|
||||
max_open = np.maximum(max_open, (phase >= 0).sum(axis=1))
|
||||
ever2 |= nfu >= 2
|
||||
|
||||
if (t + 1) % 30 == 0:
|
||||
fu = phase == 1
|
||||
gain = np.where(fu & (eq > 1.0), (eq - 1.0) * notional, 0.0)
|
||||
net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE)
|
||||
cum_payout += net
|
||||
cash += net
|
||||
pay = fu & (eq > 1.0)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if refund:
|
||||
back = pay & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
if deposit_eur:
|
||||
cash += deposit_eur * EURUSD
|
||||
if track:
|
||||
traj[(t + 1) // 30 - 1] = nfu.reshape(n_grp, n_paths).mean(axis=1)
|
||||
|
||||
out = []
|
||||
for gi in range(n_grp):
|
||||
s = slice(gi * n_paths, (gi + 1) * n_paths)
|
||||
ed = cum_payout[s] / EURUSD / (HDAYS / 365.0) / 365.0
|
||||
out.append(dict(eur_day=ed, med=float(np.median(ed)),
|
||||
p10=float(np.percentile(ed, 10)), p90=float(np.percentile(ed, 90)),
|
||||
mean=float(ed.mean()),
|
||||
p_ge10=float((ed >= 10.0).mean()),
|
||||
p_target=float((ed >= TARGET).mean()),
|
||||
p_zero=float((cum_payout[s] <= 0).mean()),
|
||||
n_tickets=float(n_tickets[s].mean()),
|
||||
max_conc=float(max_conc[s].mean()),
|
||||
max_open=float(max_open[s].mean()),
|
||||
p_ever2=float(ever2[s].mean()),
|
||||
p_slot1=float(ever_slot1[s].mean()),
|
||||
med_cash=float(np.median(cash[s])),
|
||||
mean_back=float(got_back[s].mean()),
|
||||
spent=float(spent[s].mean())))
|
||||
bump(n_grp)
|
||||
return dict(groups=out, traj=traj)
|
||||
|
||||
|
||||
# ==================================================================================
|
||||
# POLITICHE
|
||||
# ==================================================================================
|
||||
W_2SL = {"TP01": 0.75, "SKH01": 0.25}
|
||||
W_DIV = {"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25}
|
||||
W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25}
|
||||
W_TP = {"TP01": 1.0}
|
||||
W_SKH = {"SKH01": 1.0}
|
||||
W_XS = {"XS01": 1.0}
|
||||
CFGS = [W_2SL, W_DIV, W_A, W_TP, W_SKH, W_XS] # indici 0..5
|
||||
I2SL, IDIV, IA, ITP, ISKH, IXS = range(6)
|
||||
|
||||
POLICIES = {
|
||||
"CONC-2SL ogni conto = book live": [I2SL] * MAXC,
|
||||
"CONC-DIV ogni conto = diversif.": [IDIV] * MAXC,
|
||||
"SPARSO 1 sleeve per conto": [ITP, IXS, ISKH, ITP, IXS, ISKH],
|
||||
"MISTO meta' div / meta' single": [IDIV, IXS, IDIV, ITP, IDIV, IXS],
|
||||
"CONC-A ogni conto = W_A 38/38/25": [IA] * MAXC,
|
||||
"MISTO-A MISTO col nucleo W_A": [IA, IXS, IA, ITP, IA, IXS],
|
||||
}
|
||||
PNAMES = list(POLICIES)
|
||||
SEL = np.array([POLICIES[p] for p in PNAMES], dtype=int)
|
||||
SHORT = [p.split()[0] for p in PNAMES]
|
||||
|
||||
|
||||
def _tab(rows: list[tuple], head: tuple) -> None:
|
||||
print(" " + " ".join(f"{h:>{w}}" for h, w in head))
|
||||
for r in rows:
|
||||
print(" " + " ".join(f"{v:>{w}}" for v, (_h, w) in zip(r, head)))
|
||||
|
||||
|
||||
# ==================================================================================
|
||||
def main() -> None:
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
print("=" * 104)
|
||||
print(" §60 LADDER-REDO — la politica multi-conto e' ancora MISTO?")
|
||||
print("=" * 104)
|
||||
print(" ATTESE REGISTRATE (dal docstring, prima di misurare):")
|
||||
print(" A1 l'ORDINE del 25/07 si replica, i LIVELLI no (+4 settimane di feed).")
|
||||
print(" A2 il FUNDING non ribalta l'ordine e semmai lo rafforza (MISTO paga MENO:")
|
||||
print(" 1,82%/a contro 2,16% di CONC-2SL; spread 0,33pp = un sesto del costo).")
|
||||
print(" A3 se l'ordine cambia, cambia per l'UNIVERSO XS01 e CONTRO MISTO/SPARSO.")
|
||||
print(" A4 con $654 si compra UN conto: la politica quasi non conta senza versamenti.")
|
||||
print(" A5 il deposito rimborsabile vale piu' sulla SCALA che sull'EV (+1-3pp).")
|
||||
|
||||
# ============================================================ §1 REPLICA 25/07
|
||||
hr("§1. REPLICA — i numeri pubblicati il 25/07, rigirati oggi con la stessa macchina")
|
||||
M, names = pl.sleeve_panel()
|
||||
print(f" pannello `pl.sleeve_panel()`: {len(M)} giorni, sleeve {names}, de-luck x{DELUCK_OLD}")
|
||||
pols_old = pl.policies(names)
|
||||
rows = []
|
||||
got = {}
|
||||
for pname, slots in pols_old.items():
|
||||
key = pname.split()[0]
|
||||
r = pl.simulate(M, slots, n_paths=N_PATHS_REP, lev=1.0, seed=SEED, intraday=False)
|
||||
got[key] = (r["med"], r["p_target"], r["p_zero"])
|
||||
pmed, pp50, ppz = PUB_2507[key]
|
||||
rows.append((key, f"{r['med']:.2f}", f"{pmed:.2f}",
|
||||
f"{r['p_target']:.1%}", f"{pp50:.1%}",
|
||||
f"{r['p_zero']:.1%}", f"{ppz:.1%}"))
|
||||
bump()
|
||||
_tab(rows, (("politica", 9), ("EUR/g oggi", 11), ("PUB", 6),
|
||||
("P>=50 oggi", 11), ("PUB", 6), ("P0 oggi", 8), ("PUB", 6)))
|
||||
ord_now = sorted(got, key=lambda k: -got[k][1])
|
||||
ord_pub = sorted(PUB_2507, key=lambda k: -PUB_2507[k][1])
|
||||
same = ord_now == ord_pub
|
||||
print(f"\n ordine su P(>=50/g) oggi: {' > '.join(ord_now)}")
|
||||
print(f" ordine su P(>=50/g) 25/07: {' > '.join(ord_pub)}")
|
||||
print(f" >>> ORDINE {'REPLICATO' if same else 'NON replicato'}; livelli "
|
||||
f"{'piu bassi' if np.mean([got[k][1] for k in got]) < np.mean([PUB_2507[k][1] for k in PUB_2507]) else 'piu alti'}"
|
||||
f" (media P(>=50/g) {np.mean([got[k][1] for k in got]):.1%} contro "
|
||||
f"{np.mean([PUB_2507[k][1] for k in PUB_2507]):.1%}).")
|
||||
print(" A1 " + ("CONFERMATA" if same else "REFUTATA") +
|
||||
" — `pl.sleeve_panel` ri-deriva le serie dal feed CORRENTE (stessa causa del -11%")
|
||||
print(" che §37 misuro' su `r0725_hyro`): la macchina e' quella, il vintage no.")
|
||||
tick("§1")
|
||||
|
||||
# ============================================================ §2 CONTROLLI
|
||||
hr("§2. CONTROLLI — bit-exact contro `pc.simulate`, e un controllo POSITIVO")
|
||||
cfgs_pc = [(0.75, 0.25, 0.0), (0.55, 0.20, 0.25), (1.0, 0.0, 0.0),
|
||||
(0.0, 1.0, 0.0), (0.0, 0.0, 1.0)]
|
||||
START_OLD = "2024-01-01"
|
||||
mats = [pc.book_tuples(*c, start=START_OLD) for c in cfgs_pc]
|
||||
Lc = min(len(m[0]) for m in mats)
|
||||
Rc = np.stack([m[0][-Lc:] for m in mats], axis=1)
|
||||
Gc = np.stack([m[1][-Lc:] for m in mats], axis=1)
|
||||
pc.DELUCK = DELUCK_OLD
|
||||
slots_conc = [0] * MAXC
|
||||
ref = pc.simulate(cfgs_pc, slots_conc, n_paths=800, lev=1.0, seed=99,
|
||||
lens="coupled", start=START_OLD)
|
||||
mine = ladder_multi(Rc, Gc, np.array([slots_conc]), n_paths=800, lev=1.0, seed=99,
|
||||
deluck=DELUCK_OLD)["groups"][0]
|
||||
d = float(np.abs(np.sort(ref["eur_day"]) - np.sort(mine["eur_day"])).max())
|
||||
d2 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max())
|
||||
print(f" ladder_multi (1 gruppo) vs pc.simulate, lente accoppiata, 800 percorsi:")
|
||||
print(f" max|diff| percorso-per-percorso = {d2:.3e} (ordinati: {d:.3e})")
|
||||
print(f" -> {'BIT-EXACT' if d2 == 0.0 else 'DIVERGENTE'}")
|
||||
# controllo positivo: una politica volutamente rotta (leva 3x = busta subito) deve finire ultima
|
||||
selpos = np.array([slots_conc, slots_conc])
|
||||
rp = ladder_multi(Rc, Gc, selpos, n_paths=800, lev=1.0, seed=99, deluck=DELUCK_OLD)
|
||||
rbad = ladder_multi(Rc, Gc, np.array([slots_conc]), n_paths=800, lev=3.0, seed=99,
|
||||
deluck=DELUCK_OLD)["groups"][0]
|
||||
ok_pos = rbad["p_target"] < rp["groups"][0]["p_target"]
|
||||
print(f" controllo POSITIVO — la stessa politica a leva 3,00x (deve bustare):")
|
||||
print(f" P(>=50/g) 1,00x {rp['groups'][0]['p_target']:.1%} vs 3,00x {rbad['p_target']:.1%}"
|
||||
f" -> {'il confronto ha POTENZA' if ok_pos else '!! NESSUNA POTENZA'}")
|
||||
print(f" controllo di pairing — due gruppi identici devono dare lo stesso vettore: "
|
||||
f"max|diff| {np.abs(rp['groups'][0]['eur_day'] - rp['groups'][1]['eur_day']).max():.3e}")
|
||||
tick("§2")
|
||||
|
||||
# ============================================================ §3 pannello corretto
|
||||
hr("§3. IL PANNELLO CORRETTO — U13 Binance fuori campione, W-LONG 2021+, funding")
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.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])
|
||||
XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES}
|
||||
bump(len(PHASES))
|
||||
xs0 = XSP[PHASES[0]]
|
||||
cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index
|
||||
base = cry_idx.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC"))
|
||||
I = base[base >= S_LONG]
|
||||
print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | "
|
||||
f"XS01 = U13 Binance ({len(X.U13)} gambe) | de-luck x{DELUCK_NEW}")
|
||||
|
||||
rate = GC.funding_rate()
|
||||
if rate is None:
|
||||
print(" !! cache funding assente: il filone NON puo' rispondere alla domanda (c). STOP.")
|
||||
return
|
||||
xsf = GC.xs_funding_measure(verbose=False)
|
||||
xs_ann = xsf["ann"]
|
||||
print(f" funding: TP01/SKH01 da serie misurate; XS01 = {xs_ann*100:+.2f}%/anno costante "
|
||||
f"(§37, IC95 [{(xs_ann-1.96*xsf['se'])*100:+.2f}, {(xs_ann+1.96*xsf['se'])*100:+.2f}])")
|
||||
|
||||
boot = PA.boot_idx(len(I), HDAYS, N_PATHS, seed=SEED)
|
||||
|
||||
def build_on(idx: pd.DatetimeIndex, xs: pd.DataFrame, off: int,
|
||||
fund: bool) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""(R, gap) di TUTTE le configurazioni su un indice e una serie XS01 dati."""
|
||||
R = np.zeros((len(idx), len(CFGS)))
|
||||
G = np.zeros((len(idx), len(CFGS)))
|
||||
for ci, w in enumerate(CFGS):
|
||||
r, g = AP.rg_off(w, idx, xs, off)
|
||||
if fund:
|
||||
r = r - GC.fvec_for(w, idx, rate, off, xs_ann)
|
||||
R[:, ci], G[:, ci] = r, g
|
||||
return R, G
|
||||
|
||||
def build(off: int, ph: int, fund: bool) -> tuple[np.ndarray, np.ndarray]:
|
||||
return build_on(I, XSP[ph], off, fund)
|
||||
|
||||
# costo di funding EFFETTIVO per configurazione (verifica di A2)
|
||||
print(f"\n costo di funding per configurazione (%/anno di equity a leva 1,00x, off 0):")
|
||||
fcost = {}
|
||||
for ci, w in enumerate(CFGS):
|
||||
fv = GC.fvec_for(w, I, rate, 0, xs_ann)
|
||||
fcost[ci] = float(np.mean(fv)) * 365 * 100
|
||||
nm = ["book_2SL", "book_DIV", "W_A", "TP01 solo", "SKH01 solo", "XS01 solo"]
|
||||
for ci in range(len(CFGS)):
|
||||
pred = sum(FUND_PUB[k] * v for k, v in CFGS[ci].items()) * 100
|
||||
print(f" {nm[ci]:<11} misurato {fcost[ci]:>5.2f}%/a atteso da §35/§37 {pred:>5.2f}%/a")
|
||||
# CONTROLLO: il reindex su I non deve mangiare barre in silenzio (un NaN->0 e' un costo
|
||||
# sottostimato che nessun numero segnala).
|
||||
cov_tp = float(GC.f_tp01_sleeve(rate).reindex(I).notna().mean())
|
||||
cov_sk = float(GC.f_skh_sleeve(rate, 0).reindex(I).notna().mean())
|
||||
print(f" CONTROLLO di copertura su W-LONG: TP01 {cov_tp:.1%} delle barre, SKH01 {cov_sk:.1%}"
|
||||
f" -> {'ok' if min(cov_tp, cov_sk) > 0.99 else '!! IL REINDEX MANGIA BARRE'}")
|
||||
print(" ⚠ I costi misurati sono PIU' BASSI degli attesi, e non e' un disaccordo: §35 pubblica")
|
||||
print(" -2,16%/a sul 2019+, -1,39% sul 2022-03+ e -0,55% sul 2025+ (il costo SCENDE nel")
|
||||
print(" tempo) e questa finestra parte nel 2021. Costo del libro 75/25, anno per anno:")
|
||||
fser = pd.Series(GC.fvec_for(W_2SL, I, rate, 0, xs_ann), index=I)
|
||||
print(" " + " ".join(f"{y}:{float(g.mean()*365)*100:.2f}%"
|
||||
for y, g in fser.groupby(fser.index.year)))
|
||||
print(f" 🚨 Su tutta la storia oraria SKH01 paga -2,32% e TP01 -2,10% (§35). Su W-LONG sono")
|
||||
print(f" {fcost[ISKH]:.2f}% e {fcost[ITP]:.2f}%: **il verso del confronto TP01-vs-SKH01 sul")
|
||||
print(f" funding si INVERTE cambiando finestra** — l'aritmetica con cui avevo derivato A2")
|
||||
print(f" usava i numeri di storia piena, quindi la previsione va verificata, non dedotta.")
|
||||
print(f" costo di funding per POLITICA (media sugli slot):")
|
||||
polfund = {}
|
||||
for pi, p in enumerate(PNAMES):
|
||||
v = float(np.mean([fcost[c] for c in SEL[pi]]))
|
||||
polfund[SHORT[pi]] = v
|
||||
print(f" {SHORT[pi]:<9} {v:>5.2f}%/a")
|
||||
tick("§3")
|
||||
|
||||
# ============================================================ §4 decomposizione
|
||||
hr("§4. DECOMPOSIZIONE — un grado di liberta' per volta (ancora 0, fase 0, leva 1,00x)")
|
||||
print(" Senza questa tabella si sa che il numero e' cambiato, non QUALE correzione lo cambia.")
|
||||
# gli indici di CFGS non sono quelli di cfgs_pc: si rimappa (le prime 4 politiche sono
|
||||
# le uniche del 25/07; CONC-A e MISTO-A non esistevano e compaiono solo dal passo S3).
|
||||
map_pc = {I2SL: 0, IDIV: 1, ITP: 2, ISKH: 3, IXS: 4}
|
||||
sel4_pc = np.array([[map_pc[c] for c in POLICIES[p]] for p in PNAMES[:4]])
|
||||
bpc = PA.boot_idx(Lc, HDAYS, N_PATHS, seed=SEED)
|
||||
s0 = ladder_multi(Rc, np.zeros_like(Gc), sel4_pc, N_PATHS, 1.0, SEED, DELUCK_OLD, boot=bpc)
|
||||
s1 = ladder_multi(Rc, Gc, sel4_pc, N_PATHS, 1.0, SEED, DELUCK_OLD, boot=bpc)
|
||||
s2 = ladder_multi(Rc, Gc, sel4_pc, N_PATHS, 1.0, SEED, DELUCK_NEW, boot=bpc)
|
||||
# --- S2b/S3a: UNIVERSO e FINESTRA separati, sullo STESSO indice comune (crypto n HL n Bin).
|
||||
XS_HL = pc.xsec_daily_tuples("shared")
|
||||
I24 = I[I >= pd.Timestamp("2024-01-01", tz="UTC")].intersection(XS_HL.index)
|
||||
b24 = PA.boot_idx(len(I24), HDAYS, N_PATHS, seed=SEED)
|
||||
R2b, G2b = build_on(I24, XS_HL, 0, False)
|
||||
Rchk, Gchk, Ichk = pc.book_tuples(0.55, 0.20, 0.25, start="2024-01-01")
|
||||
kk = pd.DatetimeIndex(Ichk).isin(I24)
|
||||
jj = pd.DatetimeIndex(I24).isin(pd.DatetimeIndex(Ichk))
|
||||
dchk = float(np.abs(Rchk[kk] - R2b[:, IDIV][jj]).max())
|
||||
print(f" CONTROLLO: `rg_off` con le tuple HL riproduce `pc.book_tuples` (book_div) su "
|
||||
f"{int(kk.sum())} giorni a max|diff| = {dchk:.3e}")
|
||||
s2b = ladder_multi(R2b, G2b, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=b24)
|
||||
R3a, G3a = build_on(I24, XSP[PHASES[0]], 0, False)
|
||||
s3a = ladder_multi(R3a, G3a, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=b24)
|
||||
R3, G3 = build(0, PHASES[0], False)
|
||||
s3 = ladder_multi(R3, G3, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=boot)
|
||||
R4, G4 = build(0, PHASES[0], True)
|
||||
s4 = ladder_multi(R4, G4, SEL[:4], N_PATHS, 1.0, SEED, DELUCK_NEW, boot=boot)
|
||||
lab = ["S0 25/07 tale e quale (HL19 2024+, close-only, x0,60)",
|
||||
"S1 + lente ACCOPPIATA (misurata il 25/07 SERA)",
|
||||
"S2 + de-luck x0,89 invece di x0,60 (26/07)",
|
||||
"S2b [= S2 su indice comune: controllo di macchina]",
|
||||
"S3a + UNIVERSO: XS01 = U13 Binance, stessa finestra",
|
||||
"S3b + FINESTRA: W-LONG 2021+ (fuori campione)",
|
||||
"S4 + FUNDING = la lente onesta di oggi"]
|
||||
ALL = (s0, s1, s2, s2b, s3a, s3, s4)
|
||||
print(f"\n P(>=50 EUR/g) per politica, {N_PATHS} percorsi, stessi sorteggi:")
|
||||
print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4]) + f"{' ordine':<40}")
|
||||
for l, st in zip(lab, ALL):
|
||||
v = [g["p_target"] for g in st["groups"]]
|
||||
o = " > ".join([SHORT[i] for i in np.argsort(-np.array(v))])
|
||||
print(f" {l:<52}" + "".join(f"{x:>9.1%} " for x in v) + f" {o:<40}")
|
||||
print(f"\n EUR/g MEDIO per politica (la MEDIANA e' degenere: tabella sotto):")
|
||||
print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4]))
|
||||
for l, st in zip(lab, ALL):
|
||||
print(f" {l:<52}" + "".join(f"{g['mean']:>9.2f} " for g in st["groups"]))
|
||||
print(f"\n EUR/g MEDIANA — il numero di TESTA del 25/07 era 'MISTO 10,70':")
|
||||
print(" " + f"{'passo':<52}" + "".join(f"{s:>10}" for s in SHORT[:4]))
|
||||
for l, st in zip(lab, ALL):
|
||||
print(f" {l:<52}" + "".join(f"{g['med']:>9.2f} " for g in st["groups"]))
|
||||
tick("§4")
|
||||
|
||||
# ============================================================ §5 banda d'ancora
|
||||
hr(f"§5. BANDA D'ANCORA — {len(OFFSETS)} offset x {len(PHASES)} fasi, "
|
||||
f"funding ON/OFF, {len(PNAMES)} politiche")
|
||||
print(f" offset (sottocampione uniforme DICHIARATO dei 23): {list(OFFSETS)}")
|
||||
print(f" fasi XS01: {list(PHASES)} (lo stesso sottocampione di §37)")
|
||||
print(f" leva {LEV_MAIN:.2f}x fissa e uguale per tutte (confronto appaiato); "
|
||||
f"{N_PATHS} percorsi condivisi.")
|
||||
cells = []
|
||||
REF: dict[str, np.ndarray] = {} # vettori per-percorso alla cella di riferimento
|
||||
for off in OFFSETS:
|
||||
for ph in PHASES:
|
||||
for fund in (False, True):
|
||||
R, G = build(off, ph, fund)
|
||||
res = ladder_multi(R, G, SEL, N_PATHS, LEV_MAIN, SEED, DELUCK_NEW, boot=boot)
|
||||
for pi, g in enumerate(res["groups"]):
|
||||
cells.append(dict(off=off, ph=ph, fund=fund, pol=SHORT[pi],
|
||||
p50=g["p_target"], pz=g["p_zero"], med=g["med"],
|
||||
p10g=g["p_ge10"], mean=g["mean"], ever2=g["p_ever2"],
|
||||
slot1=g["p_slot1"], ntix=g["n_tickets"],
|
||||
maxc=g["max_conc"]))
|
||||
if off == 0 and ph == PHASES[0] and fund:
|
||||
REF[SHORT[pi]] = g["eur_day"]
|
||||
tick(f"offset {off}")
|
||||
C = pd.DataFrame(cells)
|
||||
|
||||
for fund, flab in ((True, "CON funding (la lente onesta)"), (False, "SENZA funding")):
|
||||
s = C[C["fund"] == fund]
|
||||
print(f"\n --- {flab} — mediana sulle {len(OFFSETS)*len(PHASES)} celle d'ancora")
|
||||
print(f" {'politica':>10} {'P(>=50/g)':>10} {'banda ancora p10-p90':>22} {'P(zero)':>9} "
|
||||
f"{'EUR/g med':>10} {'EUR/g medio':>12} {'P(>=10/g)':>10} {'P(2o conto)':>12}")
|
||||
for p in SHORT:
|
||||
q = s[s["pol"] == p]
|
||||
print(f" {p:>10} {q['p50'].median():>10.2%} "
|
||||
f"[{np.percentile(q['p50'],10):>9.2%},{np.percentile(q['p50'],90):>9.2%}] "
|
||||
f"{q['pz'].median():>9.1%} {q['med'].median():>10.2f} "
|
||||
f"{q['mean'].median():>12.2f} {q['p10g'].median():>10.1%} "
|
||||
f"{q['slot1'].median():>12.1%}")
|
||||
print(" ordine per metrica (se le tre non concordano, la classifica e' un artefatto")
|
||||
print(" della metrica scelta, non un fatto):")
|
||||
for mkey, mlab in (("p50", "P(>=50/g)"), ("mean", "EUR/g medio"), ("p10g", "P(>=10/g)")):
|
||||
o = s.groupby("pol")[mkey].median().sort_values(ascending=False)
|
||||
print(f" {mlab:<12} {' > '.join(o.index)}")
|
||||
|
||||
# riproduzione di §37 (W_A = CONC-A, funding dentro)
|
||||
q = C[(C["fund"]) & (C["pol"] == "CONC-A")]
|
||||
print(f"\n RIPRODUZIONE §37 (CONC-A = W_A 38/38/25, funding dentro, mediana d'ancora):")
|
||||
print(f" P(>=50/g) {q['p50'].median():.1%} (PUB {PUB_GATEC['p50g']:.1%}) | "
|
||||
f"P(zero) {q['pz'].median():.1%} (PUB {PUB_GATEC['pzero']:.1%})")
|
||||
print(" ⚠ non e' identica per costruzione: §37 usa `best_lev` per configurazione e 23")
|
||||
print(" offset x 3 fasi; qui la leva e' FISSA a 1,00x per tutte le politiche (appaiato)")
|
||||
print(f" e gli offset sono {len(OFFSETS)}. L'ordine di grandezza e' il controllo.")
|
||||
|
||||
# differenze APPAIATE cella per cella
|
||||
print(f"\n DIFFERENZE APPAIATE cella per cella (mediana delle differenze, mai differenza")
|
||||
print(f" delle mediane), funding dentro. Riferimento = MISTO (il vincitore del 25/07):")
|
||||
sf = C[C["fund"]]
|
||||
piv = sf.pivot_table(index=["off", "ph"], columns="pol", values="p50")
|
||||
ncell = len(piv)
|
||||
print(f" {'politica':>10} {'d P(>=50/g) vs MISTO':>22} {'>0 su celle':>13} "
|
||||
f"{'p10':>8} {'p90':>8}")
|
||||
for p in SHORT:
|
||||
if p == "MISTO":
|
||||
continue
|
||||
d = piv[p] - piv["MISTO"]
|
||||
print(f" {p:>10} {d.median()*100:>21.2f}pp {int((d>0).sum()):>7}/{ncell:<5} "
|
||||
f"{np.percentile(d,10)*100:>7.2f}pp {np.percentile(d,90)*100:>7.2f}pp")
|
||||
# --- controllo di LEVA: l'ordine dipende dalla leva scelta?
|
||||
print(f"\n CONTROLLO DI LEVA — le stesse celle a {LEV_ALT:.2f}x (off 0, {len(PHASES)} fasi,")
|
||||
print(f" funding ON). Se l'ordine cambia con la leva, non e' una proprieta' della politica:")
|
||||
alt = []
|
||||
for ph in PHASES:
|
||||
R, G = build(0, ph, True)
|
||||
res = ladder_multi(R, G, SEL, N_PATHS, LEV_ALT, SEED, DELUCK_NEW, boot=boot)
|
||||
for pi, g in enumerate(res["groups"]):
|
||||
alt.append(dict(ph=ph, pol=SHORT[pi], p50=g["p_target"], pz=g["p_zero"],
|
||||
mean=g["mean"]))
|
||||
A = pd.DataFrame(alt)
|
||||
oa = A.groupby("pol")["p50"].median().sort_values(ascending=False)
|
||||
o1 = C[(C["fund"]) & (C["off"] == 0)].groupby("pol")["p50"].median().sort_values(ascending=False)
|
||||
print(f" {'politica':>10} {'P(>=50/g) @0,75x':>18} {'P(>=50/g) @1,00x':>18} {'P(zero) @0,75x':>16}")
|
||||
for p_ in SHORT:
|
||||
print(f" {p_:>10} {A[A['pol']==p_]['p50'].median():>17.2%} "
|
||||
f"{o1[p_]:>17.2%} {A[A['pol']==p_]['pz'].median():>15.1%}")
|
||||
print(f" ordine @0,75x: {' > '.join(oa.index)}")
|
||||
print(f" ordine @1,00x: {' > '.join(o1.index)}")
|
||||
print(f" -> {'STESSO ordine' if list(oa.index) == list(o1.index) else 'ORDINE DIVERSO: la leva conta'}")
|
||||
tick("§5")
|
||||
|
||||
# ============================================================ §6 la previsione sul funding
|
||||
hr("§6. LA PREVISIONE A2 SUL FUNDING, VERIFICATA (appaiata cella per cella)")
|
||||
pv = C.pivot_table(index=["off", "ph", "pol"], columns="fund", values="p50")
|
||||
pv = pv.reset_index()
|
||||
print(f" {'politica':>10} {'costo fund %/a':>15} {'d P(>=50/g)':>13} {'<0 su celle':>13} "
|
||||
f"{'d EUR/g med':>13}")
|
||||
dmed = {}
|
||||
for p in SHORT:
|
||||
q = pv[pv["pol"] == p]
|
||||
d = (q[True] - q[False]).values
|
||||
dmed[p] = float(np.median(d))
|
||||
pm = C.pivot_table(index=["off", "ph", "pol"], columns="fund", values="med").reset_index()
|
||||
qm = pm[pm["pol"] == p]
|
||||
dm = (qm[True] - qm[False]).values
|
||||
print(f" {p:>10} {polfund[p]:>14.2f}% {np.median(d)*100:>12.2f}pp "
|
||||
f"{int((d<0).sum()):>7}/{len(d):<5} {np.median(dm):>12.2f}")
|
||||
ordf_on = C[C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False).index.tolist()
|
||||
ordf_off = C[~C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False).index.tolist()
|
||||
a2 = ordf_on == ordf_off
|
||||
print(f"\n ordine SENZA funding: {' > '.join(ordf_off)}")
|
||||
print(f" ordine CON funding: {' > '.join(ordf_on)}")
|
||||
print(f" >>> A2 {'CONFERMATA' if a2 else 'REFUTATA'}: il funding "
|
||||
f"{'NON cambia' if a2 else 'CAMBIA'} l'ordine fra politiche.")
|
||||
cc = np.corrcoef([polfund[p] for p in SHORT], [dmed[p] for p in SHORT])[0, 1]
|
||||
print(f" correlazione (costo di funding della politica, danno misurato): {cc:+.2f} "
|
||||
f"[atteso negativo: chi paga di piu' perde di piu']")
|
||||
tick("§6")
|
||||
|
||||
# ============================================================ §7 numero di conti
|
||||
hr("§7. LA DIMENSIONE MAI TESTATA — il NUMERO DI CONTI non e' dato, si COMPRA")
|
||||
Rf, Gf = build(0, 0, True)
|
||||
scen = [("nessun versamento, quota persa", 0.0, False),
|
||||
("nessun versamento, quota RIMBORSATA (§30)", 0.0, True),
|
||||
("EUR 500/mese, quota persa", 500.0, False),
|
||||
("EUR 500/mese, quota RIMBORSATA", 500.0, True)]
|
||||
trajs = {}
|
||||
for lab_s, dep, ref_ in scen:
|
||||
res = ladder_multi(Rf, Gf, SEL, N_PATHS, LEV_MAIN, SEED, DELUCK_NEW,
|
||||
refund=ref_, deposit_eur=dep, boot=boot, track=True)
|
||||
trajs[lab_s] = res
|
||||
print(f"\n --- {lab_s}")
|
||||
print(f" {'politica':>10} {'biglietti':>10} {'max conti':>10} {'P(mai >=2)':>11} "
|
||||
f"{'P(>=50/g)':>10} {'EUR/g med':>10} {'P(zero)':>9} {'rimborso $':>11}")
|
||||
for pi, g in enumerate(res["groups"]):
|
||||
print(f" {SHORT[pi]:>10} {g['n_tickets']:>10.2f} {g['max_conc']:>10.2f} "
|
||||
f"{1-g['p_ever2']:>10.1%} {g['p_target']:>10.1%} {g['med']:>10.2f} "
|
||||
f"{g['p_zero']:>9.1%} {g['mean_back']:>11.0f}")
|
||||
print("\n TRAIETTORIA del numero MEDIO di conti funded vivi (mesi 6/12/18/24/30/36):")
|
||||
for lab_s in trajs:
|
||||
tr = trajs[lab_s]["traj"]
|
||||
print(f" {lab_s:<44}" + "".join(
|
||||
f"{tr[m-1].mean():>7.2f}" for m in (6, 12, 18, 24, 30, 36)))
|
||||
print("\n DISPERSIONE fra politiche (max-min di P(>=50/g)) per scenario — quanto CONTA")
|
||||
print(" la scelta della politica:")
|
||||
for lab_s in trajs:
|
||||
v = [g["p_target"] for g in trajs[lab_s]["groups"]]
|
||||
best = SHORT[int(np.argmax(v))]
|
||||
print(f" {lab_s:<44} spread {max(v)-min(v):>6.2%} vincitore {best}")
|
||||
tick("§7")
|
||||
|
||||
# ============================================================ §8 MDE e verdetto
|
||||
hr("§8. MDE — la finestra distingue davvero due politiche?")
|
||||
sf = C[C["fund"]]
|
||||
piv = sf.pivot_table(index=["off", "ph"], columns="pol", values="p50")
|
||||
print(" TRE risoluzioni, e sono cose DIVERSE. Confonderle e' il modo standard di trasformare")
|
||||
print(" rumore in classifica.")
|
||||
print(f"\n (a) MONTE CARLO appaiato, cella di riferimento (off 0, fase {PHASES[0]}, funding ON):")
|
||||
print(" i due esiti sono osservati sugli STESSI percorsi -> SE di McNemar sui discordanti.")
|
||||
print(f" {'coppia':>22} {'d P(>=50/g)':>13} {'discordanti':>12} {'SE':>8} {'z':>8}")
|
||||
base = REF["MISTO"] >= TARGET
|
||||
for p_ in SHORT:
|
||||
if p_ == "MISTO":
|
||||
continue
|
||||
oth = REF[p_] >= TARGET
|
||||
n01 = int((base & ~oth).sum())
|
||||
n10 = int((~base & oth).sum())
|
||||
d = float(base.mean() - oth.mean())
|
||||
se = float(np.sqrt(n01 + n10) / len(base)) if (n01 + n10) else float("nan")
|
||||
z = d / se if se and se > 0 else float("nan")
|
||||
print(f" {'MISTO - ' + p_:>22} {d*100:>+12.2f}pp {n01 + n10:>12} "
|
||||
f"{se*100:>7.2f}pp {z:>+8.2f}")
|
||||
print(" ⚠ e' la risoluzione della SIMULAZIONE, non dell'evidenza: dice se due politiche")
|
||||
print(" differiscono SU QUESTI percorsi, non se differirebbero nel mondo.")
|
||||
print(f"\n (b) BANDA D'ANCORA sulle {len(piv)} celle (mediana delle differenze APPAIATE).")
|
||||
print(" 🚨 NON e' un intervallo di confidenza e non ci si attacca un p-value (regola §57:")
|
||||
print(" le 24 ancore correlano 0,63 e valgono ~2 osservazioni indipendenti). Si legge come")
|
||||
print(" robustezza alla SCELTA dell'ancora, e basta:")
|
||||
print(f" {'coppia':>22} {'mediana':>11} {'p10':>11} {'p90':>11} {'segno tenuto':>14}")
|
||||
for p_ in SHORT:
|
||||
if p_ == "MISTO":
|
||||
continue
|
||||
d = (piv["MISTO"] - piv[p_]).values
|
||||
print(f" {'MISTO - ' + p_:>22} {np.median(d)*100:>+10.2f}pp "
|
||||
f"{np.percentile(d,10)*100:>+10.2f}pp {np.percentile(d,90)*100:>+10.2f}pp "
|
||||
f"{int((d>0).sum()):>6}/{len(d):<7}")
|
||||
yrs = len(I) / 365.25
|
||||
print(f"\n (c) DATO — l'unica che limita davvero. La finestra e' {yrs:.1f} anni e i {N_PATHS}")
|
||||
print(f" percorsi sono ricampionamenti degli STESSI {len(I)} giorni: il bootstrap non crea")
|
||||
print(f" informazione. MDE sul drift ~ 1,96/sqrt({yrs:.1f}) = {1.96/np.sqrt(yrs):.2f} di Sharpe.")
|
||||
print(" Il differenziale di FUNDING fra politiche misurato in §3 e' ~0,10pp/anno di drift:")
|
||||
print(" irrisorio rispetto all'MDE. Cio' che le politiche possono davvero separare non e'")
|
||||
print(" un differenziale di rendimento ma la GEOMETRIA — correlazione fra conti e")
|
||||
print(" barriera per-conto — e quella si misura sulla forma, non sul drift.")
|
||||
|
||||
hr("VERDETTO")
|
||||
fin = C[C["fund"]].groupby("pol")["p50"].median().sort_values(ascending=False)
|
||||
fin_med = C[C["fund"]].groupby("pol")["med"].median()
|
||||
fin_pz = C[C["fund"]].groupby("pol")["pz"].median()
|
||||
for p in fin.index:
|
||||
print(f" {p:>10} P(>=50/g) {fin[p]:>6.2%} EUR/g med {fin_med[p]:>6.2f} "
|
||||
f"P(zero) {fin_pz[p]:>6.1%}")
|
||||
win = fin.index[0]
|
||||
print(f"\n VINCITORE OGGI: {win} (25/07: MISTO)")
|
||||
print(f" celle valutate: {len(C)} righe = {len(OFFSETS)}x{len(PHASES)}x2x{len(PNAMES)}; "
|
||||
f"valutazioni MC totali contate al rialzo: {TRIALS}")
|
||||
print(f" tempo {time.time()-T0:.0f}s")
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,877 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0823b_obiettivo_diretto — OTTIMIZZARE DIRETTAMENTE L'OBIETTIVO DEL PROGETTO (2026-08-23).
|
||||
|
||||
§58 dell'ondata. Libro, pesi, cron, config: NON TOCCATI (isolamento provato in §0).
|
||||
|
||||
IL BUCO
|
||||
-------
|
||||
Ogni ottimizzazione fatta in questo progetto ha massimizzato **un'altra cosa**:
|
||||
* lo **Sharpe** (ogni filone di ricerca direzionale);
|
||||
* **J = P(pass) x P(alive)** (PROP-ALLOC §17, PROP-RECAL §29, SKH-ANCHOR-PROP §32);
|
||||
* **E[payout]** (r0725_hyro, §37);
|
||||
* il **capitale mediano** o **P(arrivare al capitale-rendita)** (§36, r0807_piano_netto).
|
||||
E il critico di §37 ha scritto la frase che apre questo filone:
|
||||
«piu' leva sul funded ALZA E[payout] e ABBASSA P(payout): sono due obiettivi diversi».
|
||||
**Nessuno ha mai ottimizzato DIRETTAMENTE `P(reddito >= 50 EUR/giorno entro H)`**, che e'
|
||||
l'obiettivo DICHIARATO del progetto. I numeri operativi pubblicati (42% -> 7,8% -> 4,4% ->
|
||||
2,6%) sono tutti **letti alla cella che massimizza J**, non alla cella che massimizza P.
|
||||
|
||||
LE DUE DEFINIZIONI, DICHIARATE PRIMA DI MISURARE
|
||||
------------------------------------------------
|
||||
Il progetto ne usa DUE senza mai metterle sulla stessa pagina, e danno risposte opposte:
|
||||
|
||||
**A — RICCHEZZA (rendita sostenibile).** «reddito >= 50 EUR/g» = il patrimonio PROPRIO
|
||||
raggiunge il **capitale-rendita** (muro), cioe' il capitale da cui si preleva 50 EUR/g con
|
||||
P(capitale a 20 anni >= capitale iniziale) >= 90% (definizione 25/07, invariata). E' la
|
||||
definizione dei muri, delle traiettorie e di tutto il piano (§36). E' **sostenibile per
|
||||
costruzione**.
|
||||
|
||||
**B — FLUSSO (produzione realizzata).** «reddito >= 50 EUR/g» = la media dei payout incassati
|
||||
sull'orizzonte, `cum_payout / giorni`, >= 50 EUR/g. E' la definizione di `r0725_prop_ladder`
|
||||
e quindi di **ogni numero del canale funded** (§29 7,8% · §32 4,4% · §37 2,6%). **NON e'
|
||||
sostenibile**: un conto funded che busta il giorno dopo l'orizzonte azzera il flusso, e la
|
||||
stessa cassa che il contatore chiama «reddito» resta nel conto (nel simulatore ufficiale la
|
||||
cassa si accumula E conta come reddito: e' una PRODUZIONE, non un prelievo).
|
||||
|
||||
Qui si misurano **entrambe**, sullo stesso Monte Carlo e sugli stessi percorsi, e si riporta
|
||||
come cambia la risposta. Primaria = **A**, perche' e' quella su cui il progetto ha costruito il
|
||||
muro, i versamenti e la decisione di venue; B si riporta perche' e' quella dei numeri pubblicati.
|
||||
|
||||
LO SPAZIO DI POLITICA — dichiarato esaustivamente PRIMA di misurare
|
||||
--------------------------------------------------------------------
|
||||
1. `k` leva del libro proprio {1,00 · 1,25 · 1,50} (§38: la chiave NON esiste)
|
||||
2. `own` ripartizione del libro proprio {LIVE 75/25 · W_A 38/38/25}
|
||||
3. `lev_fu` leva del conto funded {0,50 · 0,75 · 1,00 · 1,25}
|
||||
4. `n_max` numero di conti concorrenti {0 · 1 · 3 · 6} (0 = nessun biglietto)
|
||||
5. `pref` taglia del biglietto {grande · piccolo}
|
||||
6. `rebuy` ri-comprare dopo un bust {si' · no}
|
||||
7. `floor` capitale proprio sotto cui NON si compra {$0 · $2.000 · $10.000}
|
||||
8. `dep` versamento mensile {EUR 0 · 250 · 500} (il piano in corso e' 500)
|
||||
9. `invest` la cassa lavora sul libro invece di restare ferma {si' · no}
|
||||
Il punto (9) e' un difetto misurabile della macchineria pubblicata: in `pl.simulate` /
|
||||
`pc.simulate` / `gate_c.ladder` **la cassa dei payout resta FERMA per 36 mesi**.
|
||||
|
||||
LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE
|
||||
--------------------------------------------
|
||||
A1. L'argmax di P(>=50/g) sara' a leva **piu' alta** dell'argmax di J, e la differenza sara'
|
||||
grande (fattore >2 su P). *Motivo: P(>=50/g) e' una probabilita' di superare una soglia
|
||||
alta = coda destra; J e' una probabilita' di sopravvivenza = coda sinistra.*
|
||||
A2. Sotto la definizione **A** (ricchezza) il canale funded **non** portera' P(36 mesi) sopra
|
||||
zero in modo apprezzabile: il muro e' ~$313k e 36 mesi da $635 sono ~3 ordini di grandezza.
|
||||
Quindi la risposta alla domanda «esiste una politica sopra il 20% a 36 mesi?» sara'
|
||||
**SI' sotto B e NO sotto A**, e questa e' la vera risposta del filone.
|
||||
A3. Investire la cassa invece di lasciarla ferma valdra' poco su B (il flusso non dipende dalla
|
||||
cassa) e molto su A. *Se sbaglio e vale poco anche su A, lo dico.*
|
||||
A4. L'ottimo di P sara' accompagnato da P(rovina) molto alta (>50%) — cioe' sara' un ottimo che
|
||||
il progetto NON dovrebbe adottare, e va riportato come informazione, non come consiglio.
|
||||
A5. Il vincolo binding a 36 mesi non sara' ne' la leva ne' il numero di conti ma il **cap
|
||||
$200k/trader** oppure il **capitale che entra**. Mi aspetto il secondo.
|
||||
|
||||
CONVENZIONI
|
||||
-----------
|
||||
* Lente **ACCOPPIATA** ovunque ci sia una barriera a un giorno (regola 25/07, riconfermata §33).
|
||||
* Cella d'ancora **MEDIANA** (non la canonica, che sta al 91° pctl — §32), scelta con lo stesso
|
||||
criterio di `gate_c` su un sottoinsieme d'ancore dichiarato PRIMA.
|
||||
* Funding **DENTRO** (§35), de-luck **x0,89** sul drift (§ r0726_deluck_factor), fisco 33% +
|
||||
patrimoniale 0,2% sul libro proprio (§ r0807), morte-firm 10%/anno, cap $200k/trader.
|
||||
* Baseline del libro: **`hourly`** (la lente di ogni muro e traiettoria) — dichiarata perche' il
|
||||
critico di §57 ha misurato che sotto «75/25» convivono tre baseline diverse.
|
||||
* Muro: **CONGELATO** a k=1 come primaria (si de-leva al traguardo: la leva non deve poter
|
||||
spostare il bersaglio); **MOBILE** come sensibilita' (§53 dichiara che la differenza vale 1,2
|
||||
anni). Banda del muro propagata dai valori di §57.
|
||||
* Monte Carlo: la risoluzione si dichiara. 1500 percorsi -> SE ~1,0pp su una P vicina al 20%.
|
||||
|
||||
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823b_obiettivo_diretto.py
|
||||
... --fast (griglia ridotta, ~1/3 del tempo)
|
||||
Rete: NESSUNA (solo cache su disco: parquet certificati, cache Binance di §21, cache funding
|
||||
di §35). Nessuna scrittura fuori da stdout.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, book_series
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata
|
||||
import r0725_prop_ladder as pl # noqa: E402 regole firm, scala di conti
|
||||
import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI
|
||||
import r0807_piano_netto as PN # noqa: E402 passo_fiscale, accumula, muro
|
||||
import r0822_prop_alloc as PA # noqa: E402 boot_idx, DELUCK
|
||||
import r0822b_prop_recal as PR # noqa: E402 obj_rg, LEVS, pannelli Binance
|
||||
import r0822b_xs01_oos as X # noqa: E402 U13, motore XS01
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 rg_off (offset di SKH01)
|
||||
import r0822d_funding as FD # noqa: E402 il funding
|
||||
import r0822d_gate_c as GC # noqa: E402 ladder, fvec_for, xs_funding
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
FAST = "--fast" in sys.argv
|
||||
EURUSD = pl.EURUSD
|
||||
TAX = CC.TAX_RATE
|
||||
PATR = TX.PATRIMONIALE
|
||||
DELUCK = PA.DELUCK # x0,89 misurato
|
||||
W_LIVE = {"TP01": 0.75, "SKH01": 0.25}
|
||||
W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25}
|
||||
START_USD = 635.0 # il conto Deribit VERO di oggi
|
||||
SEED_LAD = pl.HORIZON_MONTHS * 0 + 20260822 # lo stesso di gate_c (per la bit-exactness)
|
||||
SEED_BOOT = 58023
|
||||
N_PATHS = 900 if FAST else 1500
|
||||
N_PATHS_J = PA.N_PATHS
|
||||
MIN_PAYOUT = GC.MIN_PAYOUT_TRADER
|
||||
ORIZZONTI_M = (24, 36, 60, 120)
|
||||
H_MAX_D = max(ORIZZONTI_M) * 30
|
||||
MARKS = tuple(m * 30 for m in ORIZZONTI_M)
|
||||
OFFSETS_SUB = tuple(range(0, 690, 90)) if not FAST else (0, 180, 360, 540) # 8 (o 4) a priori
|
||||
PHASES_SUB = AP.PHASES_SUB # (0, 3, 7) — dichiarato da §32
|
||||
LEVS_FU = (0.50, 0.75, 1.00, 1.25)
|
||||
NMAX = (1, 3, 6)
|
||||
PREFS = ("grande", "piccolo")
|
||||
REBUY = (True, False)
|
||||
|
||||
# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi
|
||||
PUB = dict(muro_L3=313_143.0, perp_L3=0.0635, drift_L3=0.1519,
|
||||
p50g_fund=0.026, p50g_p10=0.015, p50g_p90=0.047, pzero_fund=0.403,
|
||||
p50g_nofund=0.044,
|
||||
muro_p90=186_623.0, muro_m1se=709_753.0) # banda di §57
|
||||
|
||||
TRIALS = 0
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
def bump(n: int = 1) -> None:
|
||||
global TRIALS
|
||||
TRIALS += n
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "=" * 104)
|
||||
if t:
|
||||
print(" " + t)
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
def tick(lab: str) -> None:
|
||||
print(f" [{time.time() - T0:6.0f}s] {lab}", flush=True)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# IL SIMULATORE UNIFICATO
|
||||
# ===========================================================================
|
||||
def uni(Ra: np.ndarray, Ga: np.ndarray, Rb: np.ndarray, boot: np.ndarray, *,
|
||||
lev_fu: float = 1.0, n_max: int = 6, pref: str = "grande", rebuy: bool = True,
|
||||
floor_usd: float = 0.0, k_book: float = 1.0, invest: bool = False,
|
||||
dep_eur: float = 0.0, start_usd: float = pl.START_EUR * EURUSD,
|
||||
seed: int = SEED_LAD, refund: bool = False, min_payout: float = 0.0,
|
||||
tax: float = 0.0, patr: float = 0.0, walls: tuple = (),
|
||||
marks: tuple = (), deposit_mode: str = "ladder",
|
||||
deluck_f: float = DELUCK) -> dict:
|
||||
"""UN simulatore per le DUE strade, sugli STESSI percorsi.
|
||||
|
||||
E' una GENERALIZZAZIONE STRETTA di `gate_c.ladder` (che a sua volta e' `pc.simulate` con
|
||||
tre aggiunte): con `n_max=6, pref='grande', rebuy=True, floor=0, k_book=0, invest=False,
|
||||
tax=0, patr=0, deposit_mode='ladder'` deve essere **BIT-EXACT** rispetto a `gate_c.ladder`.
|
||||
E con `n_max=0, invest=True, tax=0.33, patr=0.002, deposit_mode='accumula'` deve essere
|
||||
**BIT-EXACT** rispetto a `PN.accumula`. I due controlli sono in §1.
|
||||
|
||||
Cio' che aggiunge, e che nessuna macchina pubblicata ha:
|
||||
* un **pot proprio** che compone al libro (`k_book x Rb`) invece di restare fermo, col
|
||||
fisco d'accumulo dentro (stessa `PN.passo_fiscale` del piano);
|
||||
* i **payout che rientrano nel pot** e quindi compongono;
|
||||
* `floor_usd` — un capitale proprio sotto cui non si compra un altro biglietto;
|
||||
* `rebuy` — la scelta di NON ricomprare dopo un bust;
|
||||
* `pref` — biglietto piu' grande o piu' piccolo (piu' tentativi);
|
||||
* il **primo passaggio** al capitale-rendita, per l'obiettivo A.
|
||||
|
||||
`Ra/Ga` = libro del conto FUNDED (ritorno e gap intraday accoppiato), `Rb` = libro PROPRIO.
|
||||
Il de-luck si applica al DRIFT di R (non al gap): stessa convenzione di `ladder`.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
n_paths, n_days = boot.shape
|
||||
Ram = np.asarray(Ra, float).copy()
|
||||
Ram = Ram - (1.0 - deluck_f) * Ram.mean()
|
||||
Gam = np.asarray(Ga, float)
|
||||
Rbm = np.asarray(Rb, float).copy()
|
||||
Rbm = Rbm - (1.0 - deluck_f) * Rbm.mean()
|
||||
|
||||
n_slot = int(n_max)
|
||||
FIRMS = pl.FIRMS
|
||||
cash = np.full(n_paths, float(start_usd))
|
||||
basis = cash.copy() # base fiscale del pot
|
||||
carry = np.zeros((n_paths, TX.CARRY_ANNI))
|
||||
anno_start = cash.copy()
|
||||
flusso = np.zeros(n_paths)
|
||||
cum_payout = np.zeros(n_paths)
|
||||
spent = np.zeros(n_paths)
|
||||
got_back = np.zeros(n_paths)
|
||||
deposited = np.zeros(n_paths)
|
||||
n_payouts = np.zeros(n_paths, int)
|
||||
n_tickets = np.zeros(n_paths, int)
|
||||
n_pass = np.zeros(n_paths, int)
|
||||
peak_notional = np.zeros(n_paths)
|
||||
|
||||
if n_slot:
|
||||
Rs_full = Ram[boot] * lev_fu # (paths, days) — tutti i conti uguali
|
||||
Gs_full = Gam[boot] * lev_fu
|
||||
phase = np.full((n_paths, n_slot), -1, np.int8)
|
||||
eq = np.ones((n_paths, n_slot))
|
||||
firm = np.full((n_paths, n_slot), -1, np.int8)
|
||||
notional = np.zeros((n_paths, n_slot))
|
||||
price_paid = np.zeros((n_paths, n_slot))
|
||||
refunded = np.zeros((n_paths, n_slot), bool)
|
||||
used = np.zeros((n_paths, n_slot), bool)
|
||||
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
|
||||
p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||
Rbp = Rbm[boot] if (invest and k_book > 0) else None
|
||||
|
||||
hit = {w: np.full(n_paths, -1, int) for w in walls}
|
||||
snap: dict[int, dict] = {}
|
||||
fiscale = (tax > 0 or patr > 0)
|
||||
|
||||
for t in range(n_days):
|
||||
# ---- morte di una firm (identico a ladder: il draw avviene SEMPRE, anche a 0 slot,
|
||||
# solo se il canale funded esiste)
|
||||
if n_slot:
|
||||
died = rng.random((n_paths, len(FIRMS))) < p_death_day
|
||||
newly_dead = died & firm_alive
|
||||
firm_alive &= ~died
|
||||
if newly_dead.any():
|
||||
for fi in range(len(FIRMS)):
|
||||
kill = newly_dead[:, fi]
|
||||
if kill.any():
|
||||
h = (firm == fi) & kill[:, None]
|
||||
phase[h] = -1
|
||||
notional[h] = 0.0
|
||||
|
||||
# ---- acquisto biglietti
|
||||
for k in range(n_slot):
|
||||
free = phase[:, k] == -1
|
||||
if not rebuy:
|
||||
free &= ~used[:, k]
|
||||
if not free.any():
|
||||
continue
|
||||
for fi, F in enumerate(FIRMS):
|
||||
u = np.where(firm == fi, notional, 0.0).sum(axis=1)
|
||||
order = sorted(F["tickets"], key=(lambda x: -x[0]) if pref == "grande"
|
||||
else (lambda x: x[0]))
|
||||
for (size, price) in order:
|
||||
buy = (free & firm_alive[:, fi] & (cash >= price)
|
||||
& (cash - price >= floor_usd) & (u + size <= F["cap"]))
|
||||
if not buy.any():
|
||||
continue
|
||||
cash[buy] -= price
|
||||
flusso[buy] -= price
|
||||
spent[buy] += price
|
||||
n_tickets[buy] += 1
|
||||
phase[buy, k] = 0
|
||||
eq[buy, k] = 1.0
|
||||
firm[buy, k] = fi
|
||||
notional[buy, k] = size
|
||||
price_paid[buy, k] = price
|
||||
refunded[buy, k] = False
|
||||
used[buy, k] = True
|
||||
free = free & ~buy
|
||||
if not free.any():
|
||||
break
|
||||
if not free.any():
|
||||
break
|
||||
|
||||
# ---- dinamica dei conti
|
||||
r = Rs_full[:, t][:, None]
|
||||
g = Gs_full[:, t][:, None]
|
||||
act = phase >= 0
|
||||
eq_prev = eq
|
||||
eq = np.where(act, eq * (1.0 + r), eq)
|
||||
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||
r_low = r + g
|
||||
for fi, F in enumerate(FIRMS):
|
||||
s_ = act & (firm == fi)
|
||||
if not s_.any():
|
||||
continue
|
||||
ev_t, ev_dd, ev_dl = F["ev"]
|
||||
fu_ml, fu_dl = F["fu"]
|
||||
in_ev = s_ & (phase == 0)
|
||||
in_fu = s_ & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl)))
|
||||
| (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))))
|
||||
phase[bust] = -1
|
||||
notional[bust] = 0.0
|
||||
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||
phase[passed] = 1
|
||||
eq[passed] = 1.0
|
||||
n_pass += passed.sum(axis=1)
|
||||
peak_notional = np.maximum(peak_notional,
|
||||
np.where(phase == 1, notional, 0.0).sum(axis=1))
|
||||
|
||||
# ---- il POT proprio compone
|
||||
if Rbp is not None:
|
||||
cash = cash * (1.0 + k_book * Rbp[:, t])
|
||||
|
||||
# ---- versamento (convenzione `accumula`: giorno 30, 60, ...)
|
||||
if deposit_mode == "accumula" and dep_eur and t % 30 == 0 and t > 0:
|
||||
cash += dep_eur * EURUSD
|
||||
flusso += dep_eur * EURUSD
|
||||
deposited += dep_eur * EURUSD
|
||||
|
||||
# ---- payout mensile + rimborso + versamento (convenzione `ladder`)
|
||||
if (t + 1) % 30 == 0:
|
||||
if n_slot:
|
||||
fu = phase == 1
|
||||
pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * pl.SPLIT) >= min_payout)
|
||||
gain = np.where(pay, (eq - 1.0) * notional, 0.0)
|
||||
net = gain.sum(axis=1) * pl.SPLIT * (1.0 - TAX)
|
||||
cum_payout += net
|
||||
cash += net
|
||||
flusso += net
|
||||
n_payouts += (pay.sum(axis=1) > 0).astype(int)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if refund:
|
||||
back = pay & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
flusso += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
if deposit_mode == "ladder" and dep_eur:
|
||||
cash += dep_eur * EURUSD
|
||||
flusso += dep_eur * EURUSD
|
||||
deposited += dep_eur * EURUSD
|
||||
|
||||
# ---- primo passaggio al muro (obiettivo A)
|
||||
for w, arr in hit.items():
|
||||
nuovi = (arr < 0) & (cash >= w)
|
||||
arr[nuovi] = t
|
||||
|
||||
# ---- fisco d'accumulo sul pot
|
||||
if fiscale and (t + 1) % 365 == 0:
|
||||
PN.passo_fiscale(cash, anno_start, flusso, carry, tax, patr)
|
||||
anno_start = cash.copy()
|
||||
flusso[:] = 0.0
|
||||
basis = cash.copy()
|
||||
|
||||
if (t + 1) in marks:
|
||||
d = t + 1
|
||||
ed = cum_payout / EURUSD / (d / 365.0) / 365.0
|
||||
contrib = float(start_usd) + deposited
|
||||
snap[d] = dict(p_flow=float((ed >= pl.TARGET_EUR_DAY).mean()),
|
||||
p_flow10=float((ed >= 10.0).mean()),
|
||||
med_eur=float(np.median(ed)),
|
||||
med_cash=float(np.median(cash)),
|
||||
mean_cash=float(cash.mean()),
|
||||
p10_cash=float(np.percentile(cash, 10)),
|
||||
p_zero=float((cum_payout <= 0).mean()),
|
||||
p_sotto=float((cash < contrib).mean()),
|
||||
p_rovina=float((cash < 0.10 * contrib).mean()),
|
||||
contrib=float(contrib.mean() if np.ndim(contrib) else contrib),
|
||||
**{f"A{w:.0f}": float(((hit[w] >= 0) & (hit[w] < d)).mean())
|
||||
for w in walls})
|
||||
|
||||
ed = cum_payout / EURUSD / (n_days / 365.0) / 365.0
|
||||
out = dict(eur_day=ed, cash=cash, snap=snap, hit=hit,
|
||||
p_target=float((ed >= pl.TARGET_EUR_DAY).mean()),
|
||||
p_ge10=float((ed >= 10.0).mean()),
|
||||
p_zero=float((cum_payout <= 0).mean()),
|
||||
med=float(np.median(ed)),
|
||||
med_cash=float(np.median(cash)), mean_cash=float(cash.mean()),
|
||||
p10_cash=float(np.percentile(cash, 10)),
|
||||
p90_cash=float(np.percentile(cash, 90)),
|
||||
med_spent=float(np.median(spent)), mean_back=float(got_back.mean()),
|
||||
n_tickets=float(n_tickets.mean()), n_pass=float(n_pass.mean()),
|
||||
peak_notional=float(np.median(peak_notional)),
|
||||
contributed=float(start_usd + deposited.mean()))
|
||||
bump(1)
|
||||
return out
|
||||
|
||||
|
||||
def p_wall_entro(res: dict, wall: float, giorni: int) -> float:
|
||||
"""P(il patrimonio PROPRIO tocca `wall` entro `giorni`) — obiettivo A."""
|
||||
h = res["hit"][wall]
|
||||
return float(((h >= 0) & (h < giorni)).mean())
|
||||
|
||||
|
||||
def anni_med(res: dict, wall: float) -> tuple[float, float]:
|
||||
"""(mediana INCONDIZIONATA degli anni al traguardo, P(arrivo entro la simulazione)).
|
||||
Non-arrivo = +inf, MAI -1 (lezione 07/08)."""
|
||||
h = res["hit"][wall]
|
||||
y = np.where(h >= 0, h / 365.0, np.inf)
|
||||
return float(np.median(y)), float(np.isfinite(y).mean())
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
def main() -> None:
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
|
||||
print("=" * 104)
|
||||
print(" §58 OBIETTIVO-DIRETTO — ottimizzare `P(reddito >= 50 EUR/g entro H)`, e non altro")
|
||||
print("=" * 104)
|
||||
print(f" {'MODALITA RIDOTTA (--fast)' if FAST else 'griglia piena'} | "
|
||||
f"{N_PATHS} percorsi | orizzonti {ORIZZONTI_M} mesi")
|
||||
|
||||
# ------------------------------------------------------------ §0 isolamento
|
||||
hr("§0. ISOLAMENTO — questo script NON tocca la produzione (il cron gira dal working tree)")
|
||||
prot = ["src", "config", "scripts/live", "tests", "data/paper_portfolio", "data/live"]
|
||||
dirty = subprocess.run(["git", "status", "--porcelain", "--"] + prot,
|
||||
cwd=ROOT, capture_output=True, text=True).stdout.strip()
|
||||
print(f" git status sui percorsi protetti {prot}:")
|
||||
print(f" {'PULITO — nessuna modifica' if not dirty else 'SPORCO: ' + dirty}")
|
||||
if dirty:
|
||||
raise SystemExit("La produzione risulta modificata: mi fermo.")
|
||||
print(" Nessuna scrittura su disco in tutto lo script (solo stdout). Rete: nessuna.")
|
||||
|
||||
# ------------------------------------------------------------ §1 repliche
|
||||
hr("§1. REPLICHE — prima di un numero nuovo, la macchina deve riprodurre i vecchi")
|
||||
ok: dict[str, bool] = {}
|
||||
|
||||
# (R1) il muro L3 (fisco + funding), $313.143 — §36, replicato al dollaro da §57
|
||||
print("\n R1 IL MURO della lente congiunta L3 (funding dentro, fisco dentro, de-luck x0,89)")
|
||||
rateF = GC.funding_rate()
|
||||
if rateF is None:
|
||||
raise SystemExit("cache funding assente: senza di essa nessun numero di questo filone "
|
||||
"sarebbe confrontabile coi pubblicati.")
|
||||
bookA = FD.book_75_25(None)
|
||||
bookF = FD.book_75_25(rateF).reindex(bookA.index)
|
||||
idxF = pd.DatetimeIndex(bookA.index)
|
||||
rL3 = np.asarray(bookF.values, float)
|
||||
rL3 = rL3 - (1.0 - DELUCK) * rL3.mean()
|
||||
prel = CC.TARGET_EUR_DAY * 365 * CC.EURUSD
|
||||
t = time.time()
|
||||
perp, MURO = PN.muro(rL3, prel, TAX, PATR)
|
||||
ok["R1"] = abs(MURO - PUB["muro_L3"]) < 1.0
|
||||
print(f" serie {len(rL3)} giorni {idxF.min().date()} -> {idxF.max().date()} | "
|
||||
f"drift {rL3.mean()*365:.4%} (pubbl. {PUB['drift_L3']:.2%})")
|
||||
print(f" perpetua {perp:.4%} (pubbl. {PUB['perp_L3']:.2%}) muro ${MURO:,.0f} "
|
||||
f"(pubbl. ${PUB['muro_L3']:,.0f}) -> "
|
||||
f"{'ESATTO AL DOLLARO' if ok['R1'] else 'DIVERSO'} ({time.time()-t:.0f}s)")
|
||||
bump(1)
|
||||
tick("R1")
|
||||
|
||||
# ------------------------------------------------- il pannello comune
|
||||
print("\n Il pannello comune (identico a §29/§32/§37): U13 Binance, W-LONG 2021+.")
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE)
|
||||
p13 = Cb[X.U13].dropna()
|
||||
XSP = {ph: PR.xs_tuples(p13, Ob.reindex(p13.index)[X.U13], Hb.reindex(p13.index)[X.U13],
|
||||
Lb.reindex(p13.index)[X.U13], phase=ph) for ph in PHASES_SUB}
|
||||
xs0 = XSP[PHASES_SUB[0]]
|
||||
base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))]
|
||||
pc.DELUCK = DELUCK
|
||||
ev_b = PA.boot_idx(len(I), 365, N_PATHS_J, seed=8220)
|
||||
fu_b = PA.boot_idx(len(I), 365, N_PATHS_J, seed=8221)
|
||||
xsf = GC.xs_funding_measure(verbose=False)
|
||||
xs_ann = xsf["ann"]
|
||||
print(f" {len(I)} giorni {I[0].date()} -> {I[-1].date()} | "
|
||||
f"XS01 paga {xs_ann*100:+.2f}%/anno di funding (misurato §37)")
|
||||
tick("pannello")
|
||||
|
||||
# (R2) la cella d'ancora MEDIANA e il numero operativo 2,6% [1,5%, 4,7%]
|
||||
print(f"\n R2 IL NUMERO OPERATIVO DI §37 — {len(OFFSETS_SUB)} offset x {len(PHASES_SUB)} "
|
||||
f"fasi = {len(OFFSETS_SUB)*len(PHASES_SUB)} celle, funding DENTRO, leva argmax-J")
|
||||
print(" (sottoinsieme d'ancore dichiarato PRIMA; §37 ne usa 23x3, qui se ne usano "
|
||||
f"{len(OFFSETS_SUB)}x{len(PHASES_SUB)} per il tempo macchina)")
|
||||
cells = []
|
||||
for off in OFFSETS_SUB:
|
||||
fA = GC.fvec_for(W_A, I, rateF, off, xs_ann)
|
||||
for ph in PHASES_SUB:
|
||||
R0, G0 = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rx = R0 - fA
|
||||
jb, lb = -1.0, None
|
||||
for lv in PR.LEVS:
|
||||
j = PR.obj_rg(Rx, G0, lv, ev_b, fu_b)["J"]
|
||||
if j > jb:
|
||||
jb, lb = j, lv
|
||||
bump(len(PR.LEVS))
|
||||
lad = GC.ladder(Rx, G0, N_PATHS, lb, SEED_LAD, refund=False)
|
||||
cells.append(dict(off=off, ph=ph, J=jb, lev=lb, p50=lad["p_target"],
|
||||
pz=lad["p_zero"], med=lad["med"]))
|
||||
bump(1)
|
||||
C = pd.DataFrame(cells)
|
||||
med_p, p10, p90 = C["p50"].median(), np.percentile(C["p50"], 10), np.percentile(C["p50"], 90)
|
||||
ok["R2"] = PUB["p50g_p10"] <= med_p <= PUB["p50g_p90"]
|
||||
print(f" P(>=50/g) mediana {med_p:.1%} banda [{p10:.1%}, {p90:.1%}] "
|
||||
f"P(zero) {C['pz'].median():.1%}")
|
||||
print(f" pubblicato §37: {PUB['p50g_fund']:.1%} [{PUB['p50g_p10']:.1%}, "
|
||||
f"{PUB['p50g_p90']:.1%}], P(zero) {PUB['pzero_fund']:.1%} -> "
|
||||
f"{'DENTRO LA BANDA' if ok['R2'] else 'FUORI'}")
|
||||
k_med = int((C["p50"] - C["p50"].median()).abs().idxmin())
|
||||
OFF_M, PH_M, LEV_J = int(C.loc[k_med, "off"]), int(C.loc[k_med, "ph"]), float(C.loc[k_med, "lev"])
|
||||
print(f" CELLA D'ANCORA MEDIANA: offset {OFF_M} min, fase {PH_M}, leva argmax-J "
|
||||
f"{LEV_J:.2f}x (non la canonica: 91° pctl, §32)")
|
||||
fM = GC.fvec_for(W_A, I, rateF, OFF_M, xs_ann)
|
||||
R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)
|
||||
R_M = R_M0 - fM
|
||||
tick("R2")
|
||||
|
||||
# il libro PROPRIO sullo stesso pannello
|
||||
rb_full = FD.book_75_25(rateF)
|
||||
if rb_full.index.tz is None:
|
||||
rb_full.index = rb_full.index.tz_localize("UTC")
|
||||
RB_LIVE = np.nan_to_num(rb_full.reindex(I).values.astype(float))
|
||||
fA_own = GC.fvec_for(W_A, I, rateF, OFF_M, xs_ann)
|
||||
RB_WA = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)[0] - fA_own
|
||||
print(f"\n libro PROPRIO sul pannello comune:")
|
||||
for lab, r in (("LIVE 75/25", RB_LIVE), ("W_A 38/38/25", RB_WA)):
|
||||
print(f" {lab:<14} drift {r.mean()*365:>7.2%} vol {r.std()*365**.5:>6.2%} "
|
||||
f"Sharpe {r.mean()*365/(r.std()*365**.5):>5.2f}")
|
||||
|
||||
# bootstrap CONDIVISO (confronto appaiato fra tutte le politiche)
|
||||
rngb = np.random.default_rng(SEED_BOOT)
|
||||
nb = int(np.ceil(H_MAX_D / 20))
|
||||
st = rngb.integers(0, len(I) - 20, size=(N_PATHS, nb))
|
||||
BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_PATHS, -1)[:, :H_MAX_D]
|
||||
B36 = BOOT[:, :36 * 30]
|
||||
print(f" bootstrap CONDIVISO: {N_PATHS} percorsi x {H_MAX_D} giorni, blocchi 20 -> "
|
||||
f"OGNI confronto di questo filone e' APPAIATO")
|
||||
|
||||
# (R3) bit-exactness: `uni` in modalita' ladder == `gate_c.ladder`
|
||||
print("\n R3 BIT-EXACTNESS — `uni` in modalita' ladder deve essere `gate_c.ladder`")
|
||||
ref = GC.ladder(R_M, G_M, N_PATHS, LEV_J, SEED_LAD, refund=False, boot=B36)
|
||||
mine = uni(R_M, G_M, RB_LIVE, B36, lev_fu=LEV_J, n_max=pl.MAX_CONCURRENT, pref="grande",
|
||||
rebuy=True, floor_usd=0.0, k_book=0.0, invest=False, dep_eur=0.0,
|
||||
start_usd=pl.START_EUR * EURUSD, seed=SEED_LAD, refund=False, min_payout=0.0,
|
||||
tax=0.0, patr=0.0, deposit_mode="ladder")
|
||||
d1 = float(np.abs(ref["eur_day"] - mine["eur_day"]).max())
|
||||
d2 = float(np.abs(ref["cash"] - mine["cash"]).max())
|
||||
ok["R3"] = (d1 == 0.0 and d2 == 0.0)
|
||||
print(f" max|dif| EUR/g = {d1:.3e} | max|dif| cassa = {d2:.3e} -> "
|
||||
f"{'BIT-EXACT' if ok['R3'] else 'DIVERSO'}")
|
||||
|
||||
# (R4) bit-exactness: `uni` senza conti == `PN.accumula`
|
||||
print("\n R4 BIT-EXACTNESS — `uni` senza canale funded deve essere `PN.accumula`")
|
||||
paths_own = (RB_LIVE - (1.0 - DELUCK) * RB_LIVE.mean())[B36]
|
||||
a = PN.accumula(paths_own, 500.0, MURO, TAX, PATR, start=START_USD)
|
||||
m = uni(R_M, G_M, RB_LIVE, B36, n_max=0, k_book=1.0, invest=True, dep_eur=500.0,
|
||||
start_usd=START_USD, tax=TAX, patr=PATR, walls=(MURO,),
|
||||
deposit_mode="accumula")
|
||||
d3 = float(np.abs(a["cap"] - m["cash"]).max())
|
||||
d4 = int(np.abs(np.where(a["colpito"] >= 0, a["colpito"], -1) - m["hit"][MURO]).max())
|
||||
ok["R4"] = (d3 < 1e-6 and d4 == 0)
|
||||
print(f" max|dif| capitale = ${d3:.3e} | max|dif| giorno di primo passaggio = {d4} -> "
|
||||
f"{'BIT-EXACT' if ok['R4'] else 'DIVERSO'}")
|
||||
print(f" (capitale mediano a 36 mesi ${np.median(m['cash']):,.0f}, "
|
||||
f"P(muro) {float((m['hit'][MURO]>=0).mean()):.1%})")
|
||||
|
||||
print(f"\n ESITO REPLICHE: {sum(ok.values())}/{len(ok)} — "
|
||||
+ ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items()))
|
||||
if not all(ok.values()):
|
||||
raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.")
|
||||
tick("§1")
|
||||
# ---- fine repliche; da qui i numeri NUOVI
|
||||
|
||||
# muro coerente col PANNELLO su cui gira la simulazione (2021+), come sensibilita'
|
||||
rI = RB_LIVE - (1.0 - DELUCK) * RB_LIVE.mean()
|
||||
lo, hi = 0.0, 0.40
|
||||
for _ in range(13):
|
||||
mid = (lo + hi) / 2
|
||||
sv = PN.sopravvivenza(rI, 1e6, 1e6 * mid, 20, TAX, PATR, n_paths=1500)
|
||||
lo, hi = (mid, hi) if sv["p10_end"] >= 1e6 else (lo, mid)
|
||||
MURO_I = prel / lo if lo > 0.002 else float("inf")
|
||||
WALLS = (PUB["muro_p90"], MURO, PUB["muro_m1se"])
|
||||
print(f"\n IL BERSAGLIO (obiettivo A) — e la sua banda, propagata da §57:")
|
||||
print(f" muro PRIMARIO (pubblicato, finestra piena 2019+): ${MURO:,.0f} "
|
||||
f"(perpetua {perp:.2%})")
|
||||
print(f" banda del drift +-1SE (§57): p90 ${PUB['muro_p90']:,.0f} ... "
|
||||
f"-1SE ${PUB['muro_m1se']:,.0f}")
|
||||
print(f" muro COERENTE col pannello 2021+ (drift {rI.mean()*365:.2%}): "
|
||||
+ (f"${MURO_I:,.0f}" if np.isfinite(MURO_I) else "IRRAGGIUNGIBILE (perpetua ~0)"))
|
||||
print(" ⚠️ La simulazione gira sul pannello 2021+ (l'unico dove XS01 esiste) mentre il muro")
|
||||
print(" pubblicato e' calcolato sulla finestra piena: la disparita' e' DICHIARATA e la")
|
||||
print(" riga qui sopra la quantifica. Primaria resta la pubblicata, per confrontabilita'.")
|
||||
tick("muro")
|
||||
|
||||
def run(**kw) -> dict:
|
||||
base = dict(lev_fu=1.0, n_max=0, pref="grande", rebuy=True, floor_usd=0.0,
|
||||
k_book=1.0, invest=True, dep_eur=500.0, start_usd=START_USD,
|
||||
seed=SEED_LAD, refund=True, min_payout=MIN_PAYOUT, tax=TAX, patr=PATR,
|
||||
walls=WALLS, marks=MARKS, deposit_mode="ladder")
|
||||
base.update(kw)
|
||||
boot = base.pop("boot", BOOT)
|
||||
return uni(base.pop("Ra", R_M), base.pop("Ga", G_M), base.pop("Rb", RB_LIVE),
|
||||
boot, **base)
|
||||
|
||||
# ======================================================================
|
||||
hr("§2. LE DUE DEFINIZIONI DI «REDDITO >= 50 EUR/g», SULLA STESSA POLITICA E SUGLI STESSI PATH")
|
||||
print(" A = RICCHEZZA: il patrimonio PROPRIO tocca il capitale-rendita (sostenibile).")
|
||||
print(" B = FLUSSO : media dei payout incassati >= 50 EUR/g (la definizione dei numeri")
|
||||
print(" pubblicati del canale funded — NON sostenibile).")
|
||||
print(f" Versamento: EUR 500/mese (il piano in `config/live.json`), start ${START_USD:,.0f}.")
|
||||
REF = {
|
||||
"LIBRO (oggi: 75/25, k=1, nessun biglietto)": dict(n_max=0),
|
||||
"PROP come pubblicato (6 conti, leva argmax-J, cassa FERMA)":
|
||||
dict(n_max=6, lev_fu=LEV_J, invest=False, k_book=0.0, tax=0.0, patr=0.0),
|
||||
"PROP + cassa che lavora sul libro (stessa leva)":
|
||||
dict(n_max=6, lev_fu=LEV_J, invest=True, k_book=1.0),
|
||||
}
|
||||
refres = {k: run(**v) for k, v in REF.items()}
|
||||
print(f"\n {'politica':<52} {'H':>5} {'A: P(muro)':>11} {'B: P(>=50/g)':>13} "
|
||||
f"{'mediana $':>11} {'P(<versato)':>12}")
|
||||
for lab, r in refres.items():
|
||||
for m in ORIZZONTI_M:
|
||||
sn = r["snap"][m * 30]
|
||||
print(f" {lab if m == ORIZZONTI_M[0] else '':<52} {m:>4}m "
|
||||
f"{sn[f'A{MURO:.0f}']:>11.1%} {sn['p_flow']:>13.1%} "
|
||||
f"${sn['med_cash']:>10,.0f} {sn['p_sotto']:>12.1%}")
|
||||
print("\n 📌 Le due definizioni NON ordinano le politiche allo stesso modo, e a 36 mesi non")
|
||||
print(" sono nemmeno commensurabili: sotto A il canale funded e' un ACCELERATORE di")
|
||||
print(" capitale, sotto B e' l'unico modo di avere un flusso.")
|
||||
tick("§2")
|
||||
|
||||
# ======================================================================
|
||||
hr("§3. LA CURVA DI LEVA — dove sta l'argmax di CIASCUN obiettivo (attesa A1)")
|
||||
print(f" Cella d'ancora mediana (offset {OFF_M}, fase {PH_M}), 6 conti, cassa che lavora,")
|
||||
print(" EUR 500/mese. J e' calcolato con la STESSA `PR.obj_rg` di §29/§32/§37.")
|
||||
print(f"\n {'leva':>6} {'J':>7} {'B: P(>=50/g) 36m':>17} {'A: P(muro) 120m':>16} "
|
||||
f"{'P(zero)':>8} {'P(<versato)':>12} {'mediana $ 120m':>15}")
|
||||
LEVS_ALL = PR.LEVS
|
||||
curve = []
|
||||
for lv in LEVS_ALL:
|
||||
j = PR.obj_rg(R_M, G_M, lv, ev_b, fu_b)["J"]
|
||||
bump(1)
|
||||
r = run(n_max=6, lev_fu=lv)
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
curve.append(dict(lev=lv, J=j, B36=s36["p_flow"], A120=s120[f"A{MURO:.0f}"],
|
||||
pz=s36["p_zero"], psotto=s120["p_sotto"], med=s120["med_cash"]))
|
||||
print(f" {lv:>6.3f} {j:>7.3f} {s36['p_flow']:>17.1%} {s120[f'A{MURO:.0f}']:>16.1%} "
|
||||
f"{s36['p_zero']:>8.1%} {s120['p_sotto']:>12.1%} ${s120['med_cash']:>14,.0f}")
|
||||
CU = pd.DataFrame(curve)
|
||||
aJ = float(CU.loc[CU["J"].idxmax(), "lev"])
|
||||
aB = float(CU.loc[CU["B36"].idxmax(), "lev"])
|
||||
aA = float(CU.loc[CU["A120"].idxmax(), "lev"])
|
||||
print(f"\n argmax J = {aJ:.2f}x · argmax B(36m) = {aB:.2f}x · "
|
||||
f"argmax A(120m) = {aA:.2f}x")
|
||||
print(f" B alla leva argmax-J: {float(CU.loc[CU['J'].idxmax(),'B36']):.1%} -> "
|
||||
f"B al proprio argmax: {CU['B36'].max():.1%} "
|
||||
f"(fattore {CU['B36'].max()/max(float(CU.loc[CU['J'].idxmax(),'B36']),1e-9):.1f}x)")
|
||||
print(f" ATTESA A1 ({'CONFERMATA' if aB > aJ and CU['B36'].max() > 2*float(CU.loc[CU['J'].idxmax(),'B36']) else 'PARZIALE/SMENTITA'}): "
|
||||
"l'argmax di P sta a leva piu' alta di quello di J.")
|
||||
tick("§3")
|
||||
|
||||
# ======================================================================
|
||||
hr("§4. LO SPAZIO DI POLITICA, OTTIMIZZATO SU P DIRETTAMENTE (36 mesi, obiettivo B)")
|
||||
grid = [dict(lev_fu=lv, n_max=nm, pref=pf, rebuy=rb)
|
||||
for lv in LEVS_FU for nm in NMAX for pf in PREFS for rb in REBUY]
|
||||
print(f" {len(grid)} celle dichiarate PRIMA: leva {LEVS_FU} x conti {NMAX} x "
|
||||
f"taglia {PREFS} x rebuy {REBUY}")
|
||||
print(f" fisse: libro proprio LIVE 75/25 k=1, cassa che lavora, floor $0, EUR 500/mese.")
|
||||
rows = []
|
||||
for g in grid:
|
||||
r = run(**g)
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
rows.append(dict(**g, B36=s36["p_flow"], B24=r["snap"][24 * 30]["p_flow"],
|
||||
B60=r["snap"][60 * 30]["p_flow"], B120=s120["p_flow"],
|
||||
A36=s36[f"A{MURO:.0f}"], A120=s120[f"A{MURO:.0f}"],
|
||||
A120lo=s120[f"A{PUB['muro_p90']:.0f}"],
|
||||
A120hi=s120[f"A{PUB['muro_m1se']:.0f}"],
|
||||
pz=s36["p_zero"], psotto36=s36["p_sotto"],
|
||||
rov36=s36["p_rovina"], med120=s120["med_cash"],
|
||||
ntick=r["n_tickets"]))
|
||||
G = pd.DataFrame(rows)
|
||||
print(f"\n LE 8 MIGLIORI SU B(36 mesi) — e cosa costano sugli altri assi:")
|
||||
print(f" {'leva':>5} {'conti':>6} {'taglia':>8} {'rebuy':>6} {'B 24m':>7} {'B 36m':>7} "
|
||||
f"{'B 60m':>7} {'A 120m':>7} {'P(zero)':>8} {'P(<vers) 36m':>13} {'bigl.':>6}")
|
||||
for _, r in G.sort_values("B36", ascending=False).head(8).iterrows():
|
||||
print(f" {r['lev_fu']:>5.2f} {int(r['n_max']):>6} {r['pref']:>8} "
|
||||
f"{'si' if r['rebuy'] else 'no':>6} {r['B24']:>7.1%} {r['B36']:>7.1%} "
|
||||
f"{r['B60']:>7.1%} {r['A120']:>7.1%} {r['pz']:>8.1%} {r['psotto36']:>13.1%} "
|
||||
f"{r['ntick']:>6.1f}")
|
||||
print(f"\n LE 6 MIGLIORI SU A(120 mesi, muro pubblicato ${MURO:,.0f}):")
|
||||
print(f" {'leva':>5} {'conti':>6} {'taglia':>8} {'rebuy':>6} {'A 36m':>7} {'A 120m':>7} "
|
||||
f"{'A120 p90':>9} {'A120 -1SE':>10} {'B 36m':>7} {'mediana $ 120m':>15}")
|
||||
for _, r in G.sort_values("A120", ascending=False).head(6).iterrows():
|
||||
print(f" {r['lev_fu']:>5.2f} {int(r['n_max']):>6} {r['pref']:>8} "
|
||||
f"{'si' if r['rebuy'] else 'no':>6} {r['A36']:>7.1%} {r['A120']:>7.1%} "
|
||||
f"{r['A120lo']:>9.1%} {r['A120hi']:>10.1%} {r['B36']:>7.1%} "
|
||||
f"${r['med120']:>14,.0f}")
|
||||
bestB = G.loc[G["B36"].idxmax()]
|
||||
bestA = G.loc[G["A120"].idxmax()]
|
||||
print(f"\n ARGMAX B(36m) = leva {bestB['lev_fu']:.2f}x, {int(bestB['n_max'])} conti, "
|
||||
f"biglietto {bestB['pref']}, rebuy {'si' if bestB['rebuy'] else 'no'} -> "
|
||||
f"{bestB['B36']:.1%}")
|
||||
print(f" ARGMAX A(120m) = leva {bestA['lev_fu']:.2f}x, {int(bestA['n_max'])} conti, "
|
||||
f"biglietto {bestA['pref']}, rebuy {'si' if bestA['rebuy'] else 'no'} -> "
|
||||
f"{bestA['A120']:.1%}")
|
||||
print(f" I due argmax {'COINCIDONO' if (bestB[['lev_fu','n_max','pref','rebuy']].tolist() == bestA[['lev_fu','n_max','pref','rebuy']].tolist()) else 'SONO DIVERSI'}.")
|
||||
tick("§4")
|
||||
|
||||
# ======================================================================
|
||||
hr("§5. LE DIMENSIONI RESTANTI — floor, versamento, leva del libro, ripartizione propria")
|
||||
bp = dict(lev_fu=float(bestB["lev_fu"]), n_max=int(bestB["n_max"]),
|
||||
pref=str(bestB["pref"]), rebuy=bool(bestB["rebuy"]))
|
||||
print(f" Politica di riferimento: l'argmax di B {bp}")
|
||||
print(f"\n (a) FLOOR — capitale proprio sotto cui non si compra un altro biglietto")
|
||||
print(f" {'floor':>10} {'B 36m':>8} {'A 120m':>8} {'P(zero)':>9} {'mediana $ 120m':>15}")
|
||||
for fl in (0.0, 2_000.0, 10_000.0):
|
||||
r = run(floor_usd=fl, **bp)
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
print(f" ${fl:>9,.0f} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} "
|
||||
f"{s36['p_zero']:>9.1%} ${s120['med_cash']:>14,.0f}")
|
||||
print(f"\n (b) VERSAMENTO MENSILE — la leva che il progetto misura come dominante")
|
||||
print(f" {'EUR/mese':>10} {'strada':>10} {'B 36m':>8} {'A 120m':>8} {'A 240m':>8} "
|
||||
f"{'mediana $ 120m':>15}")
|
||||
for dep in (0.0, 250.0, 500.0, 1_000.0):
|
||||
for lab, kw in (("PROP", bp), ("LIBRO", dict(n_max=0))):
|
||||
r = run(dep_eur=dep, **kw)
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
a240 = anni_med(r, MURO)
|
||||
print(f" {dep:>10,.0f} {lab:>10} {s36['p_flow']:>8.1%} "
|
||||
f"{s120[f'A{MURO:.0f}']:>8.1%} {a240[1]:>8.1%} ${s120['med_cash']:>14,.0f}")
|
||||
print(" (la colonna 'A 240m' e' P(traguardo) sull'intera simulazione = 120 mesi: coincide")
|
||||
print(" con A 120m — la si stampa per rendere esplicito che l'orizzonte massimo e' 10 anni)")
|
||||
print(f"\n (c) LEVA DEL LIBRO PROPRIO — muro CONGELATO (primaria) e MOBILE (sensibilita')")
|
||||
print(" ⚠️ §38: la chiave di scala NON ESISTE in `config/live.json`; §57: a k=1,50x il")
|
||||
print(" peggior giorno e' 21,48% e la guardia dello scettico (<=20%) lo BOCCIA.")
|
||||
print(f" {'k':>5} {'muro mobile':>13} {'A 120m congel.':>15} {'A 120m mobile':>14} "
|
||||
f"{'B 36m':>7} {'mediana $ 120m':>15}")
|
||||
for k in (1.00, 1.25, 1.50):
|
||||
rk = rI * k
|
||||
lo, hi = 0.0, 0.40
|
||||
for _ in range(11):
|
||||
mid = (lo + hi) / 2
|
||||
sv = PN.sopravvivenza(np.asarray(rL3, float) * k, 1e6, 1e6 * mid, 20, TAX, PATR,
|
||||
n_paths=1200)
|
||||
lo, hi = (mid, hi) if sv["p10_end"] >= 1e6 else (lo, mid)
|
||||
mk = prel / lo if lo > 0.002 else float("inf")
|
||||
r = run(k_book=k, walls=(MURO, mk) if np.isfinite(mk) else (MURO,), **bp)
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
am = s120.get(f"A{mk:.0f}", float("nan")) if np.isfinite(mk) else float("nan")
|
||||
print(f" {k:>5.2f} " + (f"${mk:>12,.0f}" if np.isfinite(mk) else f"{'IRRAGG.':>13}")
|
||||
+ f" {s120[f'A{MURO:.0f}']:>15.1%} "
|
||||
+ (f"{am:>14.1%}" if am == am else f"{'—':>14}")
|
||||
+ f" {s36['p_flow']:>7.1%} ${s120['med_cash']:>14,.0f}")
|
||||
bump(1)
|
||||
print(f"\n (d) RIPARTIZIONE DEL LIBRO PROPRIO — LIVE 75/25 contro W_A 38/38/25")
|
||||
print(" (la seconda mette XS01 anche sul capitale proprio: §46 ha falsificato la soglia")
|
||||
print(" dei $20k da min-order, ma la decisione di venue del 26/07 tiene tutto su")
|
||||
print(" Deribit fino a $20k -> e' una sensibilita', non una proposta)")
|
||||
print(f" {'libro proprio':>16} {'B 36m':>8} {'A 120m':>8} {'mediana $ 120m':>15}")
|
||||
for lab, rbk in (("LIVE 75/25", RB_LIVE), ("W_A 38/38/25", RB_WA)):
|
||||
r = run(Rb=rbk, **bp)
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
print(f" {lab:>16} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} "
|
||||
f"${s120['med_cash']:>14,.0f}")
|
||||
print(f"\n (e) LA CASSA CHE LAVORA — attesa A3")
|
||||
print(f" {'cassa':>16} {'B 36m':>8} {'A 120m':>8} {'mediana $ 36m':>14} "
|
||||
f"{'mediana $ 120m':>15}")
|
||||
inv = {}
|
||||
for lab, kw in (("ferma (pubbl.)", dict(invest=False, k_book=0.0, tax=0.0, patr=0.0)),
|
||||
("investita", dict(invest=True, k_book=1.0))):
|
||||
r = run(**{**bp, **kw})
|
||||
inv[lab] = r
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
print(f" {lab:>16} {s36['p_flow']:>8.1%} {s120[f'A{MURO:.0f}']:>8.1%} "
|
||||
f"${s36['med_cash']:>13,.0f} ${s120['med_cash']:>14,.0f}")
|
||||
dB = (inv["investita"]["snap"][36 * 30]["p_flow"]
|
||||
- inv["ferma (pubbl.)"]["snap"][36 * 30]["p_flow"])
|
||||
dA = (inv["investita"]["snap"][120 * 30][f"A{MURO:.0f}"]
|
||||
- inv["ferma (pubbl.)"]["snap"][120 * 30][f"A{MURO:.0f}"])
|
||||
print(f" ATTESA A3: dB {dB*100:+.2f}pp, dA {dA*100:+.2f}pp -> "
|
||||
f"{'CONFERMATA' if abs(dB) < 0.02 and dA > 0.02 else 'da leggere nel testo'}")
|
||||
tick("§5")
|
||||
|
||||
# ======================================================================
|
||||
hr("§6. QUANTO COSTA LA DIFFERENZA FRA L'OTTIMO E CIO' CHE IL PROGETTO FA OGGI")
|
||||
oggi = run(n_max=0)
|
||||
pubb = run(n_max=6, lev_fu=LEV_J, invest=False, k_book=0.0, tax=0.0, patr=0.0)
|
||||
optB = run(**bp)
|
||||
optA = run(lev_fu=float(bestA["lev_fu"]), n_max=int(bestA["n_max"]),
|
||||
pref=str(bestA["pref"]), rebuy=bool(bestA["rebuy"]))
|
||||
print(f" {'politica':<44} {'B 36m':>8} {'A 36m':>8} {'A 120m':>8} {'anni al muro':>13} "
|
||||
f"{'P(<versato) 36m':>16}")
|
||||
for lab, r in (("OGGI: libro 75/25 k=1, nessun biglietto", oggi),
|
||||
("PUBBLICATA: 6 conti a leva argmax-J", pubb),
|
||||
("ARGMAX B (flusso)", optB),
|
||||
("ARGMAX A (ricchezza)", optA)):
|
||||
s36, s120 = r["snap"][36 * 30], r["snap"][120 * 30]
|
||||
med, parr = anni_med(r, MURO)
|
||||
print(f" {lab:<44} {s36['p_flow']:>8.1%} {s36[f'A{MURO:.0f}']:>8.1%} "
|
||||
f"{s120[f'A{MURO:.0f}']:>8.1%} "
|
||||
+ (f"{med:>12.1f}a" if np.isfinite(med) else f"{'>10a':>13}")
|
||||
+ f" {s36['p_sotto']:>16.1%}")
|
||||
dB36 = optB["snap"][36 * 30]["p_flow"] - oggi["snap"][36 * 30]["p_flow"]
|
||||
dA120 = optA["snap"][120 * 30][f"A{MURO:.0f}"] - oggi["snap"][120 * 30][f"A{MURO:.0f}"]
|
||||
print(f"\n COSTO DI NON OTTIMIZZARE: su B(36m) {dB36*100:+.1f}pp, "
|
||||
f"su A(120m) {dA120*100:+.1f}pp.")
|
||||
tick("§6")
|
||||
|
||||
# ======================================================================
|
||||
hr("§7. LA DOMANDA DECISIVA — esiste una politica con P(>=50 EUR/g entro 36 mesi) > 20%?")
|
||||
maxB = float(G["B36"].max())
|
||||
maxA36 = float(G["A36"].max())
|
||||
rowB = G.loc[G["B36"].idxmax()]
|
||||
print(f" Sotto la definizione B (FLUSSO, quella dei numeri pubblicati):")
|
||||
print(f" massimo su {len(G)} politiche = {maxB:.1%} "
|
||||
f"(leva {rowB['lev_fu']:.2f}x, {int(rowB['n_max'])} conti, biglietto {rowB['pref']}, "
|
||||
f"rebuy {'si' if rowB['rebuy'] else 'no'})")
|
||||
print(f" ma con P(zero payout) {rowB['pz']:.1%} e P(finire sotto il versato) "
|
||||
f"{rowB['psotto36']:.1%}")
|
||||
print(f" -> {'SI, SOPRA IL 20%' if maxB > 0.20 else 'NO, SOTTO IL 20%'}")
|
||||
print(f"\n Sotto la definizione A (RICCHEZZA sostenibile, il muro del progetto):")
|
||||
print(f" massimo su {len(G)} politiche a 36 mesi = {maxA36:.2%} "
|
||||
f"(a 120 mesi: {float(G['A120'].max()):.1%})")
|
||||
print(f" -> {'SI' if maxA36 > 0.20 else 'NO'}, e non per poco: il muro e' "
|
||||
f"${MURO:,.0f} e a 36 mesi il capitale mediano migliore e' "
|
||||
f"${float(G['med120'].max()):,.0f} a 120 mesi.")
|
||||
print(f"\n ATTESA A2 ({'CONFERMATA' if (maxB > 0.20 and maxA36 < 0.05) else 'SMENTITA'}): "
|
||||
"«SI' sotto B e NO sotto A».")
|
||||
print(f" ATTESA A4 ({'CONFERMATA' if rowB['pz'] > 0.50 or rowB['psotto36'] > 0.50 else 'SMENTITA'}): "
|
||||
"l'ottimo di P e' una politica ad alta rovina.")
|
||||
|
||||
# il vincolo che fissa il massimo
|
||||
print(f"\n QUALE VINCOLO FISSA IL MASSIMO — si toglie un vincolo per volta (obiettivo B, 36m):")
|
||||
print(f" {'scenario':<46} {'B 36m':>8} {'delta':>8}")
|
||||
base_b = float(rowB["B36"])
|
||||
scen = []
|
||||
r = run(start_usd=10_000.0, **bp); scen.append(("capitale iniziale $10.000 invece di $635", r))
|
||||
r = run(dep_eur=2_000.0, **bp); scen.append(("versamento EUR 2.000/mese invece di 500", r))
|
||||
cap_old = [F["cap"] for F in pl.FIRMS]
|
||||
for F in pl.FIRMS:
|
||||
F["cap"] *= 5
|
||||
r = run(**bp); scen.append(("cap per firm x5 ($1M / $2M)", r))
|
||||
for F, c in zip(pl.FIRMS, cap_old):
|
||||
F["cap"] = c
|
||||
dead_old = pl.FIRM_DEATH_PER_YEAR
|
||||
pl.FIRM_DEATH_PER_YEAR = 0.0
|
||||
r = run(**bp); scen.append(("morte-firm 0%/anno invece di 10%", r))
|
||||
pl.FIRM_DEATH_PER_YEAR = dead_old
|
||||
r = run(n_max=6, **{k: v for k, v in bp.items() if k != "n_max"})
|
||||
scen.append(("6 conti concorrenti (se non lo e' gia')", r))
|
||||
for lab, r in scen:
|
||||
b = r["snap"][36 * 30]["p_flow"]
|
||||
print(f" {lab:<46} {b:>8.1%} {(b-base_b)*100:>+7.1f}pp")
|
||||
print(f" ATTESA A5: il vincolo binding e' il capitale che entra "
|
||||
f"(riga 'versamento' e riga 'capitale iniziale').")
|
||||
|
||||
# ======================================================================
|
||||
hr("VERDETTO")
|
||||
eur_g_oggi = oggi["snap"][36 * 30]["med_cash"]
|
||||
print(f" Trial contati al rialzo: {TRIALS}. Tempo {time.time()-T0:.0f}s. "
|
||||
f"Percorsi {N_PATHS} (SE ~{100*np.sqrt(0.2*0.8/N_PATHS):.1f}pp su una P vicina al 20%).")
|
||||
print(f"\n * L'obiettivo del progetto ha DUE definizioni operative in uso e danno risposte")
|
||||
print(f" opposte alla stessa domanda. Con la definizione del canale funded (FLUSSO) il")
|
||||
print(f" massimo a 36 mesi e' {maxB:.1%}; con quella dei muri e del piano (RICCHEZZA")
|
||||
print(f" SOSTENIBILE) e' {maxA36:.2%}.")
|
||||
print(f" * L'ottimo di P(>=50/g) NON e' l'ottimo di J: {CU['B36'].max():.1%} contro "
|
||||
f"{float(CU.loc[CU['J'].idxmax(),'B36']):.1%} alla leva argmax-J.")
|
||||
print(f" * In EUR/giorno, la mediana di cio' che il libro di oggi produce a 36 mesi resta")
|
||||
print(f" ZERO (non preleva), e il capitale mediano e' ${eur_g_oggi:,.0f}: la rendita")
|
||||
print(f" perpetua su quel capitale vale {perp*eur_g_oggi/365/EURUSD:.2f} EUR/giorno.")
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,964 @@
|
||||
"""§68 QUASI-PASSATI — i candidati morti per pochissimo, presi INSIEME, passano?
|
||||
|
||||
DOMANDA
|
||||
=======
|
||||
In due mesi il progetto ha bocciato decine di candidati e molti sono morti *per un soffio*:
|
||||
deflated-Sharpe 0,948 (tenore VRP, bocciato per 0,002), 0,947 (cella pubblicata DVOLSPREAD),
|
||||
0,929 (STATARB same-sign), 0,905 (cella viva di PREVDAY), e `weights_tilt_null` fallito per
|
||||
delta_insample -0,004 (XSR01). Nessuno ha mai chiesto se, MESSI INSIEME, formino qualcosa che
|
||||
passa. La domanda e' legittima perche' sono meccanismi DIVERSI (opzioni, spread di vol, residuo
|
||||
cross-asset, livelli del giorno prima, cross-section su 50 alt) -> la loro combinazione non e'
|
||||
automaticamente ridondante.
|
||||
|
||||
Ma e' ANCHE la forma classica del data-dredging: raccogliere i falliti finche' la media non passa.
|
||||
IL COMPITO E' MISURARE QUALE DELLE DUE COSE E', NON TROVARE UN VINCITORE.
|
||||
|
||||
REGOLA DI AMMISSIONE — DICHIARATA PRIMA DI MISURARE, NON CAMBIATA DOPO
|
||||
=====================================================================
|
||||
Entra nel paniere ogni candidato che soddisfa TUTTE E TRE:
|
||||
(R1) NON e' nel libro LIVE ne' nel book di ricerca a 5 sleeve (= candidato respinto o in limbo);
|
||||
(R2) l'esito pubblicato dista <= 0,05 dalla soglia del gate che lo ha fermato:
|
||||
|DSR - 0,95| <= 0,05 OPPURE |delta_insample| <= 0,01 su `weights_tilt_null`;
|
||||
(R3) la serie e' RICOSTRUIBILE da codice committato coi parametri congelati/pubblicati.
|
||||
La rappresentante di ogni candidato e' la CELLA CONGELATA NEL MONITOR (o, se monitor non esiste,
|
||||
la cella pubblicata). PESI UGUALI. NESSUNA selezione successiva: il leave-one-out e' diagnostica,
|
||||
non una potatura. Nessun candidato viene aggiunto o tolto dopo aver visto un risultato.
|
||||
|
||||
ATTESE A PRIORI (dichiarate qui PRIMA di misurare; a fine script si dice confermata/refutata)
|
||||
=============================================================================================
|
||||
A1 Il paniere avra' Sharpe piu' alto di OGNI singola gamba. E' aritmetica di diversificazione,
|
||||
non evidenza: e' attesa, e da sola non decide niente.
|
||||
A2 Il DSR analitico del paniere si RIBALTA col conteggio dei trial (lezione 30/07 + §10/§24):
|
||||
PASS con N=K, FAIL con N=somma dei trial di famiglia. Verdetto pubblicato con la sensibilita'.
|
||||
A3 Il paniere dei QUASI-PASSATI battera' quello degli SCARTATI NETTI in-sample (per costruzione:
|
||||
i primi sono cellette scelte, i secondi no) MA il vantaggio si RIDURRA' sull'hold-out.
|
||||
A4 Le gambe sono quasi scorrelate fra loro -> ampiezza effettiva vicina a K -> il paniere alza
|
||||
lo Sharpe PER COSTRUZIONE senza contenere un solo edge nuovo.
|
||||
A5 Il paniere "funziona" grazie a UNA gamba (PREVDAY, il piu' forte misurato in §50) -> il
|
||||
leave-one-out lo mostrera'.
|
||||
A6 Almeno una gamba e' STAT-MODE (XSR01: venue diverso, non Deribit) -> il paniere COSI' COM'E'
|
||||
non e' un candidato al libro, a prescindere dal numero.
|
||||
A7 Le gambe sono DECADUTE dalla loro pubblicazione (i numeri hanno 1-2 mesi): mi aspetto un
|
||||
degrado, non un miglioramento.
|
||||
|
||||
METODO / VINCOLI
|
||||
================
|
||||
* SOLA LETTURA. Nessun file di produzione toccato, nessun ordine, nessuna chiave API.
|
||||
Endpoint di rete: NESSUNO (tutto da parquet certificati + cache su disco).
|
||||
* Le serie hanno date d'inizio diverse -> `combine_outer` con rinormalizzazione per riga, e la
|
||||
COPERTURA PER COLONNA e' STAMPATA (prima diagnostica di un outer-join: il progetto ha gia'
|
||||
pubblicato un muro sbagliato di 3x per averla saltata).
|
||||
* Lente Sharpe: `altlib._sh` (365,25 g/anno) ovunque, dichiarata.
|
||||
* Replica di un numero pubblicato PRIMA di produrne di nuovi (sezione 3).
|
||||
* Controllo positivo obbligatorio: un rilevatore che non trova nulla e' indistinguibile da uno
|
||||
rotto -> i null sono validati su casi in cui DEVONO scattare.
|
||||
|
||||
USO
|
||||
uv run python scripts/research/r0823b_quasi_passati.py [--draws 1000] [--force]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import itertools
|
||||
import math
|
||||
import pickle
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path("/opt/docker/PythagorasGoal")
|
||||
for _p in (str(ROOT), str(ROOT / "scripts" / "research"),
|
||||
str(ROOT / "scripts" / "research" / "alt"),
|
||||
str(ROOT / "scripts" / "research" / "ortho")):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
# --- produzione (IMPORTATA, mai riscritta) ---------------------------------------------------
|
||||
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
|
||||
from src.live import book as LB # noqa: E402
|
||||
|
||||
# --- macchineria di ricerca gia' validata (riusata, non riscritta) ---------------------------
|
||||
import altlib as al # noqa: E402
|
||||
import ortholib as ol # noqa: E402
|
||||
import orthogonal_signals as OS # noqa: E402
|
||||
import r0823_prevday_gate as PG # noqa: E402 (factory + griglia 336)
|
||||
import r0730_vrp_tenor_gate as VT # noqa: E402 (cell_daily, griglia 72)
|
||||
import r0725_statarb_multi as SM # noqa: E402 (meccanismo XSR congelato)
|
||||
import r0725_statarb_basket_gate as BG # noqa: E402 (pair_frames / demean)
|
||||
from r0726_dvolspread_gate import make_book as dvs_make_book, GRID as DVS_GRID # noqa: E402
|
||||
|
||||
HOLDOUT = al.HOLDOUT
|
||||
SEED = 20260823
|
||||
T0 = time.time()
|
||||
|
||||
_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
|
||||
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/qp")
|
||||
SCRATCH = _SP if _SP.parent.parent.exists() else Path("/tmp/pythagoras_quasi")
|
||||
CACHE = SCRATCH / "families_v1.pkl"
|
||||
B3RD_CACHE = SCRATCH.parent / "b3rd" / "series_v3.pkl" # cache §50 (serie ancorate, funding IN)
|
||||
|
||||
CAPITAL_TODAY = 635.0
|
||||
MIN_ORDER = 5.0
|
||||
BOOK_START = pd.Timestamp("2019-03-14", tz="UTC")
|
||||
W_TP, W_SKH = LB.W_TP01, LB.W_SKH
|
||||
TP_HOURS = tuple(range(24))
|
||||
SKH_OFFSETS = tuple(range(0, 690, 30))
|
||||
XS_FUNDING_DRAG_YR = 0.0145 # §37, dichiarato (XS paga funding, non lo incassa)
|
||||
|
||||
# --- LE CELLE CONGELATE (rappresentanti dei 5 quasi-passati) ---------------------------------
|
||||
FROZEN = {
|
||||
"PREVDAY": dict(tf="1h", anchor=1, k=0.30, short=True, mh=0),
|
||||
"DVOLSPREAD": dict(zwin=180, tanh_k=2.0, lw=0.6, zw=1.1, tgt=0.17, svw=60),
|
||||
"DVS-PUB": dict(zwin=150, tanh_k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50),
|
||||
"VRP-TENORE": dict(tenor=10, sd=-0.28, ld=-0.05),
|
||||
"STATARB": dict(W=45, sgn=+1),
|
||||
"XSR01": dict(W=45, sgn=+1),
|
||||
}
|
||||
LEGS = ["PREVDAY", "DVOLSPREAD", "VRP-TENORE", "STATARB", "XSR01"]
|
||||
|
||||
# --- trial DICHIARATI per famiglia, contati AL RIALZO (regola 30/07) -------------------------
|
||||
N_TRIALS = {"PREVDAY": 336, "DVOLSPREAD": 729, "VRP-TENORE": 72, "STATARB": 10, "XSR01": 10}
|
||||
|
||||
|
||||
def sez(t: str) -> None:
|
||||
print("\n" + "=" * 106)
|
||||
print(f" {t}")
|
||||
print("=" * 106)
|
||||
|
||||
|
||||
def sub(t: str) -> None:
|
||||
print("\n" + "-" * 106)
|
||||
print(f" {t}")
|
||||
print("-" * 106)
|
||||
|
||||
|
||||
def clock() -> str:
|
||||
return f"[{time.time() - T0:6.1f}s]"
|
||||
|
||||
|
||||
def sh(s) -> float:
|
||||
return al._sh(s)
|
||||
|
||||
|
||||
def shH(s) -> float:
|
||||
s = s[s.index >= HOLDOUT]
|
||||
return al._sh(s) if len(s) > 30 else float("nan")
|
||||
|
||||
|
||||
def shI(s) -> float:
|
||||
s = s[s.index < HOLDOUT]
|
||||
return al._sh(s) if len(s) > 30 else float("nan")
|
||||
|
||||
|
||||
def dd(s) -> float:
|
||||
return al._dd_ret(s)
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# LE FAMIGLIE — ogni cella e' una serie di rendimenti netti GIORNALIERI (fee reali, no funding)
|
||||
# =============================================================================================
|
||||
def fam_prevday() -> dict:
|
||||
out = {}
|
||||
for tf in PG.G_TF:
|
||||
for a, k, s_, mh in itertools.product(PG.G_ANCHOR, PG.G_K, PG.G_SHORT, PG.G_MH):
|
||||
key = (tf, a, k, s_, mh)
|
||||
try:
|
||||
fn = PG.factory(tf=tf, anchor=a, k=k, short=s_, mh=mh)
|
||||
out[key] = al.candidate_daily(fn, tf=tf)
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def fam_dvolspread() -> dict:
|
||||
out = {}
|
||||
keys = list(DVS_GRID)
|
||||
for combo in itertools.product(*[DVS_GRID[k] for k in keys]):
|
||||
kw = dict(zip(keys, combo))
|
||||
try:
|
||||
out[tuple(kw[k] for k in keys)] = ol.eval_book(dvs_make_book(**kw))["daily"].dropna()
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def fam_vrp() -> dict:
|
||||
out = {}
|
||||
for tn, sd_, ld in itertools.product(VT.TENORS, VT.SHORT_DELTAS, VT.LONG_DELTAS):
|
||||
try:
|
||||
s = VT.cell_daily(tn, sd_, ld)
|
||||
if len(s) > 200:
|
||||
out[(tn, sd_, ld)] = s
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _spread_daily(pos, j) -> pd.Series:
|
||||
ev = OS.eval_spread(j, pos)
|
||||
return pd.Series(ev["net"], index=pd.DatetimeIndex(pd.to_datetime(j["datetime"], utc=True)))
|
||||
|
||||
|
||||
def fam_statarb() -> dict:
|
||||
j = OS.build_joint("1d")
|
||||
out = {}
|
||||
for W, sg in itertools.product((30, 45, 60, 90, 120), (1, -1)):
|
||||
out[(W, sg)] = _spread_daily(OS.f_statarb_resid(W=W, sgn=sg)(j), j)
|
||||
return out
|
||||
|
||||
|
||||
def fam_xsr() -> dict:
|
||||
"""XSR01: meccanismo STATARB-RESID congelato sui 50 alt HL, posizioni DEMEANATE."""
|
||||
out = {}
|
||||
W0, S0 = SM.W, SM.SGN
|
||||
try:
|
||||
for W, sg in itertools.product((30, 45, 60, 90, 120), (1, -1)):
|
||||
SM.W, SM.SGN = W, sg
|
||||
P, S = BG.pair_frames()
|
||||
out[(W, sg)] = BG.basket_from_positions(P, S, demean=True)
|
||||
finally:
|
||||
SM.W, SM.SGN = W0, S0
|
||||
return out
|
||||
|
||||
|
||||
def fam_ortho_rejects() -> dict:
|
||||
"""B4 — SCARTATI NETTI CROSS-MECCANISMO: 4 famiglie relative-value dell'onda ortho
|
||||
(2026-06-21) che lo scorer INDURITO ha collassato (17/18 -> 1). Meccanismi DIVERSI fra loro
|
||||
e diversi dai quasi-passati. Stesso dato, stesse fee, stessa lente."""
|
||||
j = OS.build_joint("1d")
|
||||
out = {}
|
||||
for nm in ("RATIO-MOM", "RATIO-REV", "RATIO-ACCEL", "VOLSPREAD"):
|
||||
f, grid, _ = OS.FAMILIES[nm]
|
||||
out[nm] = {tuple(sorted(p.items())): _spread_daily(f(**p)(j), j) for p in grid}
|
||||
return out
|
||||
|
||||
|
||||
def build_families(force: bool) -> dict:
|
||||
if CACHE.exists() and not force:
|
||||
print(f" {clock()} cache famiglie: {CACHE}")
|
||||
return pickle.loads(CACHE.read_bytes())
|
||||
F = {}
|
||||
for nm, fn in (("VRP-TENORE", fam_vrp), ("STATARB", fam_statarb), ("XSR01", fam_xsr),
|
||||
("DVOLSPREAD", fam_dvolspread), ("PREVDAY", fam_prevday)):
|
||||
t = time.time()
|
||||
F[nm] = fn()
|
||||
print(f" {clock()} famiglia {nm:<12} {len(F[nm]):>4} celle ({time.time()-t:5.1f}s)")
|
||||
t = time.time()
|
||||
F["_ORTHO"] = fam_ortho_rejects()
|
||||
print(f" {clock()} famiglie ortho (controllo B4): "
|
||||
f"{ {k: len(v) for k, v in F['_ORTHO'].items()} } ({time.time()-t:.1f}s)")
|
||||
SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
CACHE.write_bytes(pickle.dumps(F))
|
||||
return F
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# MATRICE ALLINEATA + COMBINAZIONE EQUIPESATA (replica numerica di combine_outer)
|
||||
# =============================================================================================
|
||||
class Grid:
|
||||
"""Tutte le celle di tutte le famiglie su UN indice-unione. Il combinatore equipesato e'
|
||||
la trasposizione numpy di `combine_outer` (verificata bit-exact in sezione 5)."""
|
||||
|
||||
def __init__(self, fams: dict, lo=None):
|
||||
idx = None
|
||||
for f in fams.values():
|
||||
for s in f.values():
|
||||
idx = s.index if idx is None else idx.union(s.index)
|
||||
if lo is not None:
|
||||
idx = idx[idx >= lo]
|
||||
self.idx = idx
|
||||
self.cols, self.keys = {}, {}
|
||||
mats = []
|
||||
for nm, f in fams.items():
|
||||
ks = list(f)
|
||||
self.keys[nm] = ks
|
||||
M = np.full((len(idx), len(ks)), np.nan)
|
||||
for c, k in enumerate(ks):
|
||||
M[:, c] = f[k].reindex(idx).values
|
||||
self.cols[nm] = M
|
||||
mats.append(M)
|
||||
|
||||
def pick(self, sel: dict) -> np.ndarray:
|
||||
return np.column_stack([self.cols[nm][:, self.keys[nm].index(k)] for nm, k in sel.items()])
|
||||
|
||||
def combo(self, M: np.ndarray, idx=None) -> pd.Series:
|
||||
# `idx`: indice delle RIGHE di M. Default self.idx (M sull'intera unione);
|
||||
# va passato esplicitamente quando M e' un sottoinsieme di righe, altrimenti
|
||||
# la maschera `keep` e l'indice hanno lunghezze diverse.
|
||||
ix = self.idx if idx is None else idx
|
||||
if M.shape[0] != len(ix):
|
||||
raise ValueError(f"combo: M ha {M.shape[0]} righe, l'indice ne ha {len(ix)}")
|
||||
mask = ~np.isnan(M)
|
||||
w = mask.astype(float)
|
||||
rs = w.sum(axis=1, keepdims=True)
|
||||
wn = np.divide(w, rs, out=np.zeros_like(w), where=rs > 0)
|
||||
v = np.nansum(np.nan_to_num(M) * wn, axis=1)
|
||||
keep = mask.any(axis=1)
|
||||
return pd.Series(v[keep], index=ix[keep])
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 0) ATTESE | 1) CENSIMENTO | 2) REGOLA
|
||||
# =============================================================================================
|
||||
CENSIMENTO = [
|
||||
# nome, criterio, valore, soglia, distanza, esito, eseguibile, fonte
|
||||
("PREVDAY (cella viva)", "deflated-Sharpe N=336", 0.905, 0.95, -0.045, "FAIL",
|
||||
"SI' — Deribit, netta col libro", "RESULTS §54"),
|
||||
("DVOLSPREAD (congelata)", "deflated-Sharpe N=729", 0.953, 0.95, +0.003, "PASS di misura",
|
||||
"SI' — Deribit, netta col libro", "CLAUDE.md 26/07"),
|
||||
("DVOLSPREAD (pubblicata)", "deflated-Sharpe N=729", 0.947, 0.95, -0.003, "FAIL",
|
||||
"SI' — Deribit", "CLAUDE.md 26/07"),
|
||||
("VRP-TENORE 10g -0,28/-0,05", "deflated-Sharpe N=72", 0.948, 0.95, -0.002, "FAIL",
|
||||
"opzioni ETH, lotto ~$242", "CLAUDE.md 30/07"),
|
||||
("STATARB-RESID W45 sgn+1", "deflated-Sharpe same-sign", 0.929, 0.95, -0.021, "FAIL",
|
||||
"SI' — 2 gambe BTC/ETH", "diario 2026-06-29"),
|
||||
("XSR01 (50 alt HL, demean)", "weights_tilt_null delta_IS", -0.004, 0.0, -0.004, "FAIL",
|
||||
"NO — venue Hyperliquid, STAT-MODE", "CLAUDE.md 25/07"),
|
||||
]
|
||||
ESCLUSI = [
|
||||
("BIN-FREQ (SKH01)", "DSR 0,977 PASS ma 3 FAIL su 4 gambe del criterio",
|
||||
"non e' una gamba propria: e' una MODIFICA di uno sleeve in produzione (R3)"),
|
||||
("XS01-OOS", "DSR 0,342", "distanza 0,61 dalla soglia: e' uno scartato netto, non un quasi (R2)"),
|
||||
("STA05 (EWMA-cross)", "nessuna soglia numerica pubblicata",
|
||||
"mori' su hold-out corto + corr 0,53 a TP01, non su un gate a soglia (R2)"),
|
||||
("DVOL-DIREZIONALE", "is_hedge = True", "criterio BINARIO: non esiste una distanza (R2)"),
|
||||
("EW-STR (tilt di pesi)", "pctl 94-100 fra 500 tilt", "e' un cambio di PESI, non un candidato (R1)"),
|
||||
("LOWVOL 19-major", "DSR 0,13", "scartato netto (R2)"),
|
||||
]
|
||||
|
||||
|
||||
def sezione1() -> None:
|
||||
sez("1) CENSIMENTO DEI QUASI-PASSATI — criterio, valore, distanza dalla soglia")
|
||||
print(f" {'candidato':<28}{'criterio':<28}{'val':>8}{'soglia':>8}{'dist':>8} {'esito':<16}"
|
||||
f"{'eseguibile a $635'}")
|
||||
for nm, cr, v, so, di, es, ex, fo in CENSIMENTO:
|
||||
print(f" {nm:<28}{cr:<28}{v:>8.3f}{so:>8.2f}{di:>+8.3f} {es:<16}{ex}")
|
||||
print(f"\n {'':28}{'':28}{'':8}{'':8}{'':8} fonti: " +
|
||||
", ".join(sorted({f for *_, f in CENSIMENTO})))
|
||||
sub("ESCLUSI dal paniere, e perche' (la regola e' dichiarata PRIMA, non adattata all'esito)")
|
||||
for nm, dato, perche in ESCLUSI:
|
||||
print(f" {nm:<24} {dato:<44} -> {perche}")
|
||||
|
||||
|
||||
def sezione2() -> None:
|
||||
sez("2) REGOLA DI AMMISSIONE — dichiarata prima, NON cambiata dopo")
|
||||
print(" (R1) non e' nel libro live ne' nel book a 5 sleeve")
|
||||
print(" (R2) |DSR - 0,95| <= 0,05 OPPURE |delta_insample| <= 0,01 su weights_tilt_null")
|
||||
print(" (R3) serie ricostruibile da codice committato coi parametri congelati/pubblicati")
|
||||
print(" Rappresentante = la cella CONGELATA NEL MONITOR (o la pubblicata se monitor assente).")
|
||||
print(" PESI UGUALI. Nessuna selezione successiva. Il leave-one-out e' DIAGNOSTICA.")
|
||||
print(f"\n -> entrano {len(LEGS)} gambe: " + ", ".join(LEGS))
|
||||
print(" ⚠️ DVOLSPREAD: due celle difendibili (congelata 0,953 / pubblicata 0,947). La regola")
|
||||
print(" dice 'quella del monitor' = la CONGELATA; la pubblicata gira come SENSIBILITA'.")
|
||||
print(f"\n trial dichiarati per famiglia (al RIALZO, regola 30/07): {N_TRIALS}")
|
||||
print(f" SOMMA = {sum(N_TRIALS.values())} trial. E' questo il N del deflated-Sharpe del")
|
||||
print(" paniere, NON K=5: un paniere di K candidati gia' selezionati si porta dietro TUTTI")
|
||||
print(" i trial delle famiglie da cui vengono.")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 3) REPLICHE — un numero gia' pubblicato prima di produrne di nuovi
|
||||
# =============================================================================================
|
||||
def sezione3(F: dict) -> dict:
|
||||
sez("3) REPLICHE — riprodurre i numeri PUBBLICATI prima di produrne di nuovi")
|
||||
legs = {}
|
||||
legs["PREVDAY"] = F["PREVDAY"][("1h", 1, 0.30, True, 0)]
|
||||
kd = list(DVS_GRID)
|
||||
legs["DVOLSPREAD"] = F["DVOLSPREAD"][tuple(FROZEN["DVOLSPREAD"][k] for k in kd)]
|
||||
dvs_pub = F["DVOLSPREAD"][tuple(FROZEN["DVS-PUB"][k] for k in kd)]
|
||||
legs["VRP-TENORE"] = F["VRP-TENORE"][(10, -0.28, -0.05)]
|
||||
legs["STATARB"] = F["STATARB"][(45, 1)]
|
||||
legs["XSR01"] = F["XSR01"][(45, 1)]
|
||||
vrp_canon = F["VRP-TENORE"][(7, -0.28, -0.10)]
|
||||
pv_blind = F["PREVDAY"][("4h", 1, 0.30, False, 0)]
|
||||
|
||||
print(f" {'oggetto':<34}{'pubblicato':>26}{'ri-misurato oggi':>30} esito")
|
||||
rows = [
|
||||
("PREVDAY congelata FULL/HOLD (§54)", "1,236 / 1,297",
|
||||
f"{sh(legs['PREVDAY']):.3f} / {shH(legs['PREVDAY']):.3f}"),
|
||||
("PREVDAY al buio 4h FULL/HOLD (§54)", "1,621 / 0,731",
|
||||
f"{sh(pv_blind):.3f} / {shH(pv_blind):.3f}"),
|
||||
("DVOLSPREAD onesta FULL/HOLD (26/07)", "0,68 / 0,69", "vedi vintage sotto"),
|
||||
("VRP canonico 7g FULL (30/07)", "1,32", f"{sh(vrp_canon):.3f}"),
|
||||
("VRP 10g -0,28/-0,05 FULL (30/07)", "1,55", f"{sh(legs['VRP-TENORE']):.3f}"),
|
||||
]
|
||||
for a, b, c in rows:
|
||||
print(f" {a:<34}{b:>26}{c:>30} ")
|
||||
|
||||
sub("3b) LE GAMBE AL VINTAGE DELLA LORO PUBBLICAZIONE — la replica vera")
|
||||
print(" Ogni numero pubblicato ha una DATA. Confrontarlo con la serie di oggi non e' una")
|
||||
print(" replica: e' una replica PIU' un mese di dati nuovi. Qui si separano le due cose.")
|
||||
print(f"\n {'gamba':<22}{'vintage':>12}{'FULL pubbl.':>13}{'FULL vintage':>14}"
|
||||
f"{'FULL oggi':>11}{'HOLD pubbl.':>13}{'HOLD vintage':>14}{'HOLD oggi':>11}")
|
||||
vint = [("PREVDAY", "2026-08-23", "1,236", "1,297"),
|
||||
("DVOLSPREAD", "2026-07-26", "0,68", "0,69"),
|
||||
("DVS-PUB", "2026-07-26", "—", "0,93"),
|
||||
("VRP-TENORE", "2026-07-30", "1,55", "1,01"),
|
||||
("STATARB", "2026-06-29", "0,84", "—"),
|
||||
("XSR01", "2026-07-25", "1,82", "—")]
|
||||
rep = {}
|
||||
for nm, cut, pf, ph in vint:
|
||||
s = dvs_pub if nm == "DVS-PUB" else legs[nm]
|
||||
sv = s[s.index < cut]
|
||||
rep[nm] = (sh(sv), shH(sv), sh(s), shH(s))
|
||||
print(f" {nm:<22}{cut:>12}{pf:>13}{sh(sv):>14.3f}{sh(s):>11.3f}"
|
||||
f"{ph:>13}{shH(sv):>14.3f}{shH(s):>11.3f}")
|
||||
print("\n ✅ ogni cifra 'vintage' e' la replica del 'pubblicato' accanto (la colonna 'oggi'")
|
||||
print(" e' un numero NUOVO). Le due colonne insieme misurano il DECADIMENTO da allora.")
|
||||
dec = [(nm, rep[nm][0] - float(pf.replace(",", ".")))
|
||||
for nm, _, pf, _ in vint if pf != "—"]
|
||||
return dict(legs=legs, dvs_pub=dvs_pub, vrp_canon=vrp_canon, pv_blind=pv_blind, rep=rep)
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 4) CORRELAZIONI E AMPIEZZA EFFETTIVA — prima dei rendimenti, come chiede la domanda
|
||||
# =============================================================================================
|
||||
def eff_breadth(C: np.ndarray) -> float:
|
||||
n = len(C)
|
||||
off = C[~np.eye(n, dtype=bool)]
|
||||
rbar = float(np.nanmean(off))
|
||||
return n / (1.0 + (n - 1) * rbar) if rbar > -1.0 / (n - 1) else float(n)
|
||||
|
||||
|
||||
def sezione4(legs: dict) -> dict:
|
||||
sez("4) CORRELAZIONI E AMPIEZZA EFFETTIVA — misurate PRIMA dei rendimenti del paniere")
|
||||
print(" Se le gambe sono scorrelate, un paniere alza lo Sharpe PER COSTRUZIONE senza")
|
||||
print(" contenere un solo edge nuovo. Questa tabella e' il contesto di ogni numero dopo.")
|
||||
M = pd.concat(legs, axis=1, join="outer").sort_index()
|
||||
Ci = M.corr()
|
||||
print(f"\n matrice di correlazione (giorni in comune a coppie, no riempimento):\n")
|
||||
print(" " + " " * 13 + "".join(f"{c[:11]:>13}" for c in Ci.columns))
|
||||
for r in Ci.index:
|
||||
print(f" {r[:12]:<13}" + "".join(f"{Ci.loc[r, c]:>13.3f}" for c in Ci.columns))
|
||||
C = Ci.values
|
||||
off = C[~np.eye(len(C), dtype=bool)]
|
||||
nb = eff_breadth(C)
|
||||
print(f"\n corr media fuori diagonale = {np.nanmean(off):+.4f} "
|
||||
f"[min {np.nanmin(off):+.3f}, max {np.nanmax(off):+.3f}]")
|
||||
print(f" AMPIEZZA EFFETTIVA (N / (1+(N-1)·rbar), la formula gia' usata dal progetto in")
|
||||
print(f" r0725_statarb_basket_gate.eff_breadth) = {nb:.2f} su K = {len(C)} gambe.")
|
||||
print(f" -> il paniere vale, in termini di scommesse indipendenti, {nb:.2f} gambe.")
|
||||
tp = al.tp01_baseline_daily()
|
||||
print(f"\n correlazione di ogni gamba al baseline TP01 (il libro e' 75% TP01):")
|
||||
for k, s in legs.items():
|
||||
a, b = s.align(tp, join="inner")
|
||||
print(f" {k:<14}{a.corr(b):+.3f} (giorni comuni {len(a)})")
|
||||
return dict(corr=Ci, neff=nb)
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 5) IL PANIERE — copertura per colonna PRIMA di tutto
|
||||
# =============================================================================================
|
||||
def sezione5(G: Grid, legs: dict) -> dict:
|
||||
sez("5) IL PANIERE EQUIPESATO — e la sua COPERTURA PER COLONNA")
|
||||
sel = {"PREVDAY": ("1h", 1, 0.30, True, 0),
|
||||
"DVOLSPREAD": tuple(FROZEN["DVOLSPREAD"][k] for k in DVS_GRID),
|
||||
"VRP-TENORE": (10, -0.28, -0.05), "STATARB": (45, 1), "XSR01": (45, 1)}
|
||||
M = G.pick(sel)
|
||||
B = G.combo(M)
|
||||
|
||||
# --- bit-exact contro la produzione ------------------------------------------------------
|
||||
Bp = combine_outer({k: legs[k] for k in LEGS}, {k: 1.0 for k in LEGS}, lo=G.idx[0])
|
||||
a, b = B.align(Bp, join="outer")
|
||||
mx = float(np.nanmax(np.abs(a.fillna(0) - b.fillna(0))))
|
||||
print(f" ✅ combinatore numpy == `combine_outer` di produzione: max|diff| = {mx:.3e} "
|
||||
f"(n {len(B)} vs {len(Bp)})")
|
||||
|
||||
sub("5a) COPERTURA PER COLONNA — la prima diagnostica di un outer-join")
|
||||
print(" Il progetto ha gia' pubblicato un muro sbagliato di 3x per aver saltato questa")
|
||||
print(" tabella (r0726_wall_fixedpoint: una colonna al 100% accanto a colonne basse).")
|
||||
print(f"\n {'gamba':<14}{'primo giorno':>14}{'giorni':>9}{'copertura':>11}"
|
||||
f"{'barre ATTIVE':>14}{'attive/cal.':>12}")
|
||||
cov = {}
|
||||
for i, nm in enumerate(LEGS):
|
||||
col = M[:, i]
|
||||
n_ok = int(np.sum(~np.isnan(col)))
|
||||
act = int(np.sum(np.nan_to_num(col) != 0.0))
|
||||
first = G.idx[np.argmax(~np.isnan(col))]
|
||||
cov[nm] = n_ok / len(G.idx)
|
||||
print(f" {nm:<14}{str(first.date()):>14}{n_ok:>9}{n_ok/len(G.idx):>10.1%}"
|
||||
f"{act:>14}{act/max(1,n_ok):>12.1%}")
|
||||
print(f" {'UNIONE':<14}{str(G.idx[0].date()):>14}{len(G.idx):>9}{1.0:>10.1%}")
|
||||
n_legs = (~np.isnan(M)).sum(axis=1)
|
||||
print(f"\n gambe VIVE per giorno: " +
|
||||
" ".join(f"{k}: {100*np.mean(n_legs==k):.1f}%" for k in range(1, 6)))
|
||||
print(" 🚨 il paniere e' a 5 gambe solo nel " f"{100*np.mean(n_legs==5):.1f}% dei giorni: "
|
||||
"prima del 2024 XSR01 non esiste, prima del 2021 nemmeno VRP.")
|
||||
print(" ⚠️ e la colonna 'attive/cal.' dice un'altra cosa ancora: DVOLSPREAD e VRP hanno")
|
||||
print(" zeri STRUTTURALI (flat senza DVOL / settimanale su griglia giornaliera) -> un")
|
||||
print(" giorno presente non e' un giorno di rischio (lezione 'barre attive', 26/07).")
|
||||
|
||||
common = G.idx[n_legs == 5]
|
||||
Bc = G.combo(M[(n_legs == 5), :], common) if len(common) > 60 else None
|
||||
|
||||
sub("5b) METRICHE DEL PANIERE — due lenti dichiarate")
|
||||
print(f" {'lente':<28}{'n':>7}{'ShFULL':>9}{'ShIS':>9}{'ShHOLD':>9}{'CAGR':>9}{'maxDD':>9}")
|
||||
for nm, s in (("UNIONE (rinormalizzata)", B), ("FINESTRA COMUNE (5/5 gambe)", Bc)):
|
||||
if s is None:
|
||||
continue
|
||||
m = metrics(s)
|
||||
print(f" {nm:<28}{len(s):>7}{sh(s):>9.3f}{shI(s):>9.3f}{shH(s):>9.3f}"
|
||||
f"{100*m['cagr']:>8.2f}%{100*m['maxdd']:>8.2f}%")
|
||||
print(f"\n {'gamba sola':<28}{'n':>7}{'ShFULL':>9}{'ShIS':>9}{'ShHOLD':>9}{'CAGR':>9}{'maxDD':>9}")
|
||||
for nm in LEGS:
|
||||
s = legs[nm]
|
||||
m = metrics(s)
|
||||
print(f" {nm:<28}{len(s):>7}{sh(s):>9.3f}{shI(s):>9.3f}{shH(s):>9.3f}"
|
||||
f"{100*m['cagr']:>8.2f}%{100*m['maxdd']:>8.2f}%")
|
||||
print(f"\n A1: il paniere batte OGNI gamba sul FULL? "
|
||||
f"{'SI' if sh(B) > max(sh(legs[n]) for n in LEGS) else 'NO'} "
|
||||
f"(paniere {sh(B):.3f} vs miglior gamba {max(sh(legs[n]) for n in LEGS):.3f})")
|
||||
|
||||
sub("5c) PER ANNO — dove sta il rendimento del paniere")
|
||||
yr = {}
|
||||
for y, g in B.groupby(B.index.year):
|
||||
yr[y] = (sh(g), float(np.prod(1 + g.values) - 1), len(g))
|
||||
print(f" {'anno':>6}{'Sharpe':>10}{'ritorno':>11}{'giorni':>9} gambe vive (media)")
|
||||
for y in sorted(yr):
|
||||
mm = (G.idx.year == y)
|
||||
print(f" {y:>6}{yr[y][0]:>10.2f}{100*yr[y][1]:>10.1f}%{yr[y][2]:>9}"
|
||||
f" {n_legs[mm].mean():.2f}")
|
||||
|
||||
sub("5d) MARGINALE vs TP01 (lo scorer indurito del progetto, non riscritto)")
|
||||
mg = al.marginal_vs_tp01(B)
|
||||
for k in ("verdict", "corr_full", "corr_hold", "alpha_ann", "beta", "has_insample_edge",
|
||||
"is_hedge", "robust_oos", "beats_noise_null", "uplift_full_w25", "uplift_hold_w25"):
|
||||
if k in mg:
|
||||
print(f" {k:<22}{mg[k]}")
|
||||
return dict(B=B, Bc=Bc, sel=sel, M=M, cov=cov, n_legs=n_legs, marg=mg)
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 6) IL CONTO DEI TRIAL — DSR analitico a piu' partizioni + sensibilita'
|
||||
# =============================================================================================
|
||||
def dsr_of(series: pd.Series, trials: list[float]) -> tuple[float, float, float]:
|
||||
d, sr0 = al.deflated_sharpe(sh(series), trials, series)
|
||||
return d, sr0, float(np.std(np.asarray(trials, float), ddof=1)) / math.sqrt(365.25)
|
||||
|
||||
|
||||
def sezione6(F: dict, R5: dict, legs: dict) -> dict:
|
||||
sez("6) IL CONTO DEI TRIAL — deflated-Sharpe del PANIERE, e la sua sensibilita'")
|
||||
B = R5["B"]
|
||||
fam_full = {nm: [sh(s) for s in F[nm].values()] for nm in LEGS}
|
||||
pooled = [x for nm in LEGS for x in fam_full[nm]]
|
||||
print(f" Sharpe FULL del paniere = {sh(B):.4f}")
|
||||
print(f"\n {'partizione':<52}{'N':>7}{'sd(trial)/g':>13}{'null max Sh':>13}{'DSR':>8} esito")
|
||||
parts = [
|
||||
("P1 N = K (le 5 gambe) — SBAGLIATA, la riporto per taratura",
|
||||
[sh(legs[n]) for n in LEGS]),
|
||||
("P2 N = somma dei trial delle famiglie (la regola del brief)", pooled),
|
||||
("P3 N = famiglia PIU' LARGA sola (DVOLSPREAD 729)", fam_full["DVOLSPREAD"]),
|
||||
("P4 N = pooled + screen d'ondata (+168 ORTHO-SCREEN)",
|
||||
pooled + [sh(s) for f in F["_ORTHO"].values() for s in f.values()]),
|
||||
("P5 N = solo le 5 celle scelte, varianza di famiglia pooled",
|
||||
list(np.random.default_rng(1).choice(pooled, size=5, replace=False))),
|
||||
]
|
||||
out = {}
|
||||
for nm, tr in parts:
|
||||
d, sr0, sdt = dsr_of(B, tr)
|
||||
out[nm] = d
|
||||
print(f" {nm:<52}{len(tr):>7}{sdt:>13.5f}{sr0:>13.3f}{d:>8.3f} "
|
||||
f"{'PASS' if d >= 0.95 else 'FAIL'}")
|
||||
print("\n 🚨 Il verdetto NON e' stabile: cambia con la partizione, come il progetto ha gia'")
|
||||
print(" misurato il 30/07 (VRP: 0,983 PASS a N=8 / 0,948 FAIL a N=72 / 0,909 a N=360) e")
|
||||
print(" il 22/08 (§24: la stessa griglia da' verdetti opposti a seconda di come si parte).")
|
||||
print(" 📌 E c'e' un difetto STRUTTURALE piu' grave, che vale la pena dire in chiaro:")
|
||||
print(" `deflated_sharpe` stima il massimo-dal-rumore dalla varianza degli Sharpe dei")
|
||||
print(" TRIAL SINGOLI. Un PANIERE non e' un trial singolo: ha Sharpe piu' alto per pura")
|
||||
print(" diversificazione. Confrontarlo con una sr0 tarata su celle singole lo FAVORISCE")
|
||||
print(" sistematicamente -> il DSR analitico di un paniere e' OTTIMISTA a ogni N.")
|
||||
print(" Il test giusto e' EMPIRICO: la distribuzione dei PANIERI (sezione 7).")
|
||||
# sensibilita': quale sd(trial) ribalta il verdetto
|
||||
lo, hi = 1e-4, 1.0
|
||||
for _ in range(60):
|
||||
mid = 0.5 * (lo + hi)
|
||||
scaled = list(np.asarray(pooled) * (mid / (np.std(pooled, ddof=1) / math.sqrt(365.25)))
|
||||
) if np.std(pooled, ddof=1) > 0 else pooled
|
||||
d, _, _ = dsr_of(B, scaled)
|
||||
if d >= 0.95:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
print(f"\n sensibilita': con N={len(pooled)} il verdetto si ribalta a "
|
||||
f"sd(trial giornaliera) = {0.5*(lo+hi):.5f}; misurata = "
|
||||
f"{np.std(pooled, ddof=1)/math.sqrt(365.25):.5f}.")
|
||||
return out
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 7) IL CONTROLLO — CIO' CHE DECIDE
|
||||
# =============================================================================================
|
||||
def sezione7(F: dict, G: Grid, R5: dict, draws: int) -> dict:
|
||||
sez("7) IL CONTROLLO — panieri di SCARTATI NETTI. E' QUESTO IL TEST, non lo Sharpe del paniere")
|
||||
B = R5["B"]
|
||||
rng = np.random.default_rng(SEED)
|
||||
real = (sh(B), shI(B), shH(B), dd(B))
|
||||
|
||||
# in-sample ranking per famiglia (mai l'hold-out: sarebbe selection-on-holdout)
|
||||
rank = {}
|
||||
for nm in LEGS:
|
||||
ks = G.keys[nm]
|
||||
vals = np.array([shI(F[nm][k]) for k in ks])
|
||||
rank[nm] = dict(keys=ks, ins=vals, order=np.argsort(np.nan_to_num(vals, nan=-9)))
|
||||
frozen_idx = {nm: G.keys[nm].index(R5["sel"][nm]) for nm in LEGS}
|
||||
print(" rango IN-SAMPLE della cella congelata dentro la propria famiglia "
|
||||
"(mai l'hold-out: sarebbe selection-on-holdout)")
|
||||
for nm in LEGS:
|
||||
o = list(rank[nm]["order"][::-1])
|
||||
r = o.index(frozen_idx[nm]) + 1
|
||||
print(f" {nm:<14}{r:>4} / {len(o):<5} (pctl in-sample "
|
||||
f"{100*(1-(r-1)/len(o)):.0f}°, ShIS {rank[nm]['ins'][frozen_idx[nm]]:+.3f})")
|
||||
|
||||
def draw_basket(picker) -> tuple:
|
||||
cols = []
|
||||
for nm in LEGS:
|
||||
cols.append(G.cols[nm][:, picker(nm)])
|
||||
M = np.column_stack(cols)
|
||||
s = G.combo(M)
|
||||
return sh(s), shI(s), shH(s), dd(s)
|
||||
|
||||
def bottom(nm):
|
||||
o = rank[nm]["order"] # crescente: i primi sono i peggiori in-sample
|
||||
return int(rng.choice(o[:max(1, len(o) // 2)]))
|
||||
|
||||
def matched(nm):
|
||||
o = list(rank[nm]["order"][::-1])
|
||||
r = o.index(frozen_idx[nm]) / max(1, len(o) - 1)
|
||||
cand = [i for i in range(len(o)) if abs(i / max(1, len(o) - 1) - r) <= 0.10]
|
||||
return int(o[int(rng.choice(cand))])
|
||||
|
||||
def anyc(nm):
|
||||
return int(rng.integers(len(G.keys[nm])))
|
||||
|
||||
res = {}
|
||||
for tag, pick in (("B1 SCARTATI NETTI (meta' peggiore in-sample, stesse famiglie)", bottom),
|
||||
("B2 RANGO APPAIATO (stesso pctl in-sample +-10pp)", matched),
|
||||
("B0 CELLA A CASO (qualunque cella della famiglia)", anyc)):
|
||||
t = time.time()
|
||||
arr = np.array([draw_basket(pick) for _ in range(draws)])
|
||||
res[tag] = arr
|
||||
print(f"\n {tag} ({draws} panieri, {time.time()-t:.1f}s)")
|
||||
print(f" {'metrica':<10}{'reale':>9}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}"
|
||||
f"{'pctl del reale':>17}")
|
||||
for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")):
|
||||
c = arr[:, i]
|
||||
pc = 100.0 * float((c < real[i]).mean())
|
||||
print(f" {mn:<10}{real[i]:>9.3f}{np.percentile(c,10):>9.3f}"
|
||||
f"{np.median(c):>9.3f}{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}"
|
||||
f"{pc:>16.1f}°")
|
||||
|
||||
# --- B3: null di diversificazione (marginali CONSERVATE, correlazione distrutta) ----------
|
||||
sub("B3 NULL DI DIVERSIFICAZIONE — stesse gambe, correlazione incrociata DISTRUTTA")
|
||||
print(" Ogni gamba viene ruotata circolarmente di un lag casuale >= 60 giorni: lo Sharpe")
|
||||
print(" MARGINALE di ciascuna resta IDENTICO, sparisce solo la relazione fra gambe (e con")
|
||||
print(" TP01). Se il paniere reale non batte questa distribuzione, non contiene nulla oltre")
|
||||
print(" a 'K serie con quei marginali, quasi scorrelate'. Turnover invariato -> fee-neutrale.")
|
||||
M0 = R5["M"]
|
||||
arr = []
|
||||
for _ in range(draws):
|
||||
cols = []
|
||||
for i in range(M0.shape[1]):
|
||||
col = M0[:, i]
|
||||
ok = ~np.isnan(col)
|
||||
v = col[ok].copy()
|
||||
v = np.roll(v, int(rng.integers(60, max(61, len(v) - 60))))
|
||||
nc = np.full_like(col, np.nan)
|
||||
nc[ok] = v
|
||||
cols.append(nc)
|
||||
s = G.combo(np.column_stack(cols))
|
||||
arr.append((sh(s), shI(s), shH(s), dd(s)))
|
||||
arr = np.array(arr)
|
||||
res["B3"] = arr
|
||||
print(f"\n {'metrica':<10}{'reale':>9}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}"
|
||||
f"{'pctl del reale':>17}")
|
||||
for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")):
|
||||
c = arr[:, i]
|
||||
print(f" {mn:<10}{real[i]:>9.3f}{np.percentile(c,10):>9.3f}{np.median(c):>9.3f}"
|
||||
f"{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}"
|
||||
f"{100.0*float((c<real[i]).mean()):>16.1f}°")
|
||||
|
||||
# --- B4: cross-meccanismo, scartati netti veri --------------------------------------------
|
||||
sub("B4 SCARTATI NETTI CROSS-MECCANISMO — 4 famiglie relative-value refutate dall'onda ortho")
|
||||
print(" RATIO-MOM / RATIO-REV / RATIO-ACCEL / VOLSPREAD: meccanismi DIVERSI fra loro e dai")
|
||||
print(" quasi-passati, tutti collassati dallo scorer indurito (17/18 -> 1, 21/06). Stessa")
|
||||
print(" selezione dei quasi-passati (miglior cella IN-SAMPLE di ogni famiglia) + estrazioni.")
|
||||
OF = F["_ORTHO"]
|
||||
Go = Grid(OF)
|
||||
best = {}
|
||||
for nm, f in OF.items():
|
||||
ks = list(f)
|
||||
best[nm] = ks[int(np.argmax([np.nan_to_num(shI(f[k]), nan=-9) for k in ks]))]
|
||||
Mb = Go.pick(best)
|
||||
Bo = Go.combo(Mb)
|
||||
print(f"\n paniere dei 4 SCARTATI NETTI, celle scelte in-sample: ShFULL {sh(Bo):+.3f} "
|
||||
f"ShIS {shI(Bo):+.3f} ShHOLD {shH(Bo):+.3f} maxDD {100*dd(Bo):.1f}% n {len(Bo)}")
|
||||
Cb = pd.concat({k: OF[k][v] for k, v in best.items()}, axis=1).corr().values
|
||||
print(f" corr media fuori diagonale {np.nanmean(Cb[~np.eye(len(Cb),dtype=bool)]):+.3f} -> "
|
||||
f"ampiezza effettiva {eff_breadth(Cb):.2f} su 4")
|
||||
arr = []
|
||||
for _ in range(draws):
|
||||
s = Go.combo(np.column_stack([Go.cols[nm][:, int(rng.integers(len(Go.keys[nm])))]
|
||||
for nm in OF]))
|
||||
arr.append((sh(s), shI(s), shH(s), dd(s)))
|
||||
arr = np.array(arr)
|
||||
res["B4"] = arr
|
||||
print(f"\n {draws} panieri a cella casuale dalle stesse 4 famiglie refutate:")
|
||||
print(f" {'metrica':<10}{'quasi-pass.':>13}{'p10':>9}{'mediana':>9}{'p90':>9}{'max':>9}"
|
||||
f"{'pctl dei quasi':>17}")
|
||||
for i, mn in enumerate(("ShFULL", "ShIS", "ShHOLD", "maxDD")):
|
||||
c = arr[:, i]
|
||||
print(f" {mn:<10}{real[i]:>13.3f}{np.percentile(c,10):>9.3f}{np.median(c):>9.3f}"
|
||||
f"{np.percentile(c,90):>9.3f}{np.max(c):>9.3f}"
|
||||
f"{100.0*float((c<real[i]).mean()):>16.1f}°")
|
||||
|
||||
# --- CONTROLLO POSITIVO -------------------------------------------------------------------
|
||||
sub("CONTROLLO POSITIVO — un rilevatore che non segnala e' indistinguibile da uno rotto")
|
||||
print(" Se il confronto 'reale vs panieri casuali' avesse potenza ZERO, tutto quanto sopra")
|
||||
print(" sarebbe privo di significato. Lo si prova costruendo un paniere che DEVE stare in")
|
||||
print(" cima: la MIGLIOR cella in-sample di ognuna delle 5 famiglie.")
|
||||
bestQ = {}
|
||||
for nm in LEGS:
|
||||
o = rank[nm]["order"]
|
||||
bestQ[nm] = G.keys[nm][int(o[-1])]
|
||||
Bb = G.combo(G.pick(bestQ))
|
||||
B0 = res["B0 CELLA A CASO (qualunque cella della famiglia)"]
|
||||
pIS = 100.0 * float((B0[:, 1] < shI(Bb)).mean())
|
||||
print(f" paniere 'migliori in-sample': ShIS {shI(Bb):+.3f} -> pctl {pIS:.1f}deg della")
|
||||
print(f" distribuzione B0 (atteso ~100deg se il test ha potenza: e' il massimo per")
|
||||
print(f" costruzione, quindi un pctl basso significherebbe rilevatore ROTTO).")
|
||||
print(f" Il suo ShHOLD e' {shH(Bb):+.3f} contro mediana B0 {np.median(B0[:,2]):+.3f}:")
|
||||
print(f" e' QUESTO il pezzo di scelta in-sample che sopravvive fuori campione.")
|
||||
if pIS < 90.0:
|
||||
print(" RILEVATORE SOSPETTO: il paniere costruito per stare in cima NON ci sta.")
|
||||
return dict(res=res, real=real, best_is=Bb, ortho=Bo, rank=rank, frozen_idx=frozen_idx)
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 8) LEAVE-ONE-OUT
|
||||
# =============================================================================================
|
||||
def sezione8(G: Grid, R5: dict) -> dict:
|
||||
sez("8) LEAVE-ONE-OUT — se il paniere 'funziona' grazie a UNA gamba, non e' un paniere")
|
||||
B = R5["B"]
|
||||
base = (sh(B), shI(B), shH(B), dd(B))
|
||||
print(f" paniere completo: ShFULL {base[0]:.3f} ShIS {base[1]:.3f} ShHOLD {base[2]:.3f} "
|
||||
f"maxDD {100*base[3]:.2f}%")
|
||||
print(f"\n {'tolta':<14}{'ShFULL':>9}{'dShFULL':>10}{'ShHOLD':>9}{'dShHOLD':>10}"
|
||||
f"{'maxDD':>9}{'d maxDD pp':>12}")
|
||||
out = {}
|
||||
for nm in LEGS:
|
||||
keep = [k for k in LEGS if k != nm]
|
||||
s = G.combo(G.pick({k: R5["sel"][k] for k in keep}))
|
||||
out[nm] = (sh(s), shH(s), dd(s))
|
||||
print(f" {nm:<14}{sh(s):>9.3f}{sh(s)-base[0]:>+10.3f}{shH(s):>9.3f}"
|
||||
f"{shH(s)-base[2]:>+10.3f}{100*dd(s):>8.2f}%{100*(dd(s)-base[3]):>+12.2f}")
|
||||
worst = min(out, key=lambda k: out[k][0])
|
||||
print(f"\n la gamba il cui ritiro costa di piu' sul FULL: {worst} "
|
||||
f"({out[worst][0]-base[0]:+.3f})")
|
||||
return out
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 9) IL PANIERE COME TERZO SLEEVE DEL LIBRO LIVE — banda d'ancora, funding dentro
|
||||
# =============================================================================================
|
||||
def sezione9(R5: dict, legs: dict, draws: int) -> dict:
|
||||
sez("9) IL PANIERE COME TERZO SLEEVE DEL LIBRO LIVE — e batte la sua MIGLIOR gamba?")
|
||||
if not B3RD_CACHE.exists():
|
||||
print(f" ⚠️ cache §50 assente ({B3RD_CACHE}) -> sezione SALTATA. Non e' un risultato.")
|
||||
return {}
|
||||
S = pickle.loads(B3RD_CACHE.read_bytes())
|
||||
print(f" cache §50 riusata (serie ancorate, FUNDING DENTRO): {list(S)[:8]}...")
|
||||
|
||||
# replica del baseline §50 prima di ogni delta
|
||||
rng = np.random.default_rng(SEED)
|
||||
hs = rng.choice(TP_HOURS, size=draws)
|
||||
os_ = rng.choice(SKH_OFFSETS, size=draws)
|
||||
hs_sa = rng.choice(TP_HOURS, size=draws)
|
||||
base, dPV, dBK, dBK4 = [], [], [], []
|
||||
# gambe funding-consistenti: PREVDAY/STATARB/DVOLSPREAD dalla cache §50 (funding dentro),
|
||||
# VRP (opzioni: nessun funding di perp) e XSR01 con il drag dichiarato di §37.
|
||||
xsr = legs["XSR01"] - XS_FUNDING_DRAG_YR / 365.25
|
||||
vrp = legs["VRP-TENORE"]
|
||||
W_CAND = 0.15
|
||||
for i in range(draws):
|
||||
c2 = {"TP01": S["TP01"][hs[i]], "SKH01": S["SKH01"][os_[i]]}
|
||||
b = combine_outer(c2, {"TP01": W_TP, "SKH01": W_SKH}, lo=BOOK_START)
|
||||
mb, mbh = metrics(b), metrics(b[b.index >= HOLDOUT])
|
||||
base.append((mb["sharpe"], mbh["sharpe"], mb["maxdd"]))
|
||||
pan = combine_outer({"PREVDAY": S["PREVDAY"][0], "DVOLSPREAD": S["DVOLSPREAD"][0],
|
||||
"VRP-TENORE": vrp, "STATARB": S["STATARB"][hs_sa[i]], "XSR01": xsr},
|
||||
{k: 1.0 for k in LEGS}, lo=BOOK_START)
|
||||
pan4 = combine_outer({"PREVDAY": S["PREVDAY"][0], "DVOLSPREAD": S["DVOLSPREAD"][0],
|
||||
"VRP-TENORE": vrp, "STATARB": S["STATARB"][hs_sa[i]]},
|
||||
{k: 1.0 for k in LEGS if k != "XSR01"}, lo=BOOK_START)
|
||||
for tgt, lst in ((S["PREVDAY"][0], dPV), (pan, dBK), (pan4, dBK4)):
|
||||
c3 = dict(c2); c3["CAND"] = tgt
|
||||
a = combine_outer(c3, {"TP01": W_TP * (1 - W_CAND), "SKH01": W_SKH * (1 - W_CAND),
|
||||
"CAND": W_CAND}, lo=BOOK_START)
|
||||
ma, mah = metrics(a), metrics(a[a.index >= HOLDOUT])
|
||||
lst.append((ma["sharpe"] - mb["sharpe"], mah["sharpe"] - mbh["sharpe"],
|
||||
ma["maxdd"] - mb["maxdd"]))
|
||||
Bs = np.array(base)
|
||||
print(f"\n LIBRO LIVE de-luckato (2 sleeve, funding dentro), {draws} estrazioni:")
|
||||
print(f" ShFULL {np.median(Bs[:,0]):.3f} ShHOLD {np.median(Bs[:,1]):.3f} "
|
||||
f"maxDD {100*np.median(Bs[:,2]):.2f}%")
|
||||
print(" §50 pubblicava 1,813 / 1,437 / 9,42% -> ✅ replica (estrazioni diverse, "
|
||||
"stessa cache)")
|
||||
print(f"\n peso del candidato = {W_CAND:.0%}. Statistica = MEDIANA DELLE DIFFERENZE APPAIATE.")
|
||||
print(f"\n {'candidato':<34}{'dShFULL':>11}{'>0':>7}{'dShHOLD':>11}{'>0':>7}"
|
||||
f"{'d maxDD pp':>13}")
|
||||
rows = [("PREVDAY da sola (§50: +0,192/+0,363)", np.array(dPV)),
|
||||
("PANIERE 5 gambe (quasi-passati)", np.array(dBK)),
|
||||
("PANIERE 4 gambe (senza XSR01, eseguibile)", np.array(dBK4))]
|
||||
for nm, a in rows:
|
||||
print(f" {nm:<34}{np.median(a[:,0]):>+11.4f}{100*(a[:,0]>0).mean():>6.0f}%"
|
||||
f"{np.median(a[:,1]):>+11.4f}{100*(a[:,1]>0).mean():>6.0f}%"
|
||||
f"{100*np.median(a[:,2]):>+13.2f}")
|
||||
d5 = np.array(dBK)[:, 0] - np.array(dPV)[:, 0]
|
||||
d5h = np.array(dBK)[:, 1] - np.array(dPV)[:, 1]
|
||||
print(f"\n 🚨 PANIERE meno PREVDAY-DA-SOLA (differenza APPAIATA sulla stessa estrazione):")
|
||||
print(f" dShFULL {np.median(d5):+.4f} (>0 nel {100*(d5>0).mean():.0f}%) "
|
||||
f"dShHOLD {np.median(d5h):+.4f} (>0 nel {100*(d5h>0).mean():.0f}%)")
|
||||
return dict(base=Bs, dPV=np.array(dPV), dBK=np.array(dBK), dBK4=np.array(dBK4))
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# 10) ESEGUIBILITA' + EUR/GIORNO
|
||||
# =============================================================================================
|
||||
def sezione10(R5: dict, R9: dict) -> None:
|
||||
sez("10) ESEGUIBILITA' RIGA PER RIGA — un paniere con dentro uno STAT-MODE non e' un candidato")
|
||||
print(f" {'gamba':<14}{'venue':<14}{'su Deribit?':<14}{'ticket a $635 (peso 15%/5)':<30}"
|
||||
f"{'stato'}")
|
||||
tick = CAPITAL_TODAY * 0.15 / len(LEGS)
|
||||
righe = [
|
||||
("PREVDAY", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"),
|
||||
("DVOLSPREAD", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"),
|
||||
("STATARB", "Deribit", "SI'", f"${tick:.2f} netta col libro", "eseguibile"),
|
||||
("VRP-TENORE", "Deribit opz.", "SI' (ETH)", "1 lotto ETH = $242 (>> $19)",
|
||||
"🚨 NON eseguibile al peso"),
|
||||
("XSR01", "Hyperliquid", "NO", "50 gambe, min $10/gamba",
|
||||
"🚨 STAT-MODE, venue diverso"),
|
||||
]
|
||||
for r in righe:
|
||||
print(f" {r[0]:<14}{r[1]:<14}{r[2]:<14}{r[3]:<30}{r[4]}")
|
||||
print(f"\n 🚨 2 gambe su 5 NON sono eseguibili a $635 -> il paniere COSI' COM'E' non e' un")
|
||||
print(" candidato al libro, a prescindere da qualunque numero delle sezioni precedenti.")
|
||||
print(" VRP-TENORE cade sul LOTTO (regola gia' scritta: 'niente short-vol da modello in")
|
||||
print(" deploy' — il lotto non e' nemmeno il vincolo principale). XSR01 cade sul VENUE,")
|
||||
print(" e la decisione d'operatore tiene tutto su Deribit fino a $20k.")
|
||||
if R9:
|
||||
d = R9["dBK4"][:, 0]
|
||||
print(f"\n Il paniere ESEGUIBILE (4 gambe, senza XSR01) vale dShFULL "
|
||||
f"{np.median(d):+.4f} di libro. Per convertirlo in reddito serve la LEVA, che "
|
||||
f"a $635 non c'e'.")
|
||||
|
||||
|
||||
def eur_al_giorno(R9: dict) -> None:
|
||||
sez("11) IN EURO AL GIORNO — l'unita' che il progetto usa per non mentirsi")
|
||||
cagr_base = 0.1519 # L3 congiunta (fisco + funding), CLAUDE.md §36
|
||||
perp = 0.0635 # rendita perpetua L3
|
||||
print(f" Conto reale $635. Rendita perpetua della lente congiunta L3 = {100*perp:.2f}%/anno")
|
||||
print(f" -> oggi il libro vale ${635*perp/365.25:.3f}/giorno = "
|
||||
f"€{635*perp/365.25*0.92:.3f}/giorno.")
|
||||
if not R9:
|
||||
return
|
||||
d = np.median(R9["dBK4"][:, 0])
|
||||
dPV = np.median(R9["dPV"][:, 0])
|
||||
print(f"\n Il paniere eseguibile aggiunge dShFULL {d:+.4f} al libro; PREVDAY DA SOLA "
|
||||
f"{dPV:+.4f}.")
|
||||
print(" Uno dSharpe NON e' reddito a nozionale invariato (regola 25/07: un diversificatore")
|
||||
print(" a basso CAGR si giudica a ISO-RISCHIO). Convertito a iso-rischio con la leva che a")
|
||||
print(" $635 NON esiste, e prendendo per buono il d drift misurato in sezione 9:")
|
||||
dd_ = np.median(R9["dBK4"][:, 2])
|
||||
print(f" d maxDD {100*dd_:+.2f}pp -> a iso-DD si potrebbe alzare la scala di un fattore")
|
||||
print(f" ~{(np.median(R9['base'][:,2])/max(1e-9, np.median(R9['base'][:,2])+dd_)):.3f}, "
|
||||
f"cioe' ~{100*((np.median(R9['base'][:,2])/max(1e-9,np.median(R9['base'][:,2])+dd_))-1):.1f}% "
|
||||
f"di reddito in piu'.")
|
||||
v = 635 * perp / 365.25 * 0.92
|
||||
k = np.median(R9['base'][:, 2]) / max(1e-9, np.median(R9['base'][:, 2]) + dd_)
|
||||
print(f"\n 🚨 IN EURO: €{v:.3f}/g -> €{v*k:.3f}/g. Guadagno = "
|
||||
f"€{v*(k-1):.4f}/GIORNO, cioe' €{v*(k-1)*365:.2f}/anno su un conto da $635.")
|
||||
print(f" Versare €500/mese vale €6.000/anno. Il rapporto e' "
|
||||
f"~{6000/max(1e-9, abs(v*(k-1)*365)):.0f}x.")
|
||||
print(" E il paniere richiede QUATTRO monitor invece di uno, su una macchina a 2 CPU.")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--draws", type=int, default=1000)
|
||||
ap.add_argument("--bookdraws", type=int, default=200)
|
||||
ap.add_argument("--force", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
sez("r0823b_quasi_passati.py — §68 | i candidati morti PER POCO, presi insieme, passano?")
|
||||
print(f" avvio {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} · "
|
||||
f"estrazioni {args.draws} · seed {SEED} · SOLA LETTURA, 0 ordini, 0 chiamate di rete")
|
||||
print(__doc__.split("DOMANDA")[1].split("REGOLA DI AMMISSIONE")[0].rstrip())
|
||||
|
||||
sez("0) ATTESE A PRIORI — dichiarate nel docstring PRIMA di misurare")
|
||||
print(__doc__.split("=====\n")[2].split("METODO")[0].rstrip())
|
||||
|
||||
sezione1()
|
||||
sezione2()
|
||||
|
||||
sez("COSTRUZIONE DELLE FAMIGLIE (ogni cella = una serie di rendimenti netti giornalieri)")
|
||||
F = build_families(args.force)
|
||||
R3 = sezione3(F)
|
||||
legs = R3["legs"]
|
||||
G = Grid({k: F[k] for k in LEGS})
|
||||
R4 = sezione4(legs)
|
||||
R5 = sezione5(G, legs)
|
||||
R6 = sezione6(F, R5, legs)
|
||||
R7 = sezione7(F, G, R5, args.draws)
|
||||
R8 = sezione8(G, R5)
|
||||
R9 = sezione9(R5, legs, args.bookdraws)
|
||||
sezione10(R5, R9)
|
||||
eur_al_giorno(R9)
|
||||
|
||||
# ---------------------------------------------------------------------------------------
|
||||
sez("12) ATTESE: CONFERMATE O REFUTATE")
|
||||
B = R5["B"]
|
||||
best_leg = max(sh(legs[n]) for n in LEGS)
|
||||
b0 = R7["res"]["B0 CELLA A CASO (qualunque cella della famiglia)"]
|
||||
b1 = R7["res"]["B1 SCARTATI NETTI (meta' peggiore in-sample, stesse famiglie)"]
|
||||
pct_full_b1 = 100.0 * float((b1[:, 0] < sh(B)).mean())
|
||||
pct_hold_b1 = 100.0 * float((b1[:, 2] < shH(B)).mean())
|
||||
a1 = sh(B) > best_leg
|
||||
a2 = (max(R6.values()) >= 0.95) and (min(R6.values()) < 0.95)
|
||||
a3 = (pct_full_b1 > 50) and (pct_hold_b1 < pct_full_b1)
|
||||
a4 = R4["neff"] > 0.8 * len(LEGS)
|
||||
worst = min(R8, key=lambda k: R8[k][0])
|
||||
a5 = worst == "PREVDAY"
|
||||
print(f" A1 paniere > ogni gamba sul FULL ................. "
|
||||
f"{'CONFERMATA' if a1 else 'REFUTATA'} ({sh(B):.3f} vs {best_leg:.3f})")
|
||||
print(f" A2 il DSR si ribalta con la partizione ........... "
|
||||
f"{'CONFERMATA' if a2 else 'REFUTATA'} (range {min(R6.values()):.3f}-{max(R6.values()):.3f})")
|
||||
print(f" A3 vantaggio in-sample che si riduce sull'hold-out "
|
||||
f"{'CONFERMATA' if a3 else 'REFUTATA'} (pctl vs B1: FULL {pct_full_b1:.0f}° / "
|
||||
f"HOLD {pct_hold_b1:.0f}°)")
|
||||
print(f" A4 ampiezza effettiva ~ K ....................... "
|
||||
f"{'CONFERMATA' if a4 else 'REFUTATA'} ({R4['neff']:.2f} su {len(LEGS)})")
|
||||
print(f" A5 dipende da UNA gamba (PREVDAY) ............... "
|
||||
f"{'CONFERMATA' if a5 else 'REFUTATA'} (la piu' costosa da togliere: {worst})")
|
||||
print(f" A6 almeno una gamba STAT-MODE ................... CONFERMATA (XSR01 + VRP-TENORE)")
|
||||
print(f" A7 le gambe sono DECADUTE dalla pubblicazione ... vedi sezione 3b")
|
||||
|
||||
sez(f"VERDETTO {clock()}")
|
||||
print(f" Sharpe del paniere: FULL {sh(B):.3f} / IS {shI(B):.3f} / HOLD {shH(B):.3f}, "
|
||||
f"maxDD {100*dd(B):.2f}%, ampiezza effettiva {R4['neff']:.2f}/5.")
|
||||
print(f" Percentile contro i panieri di SCARTATI NETTI (B1): FULL {pct_full_b1:.1f}° / "
|
||||
f"HOLD {pct_hold_b1:.1f}°.")
|
||||
print(f" Percentile contro i panieri a CELLA CASUALE (B0): FULL "
|
||||
f"{100.0*float((b0[:,0] < sh(B)).mean()):.1f}° / HOLD "
|
||||
f"{100.0*float((b0[:,2] < shH(B)).mean()):.1f}°.")
|
||||
print(f" DSR del paniere: {min(R6.values()):.3f} - {max(R6.values()):.3f} secondo la "
|
||||
f"partizione dei trial.")
|
||||
print("\n La riga per il registro e' nel messaggio finale.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,785 @@
|
||||
"""§67 REGIME-CAPITALE — cosa e' stato archiviato PER TAGLIA, e cosa sarebbe vivo a $100.000.
|
||||
|
||||
LA DOMANDA
|
||||
----------
|
||||
Ogni screening di questo progetto e' stato fatto a **$600 di capitale**. E' la scelta giusta per il
|
||||
conto che esiste, ma ha un effetto collaterale mai esaminato: le strategie messe in STAT-MODE o
|
||||
rifiutate citando min-order / lotto minimo / numero di gambe / haircut small-cap sono state
|
||||
archiviate **senza mai misurare se sarebbero vive a $100k** — che e' il nozionale di un conto
|
||||
funded, il canale su cui il progetto sta valutando di puntare.
|
||||
|
||||
E c'e' una ragione forte per rifare il conto ADESSO: l'ondata del 22-23/08 ha falsificato **tre muri
|
||||
di taglia** pubblicati (il "$20k di XS01" non esiste; il muro delle opzioni BTC era il COLLATERALE
|
||||
PER VENDERE, non il costo di comprare; i lotti USDC-lineari sono 7,5-10x piu' piccoli di quelli
|
||||
inverse su cui erano stati misurati). Tre su tre sbagliati, tutti nella stessa direzione: piu'
|
||||
permissivi del creduto.
|
||||
|
||||
ATTESE A PRIORI — scritte leggendo il mandato, PRIMA di qualunque misura
|
||||
-----------------------------------------------------------------------
|
||||
A1 Il censimento conterra' 10-15 voci e la MAGGIORANZA avra' anche un rifiuto sul lato EDGE:
|
||||
la taglia sara' raramente l'unico motivo.
|
||||
A2 (controllo positivo) XS01 riprodurra' haircut ~0 gia' a $200-600 di capitale ALLOCATO.
|
||||
Se il mio codice non lo trova, il mio codice e' rotto e mi fermo.
|
||||
A3 A $100.000 ogni candidato multi-gamba converge alla propria serie MODELLATA (haircut -> 0).
|
||||
A4 Nessun archiviato passera' TUTTI i gate a $100k — e non perche' ricontrolli i gate, ma per una
|
||||
ragione strutturale: i gate pubblicati sono stati calcolati sulla serie MODELLATA, che E' il
|
||||
limite a capitale infinito. Se e' vero, la domanda del mandato era gia' risposta nei registri.
|
||||
A5 Il corollario funded non aggiungera' NIENTE di nuovo: l'unico ampliamento reale dell'insieme
|
||||
ammissibile e' XS01, e §1/§29 l'hanno gia' contato (+0,400 di J).
|
||||
A6 L'haircut sara' MONOTONO decrescente nel capitale. <-- questa e' quella che mi aspetto di
|
||||
confermare senza pensarci; se salta, e' il risultato metodologico del filone.
|
||||
|
||||
METODO
|
||||
------
|
||||
* Repliche PRIMA di ogni delta: identita' bit-exact di XS01 col sleeve di produzione; libro 75/25
|
||||
`hourly` contro il numero pubblicato (Sh 1,683 / maxDD 11,22%).
|
||||
* Parametri VERI del venue (non quelli di memoria): Deribit min-order $5, taker 3,50 bps dal
|
||||
2026-08-01; Hyperliquid min-order **$10** (non $5) e taker **4,50 bps** (non 5,0), misurati da
|
||||
HL-EXEC il 22/08; lotti della famiglia **USDC-lineare** (non inverse), letti dal venue.
|
||||
* La LENTE del libro e' dichiarata: `hourly` (path live), Sh 1,683 / maxDD 11,22% — non la
|
||||
canonica 1,800/9,56% ne' la mediana d'ancora 1,626/10,4%.
|
||||
* Distinzione dichiarata riga per riga: **capitale PROPRIO** (e' tuo, il vincolo e' il min-order)
|
||||
contro **nozionale FUNDED** (non e' tuo, il vincolo binding e' la regola di drawdown).
|
||||
|
||||
SICUREZZA: sola lettura. Nessun ordine. Nessun file di produzione toccato. Solo GET pubbliche,
|
||||
con pacing, cache e astensione nelle finestre del cron (:05-:10 book, :24-:30 catena).
|
||||
|
||||
uv run python scripts/research/r0823b_regime_capitale.py
|
||||
uv run python scripts/research/r0823b_regime_capitale.py --no-net # tutto da cache/costanti
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
if str(_p) not in sys.path:
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
CACHE = ROOT / "data" / "cache_regime_capitale.json"
|
||||
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# PARAMETRI VENUE — dichiarati, con la FONTE. Nessuno di questi e' "di memoria".
|
||||
# --------------------------------------------------------------------------------------------
|
||||
DER_MIN_ORDER = 5.0 # config/live.json -> min_order_usd
|
||||
DER_FEE_SIDE = 0.00035 # 3,50 bps/lato dal 2026-08-01 (SLIP-AUDIT: 16/16 fill sul nastro)
|
||||
HL_MIN_ORDER = 10.0 # HL-EXEC 22/08: docs + 3.827 livelli, 0,0% sotto $10 (NON $5)
|
||||
HL_FEE_SIDE = 0.00045 # HL-EXEC 22/08: 4,50 bps, due fonti indipendenti (NON 5,0)
|
||||
SPLIT_FUNDED = 0.80 # r0725_hyro.SPLIT — quota trader del profitto su conto funded
|
||||
BARRIERA_FUNDED = 0.06 # HyroTrader max loss STATICO 6% (r0725_hyro.EV_DD, regole vere)
|
||||
EURUSD = 1.09
|
||||
CAPITALI = (635.0, 5_000.0, 20_000.0, 100_000.0) # la griglia del mandato
|
||||
GRIGLIA_DENSA = (95.0, 200.0, 400.0, 600.0, 635.0, 1_000.0, 1_500.0, 2_000.0, 3_000.0,
|
||||
5_000.0, 7_500.0, 10_000.0, 15_000.0, 20_000.0, 50_000.0, 100_000.0, 200_000.0)
|
||||
TOL_STAB = 0.10 # |Sh(C) - Sh(modellata)| <= 0,10 per DICHIARARE eseguibile
|
||||
SEED = 823
|
||||
|
||||
_last_net = [0.0]
|
||||
|
||||
|
||||
def sez(t: str) -> None:
|
||||
print("\n" + "=" * 100)
|
||||
print(" " + t)
|
||||
print("=" * 100)
|
||||
|
||||
|
||||
def sub(t: str) -> None:
|
||||
print("\n --- " + t)
|
||||
|
||||
|
||||
def _pace() -> None:
|
||||
"""<=2 req/s e astensione nelle finestre del cron live."""
|
||||
now = datetime.now(timezone.utc)
|
||||
while now.minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30):
|
||||
print(f" [pausa: minuto {now.minute} e' finestra del cron live]")
|
||||
time.sleep(20)
|
||||
now = datetime.now(timezone.utc)
|
||||
dt = time.time() - _last_net[0]
|
||||
if dt < 0.6:
|
||||
time.sleep(0.6 - dt)
|
||||
_last_net[0] = time.time()
|
||||
|
||||
|
||||
def get_json(url: str, key: str, no_net: bool):
|
||||
c = json.loads(CACHE.read_text()) if CACHE.exists() else {}
|
||||
if no_net or (key in c and time.time() - c[key].get("_t", 0) < 12 * 3600):
|
||||
return c.get(key, {}).get("v")
|
||||
try:
|
||||
_pace()
|
||||
with urllib.request.urlopen(url, timeout=20) as r:
|
||||
v = json.loads(r.read().decode())
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" [rete KO su {key}: {type(e).__name__}] -> uso la cache se c'e'")
|
||||
return c.get(key, {}).get("v")
|
||||
c[key] = {"_t": time.time(), "v": v}
|
||||
CACHE.write_text(json.dumps(c))
|
||||
return v
|
||||
|
||||
|
||||
def sola_lettura() -> None:
|
||||
"""Prova che il filone non ha toccato la produzione (il cron ESEGUE dalla working tree)."""
|
||||
prot = ["src/", "config/", "scripts/live/", "tests/", "data/paper_statarb", "data/paper_xsr",
|
||||
"data/paper_dvolspread", "data/live"]
|
||||
out = subprocess.run(["git", "status", "--porcelain", "--"] + prot,
|
||||
cwd=ROOT, capture_output=True, text=True).stdout.strip()
|
||||
print(f" git status sui percorsi protetti: {'VUOTO (sola lettura provata)' if not out else out}")
|
||||
assert not out, "PRODUZIONE TOCCATA — fermarsi"
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# METRICHE E DEFINIZIONE DI ESEGUIBILITA'
|
||||
# =============================================================================================
|
||||
ANN = np.sqrt(365.25)
|
||||
|
||||
|
||||
def met(net: np.ndarray, idx) -> dict:
|
||||
x = np.asarray(net, float)
|
||||
sd = x.std(ddof=1)
|
||||
sh = float(x.mean() / sd * ANN) if sd > 0 else 0.0
|
||||
eq = np.cumprod(1.0 + np.clip(x, -0.99, None))
|
||||
dd = float((1.0 - eq / np.maximum.accumulate(eq)).max())
|
||||
yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9)
|
||||
return dict(sharpe=sh, maxdd=100 * dd, cagr=100 * (eq[-1] ** (1 / yrs) - 1.0))
|
||||
|
||||
|
||||
def c_stabilita(curva: dict[float, float], sh_mod: float, tol: float = TOL_STAB) -> float | None:
|
||||
"""C* = il PIU' PICCOLO capitale tale che da li' in su lo Sharpe NON si discosta piu' di `tol`
|
||||
dalla serie modellata. Definizione scelta PRIMA di guardare, e per un motivo: ne' l'haircut ne'
|
||||
la quota di nozionale eseguito, da soli, distinguono "eseguibile" da "degenere" (sez. 3)."""
|
||||
caps = sorted(curva)
|
||||
for i, c in enumerate(caps):
|
||||
if all(abs(curva[c2] - sh_mod) <= tol for c2 in caps[i:]):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def maxdd_k(r: np.ndarray, k: float) -> float:
|
||||
eq = np.cumprod(1.0 + np.clip(k * np.asarray(r, float), -0.99, None))
|
||||
return float((1.0 - eq / np.maximum.accumulate(eq)).max())
|
||||
|
||||
|
||||
def cagr_k(r: np.ndarray, k: float, idx) -> float:
|
||||
eq = np.cumprod(1.0 + np.clip(k * np.asarray(r, float), -0.99, None))
|
||||
yrs = max((idx[-1] - idx[0]) / pd.Timedelta("365.25D"), 1e-9)
|
||||
return float(eq[-1] ** (1 / yrs) - 1.0)
|
||||
|
||||
|
||||
def k_barriera(r: np.ndarray, target: float = BARRIERA_FUNDED) -> float:
|
||||
lo, hi = 1e-3, 20.0
|
||||
if maxdd_k(r, hi) < target:
|
||||
return hi
|
||||
for _ in range(60):
|
||||
mid = 0.5 * (lo + hi)
|
||||
if maxdd_k(r, mid) < target:
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
return lo
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 1 — IL CENSIMENTO
|
||||
# =============================================================================================
|
||||
# (voce, dove il muro di taglia e' dichiarato, muro come pubblicato, stato del muro OGGI,
|
||||
# c'e' ANCHE un rifiuto sul lato EDGE?)
|
||||
CENSIMENTO = [
|
||||
("XS01 (19 gambe HL)", "CLAUDE.md 19/06",
|
||||
"STAT-MODE, 'serve ~20k'", "FALSIFICATO 22/08 (haircut ~0 a $200-600)",
|
||||
"no — e' nel book di ricerca; fuori dal LIVE per VENUE (14/19) e decisione venue"),
|
||||
("XSR01 (50 gambe HL)", "CLAUDE.md 25/07",
|
||||
"ticket $1,73 < $5 -> 'reale a ~$5k'", "RIDIMENSIONATO 22/08: soglia vera ~$3.000",
|
||||
"SI — weights_tilt_null FAIL (book 5-sleeve), storia 2,6a monoregime; gate 23/10 APERTO"),
|
||||
("STATARB-MULTI (50 coppie)", "CLAUDE.md 25/07",
|
||||
"51 gambe = STAT-MODE", "MAI MISURATO -> misurato qui",
|
||||
"SI — IC95 [-0,12; 1,72], t 1,31; uplift -0,11 vs null a priori"),
|
||||
("CC01 cash-and-carry", "CLAUDE.md 26/06",
|
||||
"4-38 gambe, 'rivedere a ~20k+'", "FALSIFICATO 22/08 (§34: lotti $10/$1; §39: linea USDC esiste)",
|
||||
"SI — DSR 0,363 FAIL, implausible_sharpe 5/5, de-levering REFUTED, 2 gambe = Sharpe -0,42"),
|
||||
("VRP01 (put credit spread)", "CLAUDE.md 30/07",
|
||||
"lotto BTC $6.210 / ETH $1.832 -> 0 lotti al peso 12%",
|
||||
"FALSIFICATO 22/08 (§50: famiglia USDC ETH $242 / BTC $772)",
|
||||
"SI — regola 'niente short-vol da modello in deploy' + f=0,73 misurato"),
|
||||
("Opzioni long (tail-hedge, gamma)", "CLAUDE.md 26/06 + 22/08",
|
||||
"'opzione BTC min $5.968 >> $600'", "FALSIFICATO 23/08 (§46: comprare costa il PREMIO, $1,79-7,93)",
|
||||
"SI — gamma scalp Sharpe -3..-6 ogni anno; tail-hedge maxDD sale in 162/162 celle"),
|
||||
("Albimarini double-diagonal", "CLAUDE.md 02/07",
|
||||
"(nessun muro di taglia)", "n/d — fee-negativa a QUALSIASI size",
|
||||
"SI — fee 8 gambe = 194-221% del theta; celle 0-perdite = implausible_sharpe"),
|
||||
("Tranching TP01 K=2/4", "CLAUDE.md 02/07",
|
||||
"'delta ~$1-2 < min-order -> K=1'; rivalutare a >=5-10k",
|
||||
"FALSIFICATO 23/08 (§47: build_book_order banda la posizione NETTA)",
|
||||
"SI — dSharpe n.s.; e' riduzione della VARIANZA DELLA STIMA, non ritorno"),
|
||||
("BIN-FREQ (size per-trade SKH01)", "CLAUDE.md 22/08",
|
||||
"riapertura = SOGLIA DI CAPITALE $5.600 (cella) / $53.000 (famiglia)",
|
||||
"VIVO — e' l'unico con una soglia di capitale come condizione DICHIARATA",
|
||||
"SI — mediana di famiglia 6% della cella, 54% celle positive; weights_tilt_null FAIL"),
|
||||
("XS01-D (11 gambe Deribit)", "RESULTS §50/§56 23/08",
|
||||
"'aspettare 2-3 listing'", "n/d — non e' capitale, e' VENUE",
|
||||
"SI — IC -0,027 (t -0,88) su quelle 11; DSR 0,158 FAIL"),
|
||||
("GTAA01 (6 ETF)", "CLAUDE.md 25-26/07",
|
||||
"GTAA_MIN_CAPITAL $3.000", "n/d — sopra c'e' il PRIIPs: ordine RIFIUTATO sul conto reale",
|
||||
"no — regge come ricerca (LOO: unico positivo nel 100% delle estrazioni)"),
|
||||
("XS01 espanso (52 -> 51 HL)", "CLAUDE.md 19-20/06",
|
||||
"(nessun muro di taglia)", "n/d",
|
||||
"SI — il long-tail DILUISCE il momentum relativo; peggiore a ogni variante"),
|
||||
("IVOL (idio-vol cross-sectional)", "CLAUDE.md 29/06",
|
||||
"STAT-MODE", "MAI MISURATO (lead mai ripreso)",
|
||||
"SI — nato come LEAD debole nello stesso sweep in cui XSEC-V2 fa DSR 0,13"),
|
||||
("LOWVOL 19-major", "CLAUDE.md 29/06",
|
||||
"STAT-MODE", "n/d",
|
||||
"SI — deflated-Sharpe 0,13 + storia 2,5a"),
|
||||
]
|
||||
|
||||
|
||||
def sezione1() -> None:
|
||||
sez("1) IL CENSIMENTO — tutto cio' che il progetto ha archiviato CITANDO LA TAGLIA")
|
||||
print("""
|
||||
Regola di lettura: la colonna che decide non e' 'stato del muro' ma l'ULTIMA. Un muro di taglia
|
||||
che cade libera un candidato solo se non c'era anche un rifiuto sull'edge — e i gate sull'edge
|
||||
NON dipendono dal capitale (sez. 5).
|
||||
""")
|
||||
print(" " + "-" * 98)
|
||||
n_edge = 0
|
||||
for nome, fonte, muro, stato, edge in CENSIMENTO:
|
||||
n_edge += edge.startswith("SI")
|
||||
print(f" {nome:<32}[{fonte}]")
|
||||
print(f" {'':<32}muro pubblicato : {muro}")
|
||||
print(f" {'':<32}muro OGGI : {stato}")
|
||||
print(f" {'':<32}EDGE : {edge}")
|
||||
print(" " + "-" * 98)
|
||||
n = len(CENSIMENTO)
|
||||
print(f"\n VOCI: {n} con rifiuto ANCHE sull'edge: {n_edge}/{n} = {100*n_edge/n:.0f}%")
|
||||
falsi = sum(1 for _, _, _, s, _ in CENSIMENTO if s.startswith("FALSIFICATO"))
|
||||
mai = sum(1 for _, _, _, s, _ in CENSIMENTO if s.startswith("MAI"))
|
||||
print(f" muri di taglia FALSIFICATI (tutti dall'ondata 22-23/08): {falsi}/{n}")
|
||||
print(f" muri di taglia MAI MISURATI (li misuro qui dove possibile): {mai}/{n}")
|
||||
print(f"\n A1 {'CONFERMATA' if n_edge / n > 0.5 else 'REFUTATA'}: la taglia e' l'unico motivo "
|
||||
f"in {n - n_edge}/{n} voci.")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 2-3 — LE CURVE DI HAIRCUT
|
||||
# =============================================================================================
|
||||
def carica_candidati():
|
||||
"""Ricostruisce i pesi dei 3 candidati multi-gamba REALMENTE misurabili. Nessuna
|
||||
reimplementazione: si importano il sleeve di produzione e il monitor live."""
|
||||
from r0822_hl_exec import sim_libro, xs01_pesi # noqa: F401
|
||||
out = {}
|
||||
|
||||
W, dret, C, cols, dif = xs01_pesi(0)
|
||||
print(f" XS01 (19 gambe HL): identita' vs sleeves._xsec_returns max|dif| = {dif}")
|
||||
assert dif == 0.0, "XS01 non replica il sleeve di produzione"
|
||||
out["XS01 (19g, HL)"] = dict(W=W, R=dret, hedge=None, idx=C.index,
|
||||
fee=HL_FEE_SIDE, floor=HL_MIN_ORDER, legs=19, venue="HL")
|
||||
|
||||
from scripts.live.paper_xsr import FEE_LEG, build_panel
|
||||
ts, idx, Wx, Rx, rb, colx = build_panel()
|
||||
print(f" XSR01 (50 gambe HL): pannello dal MONITOR di produzione, {Wx.shape[1]} gambe, "
|
||||
f"fee monitor {FEE_LEG*1e4:.1f} bps")
|
||||
out["XSR01 (50g, HL)"] = dict(W=Wx, R=Rx, hedge=rb, idx=idx,
|
||||
fee=FEE_LEG, floor=HL_MIN_ORDER, legs=50, venue="HL")
|
||||
|
||||
import r0725_statarb_multi as SM
|
||||
base = SM.load_hl(SM.BASE)
|
||||
pc, rc = {}, {}
|
||||
for sym in SM.universe():
|
||||
try:
|
||||
tgt = SM.load_hl(sym)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
ix = base.index.intersection(tgt.index)
|
||||
if len(ix) < SM.MIN_BARS:
|
||||
continue
|
||||
p, _ = SM.signal(base[ix], tgt[ix])
|
||||
pc[sym] = pd.Series(p, index=ix)
|
||||
rc[sym] = tgt[ix].pct_change().fillna(0.0)
|
||||
P = pd.concat(pc, axis=1, sort=True).sort_index().fillna(0.0)
|
||||
Rr = pd.concat(rc, axis=1, sort=True).sort_index().reindex(P.index).fillna(0.0)[P.columns]
|
||||
k = P.shape[1]
|
||||
print(f" STATARB-MULTI: {k} coppie = {k+1} gambe (meccanismo CONGELATO W=45 sgn=+1, "
|
||||
f"da r0725_statarb_multi)")
|
||||
out["STATARB-MULTI (51g, HL)"] = dict(
|
||||
W=(P / k).to_numpy(float), R=Rr.to_numpy(float),
|
||||
hedge=base.pct_change().fillna(0.0).reindex(P.index).fillna(0.0).to_numpy(float),
|
||||
idx=P.index, fee=SM.FEE_LEG, floor=HL_MIN_ORDER, legs=k + 1, venue="HL")
|
||||
return out
|
||||
|
||||
|
||||
def curva(cand: dict, caps, floor=None) -> tuple[dict, dict, dict]:
|
||||
from r0822_hl_exec import sim_libro
|
||||
fl = cand["floor"] if floor is None else floor
|
||||
sh, noz, nets = {}, {}, {}
|
||||
for c in caps:
|
||||
s = sim_libro(cand["W"], cand["R"], cand["hedge"], c, fl, None, None, cand["fee"])
|
||||
m = met(s["net"], cand["idx"])
|
||||
sh[c] = m["sharpe"]
|
||||
noz[c] = s["pct_nozionale"]
|
||||
nets[c] = s["net"]
|
||||
s = sim_libro(cand["W"], cand["R"], cand["hedge"], 1e12, None, None, None, cand["fee"])
|
||||
nets["MOD"] = s["net"]
|
||||
return sh, noz, nets
|
||||
|
||||
|
||||
def sezione23(CAND: dict) -> dict:
|
||||
sez("2) CONTROLLO POSITIVO — XS01, il muro gia' caduto il 22/08")
|
||||
print("""
|
||||
Se il mio codice non ritrova haircut ~0 a $200-600 su XS01, e' rotto e il resto non vale niente.
|
||||
(HL-EXEC 22/08: "haircut ~0 gia' a $200-600 allocati; a $600 lo Sharpe passa 1,33 -> 1,30".)""")
|
||||
sh, noz, nets = curva(CAND["XS01 (19g, HL)"], GRIGLIA_DENSA)
|
||||
shm = met(nets["MOD"], CAND["XS01 (19g, HL)"]["idx"])["sharpe"]
|
||||
print(f"\n {'capitale ALLOCATO':>19}{'Sharpe':>9}{'haircut':>10}{'% nozionale eseguito':>23}")
|
||||
for c in GRIGLIA_DENSA:
|
||||
print(f" {c:>19,.0f}{sh[c]:>9.3f}{shm - sh[c]:>10.3f}{noz[c]:>22.1f}%")
|
||||
print(f" {'MODELLATA':>19}{shm:>9.3f}{0.0:>10.3f}{100.0:>22.1f}%")
|
||||
ok = abs(sh[200.0] - shm) <= 0.05 and abs(sh[635.0] - shm) <= 0.05
|
||||
print(f"\n A2 {'CONFERMATA' if ok else 'REFUTATA — CODICE ROTTO, MI FERMO'}: "
|
||||
f"haircut a $200 = {shm - sh[200.0]:+.3f}, a $635 = {shm - sh[635.0]:+.3f}.")
|
||||
assert ok, "controllo positivo fallito"
|
||||
for tol in (0.10, 0.05):
|
||||
cs = c_stabilita(sh, shm, tol)
|
||||
print(f" C* a tolleranza {tol:.2f} di Sharpe = "
|
||||
f"{('$%s' % f'{cs:,.0f}') if cs else '>200k'} di capitale ALLOCATO.")
|
||||
print(f"""
|
||||
PRECISAZIONE che il numero pubblicato non porta con se': i "$200-600" sono capitale ALLOCATO
|
||||
AL SLEEVE. A peso di libro 15% servono ${200/0.15:,.0f}-${635/0.15:,.0f} di LIBRO per stare li'; a un
|
||||
libro da $635 il sleeve vede $95 e l'haircut e' {shm - sh[95.0]:+.3f} — non zero.""")
|
||||
|
||||
sez("3) LE CURVE — i tre candidati multi-gamba, ai capitali del mandato")
|
||||
print(f"\n {'candidato':<26}{'gambe':>6}{'venue':>7}" +
|
||||
"".join(f"{('$%s' % f'{c:,.0f}'):>12}" for c in CAPITALI) +
|
||||
f"{'MODELL.':>10}{'C*':>12}")
|
||||
print(" " + "-" * 110)
|
||||
res = {}
|
||||
for nome, cand in CAND.items():
|
||||
shd, nozd, netsd = curva(cand, GRIGLIA_DENSA)
|
||||
shm = met(netsd["MOD"], cand["idx"])["sharpe"]
|
||||
cs = c_stabilita(shd, shm)
|
||||
res[nome] = dict(sh=shd, noz=nozd, nets=netsd, sh_mod=shm, cstar=cs, cand=cand)
|
||||
print(f" {nome:<26}{cand['legs']:>6}{cand['venue']:>7}" +
|
||||
"".join(f"{shd[c]:>12.3f}" for c in CAPITALI) +
|
||||
f"{shm:>10.3f}" + (f"{cs:>12,.0f}" if cs else f"{'>200k':>12}"))
|
||||
print(" " + "-" * 110)
|
||||
sub("3a) la curva DENSA (Sharpe sopra, % di nozionale eseguito sotto)")
|
||||
print(f" {'capitale':>10}" + "".join(f"{nm.split(' (')[0]:>13}" for nm in res))
|
||||
for c in GRIGLIA_DENSA:
|
||||
print(f" {c:>10,.0f}" + "".join(f"{res[nm]['sh'][c]:>13.3f}" for nm in res))
|
||||
print(f" {'':>10}" + "".join(f"{res[nm]['noz'][c]:>12.1f}%" for nm in res))
|
||||
print(f" {'MODELLATA':>10}" + "".join(f"{res[nm]['sh_mod']:>13.3f}" for nm in res))
|
||||
print(f"\n C* per tolleranza:")
|
||||
for tol in (0.10, 0.05):
|
||||
row = []
|
||||
for nm in res:
|
||||
cs = c_stabilita(res[nm]["sh"], res[nm]["sh_mod"], tol)
|
||||
row.append(("$%s" % f"{cs:,.0f}") if cs else ">200k")
|
||||
print(f" tol {tol:.2f}: " + " ".join(f"{nm.split(' (')[0]}={v}" for nm, v in zip(res, row)))
|
||||
|
||||
sub("3b) LA TRAPPOLA — l'haircut da solo NON rileva un muro di taglia")
|
||||
print("""
|
||||
`altlib.eval_weights_smallcap` riporta `sharpe_haircut = modellata - reale`. Su un candidato
|
||||
multi-gamba, sotto la soglia di eseguibilita' quel numero PUO' ESSERE NEGATIVO: il pavimento
|
||||
taglia quasi tutto il nozionale e cio' che resta e' una strategia DIVERSA, piccola e casuale.
|
||||
Letto da solo, un haircut negativo dice "eseguibile, anzi meglio". E' esattamente al rovescio.""")
|
||||
print(f"\n {'candidato':<26}{'cap':>10}{'Sharpe':>9}{'haircut':>10}{'% noz':>9} lettura")
|
||||
for nome, r in res.items():
|
||||
for c in (635.0, 5_000.0):
|
||||
h = r["sh_mod"] - r["sh"][c]
|
||||
deg = r["noz"][c] < 50.0
|
||||
print(f" {nome:<26}{c:>10,.0f}{r['sh'][c]:>9.3f}{h:>10.3f}{r['noz'][c]:>8.1f}% "
|
||||
f"{'DEGENERE (il numero non descrive la strategia)' if deg else 'eseguibile'}")
|
||||
st = res["STATARB-MULTI (51g, HL)"]
|
||||
print(f"""
|
||||
Il caso limpido: STATARB-MULTI a $635 fa Sharpe {st['sh'][635.0]:.3f} contro
|
||||
{st['sh_mod']:.3f} modellata -> haircut {st['sh_mod'] - st['sh'][635.0]:+.3f}, cioe' NEGATIVO,
|
||||
con il {st['noz'][635.0]:.1f}% del nozionale eseguito. Un gate scritto su `sharpe_haircut <= X`
|
||||
lo avrebbe promosso.""")
|
||||
|
||||
sub("3c) A6 — monotonia dell'haircut nel capitale")
|
||||
for nome, r in res.items():
|
||||
seq = [r["sh"][c] for c in GRIGLIA_DENSA]
|
||||
d = np.diff(seq)
|
||||
mono = bool((d >= -1e-9).all())
|
||||
print(f" {nome:<26} monotono crescente in C? {'SI' if mono else 'NO'} "
|
||||
f"(peggior passo indietro {d.min():+.3f}; max sopra la modellata "
|
||||
f"{max(seq) - r['sh_mod']:+.3f})")
|
||||
print("""
|
||||
A6 REFUTATA su 3/3. Sotto C* lo Sharpe non e' una misura della strategia: e' una misura di
|
||||
QUALI gambe il pavimento ha lasciato passare. Il segno del passo e' rumore.""")
|
||||
return res
|
||||
|
||||
|
||||
def sezione3d(CAND: dict) -> None:
|
||||
sub("3d) IL PAVIMENTO DEL VENUE SBAGLIATO — `scripts/live/paper_xsr.MIN_ORDER = 5.0`")
|
||||
print("""
|
||||
XSR01 gira su HYPERLIQUID (50 alt). Il suo monitor forward — quello che alimenta il gate
|
||||
pre-registrato del 2026-10-23 — usa `MIN_ORDER = 5.0`, che e' il pavimento di **DERIBIT**.
|
||||
Il pavimento di Hyperliquid e' **$10** (HL-EXEC 22/08: 3.827 livelli, 0,0% sotto $10).
|
||||
I due libri REAL del monitor sono $600 e $5.000: entrambi nella regione in cui il pavimento morde.""")
|
||||
c = CAND["XSR01 (50g, HL)"]
|
||||
sh5, noz5, _ = curva(c, GRIGLIA_DENSA, floor=5.0)
|
||||
sh10, noz10, nets = curva(c, GRIGLIA_DENSA, floor=10.0)
|
||||
shm = met(nets["MOD"], c["idx"])["sharpe"]
|
||||
print(f"\n {'capitale':>10}{'Sh @ $5 (monitor)':>20}{'Sh @ $10 (venue)':>19}{'differenza':>13}"
|
||||
f"{'% noz @$10':>12}")
|
||||
for cc in (600.0, 635.0, 2_000.0, 3_000.0, 5_000.0, 20_000.0, 100_000.0):
|
||||
print(f" {cc:>10,.0f}{sh5[cc]:>20.3f}{sh10[cc]:>19.3f}{sh5[cc]-sh10[cc]:>13.3f}"
|
||||
f"{noz10[cc]:>11.1f}%")
|
||||
d = np.array([sh5[cc] - sh10[cc] for cc in GRIGLIA_DENSA if cc <= 5_000.0])
|
||||
print(f"""
|
||||
Non e' "il monitor e' ottimista": nella regione che conta la differenza cambia SEGNO
|
||||
(min {d.min():+.3f}, max {d.max():+.3f}) — perche' sotto C* si stanno confrontando due
|
||||
sottoinsiemi casuali di gambe, non due stime della stessa cosa.
|
||||
C* col pavimento vero = ${c_stabilita(sh10, shm) or float('nan'):,.0f}; col pavimento del monitor
|
||||
= ${c_stabilita(sh5, shm) or float('nan'):,.0f}.
|
||||
Effetto sul gate 23/10: la sua gamba di eseguibilita' e' "haircut a $5.000 <= 40%", e a $5.000 il
|
||||
monitor misura con il pavimento sbagliato. NON anticipo il gate: registro che l'ingresso e' storto.
|
||||
6a occorrenza dello schema `fee_watch` — un controllo puntato su una configurazione diversa da
|
||||
quella che gira.""")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 4 — VRP01: lotti, non haircut
|
||||
# =============================================================================================
|
||||
def sezione4(no_net: bool) -> None:
|
||||
sez("4) VRP01 — qui il muro non e' un haircut, e' un LOTTO INDIVISIBILE")
|
||||
j = get_json("https://www.deribit.com/api/v2/public/get_instruments"
|
||||
"?currency=USDC&kind=option&expired=false", "usdc_opt", no_net)
|
||||
idx = get_json("https://www.deribit.com/api/v2/public/get_index_price?index_name=btc_usd",
|
||||
"px_btc", no_net)
|
||||
idxe = get_json("https://www.deribit.com/api/v2/public/get_index_price?index_name=eth_usd",
|
||||
"px_eth", no_net)
|
||||
px = {"BTC": (idx or {}).get("result", {}).get("index_price"),
|
||||
"ETH": (idxe or {}).get("result", {}).get("index_price")}
|
||||
mins, nstr = {}, {}
|
||||
for it in ((j or {}).get("result") or []):
|
||||
nm = it.get("instrument_name", "")
|
||||
for a in ("BTC", "ETH"):
|
||||
if nm.startswith(f"{a}_USDC-") and it.get("option_type") == "put":
|
||||
mins[a] = min(mins.get(a, 9e9), float(it.get("min_trade_amount", 9e9)))
|
||||
nstr[a] = nstr.get(a, 0) + 1
|
||||
if mins and all(px.values()):
|
||||
print(" letto dal venue ORA (GET pubblica su kind=OPTION, famiglia USDC-LINEARE = quella")
|
||||
print(" che un conto USDC puo' marginare; VRP01 VENDE, quindi il lotto e' il NOZIONALE"
|
||||
" sottostante,")
|
||||
print(" non il premio — e' la distinzione che §46 ha dovuto correggere sul lato LONG):")
|
||||
for a in ("BTC", "ETH"):
|
||||
print(f" {a}_USDC put ({nstr.get(a,0)} strumenti) min_trade_amount = "
|
||||
f"{mins.get(a)} indice ${px[a]:,.0f} -> lotto = ${mins.get(a, 0) * px[a]:,.0f}")
|
||||
lot = {a: mins[a] * px[a] for a in mins}
|
||||
else:
|
||||
print(" [venue non letto -> uso i valori verificati dal coordinatore il 22/08 (§50)]")
|
||||
lot = {"BTC": 772.0, "ETH": 242.0}
|
||||
print(f"""
|
||||
Il sleeve VRP01 e' 50/50 BTC+ETH al peso di libro 12%. Un lotto NON si divide: sotto un lotto
|
||||
la gamba non esiste, e la meta' BTC del sleeve cade prima della meta' ETH.""")
|
||||
print(f" (memoria: $242 ETH / $772 BTC, §50 22/08 — e $1.832/$6.210 sulla famiglia INVERSE,"
|
||||
f" che un conto USDC non puo' marginare)")
|
||||
print(f"\n {'capitale PROPRIO':>18}{'quota 12%':>12}{'lotti ETH':>11}{'lotti BTC':>11} sleeve")
|
||||
soglia_eth, soglia_btc = lot["ETH"] / 0.06, lot["BTC"] / 0.06
|
||||
for c in (635.0, 2_000.0, soglia_eth, 5_000.0, 6_440.0, soglia_btc, 20_000.0, 100_000.0):
|
||||
q = 0.12 * c
|
||||
ne, nb = int(q / 2 / lot["ETH"]), int(q / 2 / lot["BTC"])
|
||||
stato = ("assente" if ne + nb == 0 else "ETH-only (meta' del sleeve)" if nb == 0
|
||||
else "50/50 come modellato")
|
||||
print(f" {c:>18,.0f}{q:>12,.0f}{ne:>11}{nb:>11} {stato}")
|
||||
print(f"""
|
||||
A $100.000 il sleeve e' 50/50 con {int(0.06*100000/lot['ETH'])} lotti ETH e
|
||||
{int(0.06*100000/lot['BTC'])} BTC: il muro di taglia di VRP01 CADE del tutto.
|
||||
|
||||
CORREZIONE A UN NUMERO PUBBLICATO IERI (§50 / CLAUDE.md): "1 lotto ETH copre il peso di book 12%
|
||||
gia' da ~$2.000, e il BTC da ~$6.440" divide il lotto per 0,12 — cioe' mette TUTTO il peso del
|
||||
sleeve su UNA gamba. VRP01 e' 50/50: la gamba ETH vede il 6% del libro, non il 12%.
|
||||
Soglie vere: prima gamba (ETH) da ${soglia_eth:,.0f}; sleeve 50/50 completo da ${soglia_btc:,.0f}
|
||||
— cioe' il DOPPIO di quanto pubblicato, su entrambe. Non cambia nessuna decisione (VRP01 e' fermo
|
||||
per una regola), ma e' lo stesso errore di un fattore 2 in due punti.
|
||||
E non serve a niente: VRP01 e' fuori dal libro live per una REGOLA ("niente short-vol da modello
|
||||
in deploy") e per un `f` misurato 0,73 sulle quote vere — nessuna delle due dipende dal capitale.
|
||||
E' la forma piu' pulita del risultato di questo filone: il muro cade, la porta resta chiusa.""")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 5 — I GATE SONO INVARIANTI AL CAPITALE
|
||||
# =============================================================================================
|
||||
def sezione5(res: dict) -> dict:
|
||||
sez("5) IL PUNTO STRUTTURALE — un gate calcolato sulla serie MODELLATA E' GIA' il gate a $100k")
|
||||
print("""
|
||||
La serie modellata e' il limite a capitale infinito della serie eseguita (nessun ordine viene mai
|
||||
saltato). Quindi: se a $100k l'eseguita converge alla modellata, il verdetto dei gate a $100k E'
|
||||
gia' quello pubblicato — non serve rifarli, e soprattutto NON SI PUO' sperare che cambino.
|
||||
Verifico la premessa (convergenza) e poi la conseguenza (il verdetto non si muove).""")
|
||||
import altlib as al
|
||||
sub("5a) convergenza a $100k: max|differenza| giornaliera contro la serie modellata")
|
||||
for nome, r in res.items():
|
||||
d = np.abs(r["nets"][100_000.0] - r["nets"]["MOD"]).max()
|
||||
print(f" {nome:<26} max|dif| = {d:.3e} dSharpe = "
|
||||
f"{r['sh'][100_000.0] - r['sh_mod']:+.4f} % noz = {r['noz'][100_000.0]:.1f}%")
|
||||
print(f" A3 CONFERMATA: a $100k tutti e tre stanno entro {TOL_STAB} di Sharpe dalla modellata.")
|
||||
|
||||
sub("5b) `marginal_vs_tp01` a ogni capitale — il verdetto si muove col capitale?")
|
||||
print(f"\n {'candidato':<26}" + "".join(f"{('$%s' % f'{c:,.0f}'):>13}" for c in CAPITALI)
|
||||
+ f"{'MODELLATA':>13}")
|
||||
ver = {}
|
||||
for nome, r in res.items():
|
||||
row, vv = [], []
|
||||
for c in list(CAPITALI) + ["MOD"]:
|
||||
s = pd.Series(r["nets"][c], index=r["cand"]["idx"])
|
||||
m = al.marginal_vs_tp01(s)
|
||||
v = m.get("marginal_verdict", "?")
|
||||
row.append(v)
|
||||
vv.append(v)
|
||||
ver[nome] = vv
|
||||
print(f" {nome:<26}" + "".join(f"{v:>13}" for v in row))
|
||||
print("""
|
||||
Lettura: dove il verdetto cambia fra $635 e $100k, NON e' che la taglia sblocchi l'edge — e' che
|
||||
sotto C* la serie non e' la strategia (sez. 3b). Il verdetto che conta e' quello sulla MODELLATA,
|
||||
ed e' quello che i registri hanno gia' pubblicato.""")
|
||||
|
||||
sub("5c) `weights_tilt_null` a ogni capitale — libro LIVE 75/25, lente `hourly`, candidato 15%")
|
||||
import r0724_skh_live_weight as skl
|
||||
import r0725_capcurve as CC
|
||||
from src.portfolio.portfolio import combine_outer, metrics, weights_tilt_null
|
||||
skh = skl.skh_book(0, "hourly")
|
||||
if skh.index.tz is None:
|
||||
skh.index = skh.index.tz_localize("UTC")
|
||||
lo = pd.Timestamp("2019-03-14", tz="UTC")
|
||||
tp = CC.tp01_realistic(635.0 * 0.75)
|
||||
base = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo)
|
||||
mb = metrics(base)
|
||||
print(f" REPLICA del libro pubblicato (lente `hourly`): Sharpe {mb['sharpe']:.3f} "
|
||||
f"(pubblicato 1,683) maxDD {100*mb['maxdd']:.2f}% (pubblicato 11,22%) "
|
||||
f"-> {'OK' if abs(mb['sharpe']-1.683) < 0.02 else 'DIVERGE'}")
|
||||
print(f"\n {'candidato':<26}{'capitale':>10}{'d_insample':>12}{'pctl_hold':>11}"
|
||||
f"{'frac_rand':>11}{'gate_pass':>11}")
|
||||
for nome, r in res.items():
|
||||
for c in CAPITALI:
|
||||
s = pd.Series(r["nets"][c], index=r["cand"]["idx"])
|
||||
# TP01 va valutato allo STESSO capitale del candidato (altrimenti si confronta un
|
||||
# candidato a $100k con un TP01 a $635): e' l'errore che il progetto chiama
|
||||
# "un controllo puntato su una configurazione diversa da quella che gira".
|
||||
tpc = CC.tp01_realistic(c * 0.6375)
|
||||
g = weights_tilt_null({"TP01": tpc, "SKH01": skh, "C": s},
|
||||
{"TP01": 0.75, "SKH01": 0.25, "C": 0.0},
|
||||
{"TP01": 0.6375, "SKH01": 0.2125, "C": 0.15},
|
||||
n=300, seed=SEED)
|
||||
print(f" {nome:<26}{c:>10,.0f}{g['delta_insample']:>12.4f}{g['pctl_hold']:>11.1f}"
|
||||
f"{g['frac_random_beat_hold']:>11.2f}{str(g['gate_pass']):>11}")
|
||||
print("""
|
||||
ATTENZIONE, e vale piu' del PASS: `frac_random_beat_hold` sta a 0,8-0,9 — cioe' 8-9 tilt CASUALI
|
||||
su 10 battono i pesi correnti sull'hold-out. Dove vale cosi', "questo candidato migliora
|
||||
l'hold-out" e' un claim GENERICO (§50 lo dice sullo stesso gate). E il libro di riferimento non e'
|
||||
quello su cui XSR01 fu bocciato: CLAUDE.md riporta FAIL sul book di RICERCA a 5 sleeve, qui il
|
||||
libro e' quello LIVE a 2. Due libri, due verdetti — si dichiara quale.""")
|
||||
return ver
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 6 — BIN-FREQ
|
||||
# =============================================================================================
|
||||
def sezione6() -> None:
|
||||
sez("6) BIN-FREQ — l'UNICO archiviato la cui condizione di riapertura E' una soglia di capitale")
|
||||
eur_635 = 0.0113 # RESULTS §25: taglia misurata a $635
|
||||
rapporto_fam = 0.007 / 0.112 # mediana di famiglia / argmax
|
||||
print(f"""
|
||||
§25 (22/08) chiude BIN-FREQ con quattro gambe, e la quarta e' esplicitamente di TAGLIA:
|
||||
(1) mediana di famiglia >= meta' della cella e >65% celle positive -> FAIL (6%, 54%)
|
||||
(2) weights_tilt_null -> FAIL / vacuo
|
||||
(3) marginale + null con potenza -> PASS
|
||||
(4) taglia >= EUR 0,10/giorno -> FAIL: EUR {eur_635:.4f}/g a $635
|
||||
La (4) e' lineare nel capitale. Le altre tre NON lo sono: sono statistiche sulla serie.""")
|
||||
print(f"\n {'capitale':>10}{'EUR/g (cella)':>16}{'EUR/g (famiglia)':>19}{'gate (4)?':>12}")
|
||||
for c in (635.0, 5_600.0, 20_000.0, 53_000.0, 100_000.0, 200_000.0):
|
||||
ec = eur_635 * c / 635.0
|
||||
ef = ec * rapporto_fam
|
||||
print(f" {c:>10,.0f}{ec:>16.4f}{ef:>19.4f}{('PASS' if ef >= 0.10 else 'PASS(cella)' if ec >= 0.10 else 'FAIL'):>12}")
|
||||
print(f"""
|
||||
A $100.000 la gamba (4) PASSA anche con la stima ONESTA di famiglia (EUR
|
||||
{eur_635*100000/635*rapporto_fam:.3f}/giorno > 0,10) — e' l'unica voce del censimento per cui il
|
||||
capitale cambia davvero un verdetto pre-registrato.
|
||||
MA le gambe (1) e (2) restano FAIL, e sono invarianti al capitale: la mediana di famiglia e' il
|
||||
6% dell'argmax e il 54% delle celle e' positivo comunque si finanzi il conto.
|
||||
-> BIN-FREQ a $100k: 1 gate su 4 si sblocca, 2 restano chiusi. RESTA UN LEAD.""")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 7 — IL COROLLARIO FUNDED
|
||||
# =============================================================================================
|
||||
def sezione7(res: dict) -> None:
|
||||
sez("7) IL COROLLARIO FUNDED — il canale e' mai stato accreditato di AMPLIARE l'insieme?")
|
||||
print("""
|
||||
Distinzione obbligatoria: fin qui la colonna 'capitale' era CAPITALE PROPRIO. Un conto funded da
|
||||
$100k non e' capitale proprio: il nozionale e' della firm, il vincolo binding e' la regola di
|
||||
drawdown (max loss 6% STATICO), e il trader incassa una quota del profitto (0,80).
|
||||
Domanda: quali voci del censimento diventano ESEGUIBILI a $100k di nozionale funded e non lo sono
|
||||
a $635 di capitale proprio? E quanto vale, in Sharpe di libro e in EUR/giorno?""")
|
||||
import r0724_skh_live_weight as skl
|
||||
import r0725_capcurve as CC
|
||||
from src.portfolio.portfolio import combine_outer, metrics
|
||||
skh = skl.skh_book(0, "hourly")
|
||||
if skh.index.tz is None:
|
||||
skh.index = skh.index.tz_localize("UTC")
|
||||
lo = pd.Timestamp("2019-03-14", tz="UTC")
|
||||
tp = CC.tp01_realistic(100_000.0 * 0.6375) # libro funded $100k, non $635
|
||||
base = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo)
|
||||
|
||||
sub("7a) chi si aggiunge davvero (capitale PROPRIO $635 -> nozionale FUNDED $100k)")
|
||||
print(f" {'voce':<26}{'C* (proprio)':>14}{'a $635?':>10}{'a $100k?':>10} perche' non basta")
|
||||
righe = [
|
||||
("XS01 (19g, HL)", "SI", "SI", "gia' eseguibile a $635 -> NON e' un ampliamento di taglia; "
|
||||
"sul funded e' un ampliamento di VENUE (§30: Bybit 13/13), gia' contato da §1 (+0,400 J)"),
|
||||
("XSR01 (50g, HL)", "NO", "SI", "AMPLIAMENTO VERO — ma gate 23/10 aperto e blocchi invarianti al capitale"),
|
||||
("STATARB-MULTI (51g, HL)", "NO", "SI", "AMPLIAMENTO VERO — ma t 1,31, IC95 contiene lo zero"),
|
||||
]
|
||||
for nome, a, b, why in righe:
|
||||
cs = res[nome]["cstar"]
|
||||
print(f" {nome:<26}{(f'${cs:,.0f}' if cs else '>200k'):>14}{a:>10}{b:>10}")
|
||||
print(f" {'':<26}{why}")
|
||||
print("""
|
||||
A5: l'insieme AMMISSIBILE non si amplia (entrambi hanno un blocco che il capitale non tocca).
|
||||
L'insieme ESEGUIBILE si amplia di due voci, ed e' un fatto nuovo: nessun filone del progetto
|
||||
aveva mai misurato che a $100k STATARB-MULTI e XSR01 girano al 99,6% del nozionale.""")
|
||||
|
||||
sub("7b) quanto varrebbe, se fossero ammissibili — ISO-BARRIERA, su FINESTRA COMUNE")
|
||||
print("""
|
||||
A ISO-NOZIONALE un diversificatore a basso CAGR toglie reddito (regola 25/07 §3). Su un conto
|
||||
funded la lente giusta e' la BARRIERA: si cerca la leva k che porta il maxDD alla regola dei -6%
|
||||
e si legge il drift a quella leva. E' un LIMITE SUPERIORE, non una simulazione (breach a un
|
||||
giorno, lente wick accoppiata e morte-firm sono §37).
|
||||
FINESTRA COMUNE 2024+ obbligatoria: i candidati esistono solo da li'. Con la finestra piena
|
||||
`combine_outer` rinormalizza per riga, il maxDD del libro cade PRIMA del 2024 e le tre righe
|
||||
danno la STESSA k -> il confronto non misura niente (errore commesso e corretto in sessione).""")
|
||||
lo2 = pd.Timestamp("2024-01-01", tz="UTC")
|
||||
print(f"\n {'libro (finestra comune 2024+)':<34}{'Sharpe':>8}{'maxDD':>8}{'k@-6%':>8}"
|
||||
f"{'drift@k':>9}{'EUR/g $100k funded':>21}")
|
||||
base_rows = {}
|
||||
for lab, nome in [("LIVE 75/25 (TP01+SKH01)", None),
|
||||
("+ XSR01 @15%", "XSR01 (50g, HL)"),
|
||||
("+ STATARB-MULTI @15%", "STATARB-MULTI (51g, HL)")]:
|
||||
if nome is None:
|
||||
b = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, lo=lo2)
|
||||
else:
|
||||
s_ = pd.Series(res[nome]["nets"][100_000.0], index=res[nome]["cand"]["idx"])
|
||||
b = combine_outer({"TP01": tp, "SKH01": skh, "C": s_},
|
||||
{"TP01": 0.6375, "SKH01": 0.2125, "C": 0.15}, lo=lo2)
|
||||
b = b[b.index >= lo2]
|
||||
r = b.values
|
||||
m = metrics(b)
|
||||
k = k_barriera(r)
|
||||
g = cagr_k(r, k, b.index)
|
||||
eur = SPLIT_FUNDED * g * 100_000.0 / 365.0 / EURUSD
|
||||
base_rows[lab] = eur
|
||||
print(f" {lab:<34}{m['sharpe']:>8.3f}{100*m['maxdd']:>7.1f}%{k:>8.2f}{100*g:>8.1f}%"
|
||||
f"{eur:>21.2f}")
|
||||
d1 = base_rows["+ XSR01 @15%"] - base_rows["LIVE 75/25 (TP01+SKH01)"]
|
||||
d2 = base_rows["+ STATARB-MULTI @15%"] - base_rows["LIVE 75/25 (TP01+SKH01)"]
|
||||
print(f"""
|
||||
L'AMPLIAMENTO VALE, come LIMITE SUPERIORE: XSR01 {d1:+.2f} EUR/giorno, STATARB-MULTI {d2:+.2f}
|
||||
EUR/giorno su $100.000 di nozionale funded — e ZERO oggi, perche' nessuno dei due e' ammissibile.
|
||||
⚠️ La finestra 2024+ NON contiene un sinistro: il maxDD del libro li' e' molto piu' piccolo che
|
||||
sulla storia piena, quindi k e' gonfiata per TUTTE le righe. Il numero da leggere e' la
|
||||
DIFFERENZA, non il livello — e resta un tetto, perche' il funding dei candidati non e' contato.
|
||||
|
||||
✅ REPLICA D'ORDINE DI GRANDEZZA: la riga LIVE da' {base_rows['LIVE 75/25 (TP01+SKH01)']:.0f}
|
||||
EUR/giorno su un conto funded da $100k, contro i "~EUR 15-30/g max" che il progetto pubblica dal
|
||||
25/07 per UN conto con cap $200k/trader. Macchineria diversa (qui una k iso-barriera, li' un MC
|
||||
con breach), stesso ordine di grandezza.
|
||||
📌 E IL CONFRONTO CHE CONTA: la somma di TUTTI i lead dell'ondata 22-23/08 vale +0,036 EUR/giorno
|
||||
(§57). Il solo AMPLIAMENTO DELL'INSIEME su un conto funded vale, come tetto,
|
||||
{d1:+.2f} / {d2:+.2f} EUR/giorno — cioe' 40-100x. Non e' un argomento per aprire un funded: e' un
|
||||
argomento per smettere di valutare il canale funded SOLO sul nozionale.""")
|
||||
|
||||
|
||||
# =============================================================================================
|
||||
# SEZ. 8 — MDE E VERDETTO
|
||||
# =============================================================================================
|
||||
def sezione8(res: dict) -> None:
|
||||
sez("8) MDE, LIMITI, VERDETTO")
|
||||
for nome, r in res.items():
|
||||
idx = r["cand"]["idx"]
|
||||
yrs = (idx[-1] - idx[0]) / pd.Timedelta("365.25D")
|
||||
print(f" {nome:<26} {yrs:.2f} anni -> SE(Sharpe) ~ {1/np.sqrt(yrs):.2f}, "
|
||||
f"MDE(95%) ~ {1.96/np.sqrt(yrs):.2f} di Sharpe")
|
||||
print("""
|
||||
Cio' che questo filone PUO' dire con questi dati: dove sta la soglia di eseguibilita' (e' una
|
||||
misura deterministica sul pavimento del venue, non una stima statistica, e non ha MDE).
|
||||
Cio' che NON puo' dire: se uno di questi candidati abbia un edge — su 2,6 anni l'MDE e' ~1,2 di
|
||||
Sharpe, cioe' meta' del loro Sharpe modellato. Non lo dico: lo dicono i gate gia' pubblicati.
|
||||
|
||||
LIMITI DICHIARATI
|
||||
- I 3 candidati misurati vivono su HYPERLIQUID. Che un conto funded (Bybit, §30) listi le 50
|
||||
gambe di XSR01 NON e' verificato: §30 ha verificato le 13 di U13. E' una GET pubblica: costa
|
||||
zero e va fatta prima di credere alla sez. 7.
|
||||
- `sim_libro` applica il pavimento per gamba e non modella il lotto minimo per-strumento HL
|
||||
(lotti $0,01-7,72 sui 19 major, sotto il pavimento $10 -> non binding). Sui 50 alt della coda
|
||||
non e' verificato strumento per strumento.
|
||||
- Lo slippage NON e' nel conto: HL-EXEC lo ha refutato come rischio #1 con margine 21x, ma su
|
||||
UNA fotografia di 172 secondi, e SLIP-AUDIT ha scritto che la partecipazione scala lineare
|
||||
-> a $100k la misura di attrito a $635 NON si estrapola. La sez. 7 va riletta a quella taglia.
|
||||
- Il funding NON e' nel conto per i 3 candidati (§35 lo ha prezzato per il libro, non per loro):
|
||||
XS01 paga +1,45%/anno (§37) e i due panieri dollar-neutral hanno la stessa esposizione al
|
||||
prezzo cross-sezionale del funding. Ogni EUR/giorno della sez. 7 e' quindi OTTIMISTA.""")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--no-net", action="store_true")
|
||||
a = ap.parse_args()
|
||||
t0 = time.time()
|
||||
sez("§67 REGIME-CAPITALE — gli SCARTATI PER TAGLIA, rimisurati a $635 / $5k / $20k / $100k")
|
||||
print(f" avvio {datetime.now(timezone.utc):%Y-%m-%d %H:%M:%S} UTC seed {SEED}")
|
||||
sola_lettura()
|
||||
print(f" parametri venue: Deribit min ${DER_MIN_ORDER:.0f} / {DER_FEE_SIDE*1e4:.2f} bps ; "
|
||||
f"Hyperliquid min ${HL_MIN_ORDER:.0f} / {HL_FEE_SIDE*1e4:.2f} bps "
|
||||
f"(i due HL sono i valori MISURATI il 22/08, non i $5/5,0 bps di memoria)")
|
||||
print(f" lente del libro: `hourly` (path live) — Sh 1,683 / maxDD 11,22%")
|
||||
|
||||
sezione1()
|
||||
print("\n ricostruzione dei candidati misurabili (repliche prima di ogni delta):")
|
||||
CAND = carica_candidati()
|
||||
res = sezione23(CAND)
|
||||
sezione3d(CAND)
|
||||
sezione4(a.no_net)
|
||||
sezione5(res)
|
||||
sezione6()
|
||||
sezione7(res)
|
||||
sezione8(res)
|
||||
|
||||
sez("VERDETTO")
|
||||
n = len(CENSIMENTO)
|
||||
n_edge = sum(1 for *_, e in CENSIMENTO if e.startswith("SI"))
|
||||
nf = sum(1 for _, _, _, st, _ in CENSIMENTO if st.startswith("FALSIFICATO"))
|
||||
print(f"""
|
||||
`A $100.000 NESSUN ARCHIVIATO PASSA TUTTI I GATE — E NON PERCHE' LI ABBIA RICONTROLLATI:
|
||||
I GATE PUBBLICATI ERANO GIA' CALCOLATI SULLA SERIE MODELLATA, CHE E' IL LIMITE A CAPITALE
|
||||
INFINITO. IL MURO DI TAGLIA NON NASCONDEVA NIENTE.`
|
||||
|
||||
Il censimento ha {n} voci; {n_edge} hanno ANCHE un rifiuto sull'edge. Delle {n - n_edge} in cui la
|
||||
taglia era l'unico motivo, nessuna e' una strategia (sono XS01 — gia' sbloccato e fermo per la
|
||||
decisione di venue — e GTAA01, fermo per il PRIIPs).
|
||||
Muri di taglia caduti: {nf} su {n}, tutti nell'ondata 22-23/08, tutti nella stessa direzione.
|
||||
Muri di taglia MISURATI QUI per la prima volta: STATARB-MULTI (C* $2.000-$7.500 secondo la
|
||||
tolleranza) e la curva completa di XSR01 col pavimento del venue GIUSTO (C* $15.000-$20.000,
|
||||
contro i ~$3.000 che HL-EXEC riportava con un criterio piu' permissivo).
|
||||
|
||||
IN EUR/GIORNO — la domanda del progetto:
|
||||
Questo filone NON avvicina i 50 EUR/giorno. Non aggiunge NIENTE al conto di oggi: tutto cio'
|
||||
che sblocca vive a capitali che il conto non ha, e a quei capitali resta bloccato sull'edge.
|
||||
Il suo contributo e' +0,000 EUR/giorno.
|
||||
Cio' che TOGLIE e' reale ma non e' reddito: il gate XSR01 del 23/10 legge un monitor tarato sul
|
||||
pavimento del venue sbagliato.
|
||||
Per confronto, sullo stesso motore: EUR 250 -> 500 al mese porta P(20 anni) dal 14% all'85%.
|
||||
|
||||
Libro, pesi, cron, config INVARIATI. Nessun ordine.
|
||||
durata {time.time()-t0:.0f} s""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,909 @@
|
||||
"""r0823b_retry — §61 RETRY: la POLITICA DI RI-ACQUISTO del biglietto funded (2026-08-23).
|
||||
|
||||
IL BUCO CHE CHIUDE
|
||||
------------------
|
||||
Un conto funded che rompe una regola muore. Il progetto ha modellato tutto il resto del ciclo
|
||||
(prezzo, regole, leva, lente accoppiata, funding, morte-firm, fisco) e **mai la politica di
|
||||
RI-ACQUISTO**: quando ricomprare dopo un bust, con quali soldi, e con quale leva sul conto nuovo.
|
||||
Nei simulatori esistenti (`r0725_prop_ladder.simulate`, `r0725_prop_coupled.simulate`,
|
||||
`r0822d_gate_c.ladder`) la politica c'e' — e' **cablata**: *compra sempre, subito, il biglietto
|
||||
piu' GRANDE che la cassa permette, in tutti e 6 gli slot*. E' la politica piu' aggressiva della
|
||||
famiglia, non e' mai stata dichiarata come scelta, e **tutti i numeri operativi del canale funded
|
||||
escono da li'** (42% -> 7,8% -> 4,4% -> 2,6%).
|
||||
|
||||
E' diventata una domanda vera solo ieri, per due misure:
|
||||
(a) §30: la quota d'ingresso e' un **deposito RIMBORSABILE** ("Refundable Challenge Deposit,
|
||||
returned in full alongside your first funded payout") -> un tentativo fallito costa la
|
||||
quota, uno riuscito no. E' una struttura a OPZIONE, e le opzioni si comprano piu' volte.
|
||||
(b) §37: il biglietto batte il libro **solo perche' il piano di versamento c'e'**, cioe'
|
||||
**perche' si puo' giocare piu' di una volta** ("una lotteria a EV positivo che un conto da
|
||||
$654 puo' giocare UNA VOLTA SOLA").
|
||||
Se il valore sta nel NUMERO DI TENTATIVI, la politica di ri-acquisto e' la variabile principale
|
||||
e non e' mai stata ottimizzata.
|
||||
|
||||
LE ATTESE, REGISTRATE PRIMA DI MISURARE (poi riportate confermate/refutate)
|
||||
--------------------------------------------------------------------------
|
||||
A1. **Compromesso monotono.** IMMEDIATA massimizza sia il numero di giocate sia P(cassa a zero);
|
||||
MAI li minimizza entrambi. Nessuna politica dovrebbe dominare su tutte e due le colonne.
|
||||
A2. **La dose-risposta di §37 dovrebbe essere ripida.** Se il meccanismo e' "quante volte si
|
||||
gioca", passare da max 1 biglietto a illimitati deve muovere P(>=50 EUR/g) di molto. Se non
|
||||
lo muove, il meccanismo dichiarato da §37 e' sbagliato (o e' un'altra cosa).
|
||||
A3. **Il biglietto PICCOLO ($249/$25k) compra piu' giocate e perde sul bersaglio.** 50 EUR/g netti
|
||||
sono ~$59k di payout netti in 3 anni: con nozionale $25k servono troppi cicli. Mi aspetto
|
||||
P(zero) piu' bassa e P(>=50/g) piu' bassa. Dominato sul bersaglio, non sulla sopravvivenza.
|
||||
A4. **SOLO-PAYOUT ~= MAI senza versamenti** (senza payout non c'e' cassa per ricomprare) e
|
||||
molto piu' vicina a IMMEDIATA con versamenti.
|
||||
A5. **Il RIMBORSO alza i livelli di ~1-3pp (§37) ma NON cambia l'ordine delle politiche.**
|
||||
Previsione forte: l'ottimo e' lo stesso nelle tre convenzioni.
|
||||
A6. 🚨 **Le regole di leva condizionate all'esito passato sono NULL DEL DE-LEVERING.** In un
|
||||
bootstrap a blocchi il passato non porta informazione sul futuro: una regola "riduci la leva
|
||||
dopo un bust" puo' agire SOLO cambiando la leva MEDIA. Il controllo obbligatorio e' la leva
|
||||
COSTANTE alla stessa media realizzata. Mi aspetto REFUTED 3/3 (8ª occorrenza).
|
||||
A7. **La politica conta meno del versamento.** Mi aspetto uno spread fra politiche di pochi punti
|
||||
percentuali contro i ~+10pp del passaggio EUR 0 -> EUR 500/mese misurato da §37.
|
||||
|
||||
CONVENZIONI (tutte EREDITATE e congelate — nessuna scelta di strategia in questo file)
|
||||
--------------------------------------------------------------------------------------
|
||||
* Lente **ACCOPPIATA** (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only e' cieca.
|
||||
* Conto funded = **W_A TP01 38 / SKH01 38 / XS01 25** (centro della regione robusta di §29),
|
||||
XS01 = **U13 Binance** fuori campione, finestra **W-LONG 2021+**, de-luck sul drift **x0,89**.
|
||||
* **Funding dentro** (§35/§37), morte-firm **10%/anno** (assunzione del progetto), cap
|
||||
**$200k/trader**, fisco **33%**, split **80%**, soglia di prelievo **$100** di quota trader.
|
||||
* Banda d'ancora: **23 offset x 3 fasi (0,3,7)** = lo stesso sottocampione dichiarato da §32/§37.
|
||||
La cella di riferimento e' la **MEDIANA** della banda, non la canonica (91° pctl).
|
||||
* Cassa non spesa: **FERMA** per default (convenzione di §37) e **SUL LIBRO** come sensibilita'
|
||||
dichiarata in §7 — perche' la convenzione "ferma" penalizza per costruzione le politiche
|
||||
prudenti, che sono esattamente quelle sotto esame.
|
||||
* Bootstrap **CONDIVISO** fra tutte le politiche di una stessa tabella -> confronti **APPAIATI**.
|
||||
|
||||
COSA NON E' QUESTO FILE: non e' una selezione di strategia (meccanismo, pesi, leva ottima, lente
|
||||
e soglie sono tutti ereditati); e' la mappatura di una politica OPERATIVA che finora era cablata.
|
||||
L'argmax su ~30 politiche resta pero' una selezione: si riportano il PLATEAU e la risoluzione MC.
|
||||
|
||||
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_retry.py`
|
||||
Rete: NESSUNA (parquet certificati + cache Binance di r0822b + cache funding di r0822d).
|
||||
"""
|
||||
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]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07)
|
||||
import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07)
|
||||
import r0822_prop_alloc as PA # noqa: E402 J, bootstrap (22/08)
|
||||
import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08)
|
||||
import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (gamba b)
|
||||
import r0822d_funding as FD # noqa: E402 il funding (22/08)
|
||||
import r0822d_gate_c as GC # noqa: E402 ladder di riferimento (§37)
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
EURUSD = pl.EURUSD
|
||||
TAX_RATE = pl.TAX_RATE
|
||||
SPLIT = pl.SPLIT
|
||||
DELUCK = PA.DELUCK
|
||||
W_A = GC.W_A
|
||||
OFFSETS = AP.OFFSETS
|
||||
PHASES_SUB = AP.PHASES_SUB
|
||||
MIN_PAYOUT = GC.MIN_PAYOUT_TRADER
|
||||
HORIZON_DAYS = GC.HORIZON_DAYS
|
||||
SEED_LAD = GC.SEED_LAD
|
||||
XS_FUND_ANN = 0.0145 # §37: XS01 PAGA +1,45%/anno di funding (misurato)
|
||||
|
||||
N_BAND = 1000 # percorsi per la banda d'ancora (riproduzione §32/§37)
|
||||
N_MAIN = 2000 # percorsi per la griglia di politiche (cella di rif.)
|
||||
N_ROB = 1000 # percorsi per la robustezza fra celle
|
||||
# Sottocampione di offset dichiarato A PRIORI: gli stessi 8 uniformi di `r0726_skh_live_book`
|
||||
# (la griglia piena e' 23; su 2 CPU condivise con altri 5 processi la piena non sta nel budget,
|
||||
# e un sottocampione uniforme dichiarato prima e' l'unica forma onesta di ridurla).
|
||||
BAND_OFFSETS = tuple(range(0, 690, 90))
|
||||
|
||||
# numeri PUBBLICATI da riprodurre prima di produrne di nuovi
|
||||
PUB = dict(anchor_p50g=0.044, anchor_p10=0.025, anchor_p90=0.082, anchor_pzero=0.353,
|
||||
gatec_p50g=0.026, gatec_p10=0.015, gatec_p90=0.047, gatec_pzero=0.403,
|
||||
refund_pp_lo=0.01, refund_pp_hi=0.03)
|
||||
|
||||
TRIALS = 0
|
||||
T0 = time.time()
|
||||
CHEAPEST = min(t[1] for F in pl.FIRMS for t in F["tickets"]) # $249
|
||||
|
||||
|
||||
def bump(n: int = 1) -> None:
|
||||
global TRIALS
|
||||
TRIALS += n
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "-" * 104)
|
||||
if t:
|
||||
print(f" {t}")
|
||||
print("-" * 104)
|
||||
|
||||
|
||||
def tick(lab: str) -> None:
|
||||
print(f" [{time.time() - T0:6.0f}s] {lab}", flush=True)
|
||||
|
||||
|
||||
# ============================================================================ LE POLITICHE
|
||||
def POL(when="immediate", k_reserve=1.0, cadence=0, max_tickets=10 ** 9, size="largest",
|
||||
lev_rule="const", lev_dn=1.0, lev_up=1.0, lev_lo=0.25, lev_hi=1.50,
|
||||
max_open=pl.MAX_CONCURRENT) -> dict:
|
||||
"""Una politica di ri-acquisto. `when`:
|
||||
immediate — compra appena uno slot e' libero e la cassa basta (= il CABLATO di oggi)
|
||||
threshold — compra solo se la cassa e' >= k_reserve x il prezzo (tiene una riserva)
|
||||
earned — il PRIMO biglietto dai propri soldi, i successivi SOLO dai payout/rimborsi
|
||||
cadence — al piu' un acquisto ogni `cadence` giorni
|
||||
none — non compra MAI (controllo positivo: deve dare P(>=50/g)=0)
|
||||
`size`: 'largest' (cablato) o 'cheapest' (piu' giocate, nozionale minore).
|
||||
`max_open`: quanti conti al MASSIMO possono essere aperti insieme (cablato: 6 = tutti gli
|
||||
slot). E' l'altra faccia del ri-acquisto: giocare in SERIE invece che in PARALLELO.
|
||||
`lev_rule`: 'const' | 'down_on_bust' (x lev_dn a ogni bust) | 'up_on_pass' (x lev_up a ogni
|
||||
pass). La leva di un conto e' fissata all'ACQUISTO e non cambia piu'."""
|
||||
return dict(when=when, k_reserve=k_reserve, cadence=cadence, max_tickets=max_tickets,
|
||||
size=size, lev_rule=lev_rule, lev_dn=lev_dn, lev_up=lev_up,
|
||||
lev_lo=lev_lo, lev_hi=lev_hi, max_open=max_open)
|
||||
|
||||
|
||||
P_BASE = POL() # il cablato, bit-exact vs §37
|
||||
|
||||
|
||||
# ============================================================================ IL SIMULATORE
|
||||
def ladder_rt(R, G, n_paths, lev, seed, *, pol=None, refund_mode="none", deposit_eur=0.0,
|
||||
start_eur=pl.START_EUR, min_payout=0.0, boot=None, block=20, deluck=DELUCK,
|
||||
rb_boot=None, tax=TAX_RATE) -> dict:
|
||||
"""`r0822d_gate_c.ladder` generalizzato alla POLITICA DI RI-ACQUISTO. Tre aggiunte:
|
||||
1. `pol` — quando/quale biglietto/con quali soldi/a che leva (sopra);
|
||||
2. `refund_mode` — 'none' (spesa persa, `pc.simulate`) | 'pass' (`r0725_hyro:200`) |
|
||||
'payout' (la regola vera della firm);
|
||||
3. `rb_boot` — se dato, la cassa NON spesa gira sul LIBRO LIVE invece di restare
|
||||
ferma, con lo STESSO sorteggio di bootstrap (stesso mercato) e lo
|
||||
stesso fisco annuale di `GC.book_road`.
|
||||
Con `pol=P_BASE, refund_mode='none', deposit_eur=0, min_payout=0, rb_boot=None` dev'essere
|
||||
**BIT-EXACT** rispetto a `GC.ladder(..., refund=False)` e quindi a `pc.simulate`: e' §1.
|
||||
|
||||
🚨 Zero e' ASSORBENTE ed e' modellato come tale: senza conti vivi e con cassa sotto il
|
||||
biglietto piu' economico ($249) non si gioca piu'; solo un versamento puo' riaprire il gioco.
|
||||
"""
|
||||
pol = pol or P_BASE
|
||||
rng = np.random.default_rng(seed)
|
||||
n_days = HORIZON_DAYS
|
||||
L = len(R)
|
||||
Rm = R.astype(float).reshape(-1, 1).copy()
|
||||
Gm = G.astype(float).reshape(-1, 1).copy()
|
||||
Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True)
|
||||
if boot is None:
|
||||
n_blocks = int(np.ceil(n_days / block))
|
||||
starts = rng.integers(0, L - block, size=(n_paths, n_blocks))
|
||||
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||
else:
|
||||
idx = boot
|
||||
n_slot = pl.MAX_CONCURRENT
|
||||
# (paths, days): tutti gli slot girano lo STESSO libro -> una colonna basta. La leva
|
||||
# moltiplica per-slot (serve alle regole di leva) invece che globalmente: `x*lev` e' la
|
||||
# stessa moltiplicazione in virgola mobile, quindi il bit-exact regge (verificato in §1).
|
||||
Rb = Rm[idx][:, :, 0]
|
||||
Gb = Gm[idx][:, :, 0]
|
||||
|
||||
FIRMS = pl.FIRMS
|
||||
cash = np.full(n_paths, start_eur * EURUSD)
|
||||
basis = cash.copy()
|
||||
carry = np.zeros(n_paths)
|
||||
earned = np.zeros(n_paths) # borsellino dei soli payout/rimborsi
|
||||
phase = np.full((n_paths, n_slot), -1, np.int8)
|
||||
eq = np.ones((n_paths, n_slot))
|
||||
firm = np.full((n_paths, n_slot), -1, np.int8)
|
||||
notional = np.zeros((n_paths, n_slot))
|
||||
price_paid = np.zeros((n_paths, n_slot))
|
||||
refunded = np.zeros((n_paths, n_slot), bool)
|
||||
lev_slot = np.full((n_paths, n_slot), lev)
|
||||
# soglie della firm materializzate PER SLOT all'acquisto: la dinamica diventa una sola
|
||||
# espressione vettoriale invece di un loop sulle firm. Le maschere per firm sono disgiunte,
|
||||
# quindi il risultato e' identico (verificato bit-exact in §1).
|
||||
TH_TGT = np.zeros((n_paths, n_slot))
|
||||
TH_DD = np.ones((n_paths, n_slot))
|
||||
TH_DL = np.ones((n_paths, n_slot))
|
||||
TH_ML = np.ones((n_paths, n_slot))
|
||||
TH_DLF = np.ones((n_paths, n_slot))
|
||||
mult = np.ones(n_paths)
|
||||
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
|
||||
cum_payout = np.zeros(n_paths)
|
||||
spent = np.zeros(n_paths)
|
||||
got_back = np.zeros(n_paths)
|
||||
deposited = np.zeros(n_paths)
|
||||
n_payouts = np.zeros(n_paths, int)
|
||||
n_tickets = np.zeros(n_paths, int)
|
||||
lev_sum = np.zeros(n_paths)
|
||||
last_buy = np.full(n_paths, -10 ** 9)
|
||||
days_out = np.zeros(n_paths, int)
|
||||
ever_passed = np.zeros(n_paths, bool)
|
||||
p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||
tickets_order = {}
|
||||
for fi, F in enumerate(FIRMS):
|
||||
tickets_order[fi] = sorted(F["tickets"], key=(lambda x: x[0]) if pol["size"] == "cheapest"
|
||||
else (lambda x: -x[0]))
|
||||
|
||||
for t in range(n_days):
|
||||
died = rng.random((n_paths, len(FIRMS))) < p_death_day
|
||||
newly_dead = died & firm_alive
|
||||
firm_alive &= ~died
|
||||
if newly_dead.any():
|
||||
for fi in range(len(FIRMS)):
|
||||
kill = newly_dead[:, fi]
|
||||
if kill.any():
|
||||
hit = (firm == fi) & kill[:, None]
|
||||
phase[hit] = -1
|
||||
notional[hit] = 0.0
|
||||
|
||||
# ---------------------------------------------------------------- acquisti
|
||||
if pol["when"] == "none":
|
||||
budget = np.zeros(n_paths, int)
|
||||
elif pol["when"] == "cadence":
|
||||
budget = ((t - last_buy) >= pol["cadence"]).astype(int)
|
||||
else:
|
||||
budget = np.full(n_paths, n_slot, int)
|
||||
budget = np.minimum(budget, np.maximum(pol["max_tickets"] - n_tickets, 0))
|
||||
if pol["max_open"] < n_slot:
|
||||
budget = np.minimum(budget, np.maximum(pol["max_open"] - (phase >= 0).sum(axis=1), 0))
|
||||
if budget.any() and (cash >= CHEAPEST).any():
|
||||
for k in range(n_slot):
|
||||
free = (phase[:, k] == -1) & (budget > 0)
|
||||
if not free.any():
|
||||
continue
|
||||
for fi, F in enumerate(FIRMS):
|
||||
used = np.where(firm == fi, notional, 0.0).sum(axis=1)
|
||||
for (size, price) in tickets_order[fi]:
|
||||
buy = (free & firm_alive[:, fi] & (cash >= price)
|
||||
& (used + size <= F["cap"]))
|
||||
if pol["when"] == "threshold":
|
||||
buy &= cash >= pol["k_reserve"] * price
|
||||
elif pol["when"] == "earned":
|
||||
buy &= (n_tickets == 0) | (earned >= price)
|
||||
if not buy.any():
|
||||
continue
|
||||
cash[buy] -= price
|
||||
basis[buy] -= price
|
||||
if pol["when"] == "earned":
|
||||
take = buy & (n_tickets > 0)
|
||||
earned[take] -= price
|
||||
spent[buy] += price
|
||||
n_tickets[buy] += 1
|
||||
budget[buy] -= 1
|
||||
last_buy[buy] = t
|
||||
phase[buy, k] = 0
|
||||
eq[buy, k] = 1.0
|
||||
firm[buy, k] = fi
|
||||
notional[buy, k] = size
|
||||
price_paid[buy, k] = price
|
||||
refunded[buy, k] = False
|
||||
lv = np.clip(lev * mult, pol["lev_lo"], pol["lev_hi"])
|
||||
lev_slot[buy, k] = lv[buy]
|
||||
TH_TGT[buy, k], TH_DD[buy, k], TH_DL[buy, k] = F["ev"]
|
||||
TH_ML[buy, k], TH_DLF[buy, k] = F["fu"]
|
||||
lev_sum[buy] += lv[buy]
|
||||
free = free & ~buy
|
||||
if not free.any():
|
||||
break
|
||||
if not free.any():
|
||||
break
|
||||
|
||||
# ---------------------------------------------------------------- dinamica dei conti
|
||||
r = Rb[:, t][:, None] * lev_slot
|
||||
g = Gb[:, t][:, None] * lev_slot
|
||||
act = phase >= 0
|
||||
eq_prev = eq
|
||||
eq = np.where(act, eq * (1.0 + r), eq)
|
||||
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||
r_low = r + g
|
||||
|
||||
in_ev = act & (phase == 0)
|
||||
in_fu = act & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - TH_DD) | (r_low < -TH_DL)))
|
||||
| (in_fu & ((eq_low < 1.0 - TH_ML) | (r_low < -TH_DLF))))
|
||||
phase[bust] = -1
|
||||
notional[bust] = 0.0
|
||||
passed = in_ev & (eq >= 1.0 + TH_TGT) & ~bust
|
||||
phase[passed] = 1
|
||||
eq[passed] = 1.0
|
||||
if pol["lev_rule"] == "down_on_bust":
|
||||
mult *= pol["lev_dn"] ** bust.sum(axis=1)
|
||||
elif pol["lev_rule"] == "up_on_pass":
|
||||
mult *= pol["lev_up"] ** passed.sum(axis=1)
|
||||
if refund_mode == "pass" and passed.any():
|
||||
back = passed & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
basis += amt
|
||||
earned += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
ever_passed |= passed.any(axis=1)
|
||||
|
||||
# ---------------------------------------------------------------- la cassa propria
|
||||
if rb_boot is not None:
|
||||
cash = np.maximum(cash * (1.0 + rb_boot[:, t]), 0.0)
|
||||
|
||||
# ---------------------------------------------------------------- mensile
|
||||
if (t + 1) % 30 == 0:
|
||||
fu = phase == 1
|
||||
pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout)
|
||||
gain = np.where(pay, (eq - 1.0) * notional, 0.0)
|
||||
net = gain.sum(axis=1) * SPLIT * (1.0 - TAX_RATE)
|
||||
cum_payout += net
|
||||
cash += net
|
||||
basis += net
|
||||
earned += net
|
||||
n_payouts += (pay.sum(axis=1) > 0).astype(int)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if refund_mode == "payout":
|
||||
back = pay & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
basis += amt
|
||||
earned += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
if deposit_eur:
|
||||
cash += deposit_eur * EURUSD
|
||||
basis += deposit_eur * EURUSD
|
||||
deposited += deposit_eur * EURUSD
|
||||
# fisco annuale sulla sola cassa propria (i payout sono gia' netti)
|
||||
if rb_boot is not None and (t + 1) % 365 == 0:
|
||||
gtax = (cash - basis) + carry
|
||||
due = np.where(gtax > 0, tax * gtax, 0.0)
|
||||
carry = np.where(gtax > 0, 0.0, gtax)
|
||||
cash = cash - due
|
||||
basis = cash.copy()
|
||||
|
||||
out = (~(phase >= 0).any(axis=1)) & (cash < CHEAPEST)
|
||||
days_out += out
|
||||
|
||||
eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0
|
||||
stuck = (~(phase >= 0).any(axis=1)) & (cash < CHEAPEST)
|
||||
return dict(eur_day=eur_day, med=float(np.median(eur_day)),
|
||||
p10=float(np.percentile(eur_day, 10)), p90=float(np.percentile(eur_day, 90)),
|
||||
mean_eur=float(eur_day.mean()),
|
||||
p_ge10=float((eur_day >= 10.0).mean()),
|
||||
p_target=float((eur_day >= pl.TARGET_EUR_DAY).mean()),
|
||||
p_zero=float((cum_payout <= 0).mean()),
|
||||
p_payout=float((n_payouts > 0).mean()),
|
||||
p_pass=float(ever_passed.mean()),
|
||||
p_stuck=float(stuck.mean()),
|
||||
n_tix=n_tickets, med_tix=float(np.median(n_tickets)),
|
||||
mean_tix=float(n_tickets.mean()),
|
||||
lev_mean=float(lev_sum.sum() / max(n_tickets.sum(), 1)),
|
||||
days_out=float(np.median(days_out)),
|
||||
cash=cash, med_cash=float(np.median(cash)), mean_cash=float(cash.mean()),
|
||||
p10_cash=float(np.percentile(cash, 10)),
|
||||
p90_cash=float(np.percentile(cash, 90)),
|
||||
med_spent=float(np.median(spent)), mean_back=float(got_back.mean()),
|
||||
contributed=float(start_eur * EURUSD + deposited.mean()))
|
||||
|
||||
|
||||
def se_pair(a: np.ndarray, b: np.ndarray, thr: float = pl.TARGET_EUR_DAY) -> float:
|
||||
"""SE della DIFFERENZA fra due proporzioni APPAIATE (stessi sorteggi): McNemar.
|
||||
Con bootstrap condiviso la parte comune si cancella e la risoluzione e' molto migliore
|
||||
di quella dei livelli — ma non e' zero, e va stampata."""
|
||||
A = a >= thr
|
||||
B = b >= thr
|
||||
n01 = float((A & ~B).sum())
|
||||
n10 = float((~A & B).sum())
|
||||
n = len(a)
|
||||
return float(np.sqrt(max(n01 + n10 - (n01 - n10) ** 2 / n, 0.0)) / n)
|
||||
|
||||
|
||||
# ==================================================================================== main
|
||||
def main() -> None:
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
print("=" * 104)
|
||||
print(" §61 RETRY — la POLITICA DI RI-ACQUISTO del biglietto funded, mappata invece che")
|
||||
print(" cablata. Quando ricomprare, con quali soldi, a che leva — e quanto conta.")
|
||||
print("=" * 104)
|
||||
print(" ATTESE REGISTRATE PRIMA: A1 compromesso monotono giocate/rovina; A2 dose-risposta")
|
||||
print(" RIPIDA se il meccanismo di §37 e' 'il numero di volte che si gioca'; A3 biglietto")
|
||||
print(" piccolo = piu' giocate, meno bersaglio; A4 SOLO-PAYOUT ~= MAI senza versamenti;")
|
||||
print(" A5 il rimborso NON cambia l'ottimo; A6 le regole di leva sono null del de-levering")
|
||||
print(" (REFUTED 3/3 atteso); A7 la politica conta meno del versamento.")
|
||||
|
||||
# ============================================================== §0 pannello
|
||||
hr("§0. IL PANNELLO — le stesse serie di §29/§37 (U13 Binance, W-LONG 2021+, funding dentro)")
|
||||
rate = GC.funding_rate()
|
||||
if rate is None:
|
||||
print(" !! cache funding assente: il filone si ferma. Un numero senza funding NON e'")
|
||||
print(" confrontabile con §37, e confrontarlo sarebbe l'errore che §35 ha appena")
|
||||
print(" pagato. 'non misurabile' e' un risultato.")
|
||||
return
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.bin_loader, XS_UNIVERSE)
|
||||
p13 = Cb[X.U13].dropna()
|
||||
o13 = Ob.reindex(p13.index)[X.U13]
|
||||
h13 = Hb.reindex(p13.index)[X.U13]
|
||||
l13 = Lb.reindex(p13.index)[X.U13]
|
||||
XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB}
|
||||
xs0 = XSP[PHASES_SUB[0]]
|
||||
base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))]
|
||||
pc.DELUCK = DELUCK
|
||||
print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK} | "
|
||||
f"cassa iniziale EUR {pl.START_EUR:.0f} = ${pl.START_EUR*EURUSD:,.0f}")
|
||||
print(f" orizzonte {HORIZON_DAYS} giorni ({HORIZON_DAYS/30:.0f} versamenti da 30 giorni: "
|
||||
f"§57 avverte che 'EUR X/mese' qui vuol dire ogni 30 giorni — su 36 mesi torna esatto)")
|
||||
print(f" biglietti disponibili: " + " | ".join(
|
||||
f"{F['name']} " + ", ".join(f"${s/1000:.0f}k@${p:.0f}" for s, p in F['tickets'])
|
||||
for F in pl.FIRMS))
|
||||
print(f" biglietto piu' economico ${CHEAPEST:.0f} -> sotto questa cassa e senza conti vivi")
|
||||
print(f" il percorso e' FUORI GIOCO (zero assorbente).")
|
||||
tick("§0")
|
||||
|
||||
# ============================================================== §1 controlli
|
||||
hr("§1. CONTROLLI — bit-exact contro il simulatore pubblicato, e il controllo POSITIVO")
|
||||
R0, G0 = AP.rg_off(W_A, I, XSP[0], 0)
|
||||
f0 = GC.fvec_for(W_A, I, rate, 0, XS_FUND_ANN)
|
||||
Rc0 = R0 - f0
|
||||
print(" §1.1 la mia `ladder_rt` col cablato (immediate/largest/const, nessun rimborso,")
|
||||
print(" nessun versamento, soglia 0, cassa ferma) contro `GC.ladder` di §37:")
|
||||
a = GC.ladder(Rc0, G0, N_BAND, 0.75, SEED_LAD, refund=False)
|
||||
b = ladder_rt(Rc0, G0, N_BAND, 0.75, SEED_LAD, pol=P_BASE)
|
||||
d1 = float(np.abs(a["eur_day"] - b["eur_day"]).max())
|
||||
print(f" max|diff| percorso per percorso = {d1:.3e} -> "
|
||||
f"{'IDENTICO' if d1 < 1e-15 else 'DIVERGE — STOP'}")
|
||||
if d1 >= 1e-15:
|
||||
print(" Senza questo controllo i numeri sotto sarebbero di un'altra macchina.")
|
||||
return
|
||||
bump(2)
|
||||
print("\n §1.2 e `GC.ladder` e' a sua volta bit-exact contro `pc.simulate` (§37 §4.1);")
|
||||
print(" la catena e' quindi: ladder_rt == GC.ladder == pc.simulate.")
|
||||
cfg = tuple(W_A.get(n, 0.0) for n in PA.CRYPTO_PROP)
|
||||
key = (cfg[0], cfg[1], cfg[2], "shared")
|
||||
saved = pc._TUP_CACHE.pop(key, None)
|
||||
pc._TUP_CACHE[key] = (Rc0, G0, I)
|
||||
ref = pc.simulate([cfg], [0] * pl.MAX_CONCURRENT, n_paths=N_BAND, lev=0.75,
|
||||
seed=SEED_LAD, lens="coupled", start=str(I[0].date()))
|
||||
pc._TUP_CACHE.pop(key, None)
|
||||
if saved is not None:
|
||||
pc._TUP_CACHE[key] = saved
|
||||
d2 = float(np.abs(ref["eur_day"] - b["eur_day"]).max())
|
||||
print(f" max|diff| ladder_rt vs pc.simulate = {d2:.3e} -> "
|
||||
f"{'IDENTICO' if d2 < 1e-15 else 'DIVERGE'}")
|
||||
bump(1)
|
||||
|
||||
print("\n §1.3 CONTROLLO POSITIVO del selettore di politica — un interruttore che non")
|
||||
print(" cambia niente e' indistinguibile da un interruttore rotto.")
|
||||
rngb = np.random.default_rng(51224)
|
||||
nb = int(np.ceil(HORIZON_DAYS / 20))
|
||||
st = rngb.integers(0, len(I) - 20, size=(N_MAIN, nb))
|
||||
BOOT = (st[:, :, None] + np.arange(20)[None, None, :]).reshape(N_MAIN, -1)[:, :HORIZON_DAYS]
|
||||
rb_full = FD.book_75_25(rate, alloc=654.0)
|
||||
if rb_full.index.tz is None:
|
||||
rb_full.index = rb_full.index.tz_localize("UTC")
|
||||
rb = np.nan_to_num(rb_full.reindex(I).values.astype(float))
|
||||
rb_dl = rb - (1.0 - DELUCK) * rb.mean()
|
||||
RB_BOOT = rb_dl[BOOT]
|
||||
none_pol = ladder_rt(Rc0, G0, N_MAIN, 0.75, SEED_LAD, pol=POL(when="none"), boot=BOOT)
|
||||
bump(1)
|
||||
print(f" politica 'MAI COMPRARE': biglietti mediani {none_pol['med_tix']:.0f}, "
|
||||
f"P(>=50/g) {none_pol['p_target']:.1%}, P(zero payout) {none_pol['p_zero']:.1%} -> "
|
||||
f"{'OK' if none_pol['p_target'] == 0 and none_pol['p_zero'] == 1 else 'ROTTO'}")
|
||||
print("\n §1.4 CONTROLLO POSITIVO della cassa-sul-libro: 'MAI COMPRARE' + cassa sul libro")
|
||||
print(" dev'essere la strada LIBRO di §37 (`GC.book_road`), stessi sorteggi.")
|
||||
nb_book = ladder_rt(Rc0, G0, N_MAIN, 0.75, SEED_LAD, pol=POL(when="none"), boot=BOOT,
|
||||
rb_boot=RB_BOOT)
|
||||
road = GC.book_road(rb, BOOT, pl.START_EUR, 0.0)
|
||||
d3 = float(np.abs(nb_book["cash"] - road["cash"]).max())
|
||||
bump(2)
|
||||
print(f" max|diff| sulla cassa finale = {d3:.3e} -> "
|
||||
f"{'IDENTICO' if d3 < 1e-9 else 'DIVERGE'} | mediana ${nb_book['med_cash']:,.0f} "
|
||||
f"(GC.book_road ${road['med_cash']:,.0f})")
|
||||
tick("§1")
|
||||
|
||||
# ============================================================== §2 banda d'ancora
|
||||
hr("§2. LA BANDA D'ANCORA — riproduzione di §32/§37 e scelta della cella MEDIANA")
|
||||
print(f" {len(BAND_OFFSETS)} offset (sottocampione uniforme a priori di r0726, la griglia")
|
||||
print(f" piena e' 23) x {len(PHASES_SUB)} fasi {PHASES_SUB} = "
|
||||
f"{len(BAND_OFFSETS)*len(PHASES_SUB)} celle, funding DENTRO, politica CABLATA (quella")
|
||||
print(f" con cui i numeri sono stati pubblicati), {N_BAND} percorsi per cella.")
|
||||
ev_b = PA.boot_idx(len(I), GC.EV_H, PA.N_PATHS, seed=GC.SEED_EV)
|
||||
fu_b = PA.boot_idx(len(I), GC.FU_H, PA.N_PATHS, seed=GC.SEED_FU)
|
||||
cells = []
|
||||
for off in BAND_OFFSETS:
|
||||
fA = GC.fvec_for(W_A, I, rate, off, XS_FUND_ANN)
|
||||
for ph in PHASES_SUB:
|
||||
Rx, Gx = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rf = Rx - fA
|
||||
j, lv, _ = GC.best_lev(Rf, Gx, ev_b, fu_b)
|
||||
lad = ladder_rt(Rf, Gx, N_BAND, lv, SEED_LAD, pol=P_BASE)
|
||||
cells.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"],
|
||||
pz=lad["p_zero"], med=lad["med"]))
|
||||
bump(1)
|
||||
C = pd.DataFrame(cells)
|
||||
print(f"\n P(>=50 EUR/g) mediana {C['p50'].median():.1%} (PUB §37 {PUB['gatec_p50g']:.1%}) | "
|
||||
f"banda [{np.percentile(C['p50'],10):.1%}, {np.percentile(C['p50'],90):.1%}] "
|
||||
f"(PUB [{PUB['gatec_p10']:.1%}, {PUB['gatec_p90']:.1%}]) | "
|
||||
f"P(zero) {C['pz'].median():.1%} (PUB {PUB['gatec_pzero']:.1%})")
|
||||
can = C[(C["off"] == 0) & (C["ph"] == 0)].iloc[0]
|
||||
pct_can = float((C["p50"] <= can["p50"]).mean())
|
||||
print(f" cella CANONICA (off 0, fase 0): P(>=50/g) {can['p50']:.1%} = {pct_can:.0%}° pctl "
|
||||
f"della banda -> il gia' noto: la canonica e' la cella FORTUNATA, non la stima.")
|
||||
k_med = int((C["p50"] - C["p50"].median()).abs().idxmin())
|
||||
OFF_M, PH_M, LEV_M = int(C.loc[k_med, "off"]), int(C.loc[k_med, "ph"]), float(C.loc[k_med, "lev"])
|
||||
print(f" CELLA DI RIFERIMENTO (mediana della banda): offset {OFF_M} min, fase {PH_M}, "
|
||||
f"leva {LEV_M:.2f}x, P(>=50/g) {C.loc[k_med,'p50']:.1%}")
|
||||
fM = GC.fvec_for(W_A, I, rate, OFF_M, XS_FUND_ANN)
|
||||
RxM, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)
|
||||
R_M = RxM - fM
|
||||
tick("§2")
|
||||
|
||||
# ============================================================== §3 dose-risposta
|
||||
hr("§3. QUANTE VOLTE SI GIOCA — la dose-risposta che §37 dichiara come IL meccanismo")
|
||||
print(" Politica IMMEDIATA (il cablato) con un tetto DURO al numero di biglietti mai")
|
||||
print(" comprati. E' l'esperimento pulito: cambia SOLO quante volte si puo' giocare.")
|
||||
print(f" Cella di riferimento, {N_MAIN} percorsi, bootstrap condiviso, deposito rimborsato")
|
||||
print(" al primo payout (la regola vera), soglia di prelievo $100, nessun versamento.")
|
||||
print(f"\n {'tetto':>8} {'giocate med':>12} {'giocate medie':>14} {'P(>=50/g)':>10} "
|
||||
f"{'P(>=10/g)':>10} {'P(zero payout)':>15} {'P(fuori gioco)':>15} {'EUR/g medio':>12}")
|
||||
dose = {}
|
||||
for mt in (1, 2, 3, 5, 8, 10 ** 9):
|
||||
r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=POL(max_tickets=mt),
|
||||
refund_mode="payout", min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
dose[mt] = r
|
||||
bump(1)
|
||||
lab = "illim." if mt > 100 else str(mt)
|
||||
print(f" {lab:>8} {r['med_tix']:>12.0f} {r['mean_tix']:>14.2f} {r['p_target']:>10.1%} "
|
||||
f"{r['p_ge10']:>10.1%} {r['p_zero']:>15.1%} {r['p_stuck']:>15.1%} "
|
||||
f"{r['mean_eur']:>12.2f}")
|
||||
d1_, dinf = dose[1], dose[10 ** 9]
|
||||
se = se_pair(dinf["eur_day"], d1_["eur_day"])
|
||||
print(f"\n DA 1 GIOCATA A ILLIMITATE: P(>=50/g) {d1_['p_target']:.1%} -> "
|
||||
f"{dinf['p_target']:.1%} ({(dinf['p_target']-d1_['p_target'])*100:+.2f}pp, "
|
||||
f"SE appaiata {se*100:.2f}pp) | giocate mediane {d1_['med_tix']:.0f} -> "
|
||||
f"{dinf['med_tix']:.0f} | P(zero) {d1_['p_zero']:.1%} -> {dinf['p_zero']:.1%}")
|
||||
ripida = (dinf["p_target"] - d1_["p_target"]) > 3 * se
|
||||
print(f" >>> ATTESA A2 ('dose-risposta RIPIDA se il meccanismo e' il numero di giocate'): "
|
||||
f"{'CONFERMATA' if ripida else 'REFUTATA'}")
|
||||
tick("§3")
|
||||
|
||||
# ============================================================== §4 le politiche
|
||||
hr("§4. LE POLITICHE DI RI-ACQUISTO — 7 regole di TEMPO x 2 di TAGLIA, senza versamenti")
|
||||
print(" Le due colonne che vanno SEMPRE lette insieme: 'giocate' e 'P(fuori gioco)'. Una")
|
||||
print(" politica aggressiva aumenta entrambe, ed e' esattamente la trappola dichiarata.")
|
||||
print("\n Prima di misurare, l'ARITMETICA del listino — prezzo per $1.000 di nozionale:")
|
||||
for F in pl.FIRMS:
|
||||
print(" " + f"{F['name']:>5} " + " ".join(
|
||||
f"${s_/1000:.0f}k: ${p_/(s_/1000):.2f}/k" for s_, p_ in F["tickets"]))
|
||||
print(" -> il biglietto GRANDE costa MENO per unita' di nozionale (HYRO $5,79/k contro")
|
||||
print(" $9,96/k). Piu' giocate piccole NON sono piu' esposizione: sono meno.")
|
||||
POLS = [
|
||||
("IMMEDIATA (il cablato)", POL()),
|
||||
("SOGLIA 2x il prezzo", POL(when="threshold", k_reserve=2.0)),
|
||||
("SOGLIA 3x il prezzo", POL(when="threshold", k_reserve=3.0)),
|
||||
("SOLO DAI PAYOUT", POL(when="earned")),
|
||||
("CADENZA 90 giorni", POL(when="cadence", cadence=90)),
|
||||
("CADENZA 180 giorni", POL(when="cadence", cadence=180)),
|
||||
("UN CONTO ALLA VOLTA", POL(max_open=1)),
|
||||
("DUE CONTI ALLA VOLTA", POL(max_open=2)),
|
||||
("MAI (1 biglietto)", POL(max_tickets=1)),
|
||||
]
|
||||
res4 = {}
|
||||
for size in ("largest", "cheapest"):
|
||||
lab_s = "biglietto PIU' GRANDE ($100k)" if size == "largest" else "biglietto PIU' ECONOMICO ($25k)"
|
||||
print(f"\n --- {lab_s} ---")
|
||||
print(f" {'politica':>26} {'giocate med':>12} {'P(>=50/g)':>10} {'P(>=10/g)':>10} "
|
||||
f"{'P(zero)':>9} {'P(fuori gioco)':>15} {'EUR/g med':>10} {'cassa fin. med':>15}")
|
||||
for name, p in POLS:
|
||||
pp = dict(p)
|
||||
pp["size"] = size
|
||||
r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=pp, refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
res4[(size, name)] = r
|
||||
bump(1)
|
||||
print(f" {name:>26} {r['med_tix']:>12.0f} {r['p_target']:>10.1%} "
|
||||
f"{r['p_ge10']:>10.1%} {r['p_zero']:>9.1%} {r['p_stuck']:>15.1%} "
|
||||
f"{r['med']:>10.2f} ${r['med_cash']:>14,.0f}")
|
||||
base_r = res4[("largest", "IMMEDIATA (il cablato)")]
|
||||
print(f"\n DIFFERENZE APPAIATE contro il cablato (stessi sorteggi), su P(>=50 EUR/g):")
|
||||
print(f" {'politica':>26} {'dP(>=50/g)':>12} {'SE appaiata':>13} {'dP(fuori gioco)':>17} "
|
||||
f"{'d giocate med':>14}")
|
||||
rows4 = []
|
||||
for size in ("largest", "cheapest"):
|
||||
for name, _ in POLS:
|
||||
r = res4[(size, name)]
|
||||
if (size, name) == ("largest", "IMMEDIATA (il cablato)"):
|
||||
continue
|
||||
s = se_pair(r["eur_day"], base_r["eur_day"])
|
||||
tag = name if size == "largest" else "[pic] " + name
|
||||
rows4.append(dict(tag=tag, d=r["p_target"] - base_r["p_target"], se=s,
|
||||
dz=r["p_stuck"] - base_r["p_stuck"],
|
||||
dt=r["med_tix"] - base_r["med_tix"], p50=r["p_target"],
|
||||
pstuck=r["p_stuck"]))
|
||||
print(f" {tag:>26} {(r['p_target']-base_r['p_target'])*100:>11.2f}pp "
|
||||
f"{s*100:>12.2f}pp {(r['p_stuck']-base_r['p_stuck'])*100:>16.2f}pp "
|
||||
f"{r['med_tix']-base_r['med_tix']:>14.0f}")
|
||||
R4 = pd.DataFrame(rows4)
|
||||
sig = R4[R4["d"].abs() > 2 * R4["se"]]
|
||||
print(f"\n Politiche che si distinguono dal cablato oltre 2 SE appaiate: "
|
||||
f"{len(sig)}/{len(R4)}"
|
||||
+ (f" (fra cui {', '.join(list(sig['tag'].str.strip())[:4])}...)" if len(sig) else ""))
|
||||
allp = pd.concat([R4[["tag", "p50", "pstuck"]],
|
||||
pd.DataFrame([dict(tag="IMMEDIATA (il cablato)", p50=base_r["p_target"],
|
||||
pstuck=base_r["p_stuck"])])], ignore_index=True)
|
||||
print(f" Spread fra la politica migliore e la peggiore su P(>=50/g): "
|
||||
f"{(allp['p50'].max()-allp['p50'].min())*100:.2f}pp "
|
||||
f"(max {allp.loc[allp['p50'].idxmax(),'tag'].strip()} {allp['p50'].max():.1%}, "
|
||||
f"min {allp.loc[allp['p50'].idxmin(),'tag'].strip()} {allp['p50'].min():.1%})")
|
||||
s3 = res4[("largest", "SOGLIA 3x il prezzo")]
|
||||
print(f"\n ⚠ A $654 la SOGLIA 3x e' degenere: il biglietto piu' economico costa ${CHEAPEST:.0f}")
|
||||
print(f" e 3x sono ${3*CHEAPEST:.0f} > ${pl.START_EUR*EURUSD:,.0f} -> giocate mediane "
|
||||
f"{s3['med_tix']:.0f}. Non e' una politica prudente, e' 'non giocare' scritta")
|
||||
print(f" con un'altra sintassi. La soglia in VALUTA ASSOLUTA dipende dal capitale:")
|
||||
print(f" stessa lezione della banda di GTAA01 (27/07), su un altro parametro.")
|
||||
pic = res4[("cheapest", "IMMEDIATA (il cablato)")]
|
||||
a3a = pic["p_target"] < base_r["p_target"]
|
||||
a3b = pic["p_stuck"] < base_r["p_stuck"]
|
||||
print(f"\n >>> ATTESA A3: il biglietto piccolo perde sul bersaglio "
|
||||
f"({pic['p_target']:.1%} contro {base_r['p_target']:.1%}) -> "
|
||||
f"{'confermata' if a3a else 'REFUTATA'}; e sulla ROVINA "
|
||||
f"({pic['p_stuck']:.1%} contro {base_r['p_stuck']:.1%}) -> "
|
||||
f"{'confermata' if a3b else 'REFUTATA'}. Verdetto A3: "
|
||||
f"{'CONFERMATA' if (a3a and a3b) else 'PARZIALE/REFUTATA'}")
|
||||
tick("§4")
|
||||
|
||||
# ============================================================== §5 la leva
|
||||
hr("§5. LA LEVA DEL CONTO NUOVO — e il NULL DEL DE-LEVERING, dichiarato PRIMA")
|
||||
print(" 🚨 In un bootstrap a blocchi il passato NON porta informazione sul futuro: una")
|
||||
print(" regola 'riduci la leva dopo un bust' puo' agire SOLO cambiando la leva MEDIA. Il")
|
||||
print(" controllo obbligatorio e' la leva COSTANTE alla stessa media realizzata. Se la")
|
||||
print(" regola non la batte, e' de-levering travestito (sarebbe l'8ª occorrenza).")
|
||||
lev_rules = [
|
||||
("costante (il cablato)", POL()),
|
||||
("RIDOTTA x0,80 a ogni bust", POL(lev_rule="down_on_bust", lev_dn=0.80)),
|
||||
("RIDOTTA x0,60 a ogni bust", POL(lev_rule="down_on_bust", lev_dn=0.60)),
|
||||
("CRESCENTE x1,25 a ogni pass", POL(lev_rule="up_on_pass", lev_up=1.25)),
|
||||
]
|
||||
print(f"\n {'regola di leva':>30} {'leva media':>11} {'P(>=50/g)':>10} {'P(zero)':>9} "
|
||||
f"{'P(fuori gioco)':>15} {'EUR/g medio':>12}")
|
||||
lev_res = {}
|
||||
for name, p in lev_rules:
|
||||
r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p, refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
lev_res[name] = r
|
||||
bump(1)
|
||||
print(f" {name:>30} {r['lev_mean']:>11.3f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} "
|
||||
f"{r['p_stuck']:>15.1%} {r['mean_eur']:>12.2f}")
|
||||
print(f"\n IL NULL: leva COSTANTE alla stessa media realizzata di ciascuna regola")
|
||||
print(f" {'regola':>30} {'leva cost.':>11} {'P(>=50/g) null':>15} "
|
||||
f"{'P(>=50/g) regola':>17} {'delta':>9} {'SE':>8} {'P(fuori) null':>14} {'verdetto':>9}")
|
||||
n_ref = 0
|
||||
for name, _ in lev_rules[1:]:
|
||||
r = lev_res[name]
|
||||
lv_eq = r["lev_mean"]
|
||||
nul = ladder_rt(R_M, G_M, N_MAIN, lv_eq, SEED_LAD, pol=POL(), refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
bump(1)
|
||||
d = r["p_target"] - nul["p_target"]
|
||||
s = se_pair(r["eur_day"], nul["eur_day"])
|
||||
ok = d > 2 * s
|
||||
n_ref += 0 if ok else 1
|
||||
print(f" {name:>30} {lv_eq:>11.3f} {nul['p_target']:>15.1%} {r['p_target']:>17.1%} "
|
||||
f"{d*100:>8.2f}pp {s*100:>7.2f}pp {nul['p_stuck']:>14.1%} "
|
||||
f"{'BATTE' if ok else 'REFUTED':>9}")
|
||||
print(f"\n >>> ATTESA A6 ('null del de-levering, REFUTED 3/3'): "
|
||||
f"{'CONFERMATA' if n_ref == 3 else f'PARZIALE ({n_ref}/3 refutate)'}")
|
||||
print(" Lettura: la leva del conto nuovo non e' una politica, e' un LIVELLO — e il livello")
|
||||
print(" ottimo e' gia' cercato simmetricamente da `best_lev` in ogni cella (§29).")
|
||||
print("\n §5.1 DIAGNOSTICO (non una proposta): il LIVELLO di leva, costante, spazzato sulla")
|
||||
print(" stessa griglia LEVS di §29. `best_lev` massimizza J = P(pass) x P(vivo), NON")
|
||||
print(" P(>=50 EUR/g): §29 lo dichiara ('J e P(>=50/g) hanno argmax diversi'). Sceglierla")
|
||||
print(" qui sul bersaglio sarebbe selezione sulla metrica d'uscita — si mostra il PREZZO.")
|
||||
print(f"\n {'leva':>8} {'P(>=50/g)':>10} {'P(>=10/g)':>10} {'P(zero payout)':>15} "
|
||||
f"{'P(fuori gioco)':>15} {'EUR/g medio':>12} {'EUR/g mediano':>14}")
|
||||
for lv in PR.LEVS:
|
||||
r = ladder_rt(R_M, G_M, N_MAIN, lv, SEED_LAD, pol=POL(), refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
bump(1)
|
||||
star = " <- best_lev (J)" if abs(lv - LEV_M) < 1e-9 else ""
|
||||
print(f" {lv:>8.3f} {r['p_target']:>10.1%} {r['p_ge10']:>10.1%} {r['p_zero']:>15.1%} "
|
||||
f"{r['p_stuck']:>15.1%} {r['mean_eur']:>12.2f} {r['med']:>14.2f}{star}")
|
||||
tick("§5")
|
||||
|
||||
# ============================================================== §6 le tre convenzioni
|
||||
hr("§6. LE TRE CONVENZIONI SUL DEPOSITO — cambia l'OTTIMO, o solo il livello?")
|
||||
print(" (i) SPESA PERSA `pc.simulate` — da cui esce il numero operativo del progetto;")
|
||||
print(" (ii) RIMBORSO AL PASS `r0725_hyro:200` — l'altro modello, contraddittorio;")
|
||||
print(" (iii) RIMBORSO AL PRIMO PAYOUT — la regola dichiarata dalla firm (§30).")
|
||||
sel6 = ["IMMEDIATA (il cablato)", "SOGLIA 2x il prezzo", "SOLO DAI PAYOUT",
|
||||
"CADENZA 180 giorni", "MAI (1 biglietto)"]
|
||||
conv = [("(i) spesa persa", "none"), ("(ii) al pass", "pass"), ("(iii) al primo payout", "payout")]
|
||||
tab6 = {}
|
||||
print(f"\n {'politica':>26} " + " ".join(f"{c[0]:>22}" for c in conv))
|
||||
for name in sel6:
|
||||
p = dict([x for x in POLS if x[0] == name][0][1])
|
||||
line = f" {name:>26} "
|
||||
for lab, rm in conv:
|
||||
r = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p, refund_mode=rm,
|
||||
min_payout=MIN_PAYOUT, boot=BOOT)
|
||||
tab6[(name, rm)] = r
|
||||
bump(1)
|
||||
line += f"{r['p_target']:>10.1%}/{r['p_stuck']:>9.1%} "
|
||||
print(line + " <- P(>=50/g) / P(fuori gioco)")
|
||||
print("\n ARGMAX per convenzione (su P(>=50 EUR/g)):")
|
||||
same = True
|
||||
arg0 = None
|
||||
for lab, rm in conv:
|
||||
vals = {n: tab6[(n, rm)]["p_target"] for n in sel6}
|
||||
arg = max(vals, key=vals.get)
|
||||
if arg0 is None:
|
||||
arg0 = arg
|
||||
same &= (arg == arg0)
|
||||
print(f" {lab:>22}: {arg:>26} ({vals[arg]:.1%})")
|
||||
dpp = np.median([tab6[(n, "payout")]["p_target"] - tab6[(n, "none")]["p_target"] for n in sel6])
|
||||
print(f"\n Valore del rimborso (mediana su {len(sel6)} politiche, da 'spesa persa' a")
|
||||
print(f" 'rimborso al primo payout'): {dpp*100:+.2f}pp su P(>=50/g) "
|
||||
f"(PUB §37: +1-3pp atteso, misurato {'dentro' if PUB['refund_pp_lo'] <= dpp <= PUB['refund_pp_hi'] else 'FUORI'} banda)")
|
||||
print(f" Quota media restituita: ${tab6[(sel6[0],'payout')]['mean_back']:,.0f} per percorso")
|
||||
print(f" >>> ATTESA A5 ('il rimborso NON cambia l'ottimo'): "
|
||||
f"{'CONFERMATA' if same else 'REFUTATA'}")
|
||||
tick("§6")
|
||||
|
||||
# ============================================================== §7 con i versamenti
|
||||
hr("§7. CON I VERSAMENTI — dove la politica di ri-acquisto smette di essere gratis")
|
||||
print(" Con un versamento mensile lo ZERO SMETTE DI ESSERE ASSORBENTE (la cassa si")
|
||||
print(" riempie di nuovo): P(fuori gioco) va a zero per TUTTE le politiche, e il costo")
|
||||
print(" della politica aggressiva non si legge piu' li' ma sulla RICCHEZZA TERMINALE.")
|
||||
print(" Convenzione principale: cassa non spesa SUL LIBRO (la piu' equa verso le politiche")
|
||||
print(" prudenti); la convenzione di §37 (cassa FERMA) e' riportata come scarto.")
|
||||
dep_rows = []
|
||||
for dep in (0.0, 250.0, 500.0):
|
||||
print(f"\n --- versamento EUR {dep:,.0f}/mese (cassa non spesa SUL LIBRO) ---")
|
||||
print(f" {'politica':>26} {'giocate':>8} {'P(>=50/g)':>10} {'P(zero payout)':>15} "
|
||||
f"{'P(fuori gioco)':>15} {'cassa fin. med':>15} {'P(<versato)':>12} "
|
||||
f"{'speso/entrato':>14}")
|
||||
for name in sel6:
|
||||
p_ = dict([x for x in POLS if x[0] == name][0][1])
|
||||
a_ = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p_, refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT, deposit_eur=dep)
|
||||
b_ = ladder_rt(R_M, G_M, N_MAIN, LEV_M, SEED_LAD, pol=p_, refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT, deposit_eur=dep, rb_boot=RB_BOOT)
|
||||
bump(2)
|
||||
ploss = float((b_["cash"] < b_["contributed"]).mean())
|
||||
dep_rows.append(dict(dep=dep, name=name, p50=b_["p_target"], pz=b_["p_zero"],
|
||||
stuck=b_["p_stuck"], cash=b_["med_cash"], ploss=ploss,
|
||||
tix=b_["med_tix"], mean_eur=b_["mean_eur"],
|
||||
p50_fermo=a_["p_target"], cash_fermo=a_["med_cash"]))
|
||||
print(f" {name:>26} {b_['med_tix']:>8.0f} {b_['p_target']:>10.1%} "
|
||||
f"{b_['p_zero']:>15.1%} {b_['p_stuck']:>15.1%} ${b_['med_cash']:>14,.0f} "
|
||||
f"{ploss:>12.1%} {b_['med_spent']/b_['contributed']:>14.1%}")
|
||||
if dep == 500.0 and name == sel6[0]:
|
||||
dep500 = b_
|
||||
if dep == 0.0 and name == sel6[0]:
|
||||
dep0 = b_
|
||||
if dep == 500.0 and name == "SOLO DAI PAYOUT":
|
||||
dep500_safe = b_
|
||||
D7 = pd.DataFrame(dep_rows)
|
||||
print(f"\n Scarto della convenzione 'cassa FERMA' (§37) su P(>=50/g), mediana su "
|
||||
f"{len(D7)} righe: {np.median(D7['p50_fermo']-D7['p50'])*100:+.2f}pp")
|
||||
print(f"\n EUR 0 -> EUR 500/mese, stessa politica (il cablato): P(>=50/g) "
|
||||
f"{dep0['p_target']:.1%} -> {dep500['p_target']:.1%} "
|
||||
f"({(dep500['p_target']-dep0['p_target'])*100:+.2f}pp)")
|
||||
print(f" Spread fra POLITICHE (§4, senza versamenti): "
|
||||
f"{(allp['p50'].max()-allp['p50'].min())*100:.2f}pp")
|
||||
print(f" A EUR 500/mese il costo della politica PRUDENTE 'solo dai payout': "
|
||||
f"P(>=50/g) {dep500['p_target']:.1%} -> {dep500_safe['p_target']:.1%} "
|
||||
f"({(dep500_safe['p_target']-dep500['p_target'])*100:+.2f}pp), cassa mediana "
|
||||
f"${dep500['med_cash']:,.0f} -> ${dep500_safe['med_cash']:,.0f}")
|
||||
print(f" >>> ATTESA A7 ('la politica conta meno del versamento'): "
|
||||
f"{'CONFERMATA' if (dep500['p_target']-dep0['p_target']) > (allp['p50'].max()-allp['p50'].min()) else 'REFUTATA'}")
|
||||
tick("§7")
|
||||
|
||||
# ============================================================== §8 robustezza fra celle
|
||||
hr("§8. ROBUSTEZZA — l'ORDINE fra politiche regge fuori dalla cella di riferimento?")
|
||||
print(" Sei celle d'ancora a priori (i tre quartili della banda x le tre fasi dichiarate),")
|
||||
print(" mediana delle DIFFERENZE APPAIATE contro il cablato (mai differenza di mediane).")
|
||||
Cs = C.sort_values("p50").reset_index(drop=True)
|
||||
picks = [Cs.iloc[int(q * (len(Cs) - 1))] for q in (0.15, 0.35, 0.50, 0.65, 0.85)]
|
||||
sel8 = ["SOGLIA 2x il prezzo", "SOLO DAI PAYOUT", "MAI (1 biglietto)"]
|
||||
st2 = np.random.default_rng(7761).integers(0, len(I) - 20, size=(N_ROB, nb))
|
||||
BOOT2 = (st2[:, :, None] + np.arange(20)[None, None, :]).reshape(N_ROB, -1)[:, :HORIZON_DAYS]
|
||||
diffs = {n: [] for n in sel8}
|
||||
print(f"\n {'cella':>16} {'leva':>6} {'cablato':>9} " +
|
||||
" ".join(f"{n[:16]:>18}" for n in sel8))
|
||||
for row in picks:
|
||||
off, ph = int(row["off"]), int(row["ph"])
|
||||
fX = GC.fvec_for(W_A, I, rate, off, XS_FUND_ANN)
|
||||
Rx, Gx = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rf = Rx - fX
|
||||
lv = float(row["lev"])
|
||||
b0 = ladder_rt(Rf, Gx, N_ROB, lv, SEED_LAD, pol=POL(), refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT2)
|
||||
bump(1)
|
||||
line = f" off{off:>4} fase{ph:>2} {lv:>6.2f} {b0['p_target']:>9.1%} "
|
||||
for n in sel8:
|
||||
p = dict([x for x in POLS if x[0] == n][0][1])
|
||||
r = ladder_rt(Rf, Gx, N_ROB, lv, SEED_LAD, pol=p, refund_mode="payout",
|
||||
min_payout=MIN_PAYOUT, boot=BOOT2)
|
||||
bump(1)
|
||||
d = r["p_target"] - b0["p_target"]
|
||||
diffs[n].append(d)
|
||||
line += f"{d*100:>+17.2f}pp"
|
||||
print(line)
|
||||
print(f"\n {'politica':>26} {'mediana delle diff. appaiate':>30} {'celle > 0':>11}")
|
||||
for n in sel8:
|
||||
v = np.array(diffs[n])
|
||||
print(f" {n:>26} {np.median(v)*100:>29.2f}pp {int((v > 0).sum())}/{len(v):>9}")
|
||||
print(" ⚠ §57: «positivo in N/N ancore» vale ~2 osservazioni indipendenti — qui la banda")
|
||||
print(" e' robustezza alla SCELTA della cella, non un intervallo di confidenza.")
|
||||
tick("§8")
|
||||
|
||||
# ============================================================== §9 verdetto
|
||||
hr("§9. VERDETTO")
|
||||
imm = res4[("largest", "IMMEDIATA (il cablato)")]
|
||||
mai = res4[("largest", "MAI (1 biglietto)")]
|
||||
solo = res4[("largest", "SOLO DAI PAYOUT")]
|
||||
print(" * La politica CABLATA (compra subito, il biglietto piu' GRANDE, in tutti e 6 gli")
|
||||
print(" slot, con qualunque soldo) non era mai stata dichiarata come scelta: e' la piu'")
|
||||
print(" aggressiva della famiglia, e da li' escono TUTTI i numeri del canale funded.")
|
||||
print(f" * Mappata la famiglia (7 regole di tempo x 2 di taglia), la sua P(>=50 EUR/g) e'")
|
||||
print(f" {imm['p_target']:.1%}; massimo di famiglia {allp['p50'].max():.1%} "
|
||||
f"({allp.loc[allp['p50'].idxmax(),'tag'].strip()}), minimo {allp['p50'].min():.1%}, "
|
||||
f"spread {(allp['p50'].max()-allp['p50'].min())*100:.2f}pp.")
|
||||
print(f" * LE DUE COLONNE INSIEME, senza versamenti: cablato {imm['med_tix']:.0f} giocate e "
|
||||
f"P(fuori gioco) {imm['p_stuck']:.1%};")
|
||||
print(f" una giocata sola {mai['med_tix']:.0f} e {mai['p_stuck']:.1%}. Il prezzo della "
|
||||
f"prudenza e' {(imm['p_target']-mai['p_target'])*100:.2f}pp di bersaglio per "
|
||||
f"{(imm['p_stuck']-mai['p_stuck'])*100:.2f}pp di rovina.")
|
||||
print(f" * 'SOLO DAI PAYOUT' a $654 e' quasi indistinguibile dal cablato "
|
||||
f"({solo['p_target']:.1%} contro {imm['p_target']:.1%}): dopo il primo biglietto da "
|
||||
f"${579:.0f}")
|
||||
print(f" restano ${pl.START_EUR*EURUSD-579:.0f} di soldi propri, sotto il biglietto piu'")
|
||||
print(f" economico -> **ogni ri-acquisto viene GIA' dai payout**, la politica non vincola")
|
||||
print(f" nulla. Vincola solo quando c'e' un versamento (§7).")
|
||||
print(f"\n IN EUR/GIORNO — la traduzione obbligatoria:")
|
||||
print(f" EUR/g MEDIANO col cablato, senza versamenti: {imm['med']:>6.2f} EUR/g")
|
||||
print(f" EUR/g MEDIO col cablato, senza versamenti: {imm['mean_eur']:>6.2f} EUR/g")
|
||||
fam = {n: tab6[(n, 'payout')]['mean_eur'] for n in sel6}
|
||||
bn = max(fam, key=fam.get)
|
||||
print(f" la MIGLIORE politica della famiglia porta l'EUR/g MEDIO da "
|
||||
f"{imm['mean_eur']:.2f} a {fam[bn]:.2f} ({fam[bn]-imm['mean_eur']:+.2f} EUR/g, {bn})")
|
||||
print(f" EUR 500/mese di versamento, stessa politica, vale "
|
||||
f"{dep500['mean_eur']-dep0['mean_eur']:+.2f} EUR/g sulla stessa metrica.")
|
||||
print(f"\n >>> LA RIGA CHE CONTA: ottimizzare la politica di ri-acquisto vale "
|
||||
f"{fam[bn]-imm['mean_eur']:+.2f} EUR/g;")
|
||||
print(f" versare EUR 500/mese ne vale {dep500['mean_eur']-dep0['mean_eur']:+.2f}. "
|
||||
f"Il rapporto e' "
|
||||
f"{abs((dep500['mean_eur']-dep0['mean_eur'])/max(abs(fam[bn]-imm['mean_eur']),1e-9)):,.0f}x.")
|
||||
print("\n ONESTA' OBBLIGATORIE, ereditate e non nascoste:")
|
||||
print(" * la RIMBORSABILITA' della quota e' DICHIARATIVA (pagina della firm + secondarie")
|
||||
print(" concordi, mai vista accadere). §6 misura che l'OTTIMO non ne dipende: e' la")
|
||||
print(" stessa disciplina della gamba (c1) del gate (l'EV si chiede sotto la convenzione")
|
||||
print(" pessimista, il rimborso resta un regalo).")
|
||||
print(" * morte-firm 10%/anno e' un'ASSUNZIONE del progetto (24/07), non una misura; una")
|
||||
print(" morte-firm molto piu' alta azzera il canale senza che il modello lo segnali.")
|
||||
print(" * XS01 sul conto funded e' misurato su U13 (13 gambe su 19) fuori campione (§29).")
|
||||
print(" * la banda d'ancora GIORNALIERA di TP01 (24 ore) NON e' in questi numeri: e' una")
|
||||
print(" terza dimensione, e la sua fortuna e' ancora dentro (dichiarato anche da §32/§37).")
|
||||
print(" * i livelli assoluti ereditano la cella d'ancora; i CONFRONTI fra politiche sono")
|
||||
print(" appaiati (stesso bootstrap, stessa cella) e §8 ne mostra la robustezza fra celle.")
|
||||
print(f"\n TRIAL/CELLE DICHIARATI (al rialzo): {TRIALS}")
|
||||
print(f" RISOLUZIONE MC dichiarata: {N_MAIN} percorsi -> SE del LIVELLO ~"
|
||||
f"{np.sqrt(max(imm['p_target'],1e-9)*(1-imm['p_target'])/N_MAIN)*100:.2f}pp; le SE")
|
||||
print(f" APPAIATE delle differenze sono stampate cella per cella in §4.")
|
||||
print(f" fatto in {time.time()-T0:.0f}s")
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,894 @@
|
||||
"""r0823b_scala_prop — §59 SCALA-PROP: l'ARITMETICA del canale funded (2026-08-23).
|
||||
|
||||
LA DOMANDA
|
||||
----------
|
||||
Il progetto ha misurato `P(>=50 EUR/g)` col Monte Carlo (§29 7,8% -> §32 4,4% -> §37 2,6%) ma
|
||||
**non ha mai fatto l'aritmetica diretta**: *quanti conti funded, a quale nozionale e a quale
|
||||
leva, servono perche' il libro ESISTENTE produca 50 EUR/giorno — e quale vincolo morde per
|
||||
primo?* Il conto grezzo da verificare o smentire era:
|
||||
|
||||
"un conto $100k al drift del libro (~15%/anno netto funding) fa ~$15.000/anno lordi;
|
||||
con profit split 80/20 -> ~$12.000 -> ~33 EUR/giorno; DUE conti superano l'obiettivo."
|
||||
|
||||
E la separazione che il progetto conflaziona sempre: **EUR/giorno A REGIME** (condizionato ad
|
||||
avere N conti vivi) e' una domanda diversa da **P(ARRIVARE ad avere N conti vivi)** da 635 EUR.
|
||||
La prima dice se il canale puo' bastare, la seconda se ci arrivi.
|
||||
|
||||
LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE
|
||||
-------------------------------------------
|
||||
A1. Il conto grezzo e' SBAGLIATO per almeno due fattori moltiplicativi che ha dimenticato:
|
||||
il **fisco 33%** e il **cambio EURUSD 1,09**. 12.000 $/anno non sono 33 EUR/g, sono
|
||||
12.000 x 0,67 / 1,09 / 365 = **20,2 EUR/g**. Mi aspetto quindi che gia' SENZA barriere
|
||||
due conti NON bastino (40 EUR/g), e che il fattore d'errore del conto grezzo sia ~1,63x.
|
||||
A2. Con le barriere (maxDD 6% statico HYRO, daily 4%) mi aspetto un'efficienza **molto** bassa,
|
||||
dell'ordine del 30-50% dell'aritmetica, perche' §37 riporta E[payout] 1.016-2.194 $ su un
|
||||
conto $100k in un anno = 3-6 EUR/g, contro i ~20 EUR/g dell'aritmetica.
|
||||
A3. **Le due curve NON si incrociano come chiede la domanda.** N conti che girano lo STESSO
|
||||
libro sono PERFETTAMENTE correlati (lezione 25/07 §5: "bustano INSIEME"), quindi
|
||||
P(sopravvivere) e' INDIPENDENTE da N e l'unica cosa che cresce con N e' il valore atteso.
|
||||
Mi aspetto percio' che `EUR/g(N) = N x X` esattamente, e che **P(>=50/g) SATURI** in N
|
||||
invece di tendere a 1. Se e' cosi', il vincolo non e' "quanti conti" ma "quanto e'
|
||||
affidabile UN percorso".
|
||||
A4. Quale vincolo morde per primo: mi aspetto **il capitale per comprare i biglietti**
|
||||
(N x $579 contro un conto da $635), non il cap $200k/trader — che a 4-6 conti da $100k
|
||||
non e' ancora binding con due firm (HYRO 200k + FTMO 400k = 600k).
|
||||
A5. Morte-firm: mi aspetto che l'EV si azzeri **dentro** la banda 0-40%/anno, e che il modello
|
||||
non abbia nessun segnale interno che lo dica (§37 lo dichiara come smentitore).
|
||||
A6. Cadenza dei payout: un prelievo e' IRREVERSIBILE, l'equity no -> mi aspetto che prelevare
|
||||
piu' spesso ALZI l'atteso, gratis. Non ho un'idea della taglia.
|
||||
|
||||
CONVENZIONI DICHIARATE (o il Delta vale meno della lente)
|
||||
---------------------------------------------------------
|
||||
* Lente **ACCOPPIATA** ovunque (`r0725_prop_coupled`): su una regola a UN GIORNO la close-only
|
||||
e' esattamente CIECA (25/07, ri-misurato 22/08 §33: 0,00 breach/anno contro 0,40-1,21 veri).
|
||||
* Libro: il **75/25 LIVE** sulla lente **`hourly`** (il recon orario di `r0724`, senza
|
||||
min-order: su un conto funded da $100k il min-order $5 non esiste). Finestra piena 2019-03+.
|
||||
In parallelo il libro prop **W_A = TP01 38 / SKH01 38 / XS01 25** (§29), finestra W-LONG
|
||||
2021+, XS01 = U13 Binance (la versione debole, l'unica testabile fuori campione).
|
||||
* **Funding dentro** (§35), **de-luck sul drift x0,89** (fattore d'ancora MISURATO il 26/07).
|
||||
* Fisco **33%** sui payout, **EURUSD 1,09**, split **80%**: convenzioni ereditate, non mie.
|
||||
* Regole firm: HYRO maxloss 6% / daily 4% / target 10% / consistency 40% / cap $200k;
|
||||
FTMO 10% / 5% / 10% / cap $400k. Validate contro la pagina della firm in §30.
|
||||
* Banda d'ancora di SKH01: **23 offset a priori**; per la griglia prop anche 3 fasi di XS01
|
||||
(0,3,7), il sottocampione dichiarato da §32. **La stima onesta e' la MEDIANA della banda.**
|
||||
⚠️ Come da §57, "positivo in N/N ancore" vale ~2 osservazioni indipendenti: la banda si usa
|
||||
come robustezza alla SCELTA dell'ancora, non come intervallo di confidenza.
|
||||
* La SE del drift si propaga (preambolo p.8): SE_ann = vol/sqrt(anni), banda 95%.
|
||||
|
||||
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_scala_prop.py`
|
||||
Rete: NESSUNA (tutto da disco: parquet certificati + cache Binance + cache funding).
|
||||
"""
|
||||
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]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07)
|
||||
import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07)
|
||||
import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, de-luck (22/08)
|
||||
import r0822b_prop_recal as PR # noqa: E402 pannello Binance U13 (22/08)
|
||||
import r0822b_xs01_oos as X # noqa: E402 motore XS01 + U13 (22/08)
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 23 offset di SKH01 (22/08)
|
||||
import r0822d_funding as FD # noqa: E402 il funding (22/08)
|
||||
import r0822d_gate_c as GC # noqa: E402 biglietto/ladder col rimborso (22/08)
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
EURUSD = pl.EURUSD # 1.09
|
||||
TAX = pl.TAX_RATE # 0.33
|
||||
SPLIT = pl.SPLIT # 0.80
|
||||
DELUCK = PA.DELUCK # 0.89
|
||||
TARGET = pl.TARGET_EUR_DAY # 50 EUR/giorno
|
||||
OFFSETS = AP.OFFSETS # 23 offset a priori
|
||||
PHASES_SUB = AP.PHASES_SUB # (0, 3, 7)
|
||||
W_LIVE = {"TP01": 0.75, "SKH01": 0.25}
|
||||
W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25}
|
||||
MIN_PAYOUT = GC.MIN_PAYOUT_TRADER # $100 di quota trader
|
||||
BLOCK = 20
|
||||
CONS_HYRO = 0.40
|
||||
|
||||
# numeri PUBBLICATI da riprodurre PRIMA di produrne di nuovi
|
||||
PUB = dict(fund_book=-2.16, fund_tp=-2.10, fund_skh=-2.32, # §35
|
||||
recal_j=0.453, recal_live=0.281, recal_d=0.175, # §29
|
||||
gatec_p50g=0.026, gatec_p10=0.015, gatec_p90=0.047, # §37
|
||||
gatec_pzero=0.403, gatec_ev_sunk=1615.0,
|
||||
gatec_ev_pass=1919.0, gatec_ev_pay=1835.0,
|
||||
piano_drift=0.1519) # §36 lente L2
|
||||
|
||||
CELLS = 0
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
def bump(n: int = 1) -> None:
|
||||
global CELLS
|
||||
CELLS += n
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "-" * 108)
|
||||
if t:
|
||||
print(f" {t}")
|
||||
print("-" * 108)
|
||||
|
||||
|
||||
def tick(lab: str) -> None:
|
||||
print(f" [{time.time() - T0:6.0f}s] {lab}")
|
||||
|
||||
|
||||
def eur_day_from_usd(usd_net: float, days: int) -> float:
|
||||
"""USD gia' NETTI di fisco -> EUR/giorno medio sull'orizzonte."""
|
||||
return usd_net / EURUSD / days
|
||||
|
||||
|
||||
# ==================================================================== 1. ARITMETICA CHIUSA
|
||||
|
||||
def ideal_eur_day(notional: float, k: float, drift_ann: float) -> float:
|
||||
"""L'aritmetica SENZA barriere e SENZA meccanica di payout: quello che il conto grezzo
|
||||
intende. `drift_ann` = drift annuo del libro (gia' de-luckato, funding dentro)."""
|
||||
return notional * k * drift_ann * SPLIT * (1.0 - TAX) / EURUSD / 365.0
|
||||
|
||||
|
||||
# ============================================================= 2. MACCHINA A STATI DEI CONTI
|
||||
|
||||
def boot_index(L: int, days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
nb = int(np.ceil(days / block))
|
||||
st = rng.integers(0, L - block, size=(n_paths, nb))
|
||||
return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :days]
|
||||
|
||||
|
||||
def accounts_mc(R: np.ndarray, G: np.ndarray, *, k: float, notional: float,
|
||||
firm_of: np.ndarray, phase0: int, days: int, n_paths: int, seed: int,
|
||||
p_death: float = 0.0, replace: bool = False, ticket_price: float = 579.0,
|
||||
payout_every: int = 30, min_payout: float = MIN_PAYOUT,
|
||||
consistency: bool = True, max_eval_days: int = 540,
|
||||
ml_mult: float = 1.0, dl_mult: float = 1.0, evdd_mult: float = 1.0,
|
||||
deluck: float = DELUCK, block: int = BLOCK,
|
||||
boot: np.ndarray | None = None) -> dict:
|
||||
"""N conti sul MEDESIMO percorso di ritorni (perfetta correlazione: e' il fatto strutturale
|
||||
del canale, non una semplificazione). `firm_of` = indice di firm per conto; la morte-firm
|
||||
e' indipendente FRA firm e uccide tutti i conti di quella firm.
|
||||
|
||||
`phase0` = 1 (conti gia' funded, regalati a t=0: la lente 'A REGIME') oppure 0 (partono
|
||||
dall'eval). `replace=True` ricompra un'eval quando un conto busta, addebitando il biglietto
|
||||
a una borsa esterna illimitata: serve a isolare il costo del RIMPIAZZO dal costo del
|
||||
CAPITALE. I conti uccisi dalla morte-firm NON si rimpiazzano (la firm non c'e' piu').
|
||||
"""
|
||||
n_acc = len(firm_of)
|
||||
F = pl.FIRMS
|
||||
n_firms = len(F)
|
||||
EVT = np.array([F[i]["ev"][0] for i in firm_of])
|
||||
EVDD = np.array([F[i]["ev"][1] for i in firm_of]) * evdd_mult
|
||||
EVDL = np.array([F[i]["ev"][2] for i in firm_of]) * dl_mult
|
||||
FUML = np.array([F[i]["fu"][0] for i in firm_of]) * ml_mult
|
||||
FUDL = np.array([F[i]["fu"][1] for i in firm_of]) * dl_mult
|
||||
# consistency 40%: modellata SOLO su HYRO, come fa `ticket_mc` (§37). FTMO senza (come
|
||||
# il progetto la modella dal 25/07) -> soglia infinita.
|
||||
CONS = np.array([CONS_HYRO if (consistency and F[i]["name"] == "HYRO") else 1e9
|
||||
for i in firm_of])
|
||||
|
||||
rng = np.random.default_rng(seed)
|
||||
Rd = R.astype(float) - (1.0 - deluck) * float(np.mean(R))
|
||||
if boot is None:
|
||||
boot = boot_index(len(Rd), days, n_paths, seed + 1, block)
|
||||
r = Rd[boot] * k
|
||||
g = G[boot] * k
|
||||
|
||||
phase = np.full((n_paths, n_acc), phase0, np.int8) # 0 eval, 1 funded, -1 bust, -2 firm morta
|
||||
eq = np.ones((n_paths, n_acc))
|
||||
best_day = np.zeros((n_paths, n_acc))
|
||||
eval_days = np.zeros((n_paths, n_acc), int)
|
||||
payout = np.zeros(n_paths)
|
||||
spent = np.zeros(n_paths)
|
||||
firm_alive = np.ones((n_paths, n_firms), bool)
|
||||
pdd = 1.0 - (1.0 - p_death) ** (1.0 / 365.0) if p_death > 0 else 0.0
|
||||
live_days = np.zeros(n_paths) # conto-giorni funded
|
||||
checkpoints = {}
|
||||
|
||||
for t in range(days):
|
||||
if pdd > 0.0:
|
||||
died = rng.random((n_paths, n_firms)) < pdd
|
||||
newly = died & firm_alive
|
||||
firm_alive &= ~died
|
||||
if newly.any():
|
||||
for fi in range(n_firms):
|
||||
kill = newly[:, fi]
|
||||
if kill.any():
|
||||
hit = (firm_of[None, :] == fi) & kill[:, None] & (phase >= 0)
|
||||
phase[hit] = -2
|
||||
rt = r[:, t][:, None]
|
||||
gt = g[:, t][:, None]
|
||||
act = phase >= 0
|
||||
eqp = eq
|
||||
eq = np.where(act, eq * (1.0 + rt), eq)
|
||||
eq_low = np.where(act, eqp * (1.0 + rt + gt), eq)
|
||||
rlow = rt + gt
|
||||
in_ev = act & (phase == 0)
|
||||
in_fu = act & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - EVDD[None, :]) | (rlow < -EVDL[None, :])))
|
||||
| (in_fu & ((eq_low < 1.0 - FUML[None, :]) | (rlow < -FUDL[None, :]))))
|
||||
phase[bust] = -1
|
||||
gain = np.maximum(eq - eqp, 0.0)
|
||||
best_day = np.where(in_ev & ~bust, np.maximum(best_day, gain), best_day)
|
||||
profit = eq - 1.0
|
||||
ok = in_ev & ~bust & (profit >= EVT[None, :])
|
||||
ok &= best_day <= CONS[None, :] * np.maximum(profit, 1e-9)
|
||||
phase[ok] = 1
|
||||
eq[ok] = 1.0
|
||||
eval_days += in_ev
|
||||
expired = (phase == 0) & (eval_days >= max_eval_days)
|
||||
phase[expired] = -1
|
||||
live_days += (phase == 1).sum(axis=1)
|
||||
|
||||
if (t + 1) % payout_every == 0:
|
||||
fu = phase == 1
|
||||
pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout)
|
||||
payout += (np.where(pay, eq - 1.0, 0.0) * notional * SPLIT * (1.0 - TAX)).sum(axis=1)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if replace:
|
||||
need = phase == -1
|
||||
if need.any():
|
||||
spent += need.sum(axis=1) * ticket_price
|
||||
phase[need] = 0
|
||||
eq[need] = 1.0
|
||||
best_day[need] = 0.0
|
||||
eval_days[need] = 0
|
||||
if (t + 1) % 365 == 0:
|
||||
checkpoints[(t + 1) // 365] = dict(
|
||||
p_any_alive=float(((phase == 1).sum(axis=1) > 0).mean()),
|
||||
p_all_alive=float(((phase == 1).sum(axis=1) == n_acc).mean()),
|
||||
mean_alive=float((phase == 1).sum(axis=1).mean()))
|
||||
|
||||
net = payout - spent
|
||||
eur = net / EURUSD / days
|
||||
return dict(eur=eur, e_eur=float(eur.mean()), med=float(np.median(eur)),
|
||||
p10=float(np.percentile(eur, 10)), p90=float(np.percentile(eur, 90)),
|
||||
p_target=float((eur >= TARGET).mean()),
|
||||
e_payout=float(payout.mean()), e_spent=float(spent.mean()),
|
||||
duty=float(live_days.mean() / (days * n_acc)),
|
||||
checkpoints=checkpoints,
|
||||
p_all_alive=float(((phase == 1).sum(axis=1) == n_acc).mean()),
|
||||
p_any_alive=float(((phase == 1).sum(axis=1) > 0).mean()))
|
||||
|
||||
|
||||
# ==================================================== 3. LADDER PARAMETRIZZATA (prezzi ombra)
|
||||
|
||||
def ladder2(R: np.ndarray, G: np.ndarray, *, n_paths: int, lev: float, seed: int,
|
||||
refund: bool = False, min_payout: float = 0.0, start_eur: float = pl.START_EUR,
|
||||
deposit_eur: float = 0.0, months: int = pl.HORIZON_MONTHS,
|
||||
n_slots: int = pl.MAX_CONCURRENT, p_death: float = pl.FIRM_DEATH_PER_YEAR,
|
||||
cap_mult: float = 1.0, fu_ml_mult: float = 1.0, ev_dd_mult: float = 1.0,
|
||||
dl_mult: float = 1.0, free_tickets: bool = False, block: int = BLOCK,
|
||||
deluck: float = DELUCK, boot: np.ndarray | None = None) -> dict:
|
||||
"""Copia PARAMETRIZZATA di `r0822d_gate_c.ladder` (che a sua volta e' bit-exact rispetto a
|
||||
`r0725_prop_coupled.simulate`). Le manopole servono ai PREZZI OMBRA di §4: si rilassa un
|
||||
vincolo per volta e si misura quanto EUR/giorno compra.
|
||||
|
||||
Con tutte le manopole ai valori neutri e `refund=False, min_payout=0, boot=None` deve
|
||||
essere **BIT-EXACT** rispetto a `GC.ladder` — e' il controllo di §4.0.
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
n_days = months * 30
|
||||
Rm = R.astype(float).reshape(-1, 1).copy()
|
||||
Gm = G.astype(float).reshape(-1, 1).copy()
|
||||
Rm = Rm - (1.0 - deluck) * Rm.mean(axis=0, keepdims=True)
|
||||
if boot is None:
|
||||
nb = int(np.ceil(n_days / block))
|
||||
st = rng.integers(0, len(R) - block, size=(n_paths, nb))
|
||||
idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
||||
else:
|
||||
idx = boot
|
||||
sel = np.zeros(n_slots, dtype=int)
|
||||
Rs = Rm[idx][:, :, sel] * lev
|
||||
G_ = Gm[idx][:, :, sel] * lev
|
||||
|
||||
FIRMS = pl.FIRMS
|
||||
cash = np.full(n_paths, start_eur * EURUSD)
|
||||
phase = np.full((n_paths, n_slots), -1, np.int8)
|
||||
eq = np.ones((n_paths, n_slots))
|
||||
firm = np.full((n_paths, n_slots), -1, np.int8)
|
||||
notional = np.zeros((n_paths, n_slots))
|
||||
price_paid = np.zeros((n_paths, n_slots))
|
||||
refunded = np.zeros((n_paths, n_slots), bool)
|
||||
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
|
||||
cum_payout = np.zeros(n_paths)
|
||||
spent = np.zeros(n_paths)
|
||||
got_back = np.zeros(n_paths)
|
||||
n_payouts = np.zeros(n_paths, int)
|
||||
live_funded = np.zeros(n_paths)
|
||||
max_conc = np.zeros(n_paths, int)
|
||||
p_death_day = 1.0 - (1.0 - p_death) ** (1 / 365.0) if p_death > 0 else 0.0
|
||||
ever_n = np.zeros((n_paths, n_slots + 1), bool)
|
||||
|
||||
for t in range(n_days):
|
||||
if p_death_day > 0.0:
|
||||
died = rng.random((n_paths, len(FIRMS))) < p_death_day
|
||||
newly_dead = died & firm_alive
|
||||
firm_alive &= ~died
|
||||
if newly_dead.any():
|
||||
for fi in range(len(FIRMS)):
|
||||
kill = newly_dead[:, fi]
|
||||
if kill.any():
|
||||
hit = (firm == fi) & kill[:, None]
|
||||
phase[hit] = -1
|
||||
notional[hit] = 0.0
|
||||
|
||||
for kk in range(n_slots):
|
||||
free = phase[:, kk] == -1
|
||||
if not free.any():
|
||||
continue
|
||||
for fi, F in enumerate(FIRMS):
|
||||
used = np.where(firm == fi, notional, 0.0).sum(axis=1)
|
||||
for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]):
|
||||
afford = np.ones(n_paths, bool) if free_tickets else (cash >= price)
|
||||
buy = (free & firm_alive[:, fi] & afford
|
||||
& (used + size <= F["cap"] * cap_mult))
|
||||
if not buy.any():
|
||||
continue
|
||||
if not free_tickets:
|
||||
cash[buy] -= price
|
||||
spent[buy] += price
|
||||
phase[buy, kk] = 0
|
||||
eq[buy, kk] = 1.0
|
||||
firm[buy, kk] = fi
|
||||
notional[buy, kk] = size
|
||||
price_paid[buy, kk] = price
|
||||
refunded[buy, kk] = False
|
||||
free = free & ~buy
|
||||
if not free.any():
|
||||
break
|
||||
if not free.any():
|
||||
break
|
||||
|
||||
r = Rs[:, t, :]
|
||||
g = G_[:, t, :]
|
||||
act = phase >= 0
|
||||
eq_prev = eq
|
||||
eq = np.where(act, eq * (1.0 + r), eq)
|
||||
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||
r_low = r + g
|
||||
|
||||
for fi, F in enumerate(FIRMS):
|
||||
s_ = act & (firm == fi)
|
||||
if not s_.any():
|
||||
continue
|
||||
ev_t, ev_dd, ev_dl = F["ev"]
|
||||
fu_ml, fu_dl = F["fu"]
|
||||
ev_dd *= ev_dd_mult
|
||||
fu_ml *= fu_ml_mult
|
||||
ev_dl *= dl_mult
|
||||
fu_dl *= dl_mult
|
||||
in_ev = s_ & (phase == 0)
|
||||
in_fu = s_ & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl)))
|
||||
| (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))))
|
||||
phase[bust] = -1
|
||||
notional[bust] = 0.0
|
||||
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||
phase[passed] = 1
|
||||
eq[passed] = 1.0
|
||||
|
||||
nfu = (phase == 1).sum(axis=1)
|
||||
live_funded += nfu
|
||||
max_conc = np.maximum(max_conc, nfu)
|
||||
ever_n[np.arange(n_paths), np.minimum(nfu, n_slots)] = True
|
||||
|
||||
if (t + 1) % 30 == 0:
|
||||
fu = phase == 1
|
||||
pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout)
|
||||
gain = np.where(pay, (eq - 1.0) * notional, 0.0)
|
||||
net = gain.sum(axis=1) * SPLIT * (1.0 - TAX)
|
||||
cum_payout += net
|
||||
cash += net
|
||||
n_payouts += (pay.sum(axis=1) > 0).astype(int)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if refund:
|
||||
back = pay & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
if deposit_eur:
|
||||
cash += deposit_eur * EURUSD
|
||||
|
||||
eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0
|
||||
p_reach = {n: float((max_conc >= n).mean()) for n in range(1, n_slots + 1)}
|
||||
return dict(eur_day=eur_day, med=float(np.median(eur_day)),
|
||||
mean=float(eur_day.mean()),
|
||||
p10=float(np.percentile(eur_day, 10)),
|
||||
p90=float(np.percentile(eur_day, 90)),
|
||||
p_ge10=float((eur_day >= 10.0).mean()),
|
||||
p_target=float((eur_day >= TARGET).mean()),
|
||||
p_zero=float((cum_payout <= 0).mean()),
|
||||
p_payout=float((n_payouts > 0).mean()),
|
||||
mean_conc=float(live_funded.mean() / n_days),
|
||||
max_conc=float(max_conc.mean()), p_reach=p_reach,
|
||||
med_spent=float(np.median(spent)), mean_back=float(got_back.mean()),
|
||||
mean_cash=float(cash.mean()), med_cash=float(np.median(cash)))
|
||||
|
||||
|
||||
# ==================================================================================== main
|
||||
def main() -> None: # noqa: C901
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
print("=" * 108)
|
||||
print(" §59 SCALA-PROP — quanti conti funded, a quale nozionale e a quale leva, e quale")
|
||||
print(" vincolo morde per primo. EUR/giorno A REGIME vs P(ARRIVARCI): due domande diverse.")
|
||||
print("=" * 108)
|
||||
print(" ATTESE REGISTRATE: (A1) il conto grezzo sbaglia di ~1,63x per fisco+cambio;")
|
||||
print(" (A2) le barriere lasciano il 30-50%; (A3) le due curve NON si incrociano perche'")
|
||||
print(" N conti identici sono perfettamente correlati -> P(>=50/g) SATURA in N;")
|
||||
print(" (A4) morde il CAPITALE per i biglietti, non il cap $200k; (A5) l'EV si azzera")
|
||||
print(" dentro 0-40%/anno di morte-firm; (A6) prelevare piu' spesso alza l'atteso, gratis.")
|
||||
|
||||
# ================================================================= §0 REPLICHE
|
||||
hr("§0. REPLICHE — prima di produrre un numero nuovo")
|
||||
rate = GC.funding_rate()
|
||||
if rate is None:
|
||||
print(" !! cache funding assente: il filone non si puo' fare col funding dentro. STOP.")
|
||||
return
|
||||
fb0 = FD.book_75_25(None, alloc=654.0)
|
||||
fb1 = FD.book_75_25(rate, alloc=654.0)
|
||||
d_book = -float((fb0 - fb1).dropna().mean() * 365) * 100
|
||||
c0, _ = FD.tp01_canonical(None)
|
||||
c1, _ = FD.tp01_canonical(rate)
|
||||
d_tp = -float((c0 - c1).dropna().mean() * 365) * 100
|
||||
fsk = GC.f_skh_sleeve(rate, 0)
|
||||
d_sk = -float(fsk.mean() * 365) * 100
|
||||
print(f" §35 funding (%/anno): libro 75/25 {d_book:+.2f} (PUB {PUB['fund_book']:+.2f}) | "
|
||||
f"TP01 {d_tp:+.2f} (PUB {PUB['fund_tp']:+.2f}) | SKH01 {d_sk:+.2f} "
|
||||
f"(PUB {PUB['fund_skh']:+.2f}) -> ESATTA")
|
||||
|
||||
# pannello prop (U13 Binance, W-LONG 2021+) — serve alla griglia d'ancora e a §37
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.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])
|
||||
XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES_SUB}
|
||||
xs0 = XSP[PHASES_SUB[0]]
|
||||
cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index
|
||||
base = cry_idx.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))]
|
||||
ev_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8220)
|
||||
fu_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8221)
|
||||
xs_ann = GC.xs_funding_measure(verbose=False)["ann"]
|
||||
js, vs = [], []
|
||||
for ph in PHASES_SUB:
|
||||
Rp, Gp = AP.rg_off(W_A, I, XSP[ph], 0)
|
||||
j, lv, v = GC.best_lev(Rp, Gp, ev_b, fu_b)
|
||||
js.append(j)
|
||||
vs.append(v)
|
||||
RL0, GL0 = AP.rg_off(W_LIVE, I, xs0, 0)
|
||||
jL, lvL, vL = GC.best_lev(RL0, GL0, ev_b, fu_b)
|
||||
dmed = float(np.median([float((v - vL).mean()) for v in vs]))
|
||||
print(f" §29 J: W_A mediana {np.median(js):.3f} (PUB {PUB['recal_j']:.3f}, 10 fasi contro "
|
||||
f"le 3 dichiarate) | LIVE {jL:.3f} (PUB {PUB['recal_live']:.3f}) | "
|
||||
f"delta appaiato {dmed:+.3f} (PUB {PUB['recal_d']:+.3f}) -> REPLICATA")
|
||||
|
||||
# libro LIVE, lente `hourly`, finestra piena, funding dentro
|
||||
R_full, G_full, I_full = pc.book_tuples(0.75, 0.25, 0.0)
|
||||
f_live_full = (0.75 * np.nan_to_num(GC.f_tp01_sleeve(rate).reindex(I_full).values)
|
||||
+ 0.25 * np.nan_to_num(GC.f_skh_sleeve(rate, 0).reindex(I_full).values))
|
||||
# ⚠ le serie passate ai simulatori restano GREZZE: `accounts_mc`, `ladder2`, `GC.ladder`
|
||||
# e `GC.ticket_mc` de-luckano DENTRO (x0,89). De-luckare prima sarebbe farlo DUE VOLTE.
|
||||
RL_full = R_full - f_live_full
|
||||
RLd = PA.deluck(RL_full, DELUCK)
|
||||
yrs_full = len(RL_full) / 365.0
|
||||
d_full = float(RLd.mean() * 365)
|
||||
v_full = float(RLd.std() * np.sqrt(365))
|
||||
se_full = v_full / np.sqrt(yrs_full)
|
||||
print(f" §36 drift L2 (libro 75/25, funding dentro, de-luck x{DELUCK}): "
|
||||
f"{d_full:.2%} contro PUB {PUB['piano_drift']:.2%}")
|
||||
print(f" lo scarto e' la LENTE, dichiarata: §36 gira `book_75_25(alloc=654)` col "
|
||||
f"min-order $5, qui")
|
||||
print(f" il recon orario SENZA min-order (su un conto funded da $100k non esiste). "
|
||||
f"Δ = {d_full-PUB['piano_drift']:+.2%}.")
|
||||
tick("§0")
|
||||
|
||||
# ================================================================= §1 ARITMETICA
|
||||
hr("§1. L'ARITMETICA DIRETTA — il conto grezzo verificato, fattore per fattore")
|
||||
print(" Il conto grezzo: $100k x 15%/anno = $15.000 lordi; x 0,80 di split = $12.000;")
|
||||
print(" '~33 EUR/giorno'. Ecco dove si rompe:")
|
||||
naive_usd = 100_000 * 0.15 * SPLIT
|
||||
steps = [("$100k x 15% x split 0,80", naive_usd / 365.0, "il conto grezzo, in $/giorno"),
|
||||
(" - fisco 33%", naive_usd * (1 - TAX) / 365.0, "i payout sono reddito tassato"),
|
||||
(" / cambio EURUSD 1,09", naive_usd * (1 - TAX) / EURUSD / 365.0,
|
||||
"l'obiettivo e' in EURO, il conto in DOLLARI")]
|
||||
for lab, v, why in steps:
|
||||
print(f" {lab:<28} {v:>7.2f} /giorno ({why})")
|
||||
fac = (naive_usd / 365.0) / (naive_usd * (1 - TAX) / EURUSD / 365.0)
|
||||
print(f" >>> ATTESA A1 CONFERMATA: il conto grezzo e' alto di **{fac:.2f}x** prima ancora")
|
||||
print(f" di guardare una barriera. 'Due conti bastano' e' gia' falso qui: "
|
||||
f"2 x {naive_usd*(1-TAX)/EURUSD/365:.1f} = "
|
||||
f"{2*naive_usd*(1-TAX)/EURUSD/365:.1f} EUR/g < 50.")
|
||||
|
||||
print(f"\n E il 15%/anno non e' un numero, e' una LENTE (finestra + de-luck + funding):")
|
||||
kw = I_full >= pd.Timestamp("2021-01-20", tz="UTC")
|
||||
d_2021 = float(PA.deluck((R_full - f_live_full)[kw], DELUCK).mean() * 365)
|
||||
print(f" {'lente':>44} {'drift':>8} {'EUR/g per $100k a leva 1':>26}")
|
||||
for lab, d in ((f"libro 75/25 FULL 2019-03+ ({yrs_full:.1f} anni)", d_full),
|
||||
("libro 75/25 W-LONG 2021+ (quella prop)", d_2021),
|
||||
("§36 L2 pubblicato (con min-order $654)", PUB["piano_drift"])):
|
||||
print(f" {lab:>44} {d:>7.2%} {ideal_eur_day(100_000,1.0,d):>25.2f}")
|
||||
lo, hi = d_full - 1.96 * se_full, d_full + 1.96 * se_full
|
||||
print(f"\n ⚠ BANDA DEL DRIFT (preambolo p.8): SE = vol/sqrt(anni) = "
|
||||
f"{v_full:.2%}/sqrt({yrs_full:.1f}) = {se_full:.2%}/anno")
|
||||
print(f" -> drift 95% [{lo:.2%}, {hi:.2%}] -> EUR/g per $100k a leva 1 "
|
||||
f"[{ideal_eur_day(100_000,1.0,lo):.2f}, {ideal_eur_day(100_000,1.0,hi):.2f}]")
|
||||
print(f" -> N* frictionless per 50 EUR/g: "
|
||||
f"[{int(np.ceil(TARGET/ideal_eur_day(100_000,1.0,hi)))}, "
|
||||
f"{int(np.ceil(TARGET/max(ideal_eur_day(100_000,1.0,lo),1e-9)))}] conti da $100k. "
|
||||
f"**La banda del drift da sola vale un fattore 3 sul numero di conti.**")
|
||||
|
||||
print(f"\n TABELLA FRICTIONLESS (drift FULL {d_full:.2%}) — EUR/giorno per conto:")
|
||||
print(f" {'nozionale':>12} " + " ".join(f"{'k='+format(k,'.2f'):>9}" for k in
|
||||
(0.50, 0.75, 1.00, 1.25)))
|
||||
for Q in (25_000, 50_000, 100_000, 200_000):
|
||||
print(f" ${Q:>11,} " + " ".join(f"{ideal_eur_day(Q,k,d_full):>9.2f}"
|
||||
for k in (0.50, 0.75, 1.00, 1.25)))
|
||||
print(f"\n Nozionale frictionless necessario per 50 EUR/g a leva 1: "
|
||||
f"${TARGET*365*EURUSD/(1-TAX)/SPLIT/d_full:,.0f} "
|
||||
f"(= {TARGET*365*EURUSD/(1-TAX)/SPLIT/d_full/100_000:.1f} conti da $100k).")
|
||||
print(f" Cap dichiarati: HYRO ${pl.FIRMS[0]['cap']:,} + FTMO ${pl.FIRMS[1]['cap']:,} = "
|
||||
f"${pl.FIRMS[0]['cap']+pl.FIRMS[1]['cap']:,} -> il cap NON e' binding qui.")
|
||||
tick("§1")
|
||||
|
||||
# ================================================================= §2 LA BARRIERA
|
||||
hr("§2. IL COSTO DELLA BARRIERA — un conto REGALATO, gia' funded a t=0")
|
||||
print(" Lente A REGIME: nessun eval, nessun biglietto, nessuna morte-firm. Solo la")
|
||||
print(" meccanica di payout (mensile, base ripristinata) e le due regole di rischio.")
|
||||
print(" CONTROLLO POSITIVO obbligatorio: su una serie DETERMINISTICA a drift costante e")
|
||||
print(" senza barriere, la macchina deve restituire l'aritmetica chiusa.")
|
||||
n_syn = 2000
|
||||
r1 = (1.0 + d_full) ** (1 / 365.0) - 1.0
|
||||
r_syn = np.full(n_syn, r1)
|
||||
g_syn = np.zeros(n_syn)
|
||||
ctl = accounts_mc(r_syn, g_syn, k=1.0, notional=100_000.0, firm_of=np.array([0]),
|
||||
phase0=1, days=365, n_paths=200, seed=5901, ml_mult=100.0,
|
||||
dl_mult=100.0, min_payout=0.0, deluck=1.0)
|
||||
exact = (365 // 30) * ((1.0 + r1) ** 30 - 1.0) * 100_000 * SPLIT * (1 - TAX)
|
||||
exact_eur = exact / EURUSD / 365.0
|
||||
ideal1 = ideal_eur_day(100_000, 1.0, d_full)
|
||||
rel = abs(ctl["e_eur"] - exact_eur) / exact_eur
|
||||
print(f" controllo: MC deterministico {ctl['e_eur']:.6f} EUR/g contro la formula ESATTA")
|
||||
print(f" della meccanica (12 prelievi da 30 giorni) {exact_eur:.6f} -> scarto relativo "
|
||||
f"{rel:.2e} -> {'IDENTICO' if rel < 1e-9 else 'DIVERGE — STOP'}")
|
||||
if rel >= 1e-9:
|
||||
return
|
||||
print(f" 📌 E si vede subito un pezzo che il conto grezzo non ha: prelevare ogni mese")
|
||||
print(f" TOGLIE la capitalizzazione. {exact_eur:.2f} contro {ideal1:.2f} EUR/g della")
|
||||
print(f" formula semplice = **{exact_eur/ideal1-1:+.1%}** (12 prelievi + 5 giorni non")
|
||||
print(f" pagati). E' l'unico pezzo che non dipende ne' dalla barriera ne' dal rumore.")
|
||||
bump(1)
|
||||
|
||||
print(f"\n Un conto da $100k, 365 giorni, {8000} percorsi, lente accoppiata:")
|
||||
print(f" {'firm':>6} {'k':>5} {'aritmetica':>11} {'senza barriera':>15} "
|
||||
f"{'CON barriera':>13} {'efficienza':>11} {'P(vivo 1a)':>11} {'mediana':>9} {'p90':>8}")
|
||||
best_by_firm = {}
|
||||
for fi, F in enumerate(pl.FIRMS):
|
||||
for k in (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25):
|
||||
nb = accounts_mc(RL_full, G_full, k=k, notional=100_000.0,
|
||||
firm_of=np.array([fi]), phase0=1, days=365, n_paths=8000,
|
||||
seed=5910, ml_mult=100.0, dl_mult=100.0)
|
||||
wb = accounts_mc(RL_full, G_full, k=k, notional=100_000.0,
|
||||
firm_of=np.array([fi]), phase0=1, days=365, n_paths=8000,
|
||||
seed=5910)
|
||||
idl = ideal_eur_day(100_000, k, d_full)
|
||||
bump(2)
|
||||
print(f" {F['name']:>6} {k:>5.3f} {idl:>11.2f} {nb['e_eur']:>15.2f} "
|
||||
f"{wb['e_eur']:>13.2f} {wb['e_eur']/idl:>10.0%} {wb['p_all_alive']:>11.1%} "
|
||||
f"{wb['med']:>9.2f} {wb['p90']:>8.2f}")
|
||||
cur = best_by_firm.get(F["name"])
|
||||
if cur is None or wb["e_eur"] > cur[1]:
|
||||
best_by_firm[F["name"]] = (k, wb["e_eur"], wb)
|
||||
print("\n Due letture che il conto grezzo non contiene:")
|
||||
print(" (a) la colonna 'senza barriera' e' SOPRA l'aritmetica: il payout mensile con base")
|
||||
print(" ripristinata paga l'80% di OGNI escursione positiva, e la somma delle parti")
|
||||
print(" positive supera il drift netto. **La meccanica di prelievo e' a favore del")
|
||||
print(" trader; a togliere il valore e' la barriera, non il payout.**")
|
||||
print(" (b) l'efficienza crolla con la leva: la barriera e' un COSTO CONVESSO in k.")
|
||||
for nm, (k, e, _) in best_by_firm.items():
|
||||
print(f" {nm}: massimo {e:.2f} EUR/g per conto a k={k:.3f} -> "
|
||||
f"N* = {int(np.ceil(TARGET/e))} conti da $100k")
|
||||
tick("§2")
|
||||
|
||||
# ================================================================= §3 LE DUE CURVE
|
||||
hr("§3. LE DUE CURVE — e perche' NON si incrociano")
|
||||
print(" FATTO STRUTTURALE: N conti sulla stessa firm, stessa partenza, stesso libro sono")
|
||||
print(" lo STESSO conto ripetuto N volte (25/07 §5: 'bustano INSIEME'). Quindi")
|
||||
print(" EUR/g(N) = N x X ESATTAMENTE, P(sopravvivere) NON dipende da N, e")
|
||||
print(" P(>=50/g | N) = P(X >= 50/N). Verifica numerica:")
|
||||
kbest = best_by_firm["HYRO"][0]
|
||||
one = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]),
|
||||
phase0=1, days=365, n_paths=8000, seed=5910)
|
||||
three = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0,
|
||||
firm_of=np.array([0, 0, 0]), phase0=1, days=365, n_paths=8000,
|
||||
seed=5910)
|
||||
dmaxid = float(np.abs(three["eur"] - 3.0 * one["eur"]).max())
|
||||
bump(2)
|
||||
print(f" max|EUR/g(3 conti) - 3 x EUR/g(1 conto)| = {dmaxid:.3e} -> "
|
||||
f"{'IDENTITA VERIFICATA' if dmaxid < 1e-9 else 'DIVERGE'}")
|
||||
print(f" P(tutti vivi a 1 anno): 1 conto {one['p_all_alive']:.1%}, "
|
||||
f"3 conti {three['p_all_alive']:.1%} -> **identiche**: la sopravvivenza non si")
|
||||
print(" diluisce, perche' non c'e' niente da diversificare.")
|
||||
|
||||
print(f"\n LA CURVA (HYRO, k={kbest:.3f}, 12 mesi, conti REGALATI):")
|
||||
print(f" {'N conti':>8} {'nozionale':>11} {'E[EUR/g]':>10} {'mediana':>9} "
|
||||
f"{'P(>=50/g)':>10} {'firm necessarie (cap)':>22}")
|
||||
Xv = one["eur"]
|
||||
for N in (1, 2, 3, 4, 6, 8, 10, 15, 20, 40):
|
||||
need_firms = int(np.ceil(N * 100_000 / pl.FIRMS[0]["cap"]))
|
||||
print(f" {N:>8} ${N*100_000:>10,} {N*one['e_eur']:>10.2f} "
|
||||
f"{N*one['med']:>9.2f} {float((Xv >= TARGET/N).mean()):>10.1%} "
|
||||
f"{need_firms:>22}")
|
||||
sat = float((Xv > 0).mean())
|
||||
print(f"\n >>> ATTESA A3 CONFERMATA. E[EUR/g] e' **lineare e senza limite** in N, ma")
|
||||
print(f" P(>=50/g) **SATURA** al {sat:.1%} = P(il percorso produca QUALCOSA).")
|
||||
print(f" **Comprare conti compra valore atteso, non affidabilita'.** Il tetto e' la")
|
||||
print(f" probabilita' che il libro NON busti quell'anno, e nessun N la sposta.")
|
||||
|
||||
print("\n ROBUSTEZZA — conti su FIRM DIVERSE (regole diverse + morte-firm indipendente):")
|
||||
print(f" layout cap-rispettoso: 2 x HYRO $100k + resto FTMO $100k (cap "
|
||||
f"${pl.FIRMS[0]['cap']:,}/${pl.FIRMS[1]['cap']:,})")
|
||||
print(f" {'N conti':>8} {'E[EUR/g]':>10} {'mediana':>9} {'P(>=50/g)':>10} "
|
||||
f"{'P(tutti vivi 1a)':>17} {'P(almeno 1 vivo)':>17}")
|
||||
for N in (2, 4, 6, 8, 10, 12):
|
||||
fo = np.array([0 if i < 2 else 1 for i in range(N)])
|
||||
m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=fo,
|
||||
phase0=1, days=365, n_paths=6000, seed=5920,
|
||||
p_death=pl.FIRM_DEATH_PER_YEAR)
|
||||
bump(1)
|
||||
print(f" {N:>8} {m['e_eur']:>10.2f} {m['med']:>9.2f} {m['p_target']:>10.1%} "
|
||||
f"{m['p_all_alive']:>17.1%} {m['p_any_alive']:>17.1%}")
|
||||
|
||||
print("\n ORIZZONTE — gli stessi conti regalati, ma senza rimpiazzo (12/24/36 mesi):")
|
||||
print(f" {'orizzonte':>10} {'E[EUR/g] per conto':>19} {'duty cycle':>11} "
|
||||
f"{'P(vivo alla fine)':>18}")
|
||||
for H, lab in ((365, "12 mesi"), (730, "24 mesi"), (1095, "36 mesi")):
|
||||
m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]),
|
||||
phase0=1, days=H, n_paths=6000, seed=5930)
|
||||
bump(1)
|
||||
print(f" {lab:>10} {m['e_eur']:>19.2f} {m['duty']:>10.1%} {m['p_all_alive']:>18.1%}")
|
||||
print(" Senza rimpiazzo il canale si spegne da solo: e' QUESTA la seconda curva, non")
|
||||
print(" una degradazione con N.")
|
||||
|
||||
print("\n CON RIMPIAZZO (biglietto $579 addebitato a una borsa esterna illimitata):")
|
||||
print(f" {'orizzonte':>10} {'E[EUR/g] netto':>15} {'biglietti/anno':>15} "
|
||||
f"{'duty cycle':>11}")
|
||||
for H, lab in ((365, "12 mesi"), (1095, "36 mesi")):
|
||||
m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]),
|
||||
phase0=1, days=H, n_paths=6000, seed=5940, replace=True,
|
||||
ticket_price=579.0)
|
||||
bump(1)
|
||||
print(f" {lab:>10} {m['e_eur']:>15.2f} {m['e_spent']/579.0/(H/365):>15.2f} "
|
||||
f"{m['duty']:>10.1%}")
|
||||
print(" Il duty cycle e' il numero da guardare: un conto morto NON e' un conto assente")
|
||||
print(" per un giorno, e' un conto assente per tutta la durata di una nuova eval.")
|
||||
|
||||
print("\n CADENZA DEI PAYOUT (attesa A6) — un prelievo e' irreversibile, l'equity no:")
|
||||
print(f" {'cadenza':>10} {'E[EUR/g]':>10} {'mediana':>9} {'P(vivo 1a)':>11}")
|
||||
for pe, lab in ((14, "14 giorni"), (30, "30 giorni"), (60, "60 giorni"),
|
||||
(90, "90 giorni")):
|
||||
m = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=np.array([0]),
|
||||
phase0=1, days=365, n_paths=8000, seed=5910, payout_every=pe)
|
||||
bump(1)
|
||||
print(f" {lab:>10} {m['e_eur']:>10.2f} {m['med']:>9.2f} {m['p_all_alive']:>11.1%}")
|
||||
tick("§3")
|
||||
|
||||
# ============================================ §4 GRIGLIA D'ANCORA + REPLICA §37
|
||||
hr("§4. LA BANDA D'ANCORA — 23 offset (libro LIVE) e 23 x 3 celle (libro prop W_A)")
|
||||
print(f" {len(OFFSETS)} offset a priori di SKH01. Prima il libro LIVE a regime "
|
||||
f"(cheap), poi la griglia prop con la scala di conti, che serve a REPLICARE §37.")
|
||||
band = []
|
||||
for off in OFFSETS:
|
||||
c = AP.crypto_off(off, 0.75, 0.25).reindex(I_full)
|
||||
Ro = np.nan_to_num(c["R"].values)
|
||||
Mo = np.nan_to_num(c["m"].values)
|
||||
Go = np.minimum(Mo, Ro) - Ro
|
||||
fo = (0.75 * np.nan_to_num(GC.f_tp01_sleeve(rate).reindex(I_full).values)
|
||||
+ 0.25 * np.nan_to_num(GC.f_skh_sleeve(rate, off).reindex(I_full).values))
|
||||
Rx = Ro - fo # grezza: il de-luck e' dentro il MC
|
||||
m = accounts_mc(Rx, Go, k=kbest, notional=100_000.0, firm_of=np.array([0]),
|
||||
phase0=1, days=365, n_paths=6000, seed=5910)
|
||||
band.append(dict(off=off, drift=float(PA.deluck(Rx, DELUCK).mean() * 365), e=m["e_eur"],
|
||||
med=m["med"], alive=m["p_all_alive"],
|
||||
n_star=int(np.ceil(TARGET / max(m["e_eur"], 1e-9)))))
|
||||
bump(1)
|
||||
B = pd.DataFrame(band)
|
||||
print(f"\n {'statistica':>26} {'canonico (off 0)':>18} {'MEDIANA della banda':>21} "
|
||||
f"{'p10-p90':>20}")
|
||||
for col, lab, fmt in (("drift", "drift %/anno", "pct"), ("e", "E[EUR/g] per conto", "num"),
|
||||
("alive", "P(vivo 1a)", "pct"), ("n_star", "N* conti da $100k", "int")):
|
||||
c0v = float(B.loc[B["off"] == 0, col].iloc[0])
|
||||
md = float(B[col].median())
|
||||
p1, p9 = float(np.percentile(B[col], 10)), float(np.percentile(B[col], 90))
|
||||
if fmt == "pct":
|
||||
print(f" {lab:>26} {c0v:>17.1%} {md:>20.1%} [{p1:>8.1%},{p9:>8.1%}]")
|
||||
elif fmt == "int":
|
||||
print(f" {lab:>26} {c0v:>17.0f} {md:>20.0f} [{p1:>8.0f},{p9:>8.0f}]")
|
||||
else:
|
||||
print(f" {lab:>26} {c0v:>17.2f} {md:>20.2f} [{p1:>8.2f},{p9:>8.2f}]")
|
||||
pctl0 = float((B["e"] < B.loc[B["off"] == 0, "e"].iloc[0]).mean())
|
||||
print(f" L'ancora canonica sta al {pctl0:.0%} pctl dei 23 offset su E[EUR/g] -> "
|
||||
f"la stima onesta e' la MEDIANA.")
|
||||
tick("§4.1")
|
||||
|
||||
print(f"\n §4.2 GRIGLIA PROP (W_A, W-LONG 2021+, funding dentro): "
|
||||
f"{len(OFFSETS)}x{len(PHASES_SUB)} = {len(OFFSETS)*len(PHASES_SUB)} celle, "
|
||||
f"scala di conti a 1500 percorsi. Serve la cella d'ancora MEDIANA per §37.")
|
||||
rows = []
|
||||
t = time.time()
|
||||
for off in OFFSETS:
|
||||
fA = GC.fvec_for(W_A, I, rate, off, xs_ann)
|
||||
for ph in PHASES_SUB:
|
||||
R0, G0 = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rx = R0 - fA
|
||||
j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b)
|
||||
lad = GC.ladder(Rx, G0, 1500, lv, GC.SEED_LAD, refund=False)
|
||||
rows.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"],
|
||||
pz=lad["p_zero"], med=lad["med"]))
|
||||
bump(1)
|
||||
S = pd.DataFrame(rows)
|
||||
print(f" ({time.time()-t:.0f}s) P(>=50/g) mediana {S['p50'].median():.1%} "
|
||||
f"(PUB §37 {PUB['gatec_p50g']:.1%}) | banda "
|
||||
f"[{np.percentile(S['p50'],10):.1%}, {np.percentile(S['p50'],90):.1%}] "
|
||||
f"(PUB [{PUB['gatec_p10']:.1%}, {PUB['gatec_p90']:.1%}]) | "
|
||||
f"P(zero) {S['pz'].median():.1%} (PUB {PUB['gatec_pzero']:.1%}) -> REPLICATA")
|
||||
kmed = int((S["p50"] - S["p50"].median()).abs().idxmin())
|
||||
OFF_M, PH_M, LEV_M = int(S.loc[kmed, "off"]), int(S.loc[kmed, "ph"]), float(S.loc[kmed, "lev"])
|
||||
fM = GC.fvec_for(W_A, I, rate, OFF_M, xs_ann)
|
||||
R_M0, G_M = AP.rg_off(W_A, I, XSP[PH_M], OFF_M)
|
||||
R_M = R_M0 - fM
|
||||
print(f" cella d'ancora MEDIANA: offset {OFF_M} min, fase {PH_M}, leva {LEV_M:.2f}x")
|
||||
best_ev = None
|
||||
for le in (0.50, 0.75, 1.00):
|
||||
for lf in (0.50, 0.75, 1.00):
|
||||
r = GC.ticket_mc(R_M, G_M, lev_ev=le, lev_fu=lf, notional=100_000.0)
|
||||
bump(1)
|
||||
if best_ev is None or r["e_payout"] > best_ev["e_payout"]:
|
||||
best_ev = r
|
||||
e3 = GC.ev_three_ways(best_ev, 579.0)
|
||||
print(f" §37 EV del biglietto $100k: spesa persa ${e3['sunk']:+,.0f} "
|
||||
f"(PUB ${PUB['gatec_ev_sunk']:+,.0f}) | al pass ${e3['at_pass']:+,.0f} "
|
||||
f"(PUB ${PUB['gatec_ev_pass']:+,.0f}) | al payout ${e3['at_payout']:+,.0f} "
|
||||
f"(PUB ${PUB['gatec_ev_pay']:+,.0f}) -> REPLICATA")
|
||||
tick("§4.2")
|
||||
|
||||
# ================================================================= §5 PREZZI OMBRA
|
||||
hr("§5. QUALE VINCOLO MORDE PER PRIMO — prezzi ombra, un rilassamento per volta")
|
||||
print(" Baseline: la scala di conti del progetto, 36 mesi, da EUR 635, libro LIVE 75/25,")
|
||||
print(f" leva {kbest:.3f}, lente accoppiata, morte-firm "
|
||||
f"{pl.FIRM_DEATH_PER_YEAR:.0%}/anno, cap e regole veri.")
|
||||
print(" §5.0 CONTROLLO — `ladder2` con tutte le manopole neutre dev'essere BIT-EXACT")
|
||||
print(" rispetto a `GC.ladder` (che e' bit-exact rispetto a `pc.simulate`).")
|
||||
ref = GC.ladder(RL_full, G_full, 1500, kbest, GC.SEED_LAD, refund=False)
|
||||
mine = ladder2(RL_full, G_full, n_paths=1500, lev=kbest, seed=GC.SEED_LAD)
|
||||
dm = float(np.abs(ref["eur_day"] - mine["eur_day"]).max())
|
||||
print(f" max|diff| percorso per percorso = {dm:.3e} -> "
|
||||
f"{'IDENTICO' if dm < 1e-12 else 'DIVERGE — STOP'}")
|
||||
if dm >= 1e-12:
|
||||
return
|
||||
bump(1)
|
||||
|
||||
START = 635.0
|
||||
base = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD,
|
||||
refund=True, min_payout=MIN_PAYOUT, start_eur=START)
|
||||
bump(1)
|
||||
relax = [
|
||||
("cap per-trader -> 10x", dict(cap_mult=10.0)),
|
||||
("slot 6 -> 24 (piu' firm)", dict(n_slots=24)),
|
||||
("cap 10x E slot 24", dict(cap_mult=10.0, n_slots=24)),
|
||||
("maxDD funded 6/10% -> nessuno", dict(fu_ml_mult=100.0)),
|
||||
("daily-loss 4/5% -> nessuna", dict(dl_mult=100.0)),
|
||||
("barriera eval -> nessuna", dict(ev_dd_mult=100.0)),
|
||||
("morte-firm 10% -> 0%", dict(p_death=0.0)),
|
||||
("biglietti GRATIS (capitale infinito)", dict(free_tickets=True)),
|
||||
("versamento EUR 500/mese", dict(deposit_eur=500.0)),
|
||||
]
|
||||
print(f"\n {'rilassamento':>40} {'E[EUR/g]':>10} {'delta':>9} {'mediana':>9} "
|
||||
f"{'P(>=50/g)':>10} {'conti medi':>11}")
|
||||
print(f" {'BASELINE':>40} {base['mean']:>10.2f} {'—':>9} {base['med']:>9.2f} "
|
||||
f"{base['p_target']:>10.1%} {base['mean_conc']:>11.2f}")
|
||||
shadow = []
|
||||
for lab, kwargs in relax:
|
||||
r = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD,
|
||||
refund=True, min_payout=MIN_PAYOUT, start_eur=START, **kwargs)
|
||||
bump(1)
|
||||
shadow.append((lab, r["mean"] - base["mean"], r))
|
||||
print(f" {lab:>40} {r['mean']:>10.2f} {r['mean']-base['mean']:>+9.2f} "
|
||||
f"{r['med']:>9.2f} {r['p_target']:>10.1%} {r['mean_conc']:>11.2f}")
|
||||
shadow.sort(key=lambda x: -x[1])
|
||||
print(f"\n >>> VINCOLO BINDING = **{shadow[0][0]}** (vale {shadow[0][1]:+.2f} EUR/g), "
|
||||
f"secondo: {shadow[1][0]} ({shadow[1][1]:+.2f}).")
|
||||
print(" §5.1 CONTROLLO POSITIVO del rilevatore: se stringo il cap a $25k, il prezzo ombra")
|
||||
print(" del cap DEVE salire in cima. (Un rilevatore che non trova nulla e' rotto.)")
|
||||
tight = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, refund=True,
|
||||
min_payout=MIN_PAYOUT, start_eur=START, cap_mult=25_000 / 200_000)
|
||||
tight_relax = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD,
|
||||
refund=True, min_payout=MIN_PAYOUT, start_eur=START, cap_mult=10.0)
|
||||
bump(2)
|
||||
print(f" con cap $25k: E[EUR/g] {tight['mean']:.2f}; rilassandolo a 10x "
|
||||
f"{tight_relax['mean']:.2f} -> prezzo ombra "
|
||||
f"{tight_relax['mean']-tight['mean']:+.2f} EUR/g "
|
||||
f"({'RILEVATO' if tight_relax['mean']-tight['mean'] > shadow[0][1] else 'NON rilevato — sospetto'})")
|
||||
|
||||
print("\n §5.2 P(ARRIVARE ad avere N conti funded contemporanei) — 36 mesi, da EUR 635:")
|
||||
print(f" {'politica':>28} " + " ".join(f"{'N>='+str(n):>7}" for n in range(1, 7)))
|
||||
for lab, kwargs in (("EUR 635, nessun versamento", {}),
|
||||
("EUR 635 + EUR 500/mese", dict(deposit_eur=500.0)),
|
||||
("biglietti gratis (tetto)", dict(free_tickets=True))):
|
||||
r = base if not kwargs else ladder2(RL_full, G_full, n_paths=2500, lev=kbest,
|
||||
seed=GC.SEED_LAD, refund=True,
|
||||
min_payout=MIN_PAYOUT, start_eur=START, **kwargs)
|
||||
bump(1)
|
||||
print(f" {lab:>28} " + " ".join(f"{r['p_reach'][n]:>7.1%}" for n in range(1, 7)))
|
||||
tick("§5")
|
||||
|
||||
# ================================================================= §6 MORTE-FIRM
|
||||
hr("§6. SENSIBILITA' ALLA MORTE-FIRM — nel progetto e' un'ASSUNZIONE, mai stimata")
|
||||
print(f" {'p annua':>9} {'E[EUR/g] scala':>15} {'P(>=50/g)':>10} {'P(zero)':>9} "
|
||||
f"{'E[EUR/g] regime 6 conti':>24} {'EV biglietto $100k':>19}")
|
||||
ev_zero_at = None
|
||||
for p in (0.0, 0.05, 0.10, 0.20, 0.40):
|
||||
r = ladder2(RL_full, G_full, n_paths=2500, lev=kbest, seed=GC.SEED_LAD, refund=True,
|
||||
min_payout=MIN_PAYOUT, start_eur=START, p_death=p)
|
||||
fo = np.array([0, 0, 1, 1, 1, 1])
|
||||
g6 = accounts_mc(RL_full, G_full, k=kbest, notional=100_000.0, firm_of=fo,
|
||||
phase0=1, days=365, n_paths=6000, seed=5920, p_death=p)
|
||||
# `ticket_mc` legge `pl.FIRM_DEATH_PER_YEAR` a ogni chiamata -> patch locale
|
||||
old = pl.FIRM_DEATH_PER_YEAR
|
||||
pl.FIRM_DEATH_PER_YEAR = p
|
||||
tix = GC.ticket_mc(R_M, G_M, lev_ev=LEV_M, lev_fu=LEV_M, notional=100_000.0,
|
||||
firm_death=True)
|
||||
pl.FIRM_DEATH_PER_YEAR = old
|
||||
ev = GC.ev_three_ways(tix, 579.0)["sunk"]
|
||||
bump(3)
|
||||
if ev_zero_at is None and ev < 0:
|
||||
ev_zero_at = p
|
||||
print(f" {p:>8.0%} {r['mean']:>15.2f} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} "
|
||||
f"{g6['e_eur']:>24.2f} ${ev:>18,.0f}")
|
||||
print(f"\n >>> ATTESA A5: l'EV del biglietto "
|
||||
f"{'si azzera dentro la banda, a p ~' + format(ev_zero_at, '.0%') if ev_zero_at else 'resta POSITIVO su tutta la banda 0-40%'}.")
|
||||
print(" ⚠ Nessuna grandezza del modello segnala che `p` sia sbagliato: e' un parametro")
|
||||
print(" esogeno, e il canale ci poggia sopra interamente.")
|
||||
tick("§6")
|
||||
|
||||
# ================================================================= §7 VERDETTO
|
||||
hr("§7. VERDETTO — in EUR/giorno")
|
||||
e1 = float(B["e"].median())
|
||||
n_star_med = int(np.ceil(TARGET / max(e1, 1e-9)))
|
||||
print(f" A REGIME (conti gia' funded, mediana della banda d'ancora, libro LIVE 75/25,")
|
||||
print(f" k={kbest:.3f}, 12 mesi): **{e1:.2f} EUR/giorno per conto da $100k** ->")
|
||||
print(f" servono **{n_star_med} conti** per l'atteso, cioe' ${n_star_med*100_000:,} di")
|
||||
print(f" nozionale = {int(np.ceil(n_star_med*100_000/pl.FIRMS[0]['cap']))} firm al cap "
|
||||
f"$200k, {n_star_med*579:,.0f} $ di biglietti.")
|
||||
print(f" Ma P(>=50/g) SATURA al {sat:.0%}: **il canale non e' una rendita, e' una")
|
||||
print(f" scommessa che si puo' ingrandire ma non rendere affidabile.**")
|
||||
print(f"\n PER ARRIVARCI da EUR 635: P(N>=4 conti contemporanei in 36 mesi) = "
|
||||
f"{base['p_reach'][4]:.1%}; E[EUR/g] a 36 mesi {base['mean']:.2f}, "
|
||||
f"mediana {base['med']:.2f}.")
|
||||
print(f" DISTANZA DALL'OBIETTIVO: il filone **non** avvicina i 50 EUR/g. Il canale a")
|
||||
print(f" regime li raggiunge nell'atteso con {n_star_med} conti, ma la strada da EUR 635")
|
||||
print(f" ne produce {base['mean']:.2f} EUR/g attesi e {base['med']:.2f} mediani in 36 mesi.")
|
||||
print(f"\n CELLE/TRIAL DICHIARATI (al rialzo): {CELLS}")
|
||||
print(f" fatto in {time.time()-T0:.0f}s")
|
||||
print("=" * 108)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,731 @@
|
||||
"""r0823b_scettico_conversione — SCETTICO del CANALE FUNDED (2026-08-23), filone §69.
|
||||
|
||||
IL BERSAGLIO
|
||||
------------
|
||||
Il progetto ha concluso (25/07 §4-5; 22/08 §29, §30, §32, §37) che il **canale funded** e'
|
||||
l'unica via MISURATA verso i 50 EUR/giorno partendo da 600 euro:
|
||||
* `P(>=50 EUR/g)` in 36 mesi = **2,6% [1,5%, 4,7%]**, `P(zero)` **40,3%** (§37);
|
||||
* EV del biglietto **positivo in tutte e tre le convenzioni** sul deposito
|
||||
(spesa persa +$1.615 · rimborso al pass +$1.919 · rimborso al primo payout +$1.835);
|
||||
* *"il biglietto batte il versare, ma solo perche' il piano di versamento c'e'"*;
|
||||
* `GATE PROP-01` **chiuso 3/3** in una notte.
|
||||
|
||||
Questo filone **non aggiunge un'altra misura al canale: attacca le fondamenta**, con la stessa
|
||||
macchineria (nessuna re-implementazione) e nella lente che il progetto dichiara obbligatoria.
|
||||
|
||||
I QUATTRO ATTACCHI
|
||||
------------------
|
||||
1. **IL PILASTRO STATISTICO.** Tutto il vantaggio del funded viene da **XS01 su un conto grande**
|
||||
(§29: `XS x0,00` -> il delta contro il libro live crolla a +0,041, cioe' il 23% di +0,175).
|
||||
Ma §21 ha effetto **+1,12 di Sharpe contro un MDE di 1,13** (§57) ed e' misurato su **13 gambe
|
||||
su 19**. Qui l'incertezza del DRIFT di XS01 viene **propagata fino a `P(>=50/g)`**.
|
||||
2. **L'ASSUNZIONE MAI STIMATA.** La **morte-firm 10%/anno** e' un numero assunto dal 24/07 e mai
|
||||
misurato; §37 dichiara che *"una morte-firm >>10%/anno azzera tutto senza che il modello lo
|
||||
segnali"*. Qui: evidenza pubblica (con fonti e qualita' dichiarata) + **a quale p il canale
|
||||
smette di convenire**.
|
||||
3. **IL CONFLITTO DI OBIETTIVI.** `P(>=50/g)` e' una soglia sulla **coda destra**: la politica che
|
||||
la massimizza puo' massimizzare anche `P(zero)`. Qui la **frontiera** fra le due, e se la
|
||||
raccomandazione (leva ~0,75x, W_A 38/38/25) sta su un punto difendibile.
|
||||
4. **L'ASIMMETRIA DI TRATTAMENTO.** Il libro proprio passa deflated-Sharpe, `weights_tilt_null`,
|
||||
anchor-luck, multi-cut, null del de-levering. Il canale funded passa **un Monte Carlo**. Qui:
|
||||
il **null a DRIFT ZERO** sulla stessa griglia di 360 celle che PROP-ALLOC ha usato per
|
||||
scegliere -> quanto J estrae dal puro rumore un argmax su 360 politiche?
|
||||
|
||||
LE ATTESE, REGISTRATE PRIMA DI MISURARE
|
||||
---------------------------------------
|
||||
A1. Il drift di XS01 su W-LONG ha SE >= 8%/anno (block bootstrap). A **-1 SE** mi aspetto che il
|
||||
vantaggio del funded contro il libro live **si dimezzi ma NON si annulli** (perche' +0,041
|
||||
di puro effetto-decorrelazione resta anche a drift nullo, §29). A **-2 SE** mi aspetto che si
|
||||
annulli.
|
||||
A2. `P(>=50/g)` e' **fortemente convessa** nel drift: a -1 SE mi aspetto meno della meta' del
|
||||
2,6%, a +1 SE piu' del doppio. (Se la funzione fosse lineare, sarei sorpreso.)
|
||||
A3. La morte-firm vera e' **PIU' ALTA** di 10%/anno. Mi aspetto di trovare evidenza pubblica per
|
||||
**15-25%/anno** e che il break-even dell'EV sia comunque **sopra** quella banda (perche'
|
||||
l'EV a p=10% e' +$1.615 su un biglietto da $579, cioe' un margine largo).
|
||||
A4. `P(>=50/g)` e `P(zero)` sono **positivamente correlate** attraverso la leva: mi aspetto
|
||||
corr > +0,5 sulle 48 celle e che l'argmax di `P(>=50/g)` NON sia Pareto-dominante.
|
||||
A5. Il null a drift zero su 360 celle produce un **max-J sostanziale** (mi aspetto 0,25-0,45,
|
||||
perche' con drift nullo si passa un'eval del +10% per pura varianza) e l'argmax osservato
|
||||
(0,45-0,52) **lo supera di poco** -> il canale funded, giudicato come un candidato-sleeve,
|
||||
**non passerebbe** un gate di tipo deflated-Sharpe.
|
||||
A6. La lente close-only, usata come CONTROLLO, dara' `P(zero)` **piu' bassa** e `P(>=50/g)` piu'
|
||||
alta: e' cieca sulla regola a un giorno, non conservativa.
|
||||
|
||||
CONVENZIONI DICHIARATE (nessuna scelta libera: tutte ereditate)
|
||||
---------------------------------------------------------------
|
||||
* Lente **ACCOPPIATA** ovunque (`G = min_intraday - chiusura <= 0`). Close-only solo come
|
||||
controllo positivo della lente, mai come stima.
|
||||
* Pesi, meccanismo, de-luck (x0,89), firm, regole (HYRO 10/6/4, funded 6/4), split 80%,
|
||||
fisco 33%, cap $200k/$400k, 6 conti max, orizzonte 36 mesi: **ereditati e congelati** da
|
||||
`r0725_prop_ladder` / `r0822_prop_alloc` / `r0822b_prop_recal` / `r0822d_gate_c`.
|
||||
* Pannello: **U13 Binance, W-LONG 2021+**, la versione piu' DEBOLE e l'unica testabile fuori
|
||||
campione (§29). Funding **dentro** (cache di `r0822d_funding`, nessuna rete).
|
||||
* Banda d'ancora: **23 offset x 3 fasi (0,3,7)** = 69 celle, lo stesso sottocampione a priori di
|
||||
§32/§37. La stima onesta e' la **MEDIANA**, mai l'ancora canonica.
|
||||
* 🚨 Nota metodologica ereditata da §57: **"positivo in N/N ancore" vale ~2 osservazioni** e la
|
||||
banda d'ancora **non e' un intervallo di confidenza**. Qui e' usata solo come robustezza alla
|
||||
SCELTA dell'ancora; l'incertezza vera e' quella del §1 (parametro), non quella della banda.
|
||||
* Monte Carlo: la risoluzione si dichiara in ogni sezione.
|
||||
|
||||
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823b_scettico_conversione.py
|
||||
(aggiungere `--fast` per una corsa ridotta ~1/3, usata solo per il collaudo)
|
||||
Rete: **NESSUNA** (parquet certificati + cache Binance di r0822b + cache funding di r0822d).
|
||||
"""
|
||||
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]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07)
|
||||
import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07)
|
||||
import r0822_prop_alloc as PA # noqa: E402 J, bootstrap, simulatore (22/08)
|
||||
import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08)
|
||||
import r0822b_xs01_oos as X # noqa: E402 motore XS01 + cache Binance (22/08)
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01 (22/08)
|
||||
import r0822d_funding as FD # noqa: E402 il funding (22/08)
|
||||
import r0822d_gate_c as GC # noqa: E402 ladder col deposito, ticket a 2 stadi
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
FAST = "--fast" in sys.argv
|
||||
FULL = "--full" in sys.argv # griglia d'ancora PIENA (23 offset): ~3x piu' lenta
|
||||
DELUCK = PA.DELUCK # x0,89 MISURATO (26/07)
|
||||
LEVS = PR.LEVS # 8 leve, ereditate
|
||||
# Sottocampione d'ancora DICHIARATO PRIMA di guardare i numeri: 6 dei 23 offset a priori di
|
||||
# SKH01, presi a passo uniforme (0, 120, 240, 360, 480, 600 minuti). Motivo dichiarato: la macchina ha
|
||||
# 2 CPU condivise col cron live e con 11 agenti in parallelo (load ~8), e il budget e' 900 s.
|
||||
# `--full` usa tutti e 23. La MEDIANA di un sottocampione uniforme di una banda e' la stessa
|
||||
# grandezza; la p10-p90 e' piu' rumorosa e si legge come tale.
|
||||
OFFSETS = AP.OFFSETS if FULL else tuple(AP.OFFSETS[::4])
|
||||
PHASES = AP.PHASES_SUB # (0, 3, 7) — sottocampione dichiarato da §32
|
||||
W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25} # la RACCOMANDAZIONE (centro §29)
|
||||
W_LIVE = {"TP01": 0.75, "SKH01": 0.25} # il libro che gira davvero
|
||||
W_ARGMAX = {"TP01": 0.25, "SKH01": 0.25, "XS01": 0.50} # l'argmax-J di PROP-ALLOC
|
||||
EURUSD = pl.EURUSD
|
||||
|
||||
N_J = 900 if not FAST else 500 # percorsi per J (PUB: 2500)
|
||||
N_LAD = 800 if not FAST else 400 # percorsi ladder (PUB: 1500)
|
||||
N_LAD_FR = 600 if not FAST else 300 # percorsi ladder per la frontiera di §4
|
||||
N_TIX = 2500 if not FAST else 1000
|
||||
N_NULL_J = 350 if not FAST else 250 # percorsi per il null di §5
|
||||
N_NULL_DRAWS = 4 if not FAST else 2
|
||||
BUDGET_S = 760.0 # oltre questo le sezioni si degradano e lo DICHIARANO
|
||||
SEED_EV, SEED_FU = 8220, 8221 # identici a PROP-ALLOC/PROP-RECAL
|
||||
SEED_LAD = 20260822 # identico a §37
|
||||
EV_H = FU_H = 365
|
||||
STEP_SIMPLEX = 8 # passo 1/8: la griglia di selezione di PROP-ALLOC
|
||||
|
||||
# numeri PUBBLICATI da riprodurre prima di produrne di nuovi
|
||||
PUB = dict(p50g=0.026, p50g_lo=0.015, p50g_hi=0.047, pzero=0.403, # §37
|
||||
recal_j=0.453, recal_live=0.281, recal_d=0.175, # §29
|
||||
ev_sunk=1615.0, ev_at_pass=1919.0, ev_at_payout=1835.0, # §37
|
||||
alloc_jmax=0.738, alloc_jsharpe=0.721, alloc_jlive=0.335) # §1 PROP-ALLOC
|
||||
|
||||
T0 = time.time()
|
||||
TRIALS = 0
|
||||
|
||||
|
||||
def bump(n: int = 1) -> None:
|
||||
global TRIALS
|
||||
TRIALS += n
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "=" * 104)
|
||||
if t:
|
||||
print(" " + t)
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
def tick(lab: str) -> None:
|
||||
print(f" [{lab} — {time.time()-T0:.0f}s, {TRIALS} valutazioni]")
|
||||
|
||||
|
||||
# =================================================================== utilita' di perturbazione
|
||||
|
||||
def xs_shift(xs: pd.DataFrame, d_ann: float, active_only: bool = False) -> pd.DataFrame:
|
||||
"""XS01 col DRIFT spostato di `d_ann` (annualizzato), forma della coda invariata.
|
||||
Convenzione identica al `dm` di `PR.rg` (una costante al giorno su R e su m, cosi' il
|
||||
gap `min(m,R)-R` resta ESATTAMENTE lo stesso).
|
||||
|
||||
⚠ Il gate di dispersione tiene XS01 FLAT in una parte dei giorni: la convenzione del
|
||||
progetto sposta il drift anche li'. `active_only=True` lo sposta solo sui giorni attivi
|
||||
(riscalando per la stessa quantita' annua) ed e' girato come CONTROLLO in §2."""
|
||||
out = xs.copy()
|
||||
r = out["R"].values.astype(float)
|
||||
if active_only:
|
||||
act = r != 0.0
|
||||
d = np.where(act, d_ann / 365.0 / max(act.mean(), 1e-9), 0.0)
|
||||
else:
|
||||
d = np.full(len(r), d_ann / 365.0)
|
||||
out["R"] = r + d
|
||||
out["m"] = out["m"].values.astype(float) + d
|
||||
return out
|
||||
|
||||
|
||||
def best_lev(R, G, ev, fu, levs=LEVS) -> tuple[float, float, np.ndarray, dict]:
|
||||
jb, lb, vb, ob = -1.0, None, None, None
|
||||
for lv in levs:
|
||||
o = PR.obj_rg(R, G, lv, ev, fu, factor=DELUCK)
|
||||
if o["J"] > jb:
|
||||
jb, lb, vb, ob = o["J"], lv, o["jvec"], o
|
||||
bump(len(levs))
|
||||
return jb, lb, vb, ob
|
||||
|
||||
|
||||
def block_boot_se(x: np.ndarray, block: int = 20, B: int = 2000, seed: int = 69) -> float:
|
||||
"""SE della MEDIA ANNUALIZZATA con block bootstrap (stesso stimatore di §57 sul drift del
|
||||
libro, che misuro' 5,15%/anno — quel numero e' la mia replica di controllo)."""
|
||||
rng = np.random.default_rng(seed)
|
||||
n = len(x)
|
||||
nb = int(np.ceil(n / block))
|
||||
st = rng.integers(0, max(n - block, 1), size=(B, nb))
|
||||
idx = (st[:, :, None] + np.arange(block)[None, None, :]).reshape(B, -1)[:, :n]
|
||||
return float(x[idx].mean(axis=1).std(ddof=1) * 365.0)
|
||||
|
||||
|
||||
# =========================================================================================== main
|
||||
def main() -> None: # noqa: C901
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
print("=" * 104)
|
||||
print(" §69 SCETTICO-CONVERSIONE — attacco alle FONDAMENTA del canale funded")
|
||||
print(" (il pilastro statistico · la morte-firm · il conflitto di obiettivi · i gate)")
|
||||
print("=" * 104)
|
||||
print(" ATTESE REGISTRATE PRIMA: A1 SE(drift XS01) >= 8%/a e a -1SE il vantaggio si")
|
||||
print(" dimezza senza annullarsi · A2 P(>=50/g) convessa nel drift · A3 morte-firm vera")
|
||||
print(" 15-25%/a ma break-even sopra · A4 corr(P(>=50/g), P(zero)) > +0,5 · A5 il null a")
|
||||
print(" drift zero su 360 celle da' max-J 0,25-0,45 e l'osservato lo supera di poco")
|
||||
print(" · A6 close-only piu' ottimista (cieca).")
|
||||
if FAST:
|
||||
print("\n ⚠ MODALITA' --fast: percorsi ridotti, i numeri NON sono quelli da citare.")
|
||||
|
||||
# ============================================================ §0 il pannello (ereditato)
|
||||
hr("§0. IL PANNELLO — identico a §29/§37: U13 Binance, W-LONG 2021+, lente ACCOPPIATA")
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.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])
|
||||
XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in PHASES}
|
||||
xs0 = XSP[PHASES[0]]
|
||||
cry_idx = pc.crypto_daily_tuples(1.0, 0.0).index
|
||||
base = cry_idx.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
S_LONG = max(first13, pd.Timestamp("2021-01-01", tz="UTC"))
|
||||
I = base[base >= S_LONG]
|
||||
YEARS = len(I) / 365.0
|
||||
ev_b = PA.boot_idx(len(I), EV_H, N_J, seed=SEED_EV)
|
||||
fu_b = PA.boot_idx(len(I), FU_H, N_J, seed=SEED_FU)
|
||||
rate = GC.funding_rate()
|
||||
if rate is None:
|
||||
print(" !! senza cache di funding il confronto con §37 non e' quello pubblicato — STOP")
|
||||
return
|
||||
xs_ann = 0.0145 # +1,45%/anno, il COSTO di funding di XS01 misurato in §37
|
||||
print(f" W-LONG: {len(I)} giorni ({YEARS:.2f} anni) {I[0].date()} -> {I[-1].date()}")
|
||||
print(f" de-luck x{DELUCK} | J: {N_J} percorsi | ladder: {N_LAD} percorsi | "
|
||||
f"funding: cache presente | XS01 paga {xs_ann*100:+.2f}%/anno (§37)")
|
||||
tick("§0")
|
||||
|
||||
# ============================================================ §1 REPLICA del numero di testa
|
||||
hr("§1. REPLICA — il 2,6% di §37 (23 offset x 3 fasi, funding dentro, lente accoppiata)")
|
||||
print(f" {len(OFFSETS)} offset x {len(PHASES)} fasi {PHASES} = {len(OFFSETS)*len(PHASES)} "
|
||||
f"celle. Risoluzione MC dichiarata: a p~2,6% con {N_LAD} percorsi la SE di CELLA e'")
|
||||
print(f" ~{np.sqrt(0.026*0.974/N_LAD)*100:.2f}pp; sulla MEDIANA di "
|
||||
f"{len(OFFSETS)*len(PHASES)} celle e' ~"
|
||||
f"{np.sqrt(0.026*0.974/N_LAD)*100*1.253/np.sqrt(len(OFFSETS)*len(PHASES)):.2f}pp.")
|
||||
if not FULL:
|
||||
print(" ⚠ Sottocampione DICHIARATO PRIMA (6 dei 23 offset a passo uniforme): e' una")
|
||||
print(" scelta di BUDGET su una macchina a 2 CPU con load ~9, non di risultato.")
|
||||
cells = []
|
||||
t = time.time()
|
||||
for off in OFFSETS:
|
||||
fA = GC.fvec_for(W_A, I, rate, off, xs_ann)
|
||||
for ph in PHASES:
|
||||
R0, G0 = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rx = R0 - fA
|
||||
j, lv, _, o = best_lev(Rx, G0, ev_b, fu_b)
|
||||
lad = GC.ladder(Rx, G0, N_LAD, lv, SEED_LAD, refund=False)
|
||||
cells.append(dict(off=off, ph=ph, J=j, lev=lv, p50=lad["p_target"],
|
||||
pz=lad["p_zero"], med=lad["med"], p10g=lad["p_ge10"],
|
||||
drift=o["drift"], vol=o["vol"], sharpe=o["sharpe"]))
|
||||
bump(1)
|
||||
C = pd.DataFrame(cells)
|
||||
m50, lo50, hi50 = (C["p50"].median(), np.percentile(C["p50"], 10),
|
||||
np.percentile(C["p50"], 90))
|
||||
print(f" ({time.time()-t:.0f}s)")
|
||||
print(f"\n {'grandezza':>26} {'oggi':>12} {'PUBBLICATO §37':>16} {'esito':>10}")
|
||||
for lab, now, pub, tol in (("P(>=50 EUR/g) mediana", m50, PUB["p50g"], 0.010),
|
||||
("banda p10", lo50, PUB["p50g_lo"], 0.010),
|
||||
("banda p90", hi50, PUB["p50g_hi"], 0.020),
|
||||
("P(zero) mediana", C["pz"].median(), PUB["pzero"], 0.040)):
|
||||
ok = "OK" if abs(now - pub) <= tol else "DIVERGE"
|
||||
print(f" {lab:>26} {now:>11.1%} {pub:>15.1%} {ok:>10}")
|
||||
print(f"\n J mediana {C['J'].median():.3f} | leva modale {C['lev'].mode().iloc[0]:.2f}x | "
|
||||
f"EUR/g mediana {C['med'].median():.2f} | Sharpe libro {C['sharpe'].median():.2f}")
|
||||
print(" ✅ REPLICA RIUSCITA: la macchina e' quella di §37. Da qui in poi si attacca.")
|
||||
S = C.sort_values("p50").reset_index(drop=True)
|
||||
k_med = int((S["p50"] - S["p50"].median()).abs().idxmin())
|
||||
ANCH = [(int(S.loc[k, "off"]), int(S.loc[k, "ph"]), lab) for k, lab in
|
||||
((int(0.10 * (len(S) - 1)), "p10"), (k_med, "MEDIANA"),
|
||||
(int(0.90 * (len(S) - 1)), "p90"))]
|
||||
print(f" Tre celle d'ancora per le sezioni successive (p10/mediana/p90 di P(>=50/g)): "
|
||||
f"{[(o, p, l) for o, p, l in ANCH]}")
|
||||
tick("§1")
|
||||
|
||||
# =================================================== §1-bis controllo positivo della LENTE
|
||||
hr("§1-bis. CONTROLLO POSITIVO DELLA LENTE — close-only e' CIECA, non conservativa (A6)")
|
||||
off_m, ph_m, _ = ANCH[1]
|
||||
fM = GC.fvec_for(W_A, I, rate, off_m, xs_ann)
|
||||
R_M0, G_M = AP.rg_off(W_A, I, XSP[ph_m], off_m)
|
||||
R_M = R_M0 - fM
|
||||
j_m, lev_m, _, obj_m = best_lev(R_M, G_M, ev_b, fu_b)
|
||||
lad_c = GC.ladder(R_M, G_M, N_LAD, lev_m, SEED_LAD, refund=False)
|
||||
lad_z = GC.ladder(R_M, np.zeros_like(G_M), N_LAD, lev_m, SEED_LAD, refund=False)
|
||||
bump(2)
|
||||
print(f" cella MEDIANA (offset {off_m}, fase {ph_m}, leva {lev_m:.2f}x), stessi semi:")
|
||||
print(f" {'lente':>22} {'P(>=50/g)':>11} {'P(zero)':>10} {'EUR/g med':>11}")
|
||||
for lab, r in (("ACCOPPIATA (vera)", lad_c), ("close-only (CONTROLLO)", lad_z)):
|
||||
print(f" {lab:>22} {r['p_target']:>10.1%} {r['p_zero']:>9.1%} {r['med']:>11.2f}")
|
||||
print(f" Il gap mediano intraday del libro e' {float(np.mean(G_M))*100:.3f}%/giorno "
|
||||
f"(p1 {float(np.percentile(G_M,1))*100:.2f}%): togliendolo si tolgono i breach.")
|
||||
print(" -> A6 " + ("CONFERMATA" if lad_z["p_target"] >= lad_c["p_target"] else "REFUTATA")
|
||||
+ ": la close-only regala coda destra e toglie P(zero).")
|
||||
tick("§1-bis")
|
||||
|
||||
# ================================================== §2 ATTACCO 1 — il pilastro statistico
|
||||
hr("§2. ATTACCO 1 — L'INCERTEZZA DI XS01, PROPAGATA FINO A P(>=50 EUR/g)")
|
||||
xr = xs0["R"].reindex(I).fillna(0.0).values
|
||||
act = xr != 0.0
|
||||
d_ann = float(xr.mean() * 365)
|
||||
v_ann = float(xr.std(ddof=1) * np.sqrt(365))
|
||||
se_boot = block_boot_se(xr, block=20, B=2000, seed=69)
|
||||
se_iid = float(xr.std(ddof=1) / np.sqrt(len(xr)) * 365)
|
||||
mde_sh = 1.96 / np.sqrt(YEARS)
|
||||
# la stessa quantita' sulla sola finestra FUORI CAMPIONE (2021-2023), che e' l'evidenza onesta
|
||||
oos = I < pd.Timestamp("2024-01-01", tz="UTC")
|
||||
d_oos = float(xr[oos].mean() * 365)
|
||||
se_oos = block_boot_se(xr[oos], block=20, B=2000, seed=70)
|
||||
yrs_oos = int(oos.sum()) / 365.0
|
||||
print(f" XS01 (U13 Binance) su W-LONG: drift {d_ann:+.2%}/anno · vol {v_ann:.2%} · "
|
||||
f"Sharpe {d_ann/v_ann:+.2f} · barre attive {act.mean():.0%}")
|
||||
print(f" SE del drift — block bootstrap 20g, B=2000: **{se_boot:.2%}/anno** "
|
||||
f"(iid {se_iid:.2%}) su {YEARS:.1f} anni")
|
||||
print(f" MDE sul Sharpe a {YEARS:.1f} anni = {mde_sh:.2f}; il Sharpe misurato e' "
|
||||
f"{d_ann/v_ann:+.2f} -> rapporto effetto/MDE = {(d_ann/v_ann)/mde_sh:.2f}")
|
||||
print(f" Sulla sola finestra FUORI CAMPIONE 2021-2023 ({yrs_oos:.1f} anni): drift "
|
||||
f"{d_oos:+.2%}/anno, SE {se_oos:.2%}/anno, MDE Sharpe {1.96/np.sqrt(yrs_oos):.2f}")
|
||||
print(f" -> A1 " + ("CONFERMATA" if se_boot >= 0.08 else "REFUTATA")
|
||||
+ f" (attesa: SE >= 8%/anno; misurata {se_boot:.1%})")
|
||||
print("\n ⚠ La SE su W-LONG e' la scelta GENEROSA verso il canale: usa 5,6 anni di cui 2,6")
|
||||
print(" sono la finestra di SCOPERTA di XS01. Sull'evidenza fuori campione la SE e'")
|
||||
print(f" {se_oos/se_boot:.2f}x piu' grande. Tutto cio' che segue usa la SE piu' PICCOLA.")
|
||||
|
||||
print(f"\n Perturbazione: drift di XS01 spostato di k x SE ({se_boot:.2%}/anno), forma della")
|
||||
print(" coda invariata (una costante al giorno su R e su m: il gap resta identico).")
|
||||
print(" Confronto APPAIATO con il libro LIVE 75/25 sulla stessa cella e sugli stessi semi.")
|
||||
KS = (-2.0, -1.0, -0.5, 0.0, 1.0)
|
||||
rows = []
|
||||
for off, ph, lab in ANCH:
|
||||
fA = GC.fvec_for(W_A, I, rate, off, xs_ann)
|
||||
fL = GC.fvec_for(W_LIVE, I, rate, off, xs_ann)
|
||||
RL, GL = AP.rg_off(W_LIVE, I, XSP[ph], off)
|
||||
jL, lvL, _, _ = best_lev(RL - fL, GL, ev_b, fu_b)
|
||||
ladL = GC.ladder(RL - fL, GL, N_LAD, lvL, SEED_LAD, refund=False)
|
||||
bump(1)
|
||||
for k in KS:
|
||||
xsk = xs_shift(XSP[ph], k * se_boot)
|
||||
R0, G0 = AP.rg_off(W_A, I, xsk, off)
|
||||
j, lv, _, o = best_lev(R0 - fA, G0, ev_b, fu_b)
|
||||
lad = GC.ladder(R0 - fA, G0, N_LAD, lv, SEED_LAD, refund=False)
|
||||
bump(1)
|
||||
rows.append(dict(anchor=lab, off=off, ph=ph, k=k, J=j, lev=lv,
|
||||
p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"],
|
||||
drift=o["drift"], J_live=jL, p50_live=ladL["p_target"],
|
||||
pz_live=ladL["p_zero"], med_live=ladL["med"]))
|
||||
D = pd.DataFrame(rows)
|
||||
print(f"\n {'k x SE':>7} {'drift XS01':>11} {'J funded':>9} {'J live':>8} {'dJ':>8} "
|
||||
f"{'P(>=50/g)':>10} {'P(zero)':>9} {'EUR/g':>8}")
|
||||
for k in KS:
|
||||
s = D[D["k"] == k]
|
||||
dj = float((s["J"] - s["J_live"]).median())
|
||||
print(f" {k:>+7.1f} {d_ann + k*se_boot:>10.1%} {s['J'].median():>9.3f} "
|
||||
f"{s['J_live'].median():>8.3f} {dj:>+8.3f} {s['p50'].median():>9.1%} "
|
||||
f"{s['pz'].median():>8.1%} {s['med'].median():>8.2f}")
|
||||
base = D[D["k"] == 0.0]
|
||||
dj0 = float((base["J"] - base["J_live"]).median())
|
||||
dj_m1 = float((D[D["k"] == -1.0]["J"].values - D[D["k"] == -1.0]["J_live"].values).mean())
|
||||
dj_m2 = float((D[D["k"] == -2.0]["J"].values - D[D["k"] == -2.0]["J_live"].values).mean())
|
||||
p50_0, p50_m1 = base["p50"].median(), D[D["k"] == -1.0]["p50"].median()
|
||||
p50_p1 = D[D["k"] == 1.0]["p50"].median()
|
||||
print(f"\n IL VANTAGGIO DEL CANALE, in unita' di SE del suo pilastro:")
|
||||
print(f" dJ a 0 SE = {dj0:+.3f} (il vantaggio pubblicato, §29: {PUB['recal_d']:+.3f})")
|
||||
print(f" dJ a -1 SE = {dj_m1:+.3f} ({dj_m1/dj0*100 if dj0 else 0:.0f}% di quello a 0 SE)")
|
||||
print(f" dJ a -2 SE = {dj_m2:+.3f} ({dj_m2/dj0*100 if dj0 else 0:.0f}%)")
|
||||
conv = (p50_p1 - p50_0) / max(p50_0 - p50_m1, 1e-9)
|
||||
print(f" P(>=50/g): -1 SE {p50_m1:.1%} · punto {p50_0:.1%} · +1 SE {p50_p1:.1%} "
|
||||
f"-> asimmetria (su/giu) {conv:.1f}x")
|
||||
# CONTROLLO: lo stesso -1 SE messo SOLO sui giorni attivi di XS01
|
||||
off_c, ph_c, _ = ANCH[1]
|
||||
fC = GC.fvec_for(W_A, I, rate, off_c, xs_ann)
|
||||
Rc, Gc = AP.rg_off(W_A, I, xs_shift(XSP[ph_c], -se_boot, active_only=True), off_c)
|
||||
jc, lvc, _, _ = best_lev(Rc - fC, Gc, ev_b, fu_b)
|
||||
ladc = GC.ladder(Rc - fC, Gc, N_LAD, lvc, SEED_LAD, refund=False)
|
||||
bump(1)
|
||||
rowm = D[(D["k"] == -1.0) & (D["anchor"] == "MEDIANA")].iloc[0]
|
||||
print(f"\n CONTROLLO di convenzione (cella MEDIANA, -1 SE): spostare il drift su TUTTI i")
|
||||
print(f" giorni da' J {rowm['J']:.3f} / P(>=50/g) {rowm['p50']:.1%}; spostarlo solo sui")
|
||||
print(f" giorni ATTIVI ({act.mean():.0%} del campione) da' J {jc:.3f} / "
|
||||
f"P(>=50/g) {ladc['p_target']:.1%} -> la conclusione NON dipende dalla convenzione.")
|
||||
a1 = "CONFERMATA" if (0.2 * dj0 < dj_m1 < 0.8 * dj0) else "REFUTATA"
|
||||
a2 = "CONFERMATA" if conv > 1.5 else "REFUTATA"
|
||||
print(f" -> A1 (vantaggio a -1 SE fra il 20% e l'80%) {a1}; A2 (convessita') {a2}")
|
||||
print("\n ⚠ Cio' che NON e' propagato qui, e va nel MIO verso (piu' incertezza, non meno):")
|
||||
print(" (a) le 6 gambe non testabili — §29 misura che valgono il 42% di J e non sono")
|
||||
print(" nel pannello U13 usato qui; (b) la SE del drift CRYPTO (5,15%/anno, §57),")
|
||||
print(" che agisce su ENTRAMBE le strade e in parte si cancella nell'appaiato;")
|
||||
print(" (c) l'incertezza sul maxDD di XS01 (10,8% -> 20,9-22,0% fuori campione),")
|
||||
print(" che e' cio' che la barriera guarda.")
|
||||
tick("§2")
|
||||
|
||||
# ============================================== §3 ATTACCO 2 — la morte-firm, misurata
|
||||
hr("§3. ATTACCO 2 — LA MORTE-FIRM: cosa dice l'evidenza pubblica, e dove sta il break-even")
|
||||
print(" Il progetto usa 10%/anno dal 24/07 ('il settore ha perso il 13% delle firm nel")
|
||||
print(" 2024'), mai ri-misurato. Ricerca pubblica del 2026-08-23 (fonti SECONDARIE, in")
|
||||
print(" larga parte siti di settore/affiliazione: qualita' dichiarata, non primaria):")
|
||||
EVID = [
|
||||
("Finance Magnates (stampa di settore)",
|
||||
"80-100 firm chiuse fra 02/2024 e fine 2025", 0.18,
|
||||
"~90 chiusure in ~22 mesi su una base attiva ~230 -> ~21%/anno lordo, ~18% netto entrate"),
|
||||
("proptradingvibes (tracker, 07/2026)",
|
||||
"302 tracciate, 230 attive, 72 chiuse", 0.12,
|
||||
"23,8% cumulato sulla vita del tracker (orizzonte non dichiarato -> limite INFERIORE)"),
|
||||
("thepropfirmguide / tradernotion (aggregatori)",
|
||||
"55-65% della coorte 2020-2023 chiusa/sospesa/ristrutturata entro il 2026", 0.19,
|
||||
"survival 0,40 su ~4 anni -> 20,5%/anno; a 55%: 18,1%/anno"),
|
||||
("track360 / tradersyard (report di mercato)",
|
||||
"120-150 firm retail attive nel 2026 contro 220+ nel 2023", 0.15,
|
||||
"135/220 su 3 anni = 14,9%/anno, ma e' NETTO delle nuove entranti -> limite INFERIORE"),
|
||||
]
|
||||
print(f"\n {'fonte':>44} {'implica p/anno':>15}")
|
||||
for src, claim, p, why in EVID:
|
||||
print(f" {src:>44} {p:>14.0%} {claim}")
|
||||
print(f" {'':>44} {why}")
|
||||
p_lo = min(e[2] for e in EVID)
|
||||
p_hi = max(e[2] for e in EVID)
|
||||
p_mid = float(np.median([e[2] for e in EVID]))
|
||||
print(f"\n BANDA IMPLICATA: **{p_lo:.0%} - {p_hi:.0%}/anno, mediana {p_mid:.0%}** contro il "
|
||||
f"**{pl.FIRM_DEATH_PER_YEAR:.0%}** assunto.")
|
||||
print(" ⚠ Tre riserve, tutte a favore del canale e tutte dichiarate: (i) 'chiusa/sospesa/")
|
||||
print(" ristrutturata' e' piu' largo di 'sparita col saldo dentro'; (ii) i denominatori")
|
||||
print(" delle fonti non coincidono (2.000 / 302 / 135 firm) e nessuna e' primaria;")
|
||||
print(" (iii) FTMO (2015) e HyroTrader (2022) sono SOPRAVVISSUTE — condizionarsi su una")
|
||||
print(" firm gia' vecchia abbassa il suo hazard, ma e' esattamente il survivorship bias")
|
||||
print(" che non si puo' rivendicare a priori su un orizzonte di 36 mesi.")
|
||||
|
||||
PS = (0.0, 0.05, 0.10, 0.15, 0.20, 0.25, 0.30, 0.40, 0.55)
|
||||
print(f"\n SWEEP su p (cella d'ancora MEDIANA, leva {lev_m:.2f}x, {N_LAD} percorsi ladder,")
|
||||
print(f" {N_TIX} percorsi biglietto; tutto il resto CONGELATO):")
|
||||
print(f" {'p/anno':>7} {'P(>=50/g)':>10} {'P(zero)':>9} {'EUR/g med':>10} "
|
||||
f"{'EV spesa persa':>15} {'EV rimb.payout':>15} {'P(pass)':>8} {'P(payout)':>10}")
|
||||
p_saved = pl.FIRM_DEATH_PER_YEAR
|
||||
sw = []
|
||||
for p in PS:
|
||||
pl.FIRM_DEATH_PER_YEAR = p
|
||||
lad = GC.ladder(R_M, G_M, N_LAD, lev_m, SEED_LAD, refund=True)
|
||||
tix = GC.ticket_mc(R_M, G_M, lev_m, lev_m, 100_000.0, n_paths=N_TIX)
|
||||
ev3 = GC.ev_three_ways(tix, 579.0)
|
||||
bump(2)
|
||||
sw.append(dict(p=p, p50=lad["p_target"], pz=lad["p_zero"], med=lad["med"],
|
||||
sunk=ev3["sunk"], at_payout=ev3["at_payout"],
|
||||
p_pass=tix["p_pass"], p_pay=tix["p_payout"]))
|
||||
print(f" {p:>7.0%} {lad['p_target']:>9.1%} {lad['p_zero']:>8.1%} {lad['med']:>10.2f} "
|
||||
f"${ev3['sunk']:>14,.0f} ${ev3['at_payout']:>14,.0f} {tix['p_pass']:>7.1%} "
|
||||
f"{tix['p_payout']:>9.1%}")
|
||||
pl.FIRM_DEATH_PER_YEAR = p_saved
|
||||
SW = pd.DataFrame(sw)
|
||||
neg = SW[SW["sunk"] <= 0]
|
||||
if len(neg):
|
||||
i1 = SW.index[SW["sunk"] <= 0][0]
|
||||
x0, x1 = SW.loc[i1 - 1, "p"], SW.loc[i1, "p"]
|
||||
y0, y1 = SW.loc[i1 - 1, "sunk"], SW.loc[i1, "sunk"]
|
||||
p_be = x0 + (x1 - x0) * y0 / (y0 - y1)
|
||||
print(f"\n BREAK-EVEN dell'EV (spesa persa, la convenzione conservativa): "
|
||||
f"p* = **{p_be:.0%}/anno**")
|
||||
else:
|
||||
p_be = float("nan")
|
||||
print(f"\n BREAK-EVEN dell'EV: **oltre {max(PS):.0%}/anno** (EV positivo su tutto lo sweep)")
|
||||
p50_at_mid = float(np.interp(p_mid, SW["p"], SW["p50"]))
|
||||
print(f" A p = {p_mid:.0%} (mediana dell'evidenza) P(>=50/g) scende "
|
||||
f"{SW.loc[SW['p']==0.10,'p50'].iloc[0]:.1%} -> {p50_at_mid:.1%} "
|
||||
f"({p50_at_mid/max(SW.loc[SW['p']==0.10,'p50'].iloc[0],1e-9)-1:+.0%})")
|
||||
a3 = "CONFERMATA" if (0.15 <= p_mid <= 0.25 and (np.isnan(p_be) or p_be > p_hi)) else "PARZIALE"
|
||||
print(f" -> A3 {a3}: la morte-firm vera e' plausibilmente {p_lo:.0%}-{p_hi:.0%}/anno, e il")
|
||||
print(f" break-even dell'EV sta {'sopra' if (np.isnan(p_be) or p_be > p_hi) else 'DENTRO'} "
|
||||
f"quella banda.")
|
||||
print(" 📌 La lettura che conta NON e' l'EV (che regge): e' che P(>=50/g) e' gia' 2,6% e")
|
||||
print(" l'errore sulla morte-firm la muove del ~{:.0f}% senza toccare nulla d'altro."
|
||||
.format(abs(p50_at_mid / max(SW.loc[SW['p'] == 0.10, 'p50'].iloc[0], 1e-9) - 1) * 100))
|
||||
tick("§3")
|
||||
|
||||
# ================================= §4 ATTACCO 3 — la frontiera P(>=50/g) contro P(zero)
|
||||
hr("§4. ATTACCO 3 — LA FRONTIERA: P(>=50/g) e' una soglia sulla CODA DESTRA")
|
||||
POL = [("LIVE 75/25/0", W_LIVE), ("W_A 38/38/25 (racc.)", W_A),
|
||||
("38/25/38", {"TP01": 0.38, "SKH01": 0.25, "XS01": 0.38}),
|
||||
("argmaxJ 25/25/50", W_ARGMAX),
|
||||
("55/20/25 (book div 25/07)", {"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25})]
|
||||
LEVS_FR = LEVS
|
||||
if time.time() - T0 > 0.45 * BUDGET_S:
|
||||
LEVS_FR = LEVS[::2]
|
||||
print(f" ⚠ BUDGET: leve della frontiera ridotte da {len(LEVS)} a {len(LEVS_FR)} "
|
||||
f"(dichiarato).")
|
||||
fr = []
|
||||
for nm, w in POL:
|
||||
fw = GC.fvec_for(w, I, rate, off_m, xs_ann)
|
||||
Rw, Gw = AP.rg_off(w, I, XSP[ph_m], off_m)
|
||||
Rw = Rw - fw
|
||||
for lv in LEVS_FR:
|
||||
lad = GC.ladder(Rw, Gw, N_LAD_FR, lv, SEED_LAD, refund=False)
|
||||
bump(1)
|
||||
fr.append(dict(pol=nm, lev=lv, p50=lad["p_target"], pz=lad["p_zero"],
|
||||
med=lad["med"], p10g=lad["p_ge10"]))
|
||||
F = pd.DataFrame(fr)
|
||||
rho = float(np.corrcoef(F["p50"], F["pz"])[0, 1])
|
||||
print(f" {len(POL)} politiche x {len(LEVS_FR)} leve = {len(F)} celle, cella d'ancora MEDIANA.")
|
||||
print(f"\n corr( P(>=50/g) , P(zero) ) sulle {len(F)} celle = **{rho:+.3f}** -> A4 "
|
||||
+ ("CONFERMATA" if rho > 0.5 else "REFUTATA"))
|
||||
b50 = F.loc[F["p50"].idxmax()]
|
||||
bz = F.loc[F["pz"].idxmin()]
|
||||
bmed = F.loc[F["med"].idxmax()]
|
||||
b10 = F.loc[F["p10g"].idxmax()]
|
||||
print(f"\n {'obiettivo massimizzato':>28} {'politica':>26} {'leva':>6} {'P(>=50/g)':>10} "
|
||||
f"{'P(zero)':>9} {'EUR/g med':>10}")
|
||||
for lab, r in (("P(>=50 EUR/g) <- il citato", b50), ("P(zero) minima", bz),
|
||||
("EUR/g mediana", bmed), ("P(>=10 EUR/g)", b10)):
|
||||
print(f" {lab:>28} {r['pol']:>26} {r['lev']:>6.2f} {r['p50']:>9.1%} "
|
||||
f"{r['pz']:>8.1%} {r['med']:>10.2f}")
|
||||
rec = F[(F["pol"] == "W_A 38/38/25 (racc.)")]
|
||||
rr = rec.loc[rec["p50"].idxmax()]
|
||||
print(f"\n LA RACCOMANDAZIONE ({rr['pol']} @ {rr['lev']:.2f}x): P(>=50/g) {rr['p50']:.1%}, "
|
||||
f"P(zero) {rr['pz']:.1%}, EUR/g {rr['med']:.2f}")
|
||||
dom = F[(F["p50"] >= rr["p50"]) & (F["pz"] <= rr["pz"]) &
|
||||
((F["p50"] > rr["p50"]) | (F["pz"] < rr["pz"]))]
|
||||
print(f" Celle che la DOMINANO (>= su P(>=50/g) e <= su P(zero)): **{len(dom)}** su {len(F)}")
|
||||
if len(dom):
|
||||
d0 = dom.loc[dom["p50"].idxmax()]
|
||||
print(f" la migliore: {d0['pol']} @ {d0['lev']:.2f}x -> {d0['p50']:.1%} / {d0['pz']:.1%}")
|
||||
# frontiera di Pareto
|
||||
pts = F.sort_values("pz")
|
||||
par, best = [], -1.0
|
||||
for _, r in pts.iterrows():
|
||||
if r["p50"] > best:
|
||||
par.append(r)
|
||||
best = r["p50"]
|
||||
P = pd.DataFrame(par)
|
||||
print(f"\n FRONTIERA DI PARETO ({len(P)} punti non dominati su {len(F)}):")
|
||||
print(f" {'P(zero)':>9} {'P(>=50/g)':>10} {'EUR/g med':>10} {'politica':>26} {'leva':>6}")
|
||||
for _, r in P.iterrows():
|
||||
star = " <== raccomandazione" if (r["pol"] == rr["pol"] and r["lev"] == rr["lev"]) else ""
|
||||
print(f" {r['pz']:>8.1%} {r['p50']:>9.1%} {r['med']:>10.2f} {r['pol']:>26} "
|
||||
f"{r['lev']:>6.2f}{star}")
|
||||
print("\n 📌 Il prezzo del punto scelto, in unita' comprensibili: passare dal punto a")
|
||||
print(" P(zero) minima a quello a P(>=50/g) massima costa "
|
||||
f"{(F['pz'].max()-F['pz'].min())*100:.0f}pp di probabilita' di finire a ZERO per "
|
||||
f"comprare {(F['p50'].max()-F['p50'].min())*100:.1f}pp di probabilita' di 50 EUR/g.")
|
||||
tick("§4")
|
||||
|
||||
# ============================== §5 ATTACCO 4 — il gate che il canale non ha mai passato
|
||||
hr("§5. ATTACCO 4 — IL NULL A DRIFT ZERO sulla griglia di 360 celle di PROP-ALLOC")
|
||||
grid = PA.simplex_grid(("TP01", "SKH01", "XS01"), STEP_SIMPLEX)
|
||||
n_cells = len(grid) * len(LEVS)
|
||||
print(f" Griglia di SELEZIONE dichiarata da PROP-ALLOC: {len(grid)} vettori di peso "
|
||||
f"(passo 1/{STEP_SIMPLEX}) x {len(LEVS)} leve = **{n_cells} celle**.")
|
||||
print(" Conteggio dei trial al RIALZO, come vuole il progetto: PROP-ALLOC 360 di selezione")
|
||||
print(" + la seconda firm (360) + PROP-RECAL su 3 finestre + le politiche del 25/07")
|
||||
print(" -> l'ordine di grandezza del multiple testing sul CANALE e' ~1.000-2.000 celle,")
|
||||
print(" e nessuno di questi filoni ha mai calcolato un deflated-Sharpe sul canale.")
|
||||
print("\n IL NULL: stesso pannello, stessa lente, stesse regole, stessa griglia — ma il")
|
||||
print(" DRIFT del libro azzerato (`R - mean(R)` per ogni vettore di peso). Domanda: quanto")
|
||||
print(" J estrae un argmax su 360 politiche quando NON c'e' edge? (Con drift zero si passa")
|
||||
print(" un'eval del +10% per pura varianza: il null NON e' zero.)")
|
||||
ev_n = PA.boot_idx(len(I), EV_H, N_NULL_J, seed=SEED_EV)
|
||||
fu_n = PA.boot_idx(len(I), FU_H, N_NULL_J, seed=SEED_FU)
|
||||
RG = {}
|
||||
for w in grid:
|
||||
key = PA.wkey(w)
|
||||
fw = GC.fvec_for(w, I, rate, off_m, xs_ann)
|
||||
Rw, Gw = AP.rg_off(w, I, XSP[ph_m], off_m)
|
||||
RG[key] = (Rw - fw, Gw)
|
||||
|
||||
def scan_grid(shift_to_zero: bool, ev_i, fu_i) -> np.ndarray:
|
||||
out = np.empty(n_cells)
|
||||
i = 0
|
||||
for w in grid:
|
||||
Rw, Gw = RG[PA.wkey(w)]
|
||||
Rz = Rw - Rw.mean() if shift_to_zero else Rw
|
||||
for lv in LEVS:
|
||||
out[i] = PR.obj_rg(Rz, Gw, lv, ev_i, fu_i, factor=DELUCK)["J"]
|
||||
i += 1
|
||||
bump(n_cells)
|
||||
return out
|
||||
|
||||
t = time.time()
|
||||
J_real = scan_grid(False, ev_n, fu_n)
|
||||
print(f"\n OSSERVATO ({time.time()-t:.0f}s): max-J su {n_cells} celle = **{J_real.max():.3f}** "
|
||||
f"| mediana {np.median(J_real):.3f} | p90 {np.percentile(J_real,90):.3f}")
|
||||
el = time.time() - T0
|
||||
per_scan = max(time.time() - t, 1.0)
|
||||
n_draws = N_NULL_DRAWS
|
||||
while n_draws > 2 and el + (n_draws + 1) * per_scan > BUDGET_S:
|
||||
n_draws -= 1
|
||||
if n_draws != N_NULL_DRAWS:
|
||||
print(f" ⚠ BUDGET: a {el:.0f}s con {per_scan:.0f}s a scansione, le estrazioni di null")
|
||||
print(f" scendono da {N_NULL_DRAWS} a {n_draws}. Dichiarato, non nascosto: la MEDIA")
|
||||
print(f" del max-J del null e' piu' rumorosa, la sua TAGLIA no.")
|
||||
nulls = []
|
||||
for b in range(n_draws):
|
||||
ev_i = PA.boot_idx(len(I), EV_H, N_NULL_J, seed=9000 + 2 * b)
|
||||
fu_i = PA.boot_idx(len(I), FU_H, N_NULL_J, seed=9001 + 2 * b)
|
||||
Jn = scan_grid(True, ev_i, fu_i)
|
||||
nulls.append(Jn)
|
||||
print(f" null #{b+1}: max-J {Jn.max():.3f} · mediana {np.median(Jn):.3f} "
|
||||
f"({time.time()-t:.0f}s)")
|
||||
N = np.vstack(nulls)
|
||||
nmax = N.max(axis=1)
|
||||
print(f"\n NULL A DRIFT ZERO ({n_draws} estrazioni, {N_NULL_J} percorsi ciascuna):")
|
||||
print(f" max-J atteso dal PURO RUMORE = **{nmax.mean():.3f}** "
|
||||
f"[min {nmax.min():.3f}, max {nmax.max():.3f}]")
|
||||
print(f" OSSERVATO {J_real.max():.3f} -> eccesso **{J_real.max()-nmax.mean():+.3f}** "
|
||||
f"({(J_real.max()/max(nmax.mean(),1e-9)-1)*100:+.0f}%)")
|
||||
print(f" estrazioni di null che BATTONO l'osservato: {int((nmax>=J_real.max()).sum())}"
|
||||
f"/{n_draws}")
|
||||
a5 = "CONFERMATA" if (0.25 <= nmax.mean() <= 0.45) else "REFUTATA"
|
||||
print(f" -> A5 (max-J del null fra 0,25 e 0,45) {a5}")
|
||||
# crescita del massimo col numero di celle: la sensibilita' del verdetto al CONTEGGIO
|
||||
print(f"\n Sensibilita' al CONTEGGIO dei trial (max-J del null su sottoinsiemi casuali):")
|
||||
rng = np.random.default_rng(4242)
|
||||
print(f" {'k celle':>9} {'max-J null':>11} {'osservato':>11} {'eccesso':>9}")
|
||||
for k in (45, 90, 180, 360):
|
||||
mk = np.array([N[b, rng.choice(n_cells, size=min(k, n_cells), replace=False)].max()
|
||||
for b in range(n_draws) for _ in range(4)])
|
||||
ok = np.array([J_real[rng.choice(n_cells, size=min(k, n_cells), replace=False)].max()
|
||||
for _ in range(20)]).mean()
|
||||
print(f" {k:>9} {mk.mean():>11.3f} {ok:>11.3f} {ok-mk.mean():>+9.3f}")
|
||||
# il gate STANDARD del progetto, puntato sul canale — e la sua CECITA' alla leva
|
||||
import altlib as AL # noqa: E402
|
||||
sh_w, ret_w = {}, {}
|
||||
for w in grid:
|
||||
Rw, Gw = RG[PA.wkey(w)]
|
||||
Rd = PA.deluck(Rw, DELUCK)
|
||||
v = float(Rd.std(ddof=1) * np.sqrt(365))
|
||||
sh_w[PA.wkey(w)] = float(Rd.mean() * 365 / v) if v > 0 else 0.0
|
||||
ret_w[PA.wkey(w)] = Rd
|
||||
kb = max(sh_w, key=sh_w.get)
|
||||
dsr, sr0 = AL.deflated_sharpe(sh_w[kb], list(sh_w.values()), ret_w[kb])
|
||||
print(f"\n IL GATE STANDARD, per confronto — `altlib.deflated_sharpe` sulla stessa griglia:")
|
||||
print(f" Sharpe migliore {sh_w[kb]:.3f} ({kb}) · attesa dal rumore su N={len(sh_w)} "
|
||||
f"trial {sr0:.3f} · **DSR {dsr:.3f} {'PASS' if dsr >= 0.95 else 'FAIL'}**")
|
||||
print(f" 🚨 Ma N e' **{len(sh_w)}, non {n_cells}**: lo Sharpe e' INVARIANTE ALLA LEVA")
|
||||
print(f" (`obj_rg` lo calcola su `Rd`, non su `Rd*lev`), quindi le {n_cells-len(sh_w)}")
|
||||
print(" celle che differiscono SOLO per la leva sono **invisibili** al gate standard.")
|
||||
print(" E' esattamente la lezione di §33/§53: la leva non la vede nessun gate del")
|
||||
print(" progetto. Su un conto a BARRIERA la leva e' la variabile che decide.")
|
||||
|
||||
# il libro LIVE, che NON e' stato selezionato, come incumbent
|
||||
fL = GC.fvec_for(W_LIVE, I, rate, off_m, xs_ann)
|
||||
RL, GL = AP.rg_off(W_LIVE, I, XSP[ph_m], off_m)
|
||||
jL_all = [PR.obj_rg(RL - fL, GL, lv, ev_n, fu_n, factor=DELUCK)["J"] for lv in LEVS]
|
||||
bump(len(LEVS))
|
||||
print(f"\n L'INCUMBENT (libro LIVE 75/25, che NON e' stato selezionato su questa griglia):")
|
||||
print(f" J max sulle 8 leve = {max(jL_all):.3f} -> anche l'incumbent "
|
||||
f"{'SUPERA' if max(jL_all) > nmax.mean() else 'NON supera'} il max del null.")
|
||||
print(" ⚠ CONTROLLO NEGATIVO obbligatorio: una estrazione di null trattata come se fosse")
|
||||
print(" il candidato deve dare eccesso ~0 contro le altre.")
|
||||
held = nmax[0]
|
||||
rest = nmax[1:]
|
||||
print(f" null #1 come candidato: max-J {held:.3f} contro media delle altre "
|
||||
f"{rest.mean():.3f} -> eccesso {held-rest.mean():+.3f} "
|
||||
f"({'OK, ~0' if abs(held-rest.mean()) < 0.05 else 'ATTENZIONE'})")
|
||||
print("\n 📌 Cosa dice e cosa NON dice: **non** dice che il canale sia rumore — l'eccesso")
|
||||
print(" e' misurato e positivo. Dice che **una parte non piccola del J pubblicato la")
|
||||
print(" produce la varianza attraverso una barriera**, e che nessuno l'aveva sottratta.")
|
||||
tick("§5")
|
||||
|
||||
# ================================================================== §6 il verdetto
|
||||
hr("§6. LA DOMANDA FINALE — se il canale funded fosse un CANDIDATO-SLEEVE, passerebbe?")
|
||||
ded = J_real.max() - nmax.mean()
|
||||
checks = [
|
||||
("edge in-sample (J > incumbent)", dj0 > 0.05,
|
||||
f"dJ {dj0:+.3f} contro il libro live"),
|
||||
("deflated / null di selezione", ded > 0.10,
|
||||
f"eccesso sul max del null a drift zero {ded:+.3f} su {n_cells} celle"),
|
||||
("robustezza al PARAMETRO (-1 SE)", dj_m1 > 0.05,
|
||||
f"dJ a -1 SE del drift di XS01 = {dj_m1:+.3f}"),
|
||||
("robustezza a -2 SE", dj_m2 > 0.05, f"dJ a -2 SE = {dj_m2:+.3f}"),
|
||||
("assunzione esogena stimata", (not np.isnan(p_be)) and p_be > p_hi,
|
||||
f"morte-firm: assunta {p_saved:.0%}, evidenza {p_lo:.0%}-{p_hi:.0%}, "
|
||||
f"break-even {p_be:.0%}" if not np.isnan(p_be) else
|
||||
f"morte-firm: assunta {p_saved:.0%}, evidenza {p_lo:.0%}-{p_hi:.0%}, break-even >55%"),
|
||||
("obiettivo non degenere", len(dom) == 0,
|
||||
f"{len(dom)} celle dominano la raccomandazione su (P(>=50/g), P(zero))"),
|
||||
("deflated-Sharpe standard (cieco alla leva)", dsr >= 0.95,
|
||||
f"DSR {dsr:.3f} su N={len(sh_w)} trial visibili di {n_cells} reali"),
|
||||
("fuori dalla finestra di scoperta", True,
|
||||
"§21: XS01 regge fuori campione (+1,12), MA a 13 gambe su 19 e a effetto/MDE ~1,0"),
|
||||
("eseguibilita' verificata al venue", True,
|
||||
"§30: 13/13 gambe `Trading` su Bybit, min notional $5 — PASS misurato"),
|
||||
]
|
||||
print(f" {'gate':>36} {'esito':>8} dettaglio")
|
||||
npass = 0
|
||||
for nm, ok, det in checks:
|
||||
npass += int(ok)
|
||||
print(f" {nm:>36} {'PASS' if ok else 'FAIL':>8} {det}")
|
||||
print(f"\n {npass}/{len(checks)} gate superati.")
|
||||
|
||||
hr("VERDETTO")
|
||||
print(f" 1. REPLICA: P(>=50 EUR/g) {m50:.1%} contro il pubblicato {PUB['p50g']:.1%}, "
|
||||
f"P(zero) {C['pz'].median():.1%} contro {PUB['pzero']:.1%} -> **la macchina e' quella**.")
|
||||
print(f" 2. IL PILASTRO: SE del drift di XS01 = {se_boot:.1%}/anno. A -1 SE il vantaggio "
|
||||
f"del canale passa da {dj0:+.3f} a {dj_m1:+.3f} di J e P(>=50/g) da {p50_0:.1%} a "
|
||||
f"{p50_m1:.1%}.")
|
||||
print(f" 3. LA MORTE-FIRM: evidenza pubblica {p_lo:.0%}-{p_hi:.0%}/anno contro il "
|
||||
f"{p_saved:.0%} assunto; l'EV regge (break-even "
|
||||
f"{'>' + f'{max(PS):.0%}' if np.isnan(p_be) else f'{p_be:.0%}'}), "
|
||||
f"ma P(>=50/g) scende a {p50_at_mid:.1%}.")
|
||||
print(f" 4. LA FRONTIERA: corr(P(>=50/g), P(zero)) = {rho:+.3f}; "
|
||||
f"{len(dom)} celle dominano il punto raccomandato.")
|
||||
print(f" 5. IL GATE MANCANTE: max-J dal puro rumore su {n_cells} celle = {nmax.mean():.3f}, "
|
||||
f"osservato {J_real.max():.3f}, eccesso {ded:+.3f}.")
|
||||
eur_pt = float(C["med"].median())
|
||||
eur_m1 = float(D[D["k"] == -1.0]["med"].median())
|
||||
eur_live = float(D[D["k"] == 0.0]["med_live"].median())
|
||||
print(f"\n IN EUR/GIORNO (mediana, 36 mesi, da 600 euro, funding e fisco dentro):")
|
||||
print(f" canale funded al drift misurato ... {eur_pt:>6.2f} EUR/g")
|
||||
print(f" canale funded a -1 SE ............ {eur_m1:>6.2f} EUR/g")
|
||||
print(f" libro LIVE sullo stesso conto .... {eur_live:>6.2f} EUR/g")
|
||||
print(f" obiettivo dichiarato ............. {pl.TARGET_EUR_DAY:>6.2f} EUR/g")
|
||||
print(f"\n Questo filone NON avvicina i 50 EUR/giorno: non produce ritorno, "
|
||||
f"riduce la fiducia")
|
||||
print(f" nel numero gia' pubblicato. Il suo effetto sulla rendita attesa e' "
|
||||
f"**{eur_m1-eur_pt:+.2f} EUR/g**")
|
||||
print(f" se si crede a -1 SE, e **0,00 EUR/g** se non si cambia nulla (ed e' cio' che "
|
||||
f"raccomanda).")
|
||||
print(f"\n [{TRIALS} valutazioni, {time.time()-T0:.0f}s]")
|
||||
print("=" * 104)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,784 @@
|
||||
"""r0823b_split_deposito — §62 SPLIT-DEPOSITO: quanta parte del versamento va ai BIGLIETTI (2026-08-23).
|
||||
|
||||
IL BUCO CHE CHIUDE
|
||||
------------------
|
||||
`RESULTS-0822 §37` (GATE-C) ha confrontato **due strade INTERE** — tutto sul libro contro tutto in
|
||||
biglietti prop — e il biglietto ha vinto a EUR 500/mese (mediana $39.820 contro $22.444,
|
||||
P(>=50/g) 13,7% contro 0,0%). Ma **«tutto o niente» non e' una politica: e' un caso limite.**
|
||||
Nessuno ha misurato (a) la FRAZIONE ottima del versamento, (b) se debba CAMBIARE nel tempo,
|
||||
(c) su quale colonna si sta ottimizzando — e le colonne hanno ottimi diversi.
|
||||
|
||||
LE MIE ATTESE, REGISTRATE PRIMA DI MISURARE (metodo obbligatorio, punto 1)
|
||||
-------------------------------------------------------------------------
|
||||
A1. **La forma della politica ottima, PREVISTA dalla struttura** (mandato punto 4). I due canali
|
||||
hanno rendimenti di scala OPPOSTI:
|
||||
* il LIBRO e' *neutrale alla scala* — Sharpe piatto da $600 a $200k (25/07 §3), quindi
|
||||
ogni dollaro rende la stessa percentuale: la ricchezza e' LINEARE nel capitale versato;
|
||||
* il BIGLIETTO ha rendimenti di scala **DECRESCENTI e a gradini**: il prezzo e' FISSO
|
||||
($249-$1.080) e il nozionale che sblocca e' FISSO ($25k-$200k) — **indipendente dal
|
||||
proprio capitale**; sotto $249 un dollaro in piu' non fa nulla, e sopra il cap
|
||||
($200k/firm, 6 slot concorrenti) nemmeno.
|
||||
⇒ **PREVISIONE: la politica ottima e' FRONT-LOADED e SATURANTE** — biglietti prima (quando il
|
||||
libro ha una base di capitalizzazione ridicola e un biglietto sblocca 150x il conto), libro
|
||||
dopo (quando il canale biglietto e' saturo). Cioe': `TIXFIRST` batte `BOOKFIRST`, e phi
|
||||
DECRESCENTE col capitale batte phi CRESCENTE.
|
||||
A2. **Gli ottimi delle tre colonne NON coincidono**: P(>=50/g) crescente in phi con argmax a
|
||||
phi=1 (a 36-60 mesi **solo** il canale biglietto puo' produrre un prelievo); ricchezza
|
||||
MEDIANA con argmax a phi BASSO (la strada-biglietto e' bimodale, §37); P(rovina) crescente
|
||||
in phi. **Se e' cosi', «la frazione ottima» non esiste finche' non si dice quale colonna.**
|
||||
A3. **A 60 mesi l'ottimo phi SCENDE**: la morte-firm 10%/anno rende il canale biglietto un canale
|
||||
a VITA FINITA (P(entrambe le firm morte a 5 anni) ~17%), mentre il libro capitalizza per
|
||||
sempre. E' l'unica ragione strutturale per cui l'orizzonte dovrebbe muovere l'ottimo.
|
||||
A4. **Il trabocchetto §44 si eredita, e con lo stesso segno**: mettere una CONDIZIONE RARA davanti
|
||||
all'acquisto di un biglietto costa, perche' ritarda le giocate di una lotteria ripetuta.
|
||||
Mi aspetto un costo **proporzionale al tempo d'attesa**, come in §44 — e lo misuro contro un
|
||||
**gemello a ritardo deterministico** di pari attesa media, che e' il modo per separare il
|
||||
costo della CONDIZIONE da quello del RITARDO.
|
||||
A5. **La riga onesta**: mi aspetto che l'ampiezza dovuta alla frazione sia GRANDE sulla colonna
|
||||
P(>=50/g) — ma solo perche' il libro vale ZERO su quella colonna a questo orizzonte (non e'
|
||||
un difetto del libro, e' l'orizzonte: servono $313k) — e PICCOLA sulla colonna che misura la
|
||||
ricchezza. Se esce cosi', va detto che **la frazione conta sulla metrica sbagliata**.
|
||||
|
||||
LA FAMIGLIA, DICHIARATA PRIMA DI MISURARE (mandato punto 3)
|
||||
-----------------------------------------------------------
|
||||
F(phi) frazione FISSA del versamento ai biglietti, phi = 0,00 ... 1,00 (11 celle).
|
||||
phi=1 == la strada BIGLIETTO di §37; phi=0 == la strada LIBRO di §37.
|
||||
TIX(X) **biglietti prima**: phi=1 finche' il cumulato versato ai biglietti < $X,
|
||||
poi phi=0. X = 700 (= solo il capitale iniziale = "biglietti solo dai payout",
|
||||
perche' i payout tornano sempre nel pot biglietti), 2.000, 5.000, 10.000.
|
||||
BOOK(X) **libro prima**: phi=0 finche' il libro < $X, poi phi=1. X = 2.000, 5.000,
|
||||
10.000, 20.000.
|
||||
PHI_UP(X) phi CRESCE col capitale: phi = clip(libro / X, 0, 1), X = 10.000.
|
||||
PHI_DN(X) phi DECRESCE col capitale: phi = 1 - clip(libro / X, 0, 1), X = 10.000.
|
||||
CASH(B) phi=1 ma la cassa in eccesso oltre $B va al libro (B = 1.200 = il biglietto piu'
|
||||
caro). E' l'unica politica che puo' DOMINARE phi=1 per costruzione.
|
||||
RARE(d) phi=1 ma si COMPRA solo dopo un drawdown >= d dell'equity della strategia
|
||||
(d = 10%, 20%): il trabocchetto §44 applicato al biglietto.
|
||||
DELAY(g) phi=1 ma non si compra prima del giorno g: **gemello deterministico** di RARE.
|
||||
Il capitale iniziale ($654) e' trattato come il "versamento del mese 0" e segue la stessa regola.
|
||||
|
||||
CONVENZIONI EREDITATE E CONGELATE (nessuna scelta nuova => nessun trial da deflazionare)
|
||||
-----------------------------------------------------------------------------------------
|
||||
* Simulatore biglietti: copia di `r0822d_gate_c.ladder` (che e' bit-exact vs `r0725_prop_coupled.
|
||||
simulate`) con l'aggiunta del pot LIBRO. Lente **ACCOPPIATA** (tuple (R, gap) del recon): su una
|
||||
regola a UN GIORNO la close-only e' esattamente CIECA (25/07).
|
||||
* Conto funded: W_A = TP01 38 / SKH01 38 / XS01 25 (centro della regione robusta di PROP-RECAL),
|
||||
XS01 = U13 Binance, finestra W-LONG 2021+, **funding DENTRO**, de-luck sul drift **x0,89**.
|
||||
* Pot LIBRO: `FD.book_75_25(rate, alloc=654)` = libro LIVE 75/25 con min-order $5 e funding
|
||||
dentro, de-luck x0,89, fisco 33% sulla variazione annua con riporto (convenzione di
|
||||
`r0822d_gate_c.book_road` = quella di `r0807_piano_netto`). ⚠ Dichiarato: NON contiene la
|
||||
patrimoniale 0,2%/anno che `PN.accumula` invece include; la differenza e' misurata in §1.
|
||||
* Morte-firm 10%/anno, cap $200k/firm, 6 slot, deposito **rimborsato al primo payout** (§37),
|
||||
soglia di prelievo $100 di quota trader, fisco 33% sui payout, EUR/USD 1,09.
|
||||
* Rendita: `perp` e muro dalla lente **L3 CONGIUNTA** (`PN.muro`), cioe' la stessa che produce i
|
||||
$313k del piano.
|
||||
|
||||
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0823b_split_deposito.py`
|
||||
Rete: NESSUNA (`--no-net` forzato: parquet certificati + cache Binance/funding gia' su disco).
|
||||
"""
|
||||
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]
|
||||
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"):
|
||||
sys.path.insert(0, str(_p))
|
||||
|
||||
if "--no-net" not in sys.argv: # r0822d_funding legge USE_NET all'import
|
||||
sys.argv.append("--no-net")
|
||||
|
||||
import r0725_prop_coupled as pc # noqa: E402 lente accoppiata (25/07)
|
||||
import r0725_prop_ladder as pl # noqa: E402 regole firm + scala di conti (25/07)
|
||||
import r0807_piano_netto as PN # noqa: E402 muro / accumula (07/08)
|
||||
import r0727_tasse as TX # noqa: E402 patrimoniale
|
||||
import r0822_prop_alloc as PA # noqa: E402 J, bootstrap (22/08)
|
||||
import r0822b_prop_recal as PR # noqa: E402 PROP-RECAL (22/08)
|
||||
import r0822b_xs01_oos as X # noqa: E402 universo U13 + cache Binance
|
||||
import r0822c_skh_anchor_prop as AP # noqa: E402 offset di SKH01
|
||||
import r0822d_funding as FD # noqa: E402 il funding (22/08)
|
||||
import r0822d_gate_c as GC # noqa: E402 §37: ladder + book_road
|
||||
from src.portfolio.sleeves import XS_UNIVERSE # noqa: E402
|
||||
|
||||
FD.USE_NET = False
|
||||
|
||||
EURUSD = pl.EURUSD
|
||||
TAX_RATE = pl.TAX_RATE
|
||||
SPLIT = pl.SPLIT
|
||||
DELUCK = PA.DELUCK # x0,89 MISURATO (r0726_deluck_factor)
|
||||
FIRMS = pl.FIRMS
|
||||
N_SLOT = pl.MAX_CONCURRENT
|
||||
TARGET = pl.TARGET_EUR_DAY
|
||||
START_EUR = 600.0 # $654 = il conto reale (convenzione di §37)
|
||||
MIN_PAYOUT = GC.MIN_PAYOUT_TRADER # $100 di quota trader
|
||||
BLOCK = 20
|
||||
SEED = 20260823
|
||||
N_BASE = 1500 # risoluzione dichiarata: ~+-1pp su P(>=50/g)
|
||||
DEP_MAIN = 500.0 # il piano in corso (config/live.json, 26/07)
|
||||
CHEAP = min(t[1] for F in FIRMS for t in F["tickets"]) # $249
|
||||
|
||||
# celle d'ancora: sotto-campione SISTEMATICO dichiarato a priori sui 23 offset x 3 fasi di §32
|
||||
CELLS = ((0, 0), (150, 3), (300, 7), (450, 0), (600, 3))
|
||||
|
||||
# numeri PUBBLICATI da riprodurre prima di produrne di nuovi
|
||||
PUB = dict(muro=313_000.0, p50g_37=0.137, med_tix_37=39_820.0, med_book_37=22_444.0,
|
||||
med_tix_0=132.0, med_book_0=787.0, p50g_0=0.042, pzero_37=0.403, p50g_anchor=0.026)
|
||||
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
def clock() -> str:
|
||||
return f"[{time.time()-T0:5.0f}s]"
|
||||
|
||||
|
||||
def hr(t: str = "") -> None:
|
||||
print("\n" + "=" * 108)
|
||||
if t:
|
||||
print(f" {t}")
|
||||
print("=" * 108)
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# LE POLITICHE — (etichetta, tipo, parametro). Dichiarate sopra, nel docstring.
|
||||
# ===========================================================================================
|
||||
def phi_of(kind: str, par: float, book: np.ndarray, cash: np.ndarray,
|
||||
to_tix: np.ndarray) -> np.ndarray:
|
||||
"""Frazione del versamento che va al pot BIGLIETTI. Vettoriale sul blocco di percorsi."""
|
||||
if kind == "FIX":
|
||||
return np.full(len(book), float(par))
|
||||
if kind == "TIX": # biglietti prima
|
||||
return (to_tix < par).astype(float)
|
||||
if kind == "BOOK": # libro prima
|
||||
return (book >= par).astype(float)
|
||||
if kind == "PHI_UP": # cresce col capitale
|
||||
return np.clip(book / par, 0.0, 1.0)
|
||||
if kind == "PHI_DN": # decresce col capitale
|
||||
return 1.0 - np.clip(book / par, 0.0, 1.0)
|
||||
if kind in ("CASH", "RARE", "DELAY"): # phi = 1, la differenza e' altrove
|
||||
return np.ones(len(book))
|
||||
raise ValueError(kind)
|
||||
|
||||
|
||||
def make_policies(full: bool = True) -> list[tuple[str, str, float]]:
|
||||
P: list[tuple[str, str, float]] = []
|
||||
grid = np.round(np.arange(0.0, 1.001, 0.10), 2) if full else np.array([0.0, 0.25, 0.50, 0.75, 1.0])
|
||||
for f in grid:
|
||||
P.append((f"F {f:.2f}", "FIX", float(f)))
|
||||
if not full:
|
||||
return P
|
||||
for xk in (700.0, 2_000.0, 5_000.0, 10_000.0):
|
||||
P.append((f"TIX ${xk:,.0f}", "TIX", xk))
|
||||
for xk in (2_000.0, 5_000.0, 10_000.0, 20_000.0):
|
||||
P.append((f"BOOK ${xk:,.0f}", "BOOK", xk))
|
||||
P.append(("PHI_UP $10k", "PHI_UP", 10_000.0))
|
||||
P.append(("PHI_DN $10k", "PHI_DN", 10_000.0))
|
||||
P.append(("CASH-ATT $1.2k", "CASH", 1_200.0))
|
||||
P.append(("RARE dd 10%", "RARE", 0.10))
|
||||
P.append(("RARE dd 20%", "RARE", 0.20))
|
||||
P.append(("DELAY 95g", "DELAY", 95.0))
|
||||
P.append(("DELAY 713g", "DELAY", 713.0))
|
||||
return P
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
# IL SIMULATORE COMBINATO — pot BIGLIETTI + pot LIBRO sullo STESSO percorso
|
||||
# ===========================================================================================
|
||||
def sim_split(R: np.ndarray, G: np.ndarray, rb: np.ndarray, pols: list, n_base: int,
|
||||
lev: float, seed: int, n_days: int, deposit_eur: float = DEP_MAIN,
|
||||
start_eur: float = START_EUR, refund: bool = True,
|
||||
min_payout: float = MIN_PAYOUT, deluck: float = DELUCK,
|
||||
block: int = BLOCK, tax: float = TAX_RATE) -> dict:
|
||||
"""Estende `r0822d_gate_c.ladder` con un secondo pot (il LIBRO) e con una politica di SPLIT
|
||||
del versamento mensile. Le politiche sono impilate come BLOCCHI DI PERCORSI: stesso
|
||||
bootstrap, stessa morte-firm (tilata), stesso ordine di estrazione => **confronto APPAIATO
|
||||
perfetto** fra politiche.
|
||||
|
||||
Con `pols=[("x","FIX",1.0)]`, `refund=False`, `deposit=0`, `min_payout=0` dev'essere
|
||||
BIT-EXACT rispetto a `GC.ladder` (controllo R2). Con `pols=[("x","FIX",0.0)]` il pot libro
|
||||
dev'essere BIT-EXACT rispetto a `GC.book_road` (controllo R3).
|
||||
"""
|
||||
n_pol = len(pols)
|
||||
P = n_pol * n_base
|
||||
L = len(R)
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
Rd = R.astype(float) - (1.0 - deluck) * float(R.astype(float).mean())
|
||||
Gd = G.astype(float)
|
||||
Bd = rb.astype(float) - (1.0 - deluck) * float(rb.astype(float).mean())
|
||||
|
||||
n_blocks = int(np.ceil(n_days / block))
|
||||
starts0 = rng.integers(0, L - block, size=(n_base, n_blocks)) # stesso stream di GC
|
||||
starts = np.tile(starts0, (n_pol, 1)) # ...e TILATO: appaiato
|
||||
deaths0 = rng.random((n_days, n_base, len(FIRMS))) # morte-firm, tilata
|
||||
p_death_day = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
|
||||
|
||||
pid = np.repeat(np.arange(n_pol), n_base)
|
||||
sl = [slice(p * n_base, (p + 1) * n_base) for p in range(n_pol)]
|
||||
kinds = [k for _, k, _ in pols]
|
||||
pars = [float(v) for _, _, v in pols]
|
||||
|
||||
# --- stato pot BIGLIETTI (identico a GC.ladder) ---------------------------------------
|
||||
cash = np.zeros(P)
|
||||
phase = np.full((P, N_SLOT), -1, np.int8)
|
||||
eq = np.ones((P, N_SLOT))
|
||||
firm = np.full((P, N_SLOT), -1, np.int8)
|
||||
notional = np.zeros((P, N_SLOT))
|
||||
price_paid = np.zeros((P, N_SLOT))
|
||||
refunded = np.zeros((P, N_SLOT), bool)
|
||||
firm_alive = np.ones((P, len(FIRMS)), bool)
|
||||
used_f = np.zeros((P, len(FIRMS))) # nozionale aperto per firm, mantenuto INCREMENTALE
|
||||
cum_payout = np.zeros(P)
|
||||
spent = np.zeros(P)
|
||||
got_back = np.zeros(P)
|
||||
n_payouts = np.zeros(P, int)
|
||||
n_bought = np.zeros(P, int)
|
||||
to_tix = np.zeros(P) # cumulato VERSATO al pot biglietti (non i payout)
|
||||
to_book = np.zeros(P)
|
||||
# --- stato pot LIBRO (identico a GC.book_road) ----------------------------------------
|
||||
bookc = np.zeros(P)
|
||||
basis = np.zeros(P)
|
||||
carry = np.zeros(P)
|
||||
# --- equity della strategia (per la condizione RARE): osservabile, indipendente dal pot -
|
||||
eqs = np.ones(P)
|
||||
peak = np.ones(P)
|
||||
|
||||
def deposita(amount: np.ndarray) -> None:
|
||||
"""Ripartisce `amount` (per percorso) fra i due pot secondo la politica del blocco."""
|
||||
for p in range(n_pol):
|
||||
s = sl[p]
|
||||
f = phi_of(kinds[p], pars[p], bookc[s], cash[s], to_tix[s])
|
||||
a = amount[s]
|
||||
cash[s] += f * a
|
||||
to_tix[s] += f * a
|
||||
bookc[s] += (1.0 - f) * a
|
||||
basis[s] += (1.0 - f) * a
|
||||
to_book[s] += (1.0 - f) * a
|
||||
|
||||
deposita(np.full(P, start_eur * EURUSD)) # il capitale iniziale = "mese 0"
|
||||
|
||||
for t in range(n_days):
|
||||
di = starts[:, t // block] + (t % block)
|
||||
r_col = Rd[di] * lev
|
||||
g_col = Gd[di] * lev
|
||||
rb_t = Bd[di]
|
||||
|
||||
d_t = np.tile(deaths0[t], (n_pol, 1)) < p_death_day
|
||||
newly_dead = d_t & firm_alive
|
||||
firm_alive &= ~d_t
|
||||
if newly_dead.any():
|
||||
for fi in range(len(FIRMS)):
|
||||
kill = newly_dead[:, fi]
|
||||
if kill.any():
|
||||
hit = (firm == fi) & kill[:, None]
|
||||
phase[hit] = -1
|
||||
notional[hit] = 0.0
|
||||
used_f[kill, fi] = 0.0
|
||||
|
||||
# --- gating dell'acquisto: default aperto, chiuso per RARE/DELAY ------------------
|
||||
allow = np.ones(P, bool)
|
||||
for p in range(n_pol):
|
||||
if kinds[p] == "RARE":
|
||||
allow[sl[p]] = (eqs[sl[p]] / peak[sl[p]] - 1.0) <= -pars[p]
|
||||
elif kinds[p] == "DELAY":
|
||||
allow[sl[p]] = t >= pars[p]
|
||||
|
||||
# --- acquisto biglietti (ordine identico a GC.ladder) ----------------------------
|
||||
for k in range(N_SLOT):
|
||||
free = (phase[:, k] == -1) & allow
|
||||
if not free.any():
|
||||
continue
|
||||
for fi, F in enumerate(FIRMS):
|
||||
used = used_f[:, fi] # vista: si aggiorna da sola sotto
|
||||
for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]):
|
||||
buy = (free & firm_alive[:, fi] & (cash >= price)
|
||||
& (used + size <= F["cap"]))
|
||||
if not buy.any():
|
||||
continue
|
||||
cash[buy] -= price
|
||||
spent[buy] += price
|
||||
n_bought[buy] += 1
|
||||
phase[buy, k] = 0
|
||||
eq[buy, k] = 1.0
|
||||
firm[buy, k] = fi
|
||||
notional[buy, k] = size
|
||||
price_paid[buy, k] = price
|
||||
refunded[buy, k] = False
|
||||
used_f[buy, fi] += size
|
||||
free = free & ~buy
|
||||
if not free.any():
|
||||
break
|
||||
if not free.any():
|
||||
break
|
||||
|
||||
# --- il giorno di mercato ---------------------------------------------------------
|
||||
r = r_col[:, None]
|
||||
g = g_col[:, None]
|
||||
act = phase >= 0
|
||||
eq_prev = eq
|
||||
eq = np.where(act, eq * (1.0 + r), eq)
|
||||
eq_low = np.where(act, eq_prev * (1.0 + r + g), eq)
|
||||
r_low = r + g
|
||||
|
||||
for fi, F in enumerate(FIRMS):
|
||||
s_ = act & (firm == fi)
|
||||
if not s_.any():
|
||||
continue
|
||||
ev_t, ev_dd, ev_dl = F["ev"]
|
||||
fu_ml, fu_dl = F["fu"]
|
||||
in_ev = s_ & (phase == 0)
|
||||
in_fu = s_ & (phase == 1)
|
||||
bust = ((in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl)))
|
||||
| (in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))))
|
||||
if bust.any():
|
||||
used_f[:, fi] -= np.where(bust, notional, 0.0).sum(axis=1)
|
||||
phase[bust] = -1
|
||||
notional[bust] = 0.0
|
||||
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
|
||||
phase[passed] = 1
|
||||
eq[passed] = 1.0
|
||||
|
||||
# --- il pot LIBRO + l'equity osservabile della strategia --------------------------
|
||||
bookc = np.maximum(bookc * (1.0 + rb_t), 0.0)
|
||||
eqs = eqs * (1.0 + rb_t)
|
||||
peak = np.maximum(peak, eqs)
|
||||
|
||||
# --- fine mese: payout, rimborso, cassa attiva, versamento ------------------------
|
||||
if (t + 1) % 30 == 0:
|
||||
fu = phase == 1
|
||||
pay = fu & (eq > 1.0) & (((eq - 1.0) * notional * SPLIT) >= min_payout)
|
||||
gain = np.where(pay, (eq - 1.0) * notional, 0.0)
|
||||
net = gain.sum(axis=1) * SPLIT * (1.0 - tax)
|
||||
cum_payout += net
|
||||
cash += net
|
||||
n_payouts += (pay.sum(axis=1) > 0).astype(int)
|
||||
eq = np.where(pay, 1.0, eq)
|
||||
if refund:
|
||||
back = pay & ~refunded
|
||||
amt = np.where(back, price_paid, 0.0).sum(axis=1)
|
||||
cash += amt
|
||||
got_back += amt
|
||||
refunded |= back
|
||||
for p in range(n_pol): # CASH-ATT: eccesso di cassa al libro
|
||||
if kinds[p] == "CASH":
|
||||
s = sl[p]
|
||||
ex = np.maximum(cash[s] - pars[p], 0.0)
|
||||
cash[s] -= ex
|
||||
bookc[s] += ex
|
||||
basis[s] += ex
|
||||
if deposit_eur:
|
||||
deposita(np.full(P, deposit_eur * EURUSD))
|
||||
|
||||
# --- fine anno: fisco sul pot LIBRO (convenzione GC.book_road) --------------------
|
||||
if (t + 1) % 365 == 0:
|
||||
gg = (bookc - basis) + carry
|
||||
due = np.where(gg > 0, tax * gg, 0.0)
|
||||
carry = np.where(gg > 0, 0.0, gg)
|
||||
bookc = bookc - due
|
||||
basis = bookc.copy()
|
||||
|
||||
years = n_days / 365.0
|
||||
flow = cum_payout / EURUSD / years / 365.0
|
||||
wealth = cash + bookc
|
||||
contributed = start_eur * EURUSD + deposit_eur * EURUSD * (n_days // 30)
|
||||
out = []
|
||||
for p, (lab, kind, par) in enumerate(pols):
|
||||
s = sl[p]
|
||||
out.append(dict(
|
||||
lab=lab, kind=kind, par=par,
|
||||
flow=flow[s], wealth=wealth[s], cash=cash[s], book=bookc[s],
|
||||
payout=cum_payout[s], spent=spent[s], back=got_back[s],
|
||||
to_tix=to_tix[s], to_book=to_book[s], nbuy=n_bought[s],
|
||||
npay=n_payouts[s], contributed=contributed))
|
||||
return dict(rows=out, contributed=contributed, years=years)
|
||||
|
||||
|
||||
def summ(d: dict, perp: float) -> dict:
|
||||
"""Le colonne che si leggono. `rend` = ricchezza terminale x rendita perpetua (la
|
||||
definizione di EUR 50/giorno del PIANO); `flow` = payout medi (la definizione di §37)."""
|
||||
w, f = d["wealth"], d["flow"]
|
||||
rend = w * perp / 365.0 / EURUSD
|
||||
return dict(
|
||||
lab=d["lab"],
|
||||
p50_flow=float((f >= TARGET).mean()),
|
||||
p50_rend=float((rend >= TARGET).mean()),
|
||||
med_w=float(np.median(w)), mean_w=float(w.mean()),
|
||||
p10_w=float(np.percentile(w, 10)), p90_w=float(np.percentile(w, 90)),
|
||||
med_flow=float(np.median(f)), med_rend=float(np.median(rend)),
|
||||
p_loss=float((w < d["contributed"]).mean()),
|
||||
p_ruin=float((w < CHEAP).mean()),
|
||||
p_nopay=float((d["payout"] <= 0).mean()),
|
||||
med_tix=float(np.median(d["to_tix"])), nbuy=float(np.mean(d["nbuy"])))
|
||||
|
||||
|
||||
# ===========================================================================================
|
||||
def main() -> None:
|
||||
try:
|
||||
sys.stdout.reconfigure(line_buffering=True)
|
||||
except Exception: # pragma: no cover
|
||||
pass
|
||||
hr("§62 SPLIT-DEPOSITO — quanta parte del versamento mensile va ai BIGLIETTI prop?")
|
||||
print(" §37 ha confrontato due CASI LIMITE (tutto libro / tutto biglietti). Qui si misura la")
|
||||
print(" CURVA, e se la frazione debba dipendere dallo stato. Attese registrate nel docstring:")
|
||||
print(" A1 politica ottima FRONT-LOADED e saturante (TIX batte BOOK, phi decrescente batte")
|
||||
print(" crescente), perche' il libro e' neutrale alla scala e il biglietto ha rendimenti")
|
||||
print(" DECRESCENTI a gradini (prezzo fisso, nozionale fisso, cap, indivisibilita');")
|
||||
print(" A2 gli ottimi delle tre colonne NON coincidono; A3 a 60 mesi l'ottimo phi SCENDE;")
|
||||
print(" A4 il trabocchetto §44 si eredita col segno negativo; A5 la frazione conta molto")
|
||||
print(" su P(>=50/g) solo perche' il libro vale ZERO su quella colonna a 36-60 mesi.")
|
||||
|
||||
# ================================================================= dati
|
||||
hr(f"§0. IL PANNELLO — le stesse serie di §37 (U13 Binance, W-LONG 2021+) {clock()}")
|
||||
Cb, Ob, Hb, Lb = PR.ohlc_panels(PR.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])
|
||||
phases = sorted({ph for _, ph in CELLS})
|
||||
XSP = {ph: PR.xs_tuples(p13, o13, h13, l13, phase=ph) for ph in phases}
|
||||
xs0 = XSP[0]
|
||||
base = pc.crypto_daily_tuples(1.0, 0.0).index.intersection(xs0.index)
|
||||
first13 = xs0.index[np.flatnonzero(xs0["R"].values != 0.0)[0]]
|
||||
I = base[base >= max(first13, pd.Timestamp("2021-01-01", tz="UTC"))]
|
||||
print(f" W-LONG: {len(I)} giorni {I[0].date()} -> {I[-1].date()} | de-luck x{DELUCK}")
|
||||
|
||||
rate = GC.funding_rate()
|
||||
if rate is None:
|
||||
print(" !! cache funding assente: il filone NON gira senza funding (lente L3). STOP.")
|
||||
return
|
||||
xs_ann = GC.xs_funding_measure(verbose=False)["ann"]
|
||||
print(f" funding: cache OK | XS01 paga {xs_ann*100:+.2f}%/anno (riproduzione di §37)")
|
||||
|
||||
rb_full = FD.book_75_25(rate, alloc=654.0)
|
||||
if rb_full.index.tz is None:
|
||||
rb_full.index = rb_full.index.tz_localize("UTC")
|
||||
rb = np.nan_to_num(rb_full.reindex(I).values.astype(float))
|
||||
print(f" pot LIBRO (75/25 a $654, funding dentro): drift {rb.mean()*365*100:.2f}%/anno, "
|
||||
f"vol {rb.std()*np.sqrt(365)*100:.1f}%, Sharpe {rb.mean()*365/(rb.std()*np.sqrt(365)):.2f}")
|
||||
|
||||
W_A = {"TP01": 0.38, "SKH01": 0.38, "XS01": 0.25}
|
||||
ev_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8220)
|
||||
fu_b = PA.boot_idx(len(I), 365, PA.N_PATHS, seed=8221)
|
||||
RG: dict = {}
|
||||
for off, ph in CELLS:
|
||||
f = GC.fvec_for(W_A, I, rate, off, xs_ann)
|
||||
R0, G0 = AP.rg_off(W_A, I, XSP[ph], off)
|
||||
Rx = R0 - f
|
||||
j, lv, _ = GC.best_lev(Rx, G0, ev_b, fu_b)
|
||||
RG[(off, ph)] = (Rx, G0, lv, j)
|
||||
print(f"\n {len(CELLS)} celle d'ancora (sotto-campione sistematico a priori sui 23 offset x "
|
||||
f"3 fasi di §32):")
|
||||
for (off, ph), (Rx, _, lv, j) in RG.items():
|
||||
print(f" offset {off:>3} min, fase {ph} -> J {j:.3f} @ leva {lv:.3f}x "
|
||||
f"drift {Rx.mean()*365*100:+6.2f}%/anno")
|
||||
|
||||
# ================================================================= §1 repliche
|
||||
hr(f"§1. LE REPLICHE — prima di produrre un numero nuovo {clock()}")
|
||||
ok: dict[str, bool] = {}
|
||||
|
||||
# R1: muro L3 (la rendita perpetua che converte capitale in EUR/giorno)
|
||||
import r0822d_piano_vero as PV # noqa: E402
|
||||
B3 = PV.costruisci_serie()
|
||||
r_l3 = PV.deluck(B3["fund"].values.astype(float))
|
||||
netto = TARGET * 365 * EURUSD
|
||||
perp, muro = PN.muro(r_l3, netto, TAX_RATE, TX.PATRIMONIALE)
|
||||
ok["R1"] = abs(muro - PUB["muro"]) < 3_000.0
|
||||
print(f" R1 muro L3 CONGIUNTA: perpetua {perp:.2%} · ${muro:,.0f} pubblicato $313k"
|
||||
f" {'OK' if ok['R1'] else 'DIVERGE'}")
|
||||
print(f" (il pot LIBRO di questo filone gira su `alloc=654` invece del default "
|
||||
f"`alloc={FD.ALLOC:.0f}` di L3: drift "
|
||||
f"{rb.mean()*365*100:.2f}% contro {PV.deluck(B3['fund'].values.astype(float)).mean()*365*100:.2f}% "
|
||||
f"-> differenza dichiarata, non nascosta)")
|
||||
|
||||
# R2: il mio simulatore == GC.ladder (che e' bit-exact vs pc.simulate)
|
||||
cell0 = CELLS[0]
|
||||
R0, G0, LV0, _ = RG[cell0]
|
||||
ref = GC.ladder(R0, G0, N_BASE, LV0, SEED, refund=False, min_payout=0.0, deposit_eur=0.0)
|
||||
mine = sim_split(R0, G0, rb, [("ctl", "FIX", 1.0)], N_BASE, LV0, SEED, 1080,
|
||||
deposit_eur=0.0, refund=False, min_payout=0.0)
|
||||
a = np.sort(ref["eur_day"])
|
||||
b = np.sort(mine["rows"][0]["flow"])
|
||||
d2 = float(np.abs(ref["eur_day"] - mine["rows"][0]["flow"]).max())
|
||||
ok["R2"] = d2 < 1e-12
|
||||
print(f" R2 sim_split(phi=1) vs GC.ladder, percorso per percorso: max|diff| = {d2:.3e}"
|
||||
f" {'BIT-EXACT' if ok['R2'] else 'DIVERGE'} (ordinati {np.abs(a-b).max():.3e})")
|
||||
|
||||
# R3: il pot libro == GC.book_road, sugli stessi sorteggi
|
||||
n_blocks = int(np.ceil(1080 / BLOCK))
|
||||
rng_c = np.random.default_rng(SEED)
|
||||
st_c = rng_c.integers(0, len(I) - BLOCK, size=(N_BASE, n_blocks))
|
||||
BOOT_C = (st_c[:, :, None] + np.arange(BLOCK)[None, None, :]).reshape(N_BASE, -1)[:, :1080]
|
||||
brd = GC.book_road(rb, BOOT_C, START_EUR, DEP_MAIN)
|
||||
mineb = sim_split(R0, G0, rb, [("ctl", "FIX", 0.0)], N_BASE, LV0, SEED, 1080,
|
||||
deposit_eur=DEP_MAIN)
|
||||
d3 = float(np.abs(np.sort(brd["cash"]) - np.sort(mineb["rows"][0]["wealth"])).max())
|
||||
ok["R3"] = d3 < 1e-8
|
||||
print(f" R3 sim_split(phi=0) vs GC.book_road (stessi blocchi): max|diff| = ${d3:.3e}"
|
||||
f" {'IDENTICO' if ok['R3'] else 'DIVERGE'}")
|
||||
|
||||
# R4: gli estremi di §37, alla banda d'ancora (la cella mediana di §37 non e' pubblicata)
|
||||
print(f"\n R4 GLI ESTREMI DI §37 — le due strade intere, sulle {len(CELLS)} celle d'ancora")
|
||||
print(f" (§37 gira sulla SUA cella mediana, non pubblicata: qui si verifica che i suoi")
|
||||
print(f" numeri cadano DENTRO la banda, non che coincidano)")
|
||||
print(f"\n {'versamento':>11} {'strada':>10} {'mediana (banda celle)':>34} "
|
||||
f"{'media':>12} {'P(>=50/g)':>22}")
|
||||
r37, _r4 = {}, []
|
||||
for dep in (0.0, DEP_MAIN):
|
||||
for lab, phi in (("BIGLIETTO", 1.0), ("LIBRO", 0.0)):
|
||||
med, mea, p50 = [], [], []
|
||||
for c in CELLS:
|
||||
Rc, Gc, lvc, _ = RG[c]
|
||||
s = sim_split(Rc, Gc, rb, [(lab, "FIX", phi)], N_BASE, lvc, SEED, 1080,
|
||||
deposit_eur=dep)
|
||||
q = summ(s["rows"][0], perp)
|
||||
med.append(q["med_w"]); mea.append(q["mean_w"]); p50.append(q["p50_flow"])
|
||||
r37[(dep, lab)] = (np.median(med), np.median(mea), np.median(p50))
|
||||
pubm = PUB["med_tix_37"] if (dep and lab == "BIGLIETTO") else (
|
||||
PUB["med_book_37"] if dep else (PUB["med_tix_0"] if lab == "BIGLIETTO"
|
||||
else PUB["med_book_0"]))
|
||||
inside = min(med) <= pubm <= max(med)
|
||||
_r4.append(bool(inside))
|
||||
print(f" EUR{dep:>7,.0f}/m {lab:>10} ${np.median(med):>10,.0f} "
|
||||
f"[${min(med):>8,.0f},${max(med):>9,.0f}] ${np.median(mea):>11,.0f} "
|
||||
f"{np.median(p50):>9.1%} [{min(p50):>5.1%},{max(p50):>5.1%}] "
|
||||
f"{'PUB dentro' if inside else 'PUB fuori'}")
|
||||
ok["R4"] = all(_r4)
|
||||
print(f" §37 pubblica: BIGLIETTO EUR500 ${PUB['med_tix_37']:,.0f} / P {PUB['p50g_37']:.1%} · "
|
||||
f"LIBRO ${PUB['med_book_37']:,.0f} · BIGLIETTO EUR0 ${PUB['med_tix_0']:,.0f} · "
|
||||
f"LIBRO EUR0 ${PUB['med_book_0']:,.0f}")
|
||||
print(f"\n REPLICHE: {sum(ok.values())}/{len(ok)} "
|
||||
f"{'tutte OK' if all(ok.values()) else 'ATTENZIONE: ' + str([k for k,v in ok.items() if not v])}")
|
||||
if not (ok["R2"] and ok["R3"]):
|
||||
print(" Senza R2/R3 i numeri sotto sarebbero di un'altra macchina. STOP.")
|
||||
return
|
||||
|
||||
# ================================================================= §2 la curva
|
||||
hr(f"§2. LA CURVA DELLA FRAZIONE — 36 mesi, EUR {DEP_MAIN:.0f}/mese, {N_BASE} percorsi {clock()}")
|
||||
print(" Tutte le politiche girano nello STESSO Monte Carlo (blocchi impilati): stesso")
|
||||
print(" bootstrap, stessa morte-firm => ogni delta e' APPAIATO percorso per percorso.")
|
||||
POLS = make_policies(full=True)
|
||||
RES: dict = {}
|
||||
for c in CELLS:
|
||||
Rc, Gc, lvc, _ = RG[c]
|
||||
s = sim_split(Rc, Gc, rb, POLS, N_BASE, lvc, SEED, 1080, deposit_eur=DEP_MAIN)
|
||||
RES[c] = {r["lab"]: r for r in s["rows"]}
|
||||
print(f" cella {c} fatta {clock()}")
|
||||
CONTRIB = s["contributed"]
|
||||
|
||||
|
||||
fix_labs = [p[0] for p in POLS if p[1] == "FIX"]
|
||||
print(f"\n Versato in 36 mesi: ${CONTRIB:,.0f} (capitale iniziale ${START_EUR*EURUSD:,.0f} "
|
||||
f"+ 36 x EUR{DEP_MAIN:.0f})")
|
||||
print(f"\n {'phi':>6} {'P(>=50/g) FLUSSO':>26} {'P(>=50/g) RENDITA':>18} "
|
||||
f"{'ricch. MEDIANA':>26} {'media':>11} {'P(<versato)':>12} {'P(<$249)':>10} "
|
||||
f"{'EUR/g med':>10}")
|
||||
print(f" {'':>6} {'(la metrica di §37)':>26} {'(la metrica del PIANO)':>18}")
|
||||
curva = []
|
||||
for lab in fix_labs:
|
||||
q = {c: summ(RES[c][lab], perp) for c in CELLS}
|
||||
g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731
|
||||
lohi = lambda k: (min(q[c][k] for c in CELLS), max(q[c][k] for c in CELLS)) # noqa: E731
|
||||
p50, pl_, ph_ = g("p50_flow"), *lohi("p50_flow")
|
||||
mw, mlo, mhi = g("med_w"), *lohi("med_w")
|
||||
rendm = g("med_flow") + g("med_rend")
|
||||
curva.append(dict(lab=lab, phi=float(lab.split()[1]), p50=p50, med=mw,
|
||||
mean=g("mean_w"), ploss=g("p_loss"), pruin=g("p_ruin"),
|
||||
prend=g("p50_rend"), eurg=rendm))
|
||||
print(f" {lab.split()[1]:>6} {p50:>13.1%} [{pl_:>5.1%},{ph_:>5.1%}] "
|
||||
f"{g('p50_rend'):>17.1%} ${mw:>10,.0f} [${mlo:>7,.0f},${mhi:>8,.0f}] "
|
||||
f"${g('mean_w'):>10,.0f} {g('p_loss'):>12.1%} {g('p_ruin'):>10.1%} {rendm:>10.2f}")
|
||||
CV = pd.DataFrame(curva)
|
||||
a_p50 = CV.loc[CV["p50"].idxmax()]
|
||||
a_med = CV.loc[CV["med"].idxmax()]
|
||||
a_mean = CV.loc[CV["mean"].idxmax()]
|
||||
a_saf = CV.loc[CV["ploss"].idxmin()]
|
||||
print(f"\n ARGMAX PER COLONNA — la risposta al mandato punto 2:")
|
||||
print(f" P(>=50/g) flusso : phi = {a_p50['phi']:.2f} ({a_p50['p50']:.1%})")
|
||||
print(f" ricchezza MEDIANA: phi = {a_med['phi']:.2f} (${a_med['med']:,.0f})")
|
||||
print(f" ricchezza MEDIA : phi = {a_mean['phi']:.2f} (${a_mean['mean']:,.0f})")
|
||||
print(f" P(<versato) min : phi = {a_saf['phi']:.2f} ({a_saf['ploss']:.1%})")
|
||||
print(f" P(>=50/g) RENDITA: max {CV['prend'].max():.1%} su TUTTE le celle "
|
||||
f"-> {'nessuno arriva' if CV['prend'].max() < 0.005 else 'qualcuno arriva'}")
|
||||
print(f" >>> ATTESA A2 ('gli ottimi non coincidono'): "
|
||||
f"{'CONFERMATA' if len({round(a_p50['phi'],2), round(a_med['phi'],2)}) > 1 else 'SMENTITA'}")
|
||||
|
||||
# ---- risoluzione MC, misurata (mandato: riportarla accanto a ogni delta) --------------
|
||||
print(f"\n §2.1 RISOLUZIONE MONTE CARLO — misurata su 3 semi (non assunta) {clock()}")
|
||||
probe = [("F 0.00", "FIX", 0.0), ("F 0.50", "FIX", 0.5), ("F 1.00", "FIX", 1.0)]
|
||||
Rc, Gc, lvc, _ = RG[CELLS[0]]
|
||||
seeds = (SEED, SEED + 101, SEED + 202)
|
||||
tabS = {}
|
||||
for sd in seeds:
|
||||
s = sim_split(Rc, Gc, rb, probe, N_BASE, lvc, sd, 1080, deposit_eur=DEP_MAIN)
|
||||
tabS[sd] = {r["lab"]: summ(r, perp) for r in s["rows"]}
|
||||
sp_lvl = max(max(tabS[sd][l]["p50_flow"] for sd in seeds)
|
||||
- min(tabS[sd][l]["p50_flow"] for sd in seeds) for l in ("F 0.50", "F 1.00"))
|
||||
d_pair = [tabS[sd]["F 1.00"]["p50_flow"] - tabS[sd]["F 0.50"]["p50_flow"] for sd in seeds]
|
||||
sp_pair = max(d_pair) - min(d_pair)
|
||||
print(f" {'seme':>10} {'P50 phi=0.5':>13} {'P50 phi=1.0':>13} {'delta appaiato':>16} "
|
||||
f"{'med. ricch. phi=0':>18}")
|
||||
for sd in seeds:
|
||||
print(f" {sd:>10} {tabS[sd]['F 0.50']['p50_flow']:>13.2%} "
|
||||
f"{tabS[sd]['F 1.00']['p50_flow']:>13.2%} "
|
||||
f"{tabS[sd]['F 1.00']['p50_flow']-tabS[sd]['F 0.50']['p50_flow']:>+16.2%} "
|
||||
f"${tabS[sd]['F 0.00']['med_w']:>17,.0f}")
|
||||
print(f" ampiezza fra semi: LIVELLI {sp_lvl*100:.2f} pp · DIFFERENZE APPAIATE "
|
||||
f"{sp_pair*100:.2f} pp -> ogni delta sotto ~{max(sp_pair,0.0)*100:.1f} pp e' RUMORE")
|
||||
|
||||
# ================================================================= §3 politiche di stato
|
||||
hr(f"§3. LE POLITICHE DIPENDENTI DALLO STATO — famiglia dichiarata prima {clock()}")
|
||||
print(f" {'politica':>16} {'P(>=50/g)':>22} {'ricch. mediana':>15} {'media':>11} "
|
||||
f"{'P(<versato)':>12} {'P(<$249)':>10} {'$ ai biglietti':>15} {'biglietti':>10}")
|
||||
fam = [p[0] for p in POLS if p[1] in ("TIX", "BOOK", "PHI_UP", "PHI_DN", "CASH")]
|
||||
ref_lab = "F 1.00"
|
||||
qref = {c: summ(RES[c][ref_lab], perp) for c in CELLS}
|
||||
for lab in [ref_lab, "F 0.00"] + fam:
|
||||
q = {c: summ(RES[c][lab], perp) for c in CELLS}
|
||||
g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731
|
||||
dp = float(np.median([q[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS]))
|
||||
npos = sum(1 for c in CELLS if q[c]["p50_flow"] - qref[c]["p50_flow"] > 0)
|
||||
mark = "" if lab == ref_lab else f" d {dp*100:+5.2f}pp ({npos}/{len(CELLS)})"
|
||||
print(f" {lab:>16} {g('p50_flow'):>9.1%}{mark:>13} ${g('med_w'):>14,.0f} "
|
||||
f"${g('mean_w'):>10,.0f} {g('p_loss'):>12.1%} {g('p_ruin'):>10.1%} "
|
||||
f"${g('med_tix'):>14,.0f} {g('nbuy'):>10.1f}")
|
||||
tixb = float(np.median([summ(RES[c]["TIX $5,000"], perp)["p50_flow"] for c in CELLS]))
|
||||
bookb = float(np.median([summ(RES[c]["BOOK $5,000"], perp)["p50_flow"] for c in CELLS]))
|
||||
up = float(np.median([summ(RES[c]["PHI_UP $10k"], perp)["p50_flow"] for c in CELLS]))
|
||||
dn = float(np.median([summ(RES[c]["PHI_DN $10k"], perp)["p50_flow"] for c in CELLS]))
|
||||
print(f"\n >>> ATTESA A1 (front-loaded): TIX $5k {tixb:.1%} contro BOOK $5k {bookb:.1%} "
|
||||
f"-> {'CONFERMATA' if tixb > bookb else 'SMENTITA'}; "
|
||||
f"phi DECRESCENTE {dn:.1%} contro CRESCENTE {up:.1%} "
|
||||
f"-> {'CONFERMATA' if dn > up else 'SMENTITA'}")
|
||||
cashp = {c: summ(RES[c]["CASH-ATT $1.2k"], perp) for c in CELLS}
|
||||
d_cash = float(np.median([cashp[c]["med_w"] - qref[c]["med_w"] for c in CELLS]))
|
||||
d_cash50 = float(np.median([cashp[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS]))
|
||||
print(f" CASH-ATT (l'unica che puo' dominare phi=1): ricchezza mediana "
|
||||
f"${d_cash:+,.0f}, P(>=50/g) {d_cash50*100:+.2f}pp "
|
||||
f"({sum(1 for c in CELLS if cashp[c]['med_w'] > qref[c]['med_w'])}/{len(CELLS)} celle)")
|
||||
|
||||
# ================================================================= §4 il trabocchetto §44
|
||||
hr(f"§4. IL TRABOCCHETTO DI §44 — la CONDIZIONE davanti al bonifico {clock()}")
|
||||
print(" §44: «comprare meglio e comprare tardi sono la stessa mossa». Qui la condizione non")
|
||||
print(" ritarda un BONIFICO ma un ACQUISTO di biglietto. Si separa il costo della CONDIZIONE")
|
||||
print(" da quello del RITARDO con un GEMELLO DETERMINISTICO di pari attesa media.")
|
||||
print(f"\n {'politica':>14} {'attesa media (g)':>18} {'P(>=50/g)':>11} {'d appaiato':>12} "
|
||||
f"{'ricch. mediana':>15} {'d':>11}")
|
||||
# attesa media della condizione, misurata sulla stessa serie
|
||||
for lab in ("F 1.00", "RARE dd 10%", "RARE dd 20%", "DELAY 95g", "DELAY 713g"):
|
||||
q = {c: summ(RES[c][lab], perp) for c in CELLS}
|
||||
g = lambda k: float(np.median([q[c][k] for c in CELLS])) # noqa: E731
|
||||
dp = float(np.median([q[c]["p50_flow"] - qref[c]["p50_flow"] for c in CELLS]))
|
||||
dm = float(np.median([q[c]["med_w"] - qref[c]["med_w"] for c in CELLS]))
|
||||
wait = {"F 1.00": 0.0, "DELAY 95g": 95.0, "DELAY 713g": 713.0}.get(lab, float("nan"))
|
||||
if np.isnan(wait):
|
||||
d_ = float(lab.split()[-1].rstrip("%")) / 100.0
|
||||
eqp = np.cumprod(1.0 + PV.deluck(rb))
|
||||
dd = eqp / np.maximum.accumulate(eqp) - 1.0
|
||||
hit = np.flatnonzero(dd <= -d_)
|
||||
wait = float(hit[0]) if len(hit) else float("inf")
|
||||
print(f" {lab:>14} {wait:>18.0f} {g('p50_flow'):>11.1%} {dp*100:>+11.2f}pp "
|
||||
f"${g('med_w'):>14,.0f} ${dm:>+10,.0f}")
|
||||
print(" (l'attesa di RARE e' il primo giorno in cui la condizione si verifica sulla serie")
|
||||
print(" storica de-luckata: nel bootstrap e' una variabile aleatoria, qui e' un'ancora)")
|
||||
|
||||
# ================================================================= §5 la scala
|
||||
hr(f"§5. I RENDIMENTI DI SCALA — la previsione A1, misurata direttamente {clock()}")
|
||||
print(" Il canale BIGLIETTO assorbe soldi solo a gradini (prezzo fisso, 6 slot, cap firm).")
|
||||
print(" Quanto ne assorbe davvero, e quando satura?")
|
||||
Rc, Gc, lvc, _ = RG[CELLS[0]]
|
||||
print(f"\n {'versamento':>12} {'$ versati ai bigl.':>19} {'$ SPESI in biglietti':>21} "
|
||||
f"{'assorbimento':>13} {'cassa finale med':>17} {'biglietti comprati':>19}")
|
||||
for dep in (0.0, 250.0, 500.0, 1_000.0, 2_000.0):
|
||||
s = sim_split(Rc, Gc, rb, [("F 1.00", "FIX", 1.0)], N_BASE, lvc, SEED, 1080,
|
||||
deposit_eur=dep)
|
||||
r = s["rows"][0]
|
||||
got = float(np.mean(r["to_tix"]))
|
||||
sp = float(np.mean(r["spent"]))
|
||||
print(f" EUR{dep:>8,.0f}/m ${got:>18,.0f} ${sp:>20,.0f} {sp/max(got,1e-9):>12.1%} "
|
||||
f"${np.median(r['cash']):>16,.0f} {np.mean(r['nbuy']):>19.1f}")
|
||||
print(" ('assorbimento' = quanta parte dei soldi mandati al pot biglietti finisce davvero")
|
||||
print(" in un biglietto; il resto resta CASSA FERMA — e' il costo dell'indivisibilita'.)")
|
||||
|
||||
print(f"\n §5.1 LA FRAZIONE CONTRO L'IMPORTO — la domanda che decide {clock()}")
|
||||
print(f" {'versamento':>12} {'phi=0 P(>=50/g)':>16} {'phi=1 P(>=50/g)':>16} "
|
||||
f"{'ampiezza phi':>13} {'phi=0 ricch.med':>16} {'phi=1 ricch.med':>16}")
|
||||
prev = None
|
||||
imp_rows = []
|
||||
for dep in (0.0, 250.0, 500.0, 1_000.0):
|
||||
s = sim_split(Rc, Gc, rb, [("F 0.00", "FIX", 0.0), ("F 1.00", "FIX", 1.0)],
|
||||
N_BASE, lvc, SEED, 1080, deposit_eur=dep)
|
||||
q0, q1 = summ(s["rows"][0], perp), summ(s["rows"][1], perp)
|
||||
imp_rows.append((dep, q0, q1))
|
||||
print(f" EUR{dep:>8,.0f}/m {q0['p50_flow']:>16.1%} {q1['p50_flow']:>16.1%} "
|
||||
f"{(q1['p50_flow']-q0['p50_flow'])*100:>+12.1f}pp ${q0['med_w']:>15,.0f} "
|
||||
f"${q1['med_w']:>15,.0f}")
|
||||
prev = (q0, q1)
|
||||
d_phi = imp_rows[2][2]["p50_flow"] - imp_rows[2][1]["p50_flow"]
|
||||
d_dep_tix = imp_rows[2][2]["p50_flow"] - imp_rows[1][2]["p50_flow"]
|
||||
d_dep_book = imp_rows[2][1]["med_w"] - imp_rows[1][1]["med_w"]
|
||||
print(f"\n A EUR500/m: passare phi 0->1 vale {d_phi*100:+.1f}pp di P(>=50/g); passare il")
|
||||
print(f" VERSAMENTO da EUR250 a EUR500 (a phi=1) vale {d_dep_tix*100:+.1f}pp.")
|
||||
print(f" Sulla RICCHEZZA (phi=0): EUR250 -> EUR500 vale ${d_dep_book:+,.0f} di mediana.")
|
||||
|
||||
# ================================================================= §6 60 mesi
|
||||
hr(f"§6. ORIZZONTE 60 MESI — l'unica ragione strutturale per cui l'ottimo dovrebbe muoversi {clock()}")
|
||||
p5 = 1.0 - (1.0 - pl.FIRM_DEATH_PER_YEAR) ** 5
|
||||
print(f" A 5 anni ogni firm muore con p = {p5:.0%}; entrambe con p = {p5**2:.0%}. Il canale")
|
||||
print(f" biglietto ha VITA FINITA, il libro no. Attesa A3: l'ottimo phi SCENDE.")
|
||||
POLS60 = ([("F 0.00", "FIX", 0.0), ("F 0.20", "FIX", 0.20), ("F 0.50", "FIX", 0.50),
|
||||
("F 0.80", "FIX", 0.80), ("F 1.00", "FIX", 1.0),
|
||||
("TIX $5,000", "TIX", 5_000.0), ("BOOK $5,000", "BOOK", 5_000.0),
|
||||
("PHI_DN $10k", "PHI_DN", 10_000.0), ("CASH-ATT $1.2k", "CASH", 1_200.0)])
|
||||
R60: dict = {}
|
||||
for c in CELLS[:3]:
|
||||
Rc2, Gc2, lvc2, _ = RG[c]
|
||||
s = sim_split(Rc2, Gc2, rb, POLS60, N_BASE, lvc2, SEED, 1800, deposit_eur=DEP_MAIN)
|
||||
R60[c] = {r["lab"]: r for r in s["rows"]}
|
||||
print(f"\n {'politica':>16} {'P(>=50/g) 36m':>14} {'P(>=50/g) 60m':>14} {'ricch.med 60m':>15} "
|
||||
f"{'media 60m':>12} {'P(<versato)':>12} {'P(>=50/g) RENDITA 60m':>22}")
|
||||
for lab, _, _ in POLS60:
|
||||
q36 = float(np.median([summ(RES[c][lab], perp)["p50_flow"] for c in CELLS[:3]]))
|
||||
q = {c: summ(R60[c][lab], perp) for c in CELLS[:3]}
|
||||
g = lambda k: float(np.median([q[c][k] for c in CELLS[:3]])) # noqa: E731
|
||||
print(f" {lab:>16} {q36:>14.1%} {g('p50_flow'):>14.1%} ${g('med_w'):>14,.0f} "
|
||||
f"${g('mean_w'):>11,.0f} {g('p_loss'):>12.1%} {g('p50_rend'):>22.1%}")
|
||||
c60 = pd.DataFrame([dict(phi=float(l.split()[1]),
|
||||
p50=float(np.median([summ(R60[c][l], perp)["p50_flow"]
|
||||
for c in CELLS[:3]])),
|
||||
med=float(np.median([summ(R60[c][l], perp)["med_w"]
|
||||
for c in CELLS[:3]])))
|
||||
for l, k, _ in POLS60 if k == "FIX"])
|
||||
print(f"\n argmax P(>=50/g): 36 mesi phi={a_p50['phi']:.2f} · 60 mesi "
|
||||
f"phi={c60.loc[c60['p50'].idxmax(),'phi']:.2f}")
|
||||
print(f" argmax ricchezza mediana: 36 mesi phi={a_med['phi']:.2f} · 60 mesi "
|
||||
f"phi={c60.loc[c60['med'].idxmax(),'phi']:.2f}")
|
||||
print(f" >>> ATTESA A3 ('a 60 mesi l'ottimo phi scende'): "
|
||||
f"{'CONFERMATA' if c60.loc[c60['p50'].idxmax(),'phi'] < a_p50['phi'] else 'SMENTITA'}")
|
||||
|
||||
# ================================================================= §7 verdetto
|
||||
hr(f"§7. VERDETTO {clock()}")
|
||||
sp = max(sp_pair, 0.0) * 100
|
||||
rng_p50 = (CV["p50"].max() - CV["p50"].min()) * 100
|
||||
rng_med = CV["med"].max() - CV["med"].min()
|
||||
print(f" Ampiezza della CURVA fra phi=0 e phi=1 (36 mesi, EUR{DEP_MAIN:.0f}/m):")
|
||||
print(f" P(>=50/g) flusso : {rng_p50:.1f} pp (risoluzione MC {sp:.1f} pp) -> "
|
||||
f"{'SEGNALE' if rng_p50 > 3*sp else 'DENTRO IL RUMORE'}")
|
||||
print(f" ricchezza mediana: ${rng_med:,.0f}")
|
||||
print(f" P(>=50/g) rendita: {(CV['prend'].max()-CV['prend'].min())*100:.1f} pp")
|
||||
print(f" P(<versato) : {(CV['ploss'].max()-CV['ploss'].min())*100:.1f} pp")
|
||||
print(f"\n In EUR/giorno (mediana, flusso + rendita terminale — le due NON si sommano")
|
||||
print(f" senza doppio conteggio, si leggono separate):")
|
||||
for lab in ("F 0.00", "F 0.50", "F 1.00"):
|
||||
row = CV[CV["lab"] == lab].iloc[0]
|
||||
print(f" {lab}: EUR {row['eurg']:.2f}/giorno mediani a 36 mesi")
|
||||
print(f"\n Il target e' EUR {TARGET:.0f}/giorno.")
|
||||
print(f" TRIALS: nessuna cella SELEZIONATA (la famiglia e' dichiarata nel docstring e tutte")
|
||||
print(f" le celle sono riportate) -> nessun deflated-Sharpe da calcolare; e la grandezza")
|
||||
print(f" misurata e' una P, non uno Sharpe.")
|
||||
print(f"\n {clock()} fine.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user