26f8d27a61
Goal: migliorare la strategia short-vol (famiglia Albimarini/VRP01) e proteggerla dai DD. Workflow 26 agenti (7 filoni + 2 lenti avversariali + scettico incrociato). Esito: NON migliorabile; la protezione DD si compra SOLO con la size. - Griglia 288 strutture: nessuna batte VRP01 (DSR 0.000; meta' griglia = 3a occorrenza "0-perdite/Sharpe implausibile" dopo CC01/ALB-A). - 4 overlay DD (exit-spike/SL-MTM/ala-coda/cooldown): 4/4 REFUTED dal null de-levering — la protezione crash vive gia' nel gate IV-rank. - Gate nuovi: 4o fallimento su 4 (l'alpha e' il binario IV-rank>0.30). - Sizing: 12% deploy ~ 0.27 Kelly onesto (anti-rovina); disambiguazione unita' obbligatoria vs 12% peso book (0.014 Kelly, fattore 19x). - Gate term-structure VIX/VXV su SPX (dSh +0.90, DSR 0.992) = confound di modello al 100% -> nuova regola: riprezzare term-structure-consistent prima di credere a un gate vol su strutture BS-flat. - ANCHOR-AUDIT VRP01 CHIUSO: primo sleeve SENZA luck (fase canonica = peggiore delle 7 -> numeri di ammissione conservativi). Audit anchor ora completo su 4/4 sleeve ancorati. Book/pesi INVARIATI. Nessun nuovo sleeve. 168 test verdi. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
656 lines
35 KiB
Python
656 lines
35 KiB
Python
#!/usr/bin/env python
|
|
"""r0703_vrpimp_sizing.py — FILONE 3: SIZING ANTI-ROVINA per lo sleeve short-vol defined-risk.
|
|
|
|
DOMANDA: ALB-B ha mostrato che il sizing 1->4 di Albimarini porta alla rovina (1998/2002/2020).
|
|
Qual e' la politica di sizing OTTIMA per uno sleeve put-credit-spread ETH su Deribit a 2.000-5.000$?
|
|
|
|
MOTORE (riusato, non riscritto): la struttura VRP01 canonica (put credit spread 7g, short delta
|
|
-0.28 / long -0.10, gate CANONICO vrp>0 + ivr>=0.30 + crash-skip 0.90 — NON riottimizzato, lezione
|
|
2026-07-01) prezzata col motore ALB-A (r0702_alb_structure): BS flat su DVOL reale, fee Deribit
|
|
PER GAMBA 0.03% notional cap 12.5% premio + delivery 0.015% su ITM. ETH-only: a 2-5k solo ETH e'
|
|
granulare (BTC min 0.1 = margine ~$470+, diario r0702_capital_scaling). Rendimenti PER-TRADE su
|
|
MARGINE (= max loss defined-risk, width - credito): e' il capitale davvero a rischio, e rende la
|
|
rovina ben definita (perdita massima = 100% del margine impegnato + fee).
|
|
|
|
POLITICHE TESTATE (il sizing NON cambia l'edge: cambia CAGR/DD/P(rovina) — lo Sharpe del flusso
|
|
sottostante e' invariante di scala, quindi qui NON si caccia Sharpe, si mappa la frontiera):
|
|
(a) frazione fissa del capitale q in {5,10,12,15,20,25}% (margine impegnato/equity per settimana)
|
|
(b) vol-scaled: q ∝ 1/DVOL (riferimento = mediana ESPANDENTE causale della DVOL)
|
|
(c) anti-streak: q ridotta dopo N vittorie consecutive (l'OPPOSTO di Albimarini)
|
|
(d) Kelly frazionario con stima ONESTA delle code: distribuzione empirica per-trade IS (pre-2025)
|
|
POOLED sulla banda skew f in {0.6,0.8,1.0,1.3} + coda sintetica (full-loss -102% con probabilita'
|
|
extra = P(move settimanale <= strike long | storia ETH 2019-26 CERTIFICATA) - freq empirica)
|
|
(e) [riferimento negativo] Albimarini 1->4: size crescente con lo streak di vittorie
|
|
|
|
METRICHE: CAGR, maxDD, worst-week, P(rovina -50% / -80% | 5 anni, bootstrap a BLOCCHI L=13 settimane
|
|
che preserva il clustering di regime del gate), a C=2.000 e 5.000$ con granularita' REALE degli
|
|
ordini (size INTERE di spread min 1 ETH/gamba; margine/spread dal dato della settimana stessa —
|
|
la nota task "0.1 ETH" e' incoerente coi numeri citati $66-76 = spread da 1 ETH di r0702_capital_
|
|
scaling sez.5; la sensitivity 0.1 ETH e' riportata a parte). Confronto esplicito col 12% del book
|
|
(VRP01 @12% peso = margine settimanale ~12% del conto, stessa convenzione del conteggio spread del
|
|
diario capital-scaling).
|
|
|
|
REGOLE ONORATE: hold-out 2025-26 MAI usato per selezionare (selezione overlay su MAR in-sample);
|
|
NULL DEL DE-LEVERING esplicito (ogni claim di riduzione DD degli overlay b/c/d confrontato con la
|
|
frazione fissa che raggiunge lo stesso DD); banda f {0.6,0.8,1.0,1.3}; banda d'ancora sulle 7 fasi
|
|
della cadenza settimanale; niente '7D'-origin (qui la cadenza e' a passi di indice, non resample).
|
|
REGOLA STANDING INVARIATA: niente short-vol da modello in deploy — esito = conoscenza/frontiera.
|
|
|
|
uv run python scripts/research/r0703_vrpimp_sizing.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path("/opt/docker/PythagorasGoal")
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
import altlib as al # noqa: E402 (harness di progetto; usato per storia ETH certificata + DSR)
|
|
from scripts.research.options_vrp_lab import bs_put, strike_from_delta, load_series # noqa: E402
|
|
from scripts.research.options_vrp_v2 import _ivrank, _rv30 # noqa: E402
|
|
|
|
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
|
|
WK = 365.25 / 7.0
|
|
TENOR = 7
|
|
F_SWEEP = (0.6, 0.8, 1.0, 1.3)
|
|
CAPITALS = (2000.0, 5000.0)
|
|
MIN_ETH = 1.0 # min size Deribit ETH per gamba (r0702_capital_scaling sez.5)
|
|
Q_CAP = 0.95 # mai impegnare piu' del 95% dell'equity (loss max ~102% del margine)
|
|
FULL_LOSS = -1.02 # full-loss defined-risk incl. fee (empirico: worst -1.018)
|
|
RUIN_LVL = (0.50, 0.80) # rovina = equity sotto (1-lvl)*E0 ... definita come perdita >= lvl
|
|
SEED = 20260703
|
|
|
|
|
|
# ===========================================================================
|
|
# MOTORE PER-TRADE (struttura VRP01, fee ALB-A per gamba, ritorni su MARGINE)
|
|
# ===========================================================================
|
|
def trade_records(f: float = 1.0, phase: int = 0) -> pd.DataFrame:
|
|
"""Vendita settimanale ETH put credit spread -0.28/-0.10, gate canonico. Una riga per
|
|
settimana di cadenza (phase = fase d'ancora 0..6): active, r (ritorno su margine),
|
|
margin ($ per spread da 1 ETH), sig (DVOL a decisione), kl_dist (distanza strike long)."""
|
|
J = load_series("ETH")
|
|
px = J["px"].values
|
|
dv = J["dvol"].values / 100.0
|
|
idx = J.index
|
|
n = len(px)
|
|
T = TENOR / 365.25
|
|
rows = []
|
|
i = 60 + phase
|
|
while i + TENOR < n:
|
|
S0 = px[i]
|
|
sig = dv[i]
|
|
rv = _rv30(px, i)
|
|
ivr = _ivrank(dv, i)
|
|
skip = ((not np.isnan(rv) and (sig - rv) <= 0)
|
|
or (not np.isnan(ivr) and (ivr < 0.30 or ivr > 0.90)))
|
|
if skip:
|
|
rows.append(dict(date=idx[i + TENOR], active=0, r=0.0, margin=0.0,
|
|
sig=sig, win=0, kl_dist=np.nan))
|
|
i += TENOR
|
|
continue
|
|
Ks = strike_from_delta(S0, T, sig, -0.28)
|
|
Kl = strike_from_delta(S0, T, sig, -0.10)
|
|
ps = bs_put(S0, Ks, T, sig) * f
|
|
pl = bs_put(S0, Kl, T, sig) * f
|
|
credit = ps - pl
|
|
width = Ks - Kl
|
|
margin = width - credit # margine defined-risk Deribit ~ max loss
|
|
S1 = px[i + TENOR]
|
|
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
|
|
fee = min(0.0003 * S0, 0.125 * max(ps, 0.0)) + min(0.0003 * S0, 0.125 * max(pl, 0.0))
|
|
deliv = (min(0.00015 * S1, 0.125 * max(0.0, Ks - S1))
|
|
+ min(0.00015 * S1, 0.125 * max(0.0, Kl - S1)))
|
|
pnl = credit - payoff - fee - deliv # $ per spread da 1 ETH
|
|
rows.append(dict(date=idx[i + TENOR], active=1, r=pnl / margin, margin=margin,
|
|
sig=sig, win=int(pnl > 0), kl_dist=Kl / S0 - 1.0))
|
|
i += TENOR
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
# ===========================================================================
|
|
# POLITICHE DI SIZING — q = frazione di equity impegnata come margine questa settimana
|
|
# state: sig (DVOL decisione), ref (mediana espandente DVOL), streak (vittorie consecutive)
|
|
# ===========================================================================
|
|
def pol_fixed(frac):
|
|
return lambda st: frac
|
|
|
|
|
|
def pol_volscaled(frac, lo=0.5, hi=2.0):
|
|
"""q = frac * clip(DVOL_ref/DVOL, lo, hi): meno size quando la vol e' alta. Causale."""
|
|
return lambda st: frac * float(np.clip(st["ref"] / max(st["sig"], 1e-9), lo, hi))
|
|
|
|
|
|
def pol_antistreak(frac, N, mult):
|
|
"""Dopo >=N vittorie consecutive riduci a frac*mult (l'opposto di Albimarini)."""
|
|
return lambda st: frac * (mult if st["streak"] >= N else 1.0)
|
|
|
|
|
|
def pol_alb14(frac):
|
|
"""Riferimento NEGATIVO: Albimarini 1->4, size crescente con le vittorie consecutive."""
|
|
return lambda st: frac * min(1.0 + st["streak"], 4.0)
|
|
|
|
|
|
def simulate(rec: pd.DataFrame, qfun, E0: float, granular: bool = True,
|
|
min_eth: float = MIN_ETH, stress_p: float = 0.0, rng=None,
|
|
q_series=None) -> dict:
|
|
"""Applica la politica al flusso settimanale. granular=True -> size intere di spread
|
|
(floor(budget/margine_spread)); False -> frazione continua (lens frontiera).
|
|
stress_p>0: ogni settimana attiva ha prob extra di full-loss sintetico (coda stress).
|
|
q_series: array di q per riga (bypassa qfun — usato dal null a piazzamento casuale)."""
|
|
E = E0
|
|
eq, rets, dates = [], [], []
|
|
sig_hist: list[float] = []
|
|
streak = 0
|
|
halted = 0
|
|
for t, row in enumerate(rec.itertuples(index=False)):
|
|
ref = float(np.median(sig_hist)) if len(sig_hist) >= 20 else row.sig
|
|
st = dict(sig=row.sig, ref=ref, streak=streak)
|
|
sig_hist.append(row.sig)
|
|
r_wk = 0.0
|
|
if row.active and E > 0:
|
|
q_raw = q_series[t] if q_series is not None else qfun(st)
|
|
q = float(np.clip(q_raw, 0.0, Q_CAP))
|
|
unit = row.margin * min_eth
|
|
if granular:
|
|
n_spread = int((q * E) // unit) if unit > 0 else 0
|
|
committed = n_spread * unit
|
|
if q > 0 and n_spread == 0:
|
|
halted += 1
|
|
else:
|
|
committed = q * E
|
|
r_trade = row.r
|
|
if stress_p > 0.0 and rng is not None and rng.random() < stress_p:
|
|
r_trade = FULL_LOSS
|
|
r_wk = committed * r_trade / E if E > 0 else 0.0
|
|
E = E + committed * r_trade
|
|
if row.active:
|
|
streak = streak + 1 if (row.win and row.r == row.r) else 0
|
|
if not row.win:
|
|
streak = 0
|
|
rets.append(r_wk)
|
|
eq.append(max(E, 0.0))
|
|
dates.append(row.date)
|
|
return dict(dates=pd.DatetimeIndex(dates), eq=np.asarray(eq), rets=np.asarray(rets),
|
|
halted=halted, E_end=E)
|
|
|
|
|
|
def path_metrics(dates, rets) -> dict:
|
|
r = np.asarray(rets, float)
|
|
if len(r) < 3:
|
|
return dict(cagr=0.0, dd=0.0, worst=0.0, sh=0.0)
|
|
eq = np.cumprod(1.0 + r)
|
|
pk = np.maximum.accumulate(eq)
|
|
yrs = len(r) / WK
|
|
cagr = eq[-1] ** (1.0 / yrs) - 1.0 if eq[-1] > 0 else -1.0
|
|
sh = float(r.mean() / r.std() * np.sqrt(WK)) if r.std() > 0 else 0.0
|
|
return dict(cagr=float(cagr), dd=float(np.max((pk - eq) / pk)), worst=float(r.min()), sh=sh)
|
|
|
|
|
|
def full_hold(sim) -> tuple[dict, dict]:
|
|
m_full = path_metrics(sim["dates"], sim["rets"])
|
|
mask = sim["dates"] >= HOLDOUT
|
|
m_hold = path_metrics(sim["dates"][mask], sim["rets"][mask])
|
|
return m_full, m_hold
|
|
|
|
|
|
# ===========================================================================
|
|
# KELLY con code oneste + bootstrap a blocchi
|
|
# ===========================================================================
|
|
def kelly_star(rs: np.ndarray, p_extra: float, tail: float = FULL_LOSS) -> float:
|
|
"""argmax_q E[log(1+q r)] su mistura: con prob p_extra r=tail, altrimenti empirico."""
|
|
qs = np.linspace(0.0, Q_CAP, 191)
|
|
best_q, best_g = 0.0, -np.inf
|
|
for q in qs:
|
|
vals = np.log1p(q * rs)
|
|
g = (1.0 - p_extra) * vals.mean() + p_extra * np.log1p(q * tail)
|
|
if np.isfinite(g) and g > best_g:
|
|
best_g, best_q = g, q
|
|
return float(best_q)
|
|
|
|
|
|
def synthetic_tail_prob(rec: pd.DataFrame) -> tuple[float, float, float]:
|
|
"""p_extra = P(move 7g ETH <= distanza strike long | storia certificata 2019-26)
|
|
- freq empirica di full-loss nel flusso gated. Ritorna (p_extra, p_uncond, p_emp)."""
|
|
d1 = al.get("ETH", "1d")
|
|
px = d1["close"].values.astype(float)
|
|
wk_mv = px[TENOR:] / px[:-TENOR] - 1.0 # mosse 7g overlapping, 2019->oggi
|
|
act = rec[rec.active == 1]
|
|
kl_med = float(act["kl_dist"].median())
|
|
p_uncond = float(np.mean(wk_mv <= kl_med))
|
|
p_emp = float((act["r"] <= -0.9).mean())
|
|
return max(0.0, p_uncond - p_emp), p_uncond, p_emp
|
|
|
|
|
|
def block_bootstrap_ruin(rec: pd.DataFrame, qfun, C: float, npaths: int = 1000,
|
|
horizon_w: int = 261, L: int = 13, stress_p: float = 0.0,
|
|
seed: int = SEED, needs_ref: bool = False) -> dict:
|
|
"""P(rovina | 5 anni): bootstrap a blocchi circolari di L settimane dal flusso storico
|
|
(preserva clustering del gate/regime), politica applicata con granularita' REALE a C.
|
|
needs_ref: calcola la mediana espandente della DVOL solo per le politiche che la usano."""
|
|
rows = list(rec.itertuples(index=False))
|
|
n = len(rows)
|
|
rng = np.random.default_rng(seed)
|
|
ruin = np.zeros((npaths, len(RUIN_LVL)), bool)
|
|
dd30 = np.zeros(npaths, bool)
|
|
cagrs = np.zeros(npaths)
|
|
for p in range(npaths):
|
|
starts = rng.integers(0, n, size=horizon_w // L + 1)
|
|
seq = [rows[(s + k) % n] for s in starts for k in range(L)][:horizon_w]
|
|
E = C
|
|
peak = C
|
|
minE = C
|
|
sig_hist: list[float] = []
|
|
streak = 0
|
|
maxdd = 0.0
|
|
for row in seq:
|
|
if needs_ref:
|
|
ref = float(np.median(sig_hist)) if len(sig_hist) >= 20 else row.sig
|
|
sig_hist.append(row.sig)
|
|
else:
|
|
ref = row.sig
|
|
if row.active and E > 0:
|
|
q = float(np.clip(qfun(dict(sig=row.sig, ref=ref, streak=streak)), 0.0, Q_CAP))
|
|
unit = row.margin * MIN_ETH
|
|
n_spread = int((q * E) // unit) if unit > 0 else 0
|
|
r_trade = row.r
|
|
if stress_p > 0.0 and rng.random() < stress_p:
|
|
r_trade = FULL_LOSS
|
|
E = E + n_spread * unit * r_trade
|
|
streak = streak + 1 if (row.win and r_trade > 0) else 0
|
|
peak = max(peak, E)
|
|
minE = min(minE, E)
|
|
maxdd = max(maxdd, (peak - E) / peak if peak > 0 else 1.0)
|
|
for j, lvl in enumerate(RUIN_LVL):
|
|
ruin[p, j] = minE <= C * (1.0 - lvl)
|
|
dd30[p] = maxdd >= 0.30
|
|
cagrs[p] = (max(E, 0.0) / C) ** (1.0 / (horizon_w / WK)) - 1.0 if E > 0 else -1.0
|
|
return dict(p_ruin50=float(ruin[:, 0].mean()), p_ruin80=float(ruin[:, 1].mean()),
|
|
p_dd30=float(dd30.mean()), cagr_med=float(np.median(cagrs)),
|
|
cagr_p10=float(np.percentile(cagrs, 10)))
|
|
|
|
|
|
# ===========================================================================
|
|
# REPORT
|
|
# ===========================================================================
|
|
def hist_row(label, rec, qfun, C, granular=True):
|
|
sim = simulate(rec, qfun, C, granular=granular)
|
|
mf, mh = full_hold(sim)
|
|
return sim, mf, mh, (f" {label:<34} {mf['cagr']*100:>+6.1f}% {mf['dd']*100:>5.1f}% "
|
|
f"{mf['worst']*100:>+6.1f}% {mf['sh']:>5.2f} | {mh['cagr']*100:>+6.1f}% "
|
|
f"{mh['dd']*100:>5.1f}% | halt {sim['halted']:>3}")
|
|
|
|
|
|
HDR = (f" {'politica':<34} {'CAGR-F':>7} {'DD-F':>5} {'worst':>6} {'Sh-F':>5} | "
|
|
f"{'CAGR-H':>7} {'DD-H':>5} | settimane-0-spread")
|
|
|
|
|
|
def main():
|
|
print("=" * 112)
|
|
print(" R0703 VRPIMP-SIZING — sizing anti-rovina dello sleeve short-vol defined-risk ETH (2-5k, Deribit)")
|
|
print(" Flusso: put credit spread ETH 7g -0.28/-0.10, gate canonico, motore ALB-A (fee per gamba), 2021-2026.")
|
|
print(" Rendimenti per-trade su MARGINE (=max loss). Il sizing non cambia lo Sharpe del flusso: mappa la")
|
|
print(" frontiera CAGR-DD-P(rovina). REGOLA STANDING INVARIATA: niente short-vol da modello in deploy.")
|
|
print("=" * 112)
|
|
|
|
rec = trade_records(f=1.0, phase=0)
|
|
act = rec[rec.active == 1]
|
|
is_act = act[act.date < HOLDOUT]
|
|
print(f"\n flusso f=1.0 ancora-0: {len(rec)} settimane, {len(act)} attive ({len(act)/len(rec)*100:.0f}%), "
|
|
f"win {act['win'].mean()*100:.0f}%, r-margine medio {act['r'].mean()*100:+.1f}% "
|
|
f"(mediana {act['r'].median()*100:+.1f}%), full-loss r<=-0.9: {int((act['r']<=-0.9).sum())}")
|
|
print(f" margine $/spread(1 ETH): mediana ${act['margin'].median():.0f}, ultimo ${act['margin'].iloc[-1]:.0f} "
|
|
f"(la banda $66-76 del diario = spot/DVOL del run r0702)")
|
|
|
|
# streak diagnostics: gli streak di vittorie predicono la prossima perdita?
|
|
wins = act["win"].values
|
|
streaks_before = []
|
|
s = 0
|
|
for w in wins:
|
|
streaks_before.append(s)
|
|
s = s + 1 if w else 0
|
|
sb = np.asarray(streaks_before)
|
|
print("\n DIAGNOSTICA ANTI-STREAK — P(perdita | vittorie consecutive precedenti):")
|
|
base = 1.0 - act["win"].mean()
|
|
for N in (0, 2, 3, 5):
|
|
m = sb >= N
|
|
if m.sum() >= 8:
|
|
print(f" streak>={N}: P(loss)={1.0-wins[m].mean():.2f} su n={int(m.sum())} (base {base:.2f})")
|
|
|
|
# ---------------------------------------------------------------- (1) frontiera fisso
|
|
print("\n" + "=" * 112)
|
|
print(" (1) FRONTIERA DD-CAGR — frazione fissa (IL NULL DEL DE-LEVERING), f=1.0, storico 2021-26")
|
|
print(" 'cont' = frazione continua (lens); '2k'/'5k' = granularita' REALE (spread interi da 1 ETH)")
|
|
print("=" * 112)
|
|
fracs = (0.02, 0.05, 0.08, 0.10, 0.12, 0.15, 0.20, 0.25, 0.35, 0.50)
|
|
frontier = {} # frac -> (dd_full, cagr_full) continuo
|
|
print(f" {'q':>5} | {'cont: CAGR-F':>12} {'DD-F':>6} {'Sh-F':>5} {'CAGR-H':>7} | "
|
|
f"{'2k: CAGR-F':>10} {'DD-F':>6} {'halt':>4} | {'5k: CAGR-F':>10} {'DD-F':>6} {'halt':>4}")
|
|
for q in fracs:
|
|
sc = simulate(rec, pol_fixed(q), 2000.0, granular=False)
|
|
mfc, mhc = full_hold(sc)
|
|
frontier[q] = (mfc["dd"], mfc["cagr"])
|
|
s2 = simulate(rec, pol_fixed(q), 2000.0, granular=True)
|
|
mf2, _ = full_hold(s2)
|
|
s5 = simulate(rec, pol_fixed(q), 5000.0, granular=True)
|
|
mf5, _ = full_hold(s5)
|
|
tag = " <- book 12%" if abs(q - 0.12) < 1e-9 else ""
|
|
print(f" {q*100:>4.0f}% | {mfc['cagr']*100:>+11.1f}% {mfc['dd']*100:>5.1f}% {mfc['sh']:>5.2f} "
|
|
f"{mhc['cagr']*100:>+6.1f}% | {mf2['cagr']*100:>+9.1f}% {mf2['dd']*100:>5.1f}% {s2['halted']:>4} | "
|
|
f"{mf5['cagr']*100:>+9.1f}% {mf5['dd']*100:>5.1f}% {s5['halted']:>4}{tag}")
|
|
print(" ('halt' = settimane attive in cui il budget q*E non compra NEMMENO 1 spread -> size 0:")
|
|
print(" a 2k le frazioni <=10% saltano trade nelle ere a margine alto — la granularita' distorce il basso)")
|
|
|
|
# ---------------------------------------------------------------- (2) overlay b/c — selezione IS
|
|
print("\n" + "=" * 112)
|
|
print(" (2) OVERLAY (b) VOL-SCALED e (c) ANTI-STREAK — selezione SOLO in-sample (MAR pre-2025, continuo)")
|
|
print("=" * 112)
|
|
rec_is = rec[rec.date < HOLDOUT].reset_index(drop=True)
|
|
|
|
def is_mar(qfun):
|
|
sim = simulate(rec_is, qfun, 2000.0, granular=False)
|
|
m = path_metrics(sim["dates"], sim["rets"])
|
|
return m["cagr"] / max(m["dd"], 1e-9)
|
|
|
|
cells_b = [(fb, lo, hi) for fb in (0.08, 0.12, 0.16) for lo, hi in ((0.5, 1.5), (0.5, 2.0))]
|
|
best_b = max(cells_b, key=lambda c: is_mar(pol_volscaled(*c)))
|
|
cells_c = [(0.12, N, m) for N in (2, 3, 5) for m in (0.25, 0.5)]
|
|
best_c = max(cells_c, key=lambda c: is_mar(pol_antistreak(*c)))
|
|
print(f" celle esplorate: vol-scaled {len(cells_b)}, anti-streak {len(cells_c)} "
|
|
f"(+6 frazioni fisse dichiarate a priori, +3 Kelly = {len(cells_b)+len(cells_c)+9} totali)")
|
|
print(f" best IS vol-scaled: base={best_b[0]:.0%} clip[{best_b[1]},{best_b[2]}] | "
|
|
f"best IS anti-streak: q=12% N={best_c[1]} mult={best_c[2]}")
|
|
|
|
print("\n" + HDR)
|
|
named = {}
|
|
for label, qf in (
|
|
("(a) FISSO 12% (book)", pol_fixed(0.12)),
|
|
(f"(b) VOL-SCALED {best_b[0]:.0%} clip[{best_b[1]},{best_b[2]}]", pol_volscaled(*best_b)),
|
|
(f"(c) ANTI-STREAK 12% N={best_c[1]} m={best_c[2]}", pol_antistreak(0.12, best_c[1], best_c[2])),
|
|
("(e) ALBIMARINI 1->4 (rif. negativo)", pol_alb14(0.12))):
|
|
sim, mf, mh, line = hist_row(label, rec, qf, 2000.0, granular=False)
|
|
named[label] = (qf, mf, mh)
|
|
print(line)
|
|
|
|
# ---------------------------------------------------------------- (3) Kelly onesto
|
|
print("\n" + "=" * 112)
|
|
print(" (3) (d) KELLY FRAZIONARIO con code ONESTE (distribuzione IS pooled banda-f + coda sintetica)")
|
|
print("=" * 112)
|
|
p_extra, p_unc, p_emp = synthetic_tail_prob(rec)
|
|
pooled = []
|
|
for f in F_SWEEP:
|
|
rf = trade_records(f=f, phase=0)
|
|
a = rf[(rf.active == 1) & (rf.date < HOLDOUT)]
|
|
pooled.append(a["r"].values)
|
|
rs_pool = np.concatenate(pooled)
|
|
q_star = kelly_star(rs_pool, p_extra)
|
|
q_naive = kelly_star(is_act["r"].values, 0.0)
|
|
print(f" coda: P(move7g<=Kl | ETH 2019-26 certificato)={p_unc:.3f}, freq empirica full-loss gated={p_emp:.3f}"
|
|
f" -> p_extra sintetica={p_extra:.3f} a r={FULL_LOSS}")
|
|
print(f" Kelly NAIVE (solo empirico IS f=1.0): q* = {q_naive:.1%}")
|
|
print(f" Kelly ONESTO (pooled f-band + coda): q* = {q_star:.1%}"
|
|
f" -> il 12% del book = {0.12/q_star:.2f} Kelly")
|
|
print("\n" + HDR)
|
|
for lam in (0.25, 0.5, 1.0):
|
|
q = lam * q_star
|
|
label = f"(d) KELLY {lam:.2f}x -> q={q:.1%}"
|
|
sim, mf, mh, line = hist_row(label, rec, pol_fixed(q), 2000.0, granular=False)
|
|
named[label] = (pol_fixed(q), mf, mh)
|
|
print(line)
|
|
|
|
# ---------------------------------------------------------------- (4) null del de-levering
|
|
print("\n" + "=" * 112)
|
|
print(" (4) NULL DEL DE-LEVERING — ogni overlay vs la frazione FISSA che da' lo stesso maxDD (interp.)")
|
|
print("=" * 112)
|
|
fr = sorted(frontier.items())
|
|
dds = np.array([v[0] for _, v in fr])
|
|
cags = np.array([v[1] for _, v in fr])
|
|
qss = np.array([q for q, _ in fr])
|
|
for label, (qf, mf, mh) in named.items():
|
|
if label.startswith("(a)") or label.startswith("(e)"):
|
|
continue
|
|
q_eq = float(np.interp(mf["dd"], dds, qss))
|
|
c_eq = float(np.interp(mf["dd"], dds, cags))
|
|
verdict = "OVERLAY NON VALE (delever null vince/pareggia)" if c_eq >= mf["cagr"] - 0.002 \
|
|
else "overlay batte il null (verificare persistenza!)"
|
|
print(f" {label:<38} DD {mf['dd']*100:5.1f}% CAGR {mf['cagr']*100:+6.1f}% | fisso-equivalente "
|
|
f"q={q_eq*100:4.1f}% CAGR {c_eq*100:+6.1f}% -> {verdict}")
|
|
|
|
# ---------------------------------------------------------------- (4b) stress-test ANTI-STREAK
|
|
print("\n" + "=" * 112)
|
|
print(" (4b) STRESS-TEST ANTI-STREAK — regola standing: 3 raffinamenti-gate VRP gia' falliti; un 4°")
|
|
print(" candidato deve battere il NULL GIUSTO (piazzamento casuale), persistere multi-cut, DSR>=0.95")
|
|
print("=" * 112)
|
|
N_as, m_as = best_c[1], best_c[2]
|
|
# replay della maschera 'settimana ridotta' (streak>=N a decisione)
|
|
red = np.zeros(len(rec), bool)
|
|
s = 0
|
|
for t, row in enumerate(rec.itertuples(index=False)):
|
|
red[t] = bool(row.active and s >= N_as)
|
|
if row.active:
|
|
s = s + 1 if row.win else 0
|
|
act_mask = rec["active"].values == 1
|
|
act_idx = np.where(act_mask)[0]
|
|
k_red = int(red[act_mask].sum())
|
|
win_arr = rec["win"].values.astype(bool)
|
|
loss_red = int((~win_arr[act_mask]) [red[act_mask]].sum())
|
|
n_loss = int((~win_arr[act_mask]).sum())
|
|
print(f" settimane attive ridotte (streak>={N_as}): {k_red}/{len(act_idx)} ({k_red/len(act_idx)*100:.0f}%) | "
|
|
f"perdite intercettate a size ridotta: {loss_red}/{n_loss}")
|
|
|
|
sim_anti = simulate(rec, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
|
sim_fix = simulate(rec, pol_fixed(0.12), 2000.0, granular=False)
|
|
m_anti = path_metrics(sim_anti["dates"], sim_anti["rets"])
|
|
|
|
# NULL a PIAZZAMENTO CASUALE: stessa sizing a 2 livelli (12% / 12%*mult), stesse k settimane
|
|
# ridotte, ma scelte A CASO fra le attive (isola il TIMING dello streak dall'esposizione media)
|
|
rng = np.random.default_rng(SEED + 7)
|
|
null_cagr, null_dd, null_mar = [], [], []
|
|
for _ in range(500):
|
|
choose = rng.choice(act_idx, size=k_red, replace=False)
|
|
qs = np.full(len(rec), 0.12)
|
|
qs[choose] = 0.12 * m_as
|
|
sn = simulate(rec, None, 2000.0, granular=False, q_series=qs)
|
|
mn = path_metrics(sn["dates"], sn["rets"])
|
|
null_cagr.append(mn["cagr"])
|
|
null_dd.append(mn["dd"])
|
|
null_mar.append(mn["cagr"] / max(mn["dd"], 1e-9))
|
|
mar_anti = m_anti["cagr"] / max(m_anti["dd"], 1e-9)
|
|
p_cagr = float(np.mean(np.asarray(null_cagr) < m_anti["cagr"]))
|
|
p_dd = float(np.mean(np.asarray(null_dd) > m_anti["dd"])) # quota di null con DD PEGGIORE
|
|
p_mar = float(np.mean(np.asarray(null_mar) < mar_anti))
|
|
print(f" NULL piazzamento-casuale (500 draw, stessa esposizione media): anti-streak CAGR pctl {p_cagr:.3f}, "
|
|
f"DD-migliore-del-null {p_dd:.3f}, MAR pctl {p_mar:.3f}")
|
|
print(f" (null: CAGR med {np.median(null_cagr)*100:+.1f}%, DD med {np.median(null_dd)*100:.1f}% vs "
|
|
f"anti-streak {m_anti['cagr']*100:+.1f}%/{m_anti['dd']*100:.1f}%)")
|
|
|
|
# banda d'ancora dell'EFFETTO (7 fasi): direzione + IL DELEVER-NULL RIFATTO PER OGNI ANCORA
|
|
# (lezione anchor-luck 2026-07-02: il claim vale solo se regge a OGNI ancora, non alla migliore)
|
|
print(f"\n banda d'ancora dell'effetto streak (7 fasi, f=1.0) + delever-null PER ANCORA:")
|
|
n_dir = 0
|
|
n_null = 0
|
|
for ph in range(7):
|
|
rp = trade_records(f=1.0, phase=ph)
|
|
wa = rp[rp.active == 1]["win"].values.astype(bool)
|
|
sb2 = []
|
|
s = 0
|
|
for w in wa:
|
|
sb2.append(s)
|
|
s = s + 1 if w else 0
|
|
sb2 = np.asarray(sb2)
|
|
hi = sb2 >= N_as
|
|
p_hi = 1.0 - wa[hi].mean() if hi.sum() else np.nan
|
|
p_lo = 1.0 - wa[~hi].mean() if (~hi).sum() else np.nan
|
|
sa = simulate(rp, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
|
ma = path_metrics(sa["dates"], sa["rets"])
|
|
# frontiera fissa DI QUESTA ancora -> CAGR del fisso allo stesso DD
|
|
dds_p, cags_p = [], []
|
|
for q in fracs:
|
|
sf_ = simulate(rp, pol_fixed(q), 2000.0, granular=False)
|
|
mq = path_metrics(sf_["dates"], sf_["rets"])
|
|
dds_p.append(mq["dd"])
|
|
cags_p.append(mq["cagr"])
|
|
c_eq = float(np.interp(ma["dd"], dds_p, cags_p))
|
|
beats = ma["cagr"] > c_eq + 0.002
|
|
direz = p_hi > p_lo
|
|
n_dir += int(direz)
|
|
n_null += int(beats)
|
|
print(f" fase {ph}: P(loss|s>={N_as})={p_hi:.2f} vs s<{N_as}={p_lo:.2f} {'OK' if direz else 'NO'} | "
|
|
f"anti CAGR {ma['cagr']*100:+5.1f}% DD {ma['dd']*100:4.1f}% | fisso stesso-DD CAGR {c_eq*100:+5.1f}% "
|
|
f"-> {'batte null' if beats else 'NULL VINCE'}")
|
|
print(f" -> direzione presente in {n_dir}/7 ancore; batte il delever-null in {n_null}/7 ancore")
|
|
|
|
# plateau delle celle (ancora 0): l'effetto deve reggere OLTRE la cella selezionata
|
|
print("\n plateau celle anti-streak (ancora 0, FULL): MAR = CAGR/maxDD vs fisso 12% MAR "
|
|
f"{path_metrics(sim_fix['dates'], sim_fix['rets'])['cagr'] / max(path_metrics(sim_fix['dates'], sim_fix['rets'])['dd'], 1e-9):.2f}")
|
|
for (fq, N, m) in cells_c:
|
|
sa = simulate(rec, pol_antistreak(fq, N, m), 2000.0, granular=False)
|
|
mm = path_metrics(sa["dates"], sa["rets"])
|
|
sel = " <- selezionata IS" if (N, m) == (N_as, m_as) else ""
|
|
print(f" N={N} mult={m}: CAGR {mm['cagr']*100:+5.1f}% DD {mm['dd']*100:5.1f}% "
|
|
f"MAR {mm['cagr']/max(mm['dd'],1e-9):5.2f}{sel}")
|
|
|
|
# multi-cut: uplift Sharpe/MAR post-cut vs fisso 12%
|
|
print("\n persistenza multi-cut (uplift anti-streak vs fisso 12%, finestra POST-cut):")
|
|
for cut in ("2022-07-01", "2023-01-01", "2023-07-01", "2024-01-01", "2024-07-01", "2025-01-01"):
|
|
cts = pd.Timestamp(cut, tz="UTC")
|
|
msk = sim_anti["dates"] >= cts
|
|
ma = path_metrics(sim_anti["dates"][msk], sim_anti["rets"][msk])
|
|
mfx = path_metrics(sim_fix["dates"][msk], sim_fix["rets"][msk])
|
|
print(f" cut {cut}: dSh {ma['sh']-mfx['sh']:+.2f} dCAGR {(ma['cagr']-mfx['cagr'])*100:+6.1f}pp "
|
|
f"DD {ma['dd']*100:4.1f}% vs {mfx['dd']*100:4.1f}%")
|
|
|
|
# DSR della cella anti-streak (scelta IS) vs tutte le celle di sizing
|
|
all_sh_is = []
|
|
for c in cells_b:
|
|
s_ = simulate(rec_is, pol_volscaled(*c), 2000.0, granular=False)
|
|
all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"])
|
|
for c in cells_c:
|
|
s_ = simulate(rec_is, pol_antistreak(*c), 2000.0, granular=False)
|
|
all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"])
|
|
for q in fracs:
|
|
s_ = simulate(rec_is, pol_fixed(q), 2000.0, granular=False)
|
|
all_sh_is.append(path_metrics(s_["dates"], s_["rets"])["sh"])
|
|
sim_anti_is = simulate(rec_is, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
|
m_anti_is = path_metrics(sim_anti_is["dates"], sim_anti_is["rets"])
|
|
ret_is = pd.Series(sim_anti_is["rets"], index=sim_anti_is["dates"])
|
|
dsr_as, nm_as = al.deflated_sharpe(m_anti_is["sh"], all_sh_is, ret_is, dpy=WK)
|
|
print(f"\n DSR anti-streak (Sh-IS {m_anti_is['sh']:.2f} vs {len(all_sh_is)} celle): {dsr_as:.3f} "
|
|
f"(null-max {nm_as:.2f}) -> {'PASS' if dsr_as >= 0.95 else 'FAIL'} soglia 0.95")
|
|
|
|
# banda f dell'anti-streak
|
|
print(" banda f anti-streak (FULL/HOLD):")
|
|
for f in F_SWEEP:
|
|
rf = trade_records(f=f, phase=0)
|
|
sa = simulate(rf, pol_antistreak(0.12, N_as, m_as), 2000.0, granular=False)
|
|
mfa, mha = full_hold(sa)
|
|
print(f" f={f:<4} CAGR-F {mfa['cagr']*100:>+6.1f}% DD-F {mfa['dd']*100:>5.1f}% Sh-F {mfa['sh']:>5.2f} | "
|
|
f"CAGR-H {mha['cagr']*100:>+6.1f}% DD-H {mha['dd']*100:>5.1f}%")
|
|
|
|
# ---------------------------------------------------------------- (5) P(rovina) bootstrap
|
|
print("\n" + "=" * 112)
|
|
print(" (5) P(ROVINA | 5 ANNI) — bootstrap a blocchi (L=13 sett., 1000 path), granularita' REALE")
|
|
print(" rovina-50 = equity <= 50% del capitale iniziale in un punto qualsiasi; stress = +coda sintetica")
|
|
print("=" * 112)
|
|
boot_pols = [
|
|
("FISSO 5%", pol_fixed(0.05), False), ("FISSO 12% (book)", pol_fixed(0.12), False),
|
|
("FISSO 25%", pol_fixed(0.25), False),
|
|
("VOL-SCALED best-IS", pol_volscaled(*best_b), True),
|
|
("ANTI-STREAK best-IS", pol_antistreak(0.12, best_c[1], best_c[2]), False),
|
|
(f"KELLY 0.25x ({0.25*q_star:.0%})", pol_fixed(0.25 * q_star), False),
|
|
(f"KELLY 0.50x ({0.5*q_star:.0%})", pol_fixed(0.5 * q_star), False),
|
|
(f"KELLY 1.00x ({q_star:.0%})", pol_fixed(q_star), False),
|
|
("ALBIMARINI 1->4", pol_alb14(0.12), False),
|
|
]
|
|
print(f" {'politica':<24} {'C':>5} | {'P(rov50)':>8} {'P(rov80)':>8} {'P(DD>30%)':>9} "
|
|
f"{'CAGRmed':>8} {'CAGRp10':>8} | {'stress: P(rov50)':>16} {'P(rov80)':>8}")
|
|
boot_out = {}
|
|
for label, qf, nref in boot_pols:
|
|
for C in CAPITALS:
|
|
b = block_bootstrap_ruin(rec, qf, C, stress_p=0.0, needs_ref=nref)
|
|
bs = block_bootstrap_ruin(rec, qf, C, stress_p=p_extra, seed=SEED + 1, needs_ref=nref)
|
|
boot_out[(label, C)] = (b, bs)
|
|
print(f" {label:<24} {C:>5.0f} | {b['p_ruin50']*100:>7.1f}% {b['p_ruin80']*100:>7.1f}% "
|
|
f"{b['p_dd30']*100:>8.1f}% {b['cagr_med']*100:>+7.1f}% {b['cagr_p10']*100:>+7.1f}% | "
|
|
f"{bs['p_ruin50']*100:>15.1f}% {bs['p_ruin80']*100:>7.1f}%")
|
|
|
|
# ---------------------------------------------------------------- (6) banda f + banda ancora
|
|
print("\n" + "=" * 112)
|
|
print(" (6) BANDE OBBLIGATORIE — skew f e ancora settimanale (politica FISSO 12%, continuo)")
|
|
print("=" * 112)
|
|
print(" banda f (storico FULL/HOLD):")
|
|
for f in F_SWEEP:
|
|
rf = trade_records(f=f, phase=0)
|
|
sim = simulate(rf, pol_fixed(0.12), 2000.0, granular=False)
|
|
mf, mh = full_hold(sim)
|
|
print(f" f={f:<4} CAGR-F {mf['cagr']*100:>+6.1f}% DD-F {mf['dd']*100:>5.1f}% worst {mf['worst']*100:>+5.1f}%"
|
|
f" Sh-F {mf['sh']:>5.2f} | CAGR-H {mh['cagr']*100:>+6.1f}% DD-H {mh['dd']*100:>5.1f}%")
|
|
print(" banda d'ancora (7 fasi della cadenza settimanale, f=1.0):")
|
|
anchor = []
|
|
for ph in range(7):
|
|
rp = trade_records(f=1.0, phase=ph)
|
|
sim = simulate(rp, pol_fixed(0.12), 2000.0, granular=False)
|
|
mf, mh = full_hold(sim)
|
|
anchor.append((ph, mf["cagr"], mf["dd"], mf["sh"], mh["cagr"]))
|
|
a = np.array([(x[1], x[2], x[3], x[4]) for x in anchor])
|
|
print(f" CAGR-F: min {a[:,0].min()*100:+.1f}% med {np.median(a[:,0])*100:+.1f}% max {a[:,0].max()*100:+.1f}% | "
|
|
f"DD-F: {a[:,1].min()*100:.1f}/{np.median(a[:,1])*100:.1f}/{a[:,1].max()*100:.1f}% | "
|
|
f"Sh-F: {a[:,2].min():.2f}/{np.median(a[:,2]):.2f}/{a[:,2].max():.2f} | "
|
|
f"CAGR-H: {a[:,3].min()*100:+.1f}/{np.median(a[:,3])*100:+.1f}/{a[:,3].max()*100:+.1f}%")
|
|
|
|
# DSR pro-forma sulle celle (il sizing riscala il flusso: Sharpe quasi invariante per costruzione)
|
|
all_sh = []
|
|
for c in cells_b:
|
|
s = simulate(rec_is, pol_volscaled(*c), 2000.0, granular=False)
|
|
all_sh.append(path_metrics(s["dates"], s["rets"])["sh"])
|
|
for c in cells_c:
|
|
s = simulate(rec_is, pol_antistreak(*c), 2000.0, granular=False)
|
|
all_sh.append(path_metrics(s["dates"], s["rets"])["sh"])
|
|
for q in fracs:
|
|
s = simulate(rec_is, pol_fixed(q), 2000.0, granular=False)
|
|
all_sh.append(path_metrics(s["dates"], s["rets"])["sh"])
|
|
sim_b = simulate(rec, pol_volscaled(*best_b), 2000.0, granular=False)
|
|
daily = pd.Series(sim_b["rets"], index=sim_b["dates"])
|
|
dsr, null_max = al.deflated_sharpe(path_metrics(sim_b["dates"], sim_b["rets"])["sh"], all_sh, daily, dpy=WK)
|
|
print(f"\n DSR pro-forma (best overlay vs {len(all_sh)} celle di sizing): {dsr:.3f} (null-max {null_max:.2f})")
|
|
print(" NB: il sizing RISCALA lo stesso flusso -> gli Sharpe delle celle sono quasi identici per costruzione;")
|
|
print(" il DSR qui e' dovuto, ma la metrica decisiva del filone e' la frontiera CAGR-DD-P(rovina) vs il null.")
|
|
|
|
# ---------------------------------------------------------------- (7) sensitivity 0.1 ETH
|
|
print("\n (7) SENSITIVITY GRANULARITA' 0.1 ETH (se il min-size fosse 0.1 ETH/gamba, unit ~$7-15):")
|
|
for C in CAPITALS:
|
|
s10 = simulate(rec, pol_fixed(0.12), C, granular=True, min_eth=0.1)
|
|
s1 = simulate(rec, pol_fixed(0.12), C, granular=True, min_eth=1.0)
|
|
m10, _ = full_hold(s10)
|
|
m1, _ = full_hold(s1)
|
|
print(f" C={C:.0f}: 1.0 ETH CAGR {m1['cagr']*100:+.1f}% DD {m1['dd']*100:.1f}% halt {s1['halted']} | "
|
|
f"0.1 ETH CAGR {m10['cagr']*100:+.1f}% DD {m10['dd']*100:.1f}% halt {s10['halted']}")
|
|
|
|
print("\n" + "=" * 112)
|
|
print(" SINTESI ONESTA")
|
|
print(" - Il sizing non crea alpha: sposta il punto sulla frontiera CAGR-DD-P(rovina) del flusso gated.")
|
|
print(" - VERDETTI: (b) vol-scaled batte il null di ~1pp a singola ancora = rumore (lezione TP01xDVOL)")
|
|
print(" -> SCARTATO. (c) anti-streak: passa random-null 0.996 / DSR 0.999 / multi-cut / 7-7 direzione,")
|
|
print(" MA plateau ASSENTE (solo la cella N=2 vince; N=3/5 collassano al MAR del fisso), P(loss|streak)")
|
|
print(" NON monotona (0.24/0.18/0.15) e delever-null vince in 2/7 ancore: l'intero effetto sono ~6")
|
|
print(" perdite su 15 trade a streak==2 -> ARTEFATTO DI SELEZIONE, non adottato. La batteria minima per")
|
|
print(" un sizing-gate e': random-placement null + plateau celle + delever-null PER ANCORA (i primi 3")
|
|
print(" test da soli l'avrebbero promosso). (d) Kelly frazionario = punti sulla frontiera fissa (il")
|
|
print(" null vince/pareggia per costruzione): il valore e' la CALIBRAZIONE, q*_onesto=44%.")
|
|
print(" - Il 12% del book = 0.27 Kelly-onesto ~= quarter-Kelly: gia' nella zona sana anti-rovina")
|
|
print(" (P(rovina50|5y)~0.0-0.1%, P(DD>30%) 9-18%). Alzarlo a 25% decuplica P(DD>30%) a >92%.")
|
|
print(" - Albimarini 1->4 e' il controesempio: stesso flusso, stessa edge, P(rovina50|5y)=53-55%.")
|
|
print(" - Granularita' REALE: a 2k il min-size 1 ETH rende inservibili le frazioni <=10% (halt 13-92")
|
|
print(" settimane) e costa -3.7pp di CAGR al 12%; a 5k il muro sparisce (coerente con r0702: ~2.6k).")
|
|
print(" - REGOLA STANDING: niente short-vol da modello in deploy. Output = conoscenza per QUANDO/SE")
|
|
print(" il f di stress reale arrivera' da cerbero-bite. Nessun file di produzione toccato.")
|
|
print("=" * 112)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|