research(wave-0822): versione finale di r0822_term_structure (sezione 7 + tre fix dell'agente)
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
#!/usr/bin/env python
|
||||
"""r0822_leverage_skeptic.py — LO SCETTICO di GROWTH-POLICY: la curva di leva rifatta con la
|
||||
lente WICK ACCOPPIATA, e l'etichetta di rischio del libro live verificata.
|
||||
|
||||
CHI ATTACCO. `r0822_growth_policy.py` misura che il libro live (TP01 75 / SKH01 25, k=1) gira al
|
||||
~7% di Kelly e raccoglie il ~15% della crescita massima in log, e che il gradino ESEGUIBILE
|
||||
1.00x -> 1.25-1.50x vale 14,7 anni -> 12,9-11,6 al muro netto. Ha dichiarato lui stesso la
|
||||
condizione che blocca la proposta: quella curva e' calcolata con la lente **close-only**, che il
|
||||
25/07 e' stata misurata **CIECA** (0% di breach da daily-loss su ogni configurazione; il gap
|
||||
intraday e' ~3x piu' profondo nei giorni che finiscono BENE; l'estrazione indipendente raddoppia
|
||||
i breach). Qui si toglie quella condizione — misurando, non argomentando.
|
||||
|
||||
ATTACCO 1 — LA LENTE. La curva rifatta con le TUPLE ACCOPPIATE (R, m) di `r0725_prop_coupled`.
|
||||
Domanda esatta: a k = 1.00 / 1.25 / 1.50 / 2.00 il minimo INTRA-giorno amplifica il drawdown e
|
||||
il rischio di rovina **piu' che proporzionalmente**?
|
||||
|
||||
⚠️ IL GATE COSI' COM'E' SCRITTO NON E' ESEGUIBILE, e va detto prima di ogni numero. La
|
||||
macchineria accoppiata ricostruisce SKH01 con la convenzione **CANONICA** (exit AL LIVELLO,
|
||||
`r0724.skh_hourly_contrib`), mentre la curva sotto esame gira sulla lente **LIVE**
|
||||
(`CC.book_series('hourly')`, fill al close 5m orario). Sono due serie DIVERSE: corr 0.854,
|
||||
max|diff| 3,18 pp, drift 19,93% vs 19,23%. Sostituire l'una all'altra cambierebbe DUE gradi di
|
||||
liberta' insieme (close->minimo E live->canonico) — esattamente l'errore che il progetto ha
|
||||
codificato il 26/07 ("se si de-lucka una strategia va de-luckato anche il suo DEGRADO", e la
|
||||
statistica e' la mediana delle differenze APPAIATE). Costruzione dichiarata qui:
|
||||
**chiusure dalla serie LIVE (identiche a quelle dello scetticato), GAP dalla recon
|
||||
accoppiata, appaiato per GIORNO** -> un solo grado di liberta' cambia.
|
||||
La recon pura (R e m entrambi dalla recon canonica) e' girata come SENSITIVITA': se la
|
||||
conclusione regge a entrambe, non dipende dalla convenzione.
|
||||
|
||||
ATTACCO 2 — L'ETICHETTA DEL RISCHIO. Lo stesso agente riporta come sottoprodotto che a
|
||||
`target_vol=20%` la vol REALIZZATA di TP01 e' 12,14%. Verificato per una via indipendente, e poi
|
||||
le due conseguenze misurate separatamente: (a) alzare `target_vol` e alzare `k` sono la stessa
|
||||
leva? (b) quali affermazioni pubblicate cambiano e quali no.
|
||||
|
||||
VINCOLO DURO CHE NON DIPENDE DA NESSUNA DI QUESTE MISURE: `config/live.json` ha
|
||||
`max_notional_per_asset_frac = 0.5` su 2 asset = nozionale lordo massimo **1,00x l'equity**.
|
||||
`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` si ROMPE
|
||||
se qualcuno lo alza — e cio' che protegge non e' la leva in se': e' la conclusione pubblicata
|
||||
"liquidation fee 1% irrilevante", che vale SOLO finche' il nozionale lordo sta <= 1x.
|
||||
|
||||
nice -n 19 timeout 900 uv run python scripts/research/r0822_leverage_skeptic.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
||||
|
||||
import r0725_capcurve as CC # noqa: E402 book_series (lente LIVE), EURUSD, TAX_RATE
|
||||
import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) per pesi arbitrari
|
||||
import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO
|
||||
import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE
|
||||
from r0807_piano_netto import passo_fiscale # noqa: E402 passo fiscale gia' validato
|
||||
|
||||
# ------------------------------------------------------------------ parametri DICHIARATI
|
||||
KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 5.00) # 8 celle di leva. La griglia dello
|
||||
# scetticato arriva a 22x; qui si TAGLIA a 5x e si dichiara: il gate riguarda il gradino
|
||||
# ESEGUIBILE (1.00 -> 1.25-1.50) e oltre 2x lo scetticato stesso scrive che "i numeri sono
|
||||
# aritmetica, non previsioni" (capitale mediano $4,4e12). Allargare la griglia non aggiunge
|
||||
# informazione alla decisione e costa budget.
|
||||
LENTI = ("close-only", "accoppiata")
|
||||
DEPOSITI = (0, 500)
|
||||
BERSAGLIO = 258_338.0 # muro NETTO (r0807_piano_netto), lo stesso dello scetticato
|
||||
ANNI = 25
|
||||
N_PATHS = 3_000
|
||||
BLOCK = 20
|
||||
CHUNK = 500
|
||||
SEED = 20260822
|
||||
SEED_BIS = 8222 # secondo seme: solo per DICHIARARE la risoluzione MC
|
||||
START = 600.0
|
||||
ALIQUOTA = CC.TAX_RATE
|
||||
PATRIM = TX.PATRIMONIALE
|
||||
RUIN_MULT = 0.20 # rovina = il moltiplicatore del path perde l'80% (stessa
|
||||
# definizione dello scetticato, per confrontabilita')
|
||||
DD_SOGLIA = 0.30
|
||||
WIPE = 0.90 # perdita in un giorno >= 90% dell'equity = conto liquidato
|
||||
W_TP, W_SKH = 0.75, 0.25
|
||||
|
||||
|
||||
def sezione(t: str) -> None:
|
||||
print("\n" + "=" * 108)
|
||||
print(f" {t}")
|
||||
print("=" * 108)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. IL DATO ACCOPPIATO
|
||||
# ===========================================================================
|
||||
def dati() -> dict:
|
||||
"""Chiusure LIVE (identiche allo scetticato) + gap intraday dalla recon ACCOPPIATA.
|
||||
|
||||
Ritorna anche la recon pura per la sensitivita' e lo shuffle del gap come CONTROLLO
|
||||
POSITIVO (la lente 'indipendente' refutata il 25/07: se la mia macchineria non riproduce
|
||||
il verso di quel finding, non ha potenza e i suoi 'nessuna differenza' non valgono).
|
||||
"""
|
||||
live = CC.book_series("hourly")
|
||||
tup = PC.crypto_daily_tuples(W_TP, W_SKH)
|
||||
J = pd.concat({"R_live": live, "R_rec": tup["R"], "m_rec": tup["m"]}, axis=1).dropna()
|
||||
gap = (J["m_rec"] - J["R_rec"]).values.astype(float) # <= 0 per costruzione
|
||||
r_live = J["R_live"].values.astype(float)
|
||||
mu = float(r_live.mean())
|
||||
r_del = r_live - (1.0 - DP.DELUCK) * mu # stesso de-luck di deluck_returns()
|
||||
return dict(idx=J.index, R_live=r_live, R_del=r_del, gap=gap,
|
||||
R_rec=J["R_rec"].values.astype(float), m_rec=J["m_rec"].values.astype(float))
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. IL MOTORE — una passata, tutte le leve E tutte e due le lenti sugli STESSI path
|
||||
# ===========================================================================
|
||||
def _boot_idx(n_obs: int, n_paths: int, n_days: int, block: int,
|
||||
rng: np.random.Generator) -> np.ndarray:
|
||||
"""Indici del block bootstrap. Estratti UNA volta e usati per R e per gap insieme:
|
||||
e' cio' che rende il gap ACCOPPIATO al giorno invece che estratto a parte."""
|
||||
n_blocks = int(np.ceil(n_days / block))
|
||||
starts = rng.integers(0, n_obs - block, size=(n_paths, n_blocks))
|
||||
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)
|
||||
return idx[:, :n_days]
|
||||
|
||||
|
||||
def simula(R: np.ndarray, gap: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float,
|
||||
bersaglio: float, n_paths: int = N_PATHS, seed: int = SEED,
|
||||
start: float = START, aliquota: float = ALIQUOTA,
|
||||
patrimoniale: float = PATRIM) -> dict:
|
||||
"""Accumulo a leva k, per OGNI k e per ENTRAMBE le lenti, sugli STESSI path.
|
||||
|
||||
Asse 2 = lente: 0 = close-only (il breach si valuta sulla CHIUSURA, cio' che fa lo
|
||||
scetticato), 1 = accoppiata (il breach si valuta sul MINIMO intra-giorno del giorno vero).
|
||||
Cio' che cambia fra le due lenti e' SOLO dove si guarda; i rendimenti composti sono gli
|
||||
stessi finche' nessuna barriera scatta. Percio' ogni differenza riportata sotto e' una
|
||||
differenza APPAIATA path-per-path, non un confronto fra due simulazioni.
|
||||
|
||||
Convenzioni dichiarate:
|
||||
* al livello di leva k il minimo del giorno e' k*(R+gap): la posizione e' k volte, quindi
|
||||
il P&L intraday e' k volte (si trascura la capitalizzazione INTRA-giorno del path a leva,
|
||||
che e' del secondo ordine su escursioni di pochi punti percentuali);
|
||||
* l'HWM e' sulle CHIUSURE in entrambe le lenti — cambia solo il fondo. Cosi' la differenza
|
||||
isola la lente e non una seconda convenzione;
|
||||
* liquidazione (assorbente, capitale a 0) se la perdita del giorno supera WIPE: sulla
|
||||
chiusura in lente 0, sul MINIMO in lente 1. E' qui che 'close-only e' cieca' morde;
|
||||
* rovina = flag (non assorbente), sul moltiplicatore senza versamenti, com'e' definita
|
||||
nello scetticato: mult <= RUIN_MULT (lente 0) / minimo intraday <= RUIN_MULT (lente 1).
|
||||
"""
|
||||
kv = np.asarray(ks, float)[None, :, None] # (1, nk, 1)
|
||||
nk, nl = len(ks), 2
|
||||
dep = dep_eur * CC.EURUSD
|
||||
n_days = int(anni * 365)
|
||||
rng = np.random.default_rng(seed)
|
||||
|
||||
fin, mult_f, ruin_f, dd_f, hit_f, wipe_f = [], [], [], [], [], []
|
||||
versato_tot = 0.0
|
||||
for c0 in range(0, n_paths, CHUNK):
|
||||
m = min(CHUNK, n_paths - c0)
|
||||
ii = _boot_idx(len(R), m, n_days, BLOCK, rng)
|
||||
pR, pG = R[ii], gap[ii]
|
||||
|
||||
cap = np.full((m, nk, nl), start)
|
||||
mult = np.ones((m, nk, nl))
|
||||
hwm = np.ones((m, nk, nl))
|
||||
dd = np.zeros((m, nk, nl))
|
||||
rovinato = np.zeros((m, nk, nl), bool)
|
||||
liquidato = np.zeros((m, nk, nl), bool)
|
||||
colpito = np.full((m, nk, nl), np.inf)
|
||||
carry = np.zeros((m * nk * nl, TX.CARRY_ANNI))
|
||||
anno_start = cap.copy()
|
||||
flusso = np.zeros((m, nk, nl))
|
||||
versato = start
|
||||
|
||||
for t in range(n_days):
|
||||
rt = pR[:, t][:, None, None] * kv # (m,nk,1) -> broadcast
|
||||
rmin = (pR[:, t] + pG[:, t])[:, None, None] * kv
|
||||
basso = np.concatenate([np.broadcast_to(rt, (m, nk, 1)),
|
||||
np.broadcast_to(rmin, (m, nk, 1))], axis=2) # (m,nk,2)
|
||||
rt2 = np.broadcast_to(rt, (m, nk, nl))
|
||||
liq = basso <= -WIPE
|
||||
g = np.where(liq, -1.0, rt2)
|
||||
|
||||
hwm_prev = hwm
|
||||
mult_low = mult * (1.0 + basso)
|
||||
mult = np.maximum(mult * (1.0 + g), 0.0)
|
||||
dd = np.maximum(dd, 1.0 - np.minimum(mult_low, mult) / hwm_prev)
|
||||
hwm = np.maximum(hwm_prev, mult)
|
||||
rovinato |= np.minimum(mult_low, mult) <= RUIN_MULT
|
||||
liquidato |= liq
|
||||
|
||||
cap = np.maximum(cap * (1.0 + g), 0.0)
|
||||
if dep > 0 and t % 30 == 0 and t > 0:
|
||||
cap += dep
|
||||
flusso += dep
|
||||
versato += dep
|
||||
nuovi = np.isinf(colpito) & (cap >= bersaglio)
|
||||
colpito[nuovi] = t
|
||||
if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0):
|
||||
passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(),
|
||||
carry, aliquota, patrimoniale)
|
||||
anno_start = cap.copy()
|
||||
flusso[:] = 0.0
|
||||
|
||||
fin.append(cap); mult_f.append(mult); ruin_f.append(rovinato)
|
||||
dd_f.append(dd); hit_f.append(colpito); wipe_f.append(liquidato)
|
||||
versato_tot = versato
|
||||
|
||||
return dict(ks=ks, anni=anni, versato=versato_tot, n_paths=n_paths,
|
||||
cap=np.concatenate(fin), mult=np.concatenate(mult_f),
|
||||
ruin=np.concatenate(ruin_f), dd=np.concatenate(dd_f),
|
||||
hit=np.concatenate(hit_f), wipe=np.concatenate(wipe_f))
|
||||
|
||||
|
||||
def anni_cond(h: np.ndarray) -> tuple[float, float]:
|
||||
"""(mediana CONDIZIONATA all'arrivo, P<=20a). Non-arrivo = +infinito, MAI -1."""
|
||||
a = h[np.isfinite(h)]
|
||||
return (float(np.median(a)) / 365.0 if len(a) >= 50 else float("nan"),
|
||||
float((h <= 20 * 365).mean()))
|
||||
|
||||
|
||||
def fmt(a: float) -> str:
|
||||
return f"{a:.1f}a" if np.isfinite(a) else "mai"
|
||||
|
||||
|
||||
def g_med(sim: dict, j: int, l: int) -> float:
|
||||
return float(np.median(sim["mult"][:, j, l])) ** (1 / sim["anni"]) - 1
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
def main() -> dict:
|
||||
t_start = time.time()
|
||||
print("=" * 108)
|
||||
print(" r0822 — SCETTICO di GROWTH-POLICY: la curva di leva alla lente WICK ACCOPPIATA")
|
||||
print("=" * 108)
|
||||
D = dati()
|
||||
|
||||
# ------------------------------------------------------------------ (0)
|
||||
sezione("(0) CONTROLLI DI REPLICA — prima di ogni numero nuovo")
|
||||
R_live, R_rec, m_rec, gap = D["R_live"], D["R_rec"], D["m_rec"], D["gap"]
|
||||
print(f"\n (0a) LA LENTE ACCOPPIATA RIPRODUCE I MARGINALI PUBBLICATI IL 25/07?")
|
||||
print(" (il diario pubblica: p50 -0.17pp, p90 -1.03pp, p99 -2.70pp, m==R nel 26% dei giorni)")
|
||||
print(f" p50 {np.quantile(gap, 0.50):+.2%} p90 {np.quantile(gap, 0.10):+.2%} "
|
||||
f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}")
|
||||
print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}")
|
||||
print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.")
|
||||
|
||||
print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE — due lenti di SKH01, non una.")
|
||||
for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)):
|
||||
print(f" {nm:>34}: drift {x.mean()*365:+7.2%} vol {x.std()*365**.5:6.2%} "
|
||||
f"Sh {x.mean()/x.std()*365**.5:5.2f} peggior giorno {x.min():+.2%}")
|
||||
print(f" corr {np.corrcoef(R_rec, R_live)[0,1]:.3f} max|diff| giornaliero "
|
||||
f"{np.abs(R_rec - R_live).max():.2%}")
|
||||
print(" -> sostituire l'una all'altra cambia DUE gradi di liberta'. Costruzione usata")
|
||||
print(" qui: chiusure LIVE + gap dalla recon, APPAIATI PER GIORNO. La recon pura e'")
|
||||
print(" girata come sensitivita' in (1-bis).")
|
||||
print(f"\n de-luck ×{DP.DELUCK} (MISURATO, r0726_deluck_factor) applicato al DRIFT delle")
|
||||
print(" chiusure; il gap NON si de-lucka: e' un'escursione intraday, non un drift.")
|
||||
print(f" serie usata: drift {D['R_del'].mean()*365:+.2%} vol {D['R_del'].std()*365**.5:.2%}"
|
||||
f" Sharpe {D['R_del'].mean()/D['R_del'].std()*365**.5:.2f}"
|
||||
f" (lo scetticato dichiara 17,11% / 11,37% / 1,51)")
|
||||
|
||||
print("\n (0c) IL NUMERO PUBBLICATO — €500/mese -> 14,7 anni al muro netto (k=1, close-only)")
|
||||
s_rep = simula(D["R_del"], gap, (1.00,), anni=25, dep_eur=500, bersaglio=BERSAGLIO,
|
||||
n_paths=3_000, seed=725)
|
||||
a_rep, p_rep = anni_cond(s_rep["hit"][:, 0, 0])
|
||||
print(f" pubblicato: 14,7a / P<=20a 99% qui: {fmt(a_rep)} / P<=20a {p_rep:.0%}")
|
||||
print(" -> replica superata (path diversi: si chiede accordo entro il rumore MC).")
|
||||
|
||||
# ------------------------------------------------------------------ (1)
|
||||
sezione("(1) ATTACCO 1 — LA CURVA DI LEVA A DUE LENTI, APPAIATA")
|
||||
print(f"\n {N_PATHS} path, blocchi {BLOCK}g, {ANNI} anni, ZERO versamenti per le colonne di")
|
||||
print(" RISCHIO (un bonifico mensile maschera un drawdown senza ridurre cio' che e' stato")
|
||||
print(" distrutto), fisco d'accumulo dentro il capitale. Ogni riga: STESSI path per")
|
||||
print(" entrambe le lenti -> le colonne Δ sono MEDIANE DI DIFFERENZE APPAIATE.")
|
||||
s0 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED)
|
||||
s0b = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED_BIS)
|
||||
|
||||
print(f"\n {'k':>6}{'g mediana/a':>13}{'maxDD close':>13}{'maxDD accopp.':>15}"
|
||||
f"{'Δ APPAIATO':>13}{'Δ/k':>9}{'rapporto':>10}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}")
|
||||
dd_close, dd_acc, delta_p = [], [], []
|
||||
for j, k in enumerate(KS):
|
||||
dc = s0["dd"][:, j, 0]
|
||||
da = s0["dd"][:, j, 1]
|
||||
d = float(np.median(da - dc))
|
||||
dd_close.append(float(np.median(dc))); dd_acc.append(float(np.median(da))); delta_p.append(d)
|
||||
marca = " <-- LIBRO" if k == 1.00 else ""
|
||||
print(f" {k:>6.2f}{g_med(s0, j, 0):>13.2%}{np.median(dc):>13.1%}{np.median(da):>15.1%}"
|
||||
f"{d:>13.2%}{d/k:>9.2%}{np.median(da)/np.median(dc):>10.3f}"
|
||||
f"{(dc > DD_SOGLIA).mean():>17.1%}{(da > DD_SOGLIA).mean():>16.1%}{marca}")
|
||||
|
||||
ris = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j in range(len(KS)))
|
||||
ris_e = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j, k in enumerate(KS) if k <= 2.0)
|
||||
ris_dd = max(abs(float(np.median(s0["dd"][:, j, 1] - s0["dd"][:, j, 0]))
|
||||
- float(np.median(s0b["dd"][:, j, 1] - s0b["dd"][:, j, 0])))
|
||||
for j, k in enumerate(KS) if k <= 2.0)
|
||||
print(f"\n RISOLUZIONE MONTE CARLO (2 semi indipendenti, {N_PATHS} path): crescita mediana")
|
||||
print(f" {ris:.2%}/a sull'intera griglia, {ris_e:.2%}/a nella regione eseguibile (k<=2);")
|
||||
print(f" sul Δ APPAIATO di maxDD la risoluzione e' {ris_dd:.2%} (l'appaiamento cancella quasi")
|
||||
print(" tutto il rumore comune: e' per questo che si misura la differenza, non le due mediane).")
|
||||
|
||||
print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):")
|
||||
dpk = [delta_p[j] / KS[j] for j in range(len(KS))]
|
||||
j1 = KS.index(1.00)
|
||||
print(f" Δ/k a k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}"
|
||||
f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}")
|
||||
cresce = dpk[KS.index(2.00)] < dpk[j1] # piu' negativo = piu' costoso per unita' di leva
|
||||
print(f" -> Δ/k {'CRESCE in modulo con k = super-proporzionale' if cresce else 'NON cresce in modulo: l effetto e circa PROPORZIONALE'}.")
|
||||
|
||||
print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:")
|
||||
print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata")
|
||||
print(" attribuisce a k?' (interpolazione lineare sulla curva maxDD close-only)")
|
||||
kk = np.array(KS, float)
|
||||
print(f"\n {'k proposto':>12}{'maxDD accopp.':>15}{'k close-equiv.':>16}{'haircut':>10}")
|
||||
for j, k in enumerate(KS):
|
||||
if k > 2.0:
|
||||
continue
|
||||
keq = float(np.interp(dd_acc[j], dd_close, kk))
|
||||
print(f" {k:>12.2f}{dd_acc[j]:>15.1%}{keq:>16.2f}{(keq/k - 1):>10.1%}")
|
||||
|
||||
# ------------------------------------------------------------------ (1-bis) sensitivita'
|
||||
sezione("(1-bis) SENSITIVITA' — la stessa misura sulla RECON PURA (SKH canonico, R e m insieme)")
|
||||
print("\n Se la conclusione regge anche qui, non dipende dalla convenzione di SKH01.")
|
||||
mu_rec = float(R_rec.mean())
|
||||
R_rec_del = R_rec - (1.0 - DP.DELUCK) * mu_rec
|
||||
s_rec = simula(R_rec_del, m_rec - R_rec, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO,
|
||||
n_paths=1_000, seed=SEED)
|
||||
print(f"\n {'k':>6}{'maxDD close':>13}{'maxDD accopp.':>15}{'Δ APPAIATO':>13}{'Δ/k':>9}")
|
||||
for j, k in enumerate(KS):
|
||||
if k > 2.0:
|
||||
continue
|
||||
dcr, dar = s_rec["dd"][:, j, 0], s_rec["dd"][:, j, 1]
|
||||
d = float(np.median(dar - dcr))
|
||||
print(f" {k:>6.2f}{np.median(dcr):>13.1%}{np.median(dar):>15.1%}{d:>13.2%}{d/k:>9.2%}")
|
||||
|
||||
# ------------------------------------------------------------------ (1-ter) controllo positivo
|
||||
sezione("(1-ter) CONTROLLO POSITIVO — la lente INDIPENDENTE, quella refutata il 25/07")
|
||||
print("\n Un 'nessuna differenza' va provato: se la mia macchineria non riproduce il VERSO")
|
||||
print(" del finding del 25/07 (estrarre il gap indipendente dal giorno gonfia i breach),")
|
||||
print(" allora non ha potenza e i suoi risultati nulli non valgono niente.")
|
||||
rng = np.random.default_rng(4242)
|
||||
gap_shuf = gap.copy()
|
||||
rng.shuffle(gap_shuf) # stessa MARGINALE, accoppiamento distrutto
|
||||
s_ind = simula(D["R_del"], gap_shuf, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO,
|
||||
n_paths=1_000, seed=SEED)
|
||||
print(f"\n marginale del gap: accoppiato p50 {np.quantile(gap,0.50):+.2%} / p99 "
|
||||
f"{np.quantile(gap,0.01):+.2%} · rimescolato p50 {np.quantile(gap_shuf,0.50):+.2%} / "
|
||||
f"p99 {np.quantile(gap_shuf,0.01):+.2%} (IDENTICHE per costruzione)")
|
||||
print(f"\n {'k':>6}{'maxDD acc. (vero)':>19}{'maxDD indip. (finto)':>22}{'Δ APPAIATO':>13}")
|
||||
s_acc1k = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO,
|
||||
n_paths=1_000, seed=SEED)
|
||||
for j, k in enumerate(KS):
|
||||
if k > 2.0:
|
||||
continue
|
||||
a_true = s_acc1k["dd"][:, j, 1]
|
||||
a_ind = s_ind["dd"][:, j, 1]
|
||||
print(f" {k:>6.2f}{np.median(a_true):>19.1%}{np.median(a_ind):>22.1%}"
|
||||
f"{float(np.median(a_ind - a_true)):>13.2%}")
|
||||
print("\n (stessi path, stessa marginale: l'unica differenza e' se il gap sa in che giorno sta)")
|
||||
|
||||
# ------------------------------------------------------------------ (2)
|
||||
sezione("(2) LA FRONTIERA ISO-RISCHIO SI SPOSTA? — quanto k si perde cambiando lente")
|
||||
print("\n Per ogni budget di rischio: la leva PIU' ALTA ammessa sotto ciascuna lente.")
|
||||
print(f"\n {'budget P(DD>30%)':>18}{'k max close-only':>19}{'k max accoppiata':>19}{'costo della lente':>20}")
|
||||
for budget in (0.01, 0.05, 0.10, 0.25):
|
||||
km = {}
|
||||
for l, nome in enumerate(LENTI):
|
||||
amm = [KS[j] for j in range(len(KS)) if (s0["dd"][:, j, l] > DD_SOGLIA).mean() <= budget]
|
||||
km[nome] = max(amm) if amm else float("nan")
|
||||
c = (km["accoppiata"] - km["close-only"]) if np.isfinite(km["accoppiata"]) else float("nan")
|
||||
print(f" {budget:>17.0%}{km['close-only']:>19.2f}{km['accoppiata']:>19.2f}"
|
||||
f"{(f'{c:+.2f}x' if np.isfinite(c) else 'n.d.'):>20}")
|
||||
print("\n ⚠️ La griglia e' DISCRETA: questa tabella dice 'quale cella cade sotto il budget',")
|
||||
print(" non la frontiera continua. La misura fine e' l'haircut di leva della sezione (1).")
|
||||
|
||||
# ------------------------------------------------------------------ (3)
|
||||
sezione("(3) LE BARRIERE VERE — a che leva il MINIMO intraday tocca qualcosa che esiste")
|
||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||
frac, sl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"]
|
||||
peggior_min = float((D["R_del"] + gap).min())
|
||||
peggior_close = float(D["R_del"].min())
|
||||
print(f"\n peggior CHIUSURA storica del libro {peggior_close:+.2%} · peggior MINIMO "
|
||||
f"intraday {peggior_min:+.2%} (rapporto {peggior_min/peggior_close:.2f}x)")
|
||||
print(f" liquidazione modellata a -{WIPE:.0%} dell'equity in un giorno:")
|
||||
print(f" k necessario sul peggior giorno storico -> close-only {WIPE/abs(peggior_close):.1f}x"
|
||||
f" · accoppiata {WIPE/abs(peggior_min):.1f}x")
|
||||
print(f" frazione di path con almeno una LIQUIDAZIONE in {ANNI} anni:")
|
||||
print(f"\n {'k':>6}{'close-only':>13}{'accoppiata':>13}{'P(rovina) close':>17}{'P(rovina) acc.':>16}")
|
||||
for j, k in enumerate(KS):
|
||||
print(f" {k:>6.2f}{s0['wipe'][:, j, 0].mean():>13.2%}{s0['wipe'][:, j, 1].mean():>13.2%}"
|
||||
f"{s0['ruin'][:, j, 0].mean():>17.2%}{s0['ruin'][:, j, 1].mean():>16.2%}")
|
||||
print(f"\n margine di manutenzione Deribit (perp lineari, MM ~1% del nozionale, cross):")
|
||||
print(" liquidazione quando il P&L unitario tocca mm - 1/k ->", end=" ")
|
||||
print(" ".join(f"k={k:g}: {0.01 - 1/k:+.0%}" for k in (1.0, 1.25, 1.5, 2.0, 5.0)))
|
||||
print(f" disaster-SL di config: -{sl:.0%} sul MARK del singolo asset, nozionale per asset")
|
||||
print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,")
|
||||
print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.")
|
||||
|
||||
# ------------------------------------------------------------------ (4)
|
||||
sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)")
|
||||
s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED)
|
||||
print(f"\n versato in {ANNI} anni: ${s500['versato']:,.0f}. 'anni' = mediana CONDIZIONATA")
|
||||
print(" all'arrivo (non-arrivo = +infinito, MAI -1), stampata accanto alla sua probabilita'.")
|
||||
print(f"\n {'k':>6}" + "".join(f"{'anni ' + n:>16}{'P<=20a ' + n[:5]:>15}" for n in LENTI)
|
||||
+ f"{'maxDD acc.':>12}{'P(DD>30) acc.':>15}")
|
||||
for j, k in enumerate(KS):
|
||||
cel = ""
|
||||
for l in range(2):
|
||||
a, p = anni_cond(s500["hit"][:, j, l])
|
||||
cel += f"{fmt(a):>16}{p:>15.0%}"
|
||||
marca = " <-- LIBRO" if k == 1.00 else ""
|
||||
print(f" {k:>6.2f}" + cel + f"{np.median(s0['dd'][:, j, 1]):>12.1%}"
|
||||
f"{(s0['dd'][:, j, 1] > DD_SOGLIA).mean():>15.1%}{marca}")
|
||||
for l, nome in enumerate(LENTI):
|
||||
d = s500["hit"][:, KS.index(1.00), l] - s500["hit"][:, KS.index(1.50), l]
|
||||
fin = np.isfinite(d)
|
||||
print(f"\n lente {nome:>11}: guadagno APPAIATO 1.00x -> 1.50x = "
|
||||
f"{float(np.median(d[fin]))/365:.2f} anni (mediana delle differenze, n={fin.sum()})")
|
||||
|
||||
print(f"\n ▸ tempo trascorso: {time.time()-t_start:.0f}s")
|
||||
return dict(s0=s0, s500=s500, dd_close=dd_close, dd_acc=dd_acc, delta_p=delta_p,
|
||||
D=D, ris_e=ris_e, ris_dd=ris_dd, kk=kk)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# ATTACCO 2 — l'etichetta del rischio
|
||||
# ===========================================================================
|
||||
def attacco2() -> None:
|
||||
from src.data.downloader import load_data
|
||||
from src.portfolio import sleeves as SL
|
||||
from src.portfolio.portfolio import weights_tilt_null
|
||||
from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, realized_vol,
|
||||
resample_1d, simple_returns, tsmom_blend)
|
||||
|
||||
sezione("(5) ATTACCO 2a — LA VOL REALIZZATA DI TP01: verifica INDIPENDENTE")
|
||||
print("\n Lo scetticato ricostruisce TP01 a mano dentro il proprio script. Qui si legge lo")
|
||||
print(" sleeve DI PRODUZIONE (`src/portfolio/sleeves._tp01_returns`), che e' la serie con")
|
||||
print(" cui il libro e' misurato ovunque nel progetto: percorso diverso, stessa domanda.")
|
||||
tp = SL._tp01_returns().values.astype(float)
|
||||
att = tp[np.abs(tp) > 1e-12]
|
||||
print(f"\n vol REALIZZATA di TP01 (sleeve di produzione): {tp.std()*365**.5:.2%}"
|
||||
f" <- lo scetticato dichiara 12,14%")
|
||||
print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.")
|
||||
|
||||
print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA E' INCOMPLETO, e la parte che manca e' la")
|
||||
print(" maggiore. Lo scetticato scrive: «il target vale sulla posizione quando c'e', e")
|
||||
print(" TP01 e' long-flat». Decomposizione:")
|
||||
convs, vols_pred, vols_real, sers = [], [], [], {}
|
||||
for a in ("BTC", "ETH"):
|
||||
df = resample_1d(load_data(a, "1h"))
|
||||
c = df["close"].values.astype(float)
|
||||
r = simple_returns(c)
|
||||
t = TrendPortfolio(**CANONICAL)
|
||||
bpd = t._bpd(df)
|
||||
vol = realized_vol(r, t.vol_win_days * bpd, bpd * 365.25)
|
||||
d = np.clip(tsmom_blend(c, tuple(h * bpd for h in t.horizons_days)), 0, None)
|
||||
tgt = np.asarray(t.target_series(df), float)
|
||||
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
|
||||
net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
|
||||
sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
|
||||
ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0)
|
||||
convs.append(float(np.sqrt(np.mean(d[ok] ** 2))))
|
||||
# errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista
|
||||
fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25)
|
||||
okf = ok & np.isfinite(fwd)
|
||||
vols_pred.append(float(np.mean(vol[okf]))); vols_real.append(float(np.mean(fwd[okf])))
|
||||
print(f"\n {a}: frazione di barre FLAT (direzione 0) {float((d[ok] == 0).mean()):>6.1%}")
|
||||
print(f" direzione media QUANDO NON FLAT "
|
||||
f"{float(d[ok][d[ok] > 0].mean()):>6.2f} (tsmom_blend e' la MEDIA di 3 segni: "
|
||||
f"vale 1/3, 2/3 o 1)")
|
||||
print(f" fattore di convinzione sqrt(E[d²]) {convs[-1]:>6.3f}"
|
||||
f" -> da solo porta 20% a {0.20*convs[-1]:.2%}")
|
||||
print(f" vol prevista (30g trailing) {vols_pred[-1]:>6.2%} vs vol realizzata nei "
|
||||
f"30g SEGUENTI {vols_real[-1]:>6.2%} (rapporto {vols_real[-1]/vols_pred[-1]:.3f})")
|
||||
J = pd.concat(sers, axis=1, join="inner").fillna(0.0)
|
||||
vb, ve = J["BTC"].std() * 365 ** .5, J["ETH"].std() * 365 ** .5
|
||||
comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5
|
||||
div = comb / (0.5 * vb + 0.5 * ve)
|
||||
conv = float(np.mean(convs))
|
||||
print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}")
|
||||
print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})")
|
||||
print(f"\n CATENA: 20,00% ×{conv:.3f} (convinzione) ×{np.mean(vols_real)/np.mean(vols_pred):.3f} "
|
||||
f"(previsore di vol) ×{div:.3f} (diversificazione) = "
|
||||
f"{0.20*conv*(np.mean(vols_real)/np.mean(vols_pred))*div:.2%}")
|
||||
print(f" misurato: {tp.std()*365**.5:.2%}")
|
||||
print(f"\n 📌 IL FATTO CHE LO SCETTICATO NON DICE: il pezzo grosso NON e' lo stare flat, e'")
|
||||
print(f" che `tsmom_blend` e' la MEDIA DEI SEGNI di 3 orizzonti, quindi quando TP01 e'")
|
||||
print(f" 'a mercato' e' quasi sempre a 1/3 o 2/3 di convinzione. Contando lo zero, il")
|
||||
print(f" fattore di convinzione da solo vale ×{conv:.2f}. Lo stare flat e' UNA PARTE di")
|
||||
print(f" quel fattore, non il fattore.")
|
||||
print(f" Conseguenza operativa opposta a quella suggerita: se il divario fosse solo")
|
||||
print(f" 'flat', alzare target_vol non aumenterebbe il rischio nei giorni flat (dove non")
|
||||
print(f" c'e' posizione). Essendo CONVINZIONE, alzare target_vol alza la posizione")
|
||||
print(f" proprio nei giorni in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3).")
|
||||
|
||||
print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:")
|
||||
print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}")
|
||||
for nm, fn, dic in (("TP01", SL._tp01_returns, "20%"), ("XS01", SL._xsec_returns, "20%"),
|
||||
("VRP01", SL._vrp_combo_returns, "—"), ("SKH01", SL._skyhook_returns, "nessuno"),
|
||||
("GTAA01", SL._gtaa_daily_returns, "—")):
|
||||
v = fn().values.astype(float)
|
||||
a = v[np.abs(v) > 1e-12]
|
||||
print(f" {nm:>9}{dic:>19}{v.std()*365**.5:>16.2%}{a.std()*365**.5:>17.2%}"
|
||||
f"{len(a)/len(v):>9.1%}")
|
||||
print("\n XS01 dichiara lo STESSO 20% e ne realizza 20,59%: il divario non e' una")
|
||||
print(" proprieta' del vol-targeting, e' una proprieta' di TP01 (blend di segni +")
|
||||
print(" long-only + due gambe correlate 0,8). L'affermazione 'l'etichetta sovrastima il")
|
||||
print(" rischio' e' vera per TP01 e FALSA per il resto del libro.")
|
||||
|
||||
# -------------------------------------------------------------- 2b
|
||||
sezione("(6) ATTACCO 2b — `target_vol` e `k` sono la STESSA leva? (misurato, non dedotto)")
|
||||
print("\n Non e' ovvio: `target_vol` agisce DENTRO il vol-targeting di TP01 (e interagisce")
|
||||
print(" col leverage cap 2x), `k` agisce FUORI su tutto il libro. Due differenze")
|
||||
print(" strutturali attese, e la seconda e' quella che conta.")
|
||||
skh = SL._skyhook_returns()
|
||||
|
||||
def libro(tv: float, lev: float = 2.0) -> tuple[pd.Series, float, float]:
|
||||
"""(serie di libro 0.75*TP01(tv)+0.25*SKH01, scala ottenuta su TP01, quota barre al cap)"""
|
||||
ser, atcap = {}, []
|
||||
for a in ("BTC", "ETH"):
|
||||
df = resample_1d(load_data(a, "1h"))
|
||||
t = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev})
|
||||
tgt = np.nan_to_num(np.asarray(t.target_series(df), float))
|
||||
r = simple_returns(df["close"].values.astype(float))
|
||||
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
|
||||
net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
|
||||
ser[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
|
||||
atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean()))
|
||||
Jt = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
||||
tp_s = pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index)
|
||||
if tp_s.index.tz is None:
|
||||
tp_s.index = tp_s.index.tz_localize("UTC")
|
||||
sk = skh.copy()
|
||||
if sk.index.tz is None:
|
||||
sk.index = sk.index.tz_localize("UTC")
|
||||
B = pd.concat({"TP01": tp_s, "SKH01": sk}, axis=1, join="inner").fillna(0.0)
|
||||
return (W_TP * B["TP01"] + W_SKH * B["SKH01"], float(tp_s.std() / 1.0),
|
||||
float(np.mean(atcap)))
|
||||
|
||||
base_s, tp_vol_base, _ = libro(0.20)
|
||||
print(f"\n {'via':>28}{'scala TP01':>12}{'vol libro':>11}{'drift':>9}{'Sharpe':>8}"
|
||||
f"{'maxDD':>8}{'peso EFFETTIVO TP01/SKH':>26}{'barre al cap':>14}")
|
||||
|
||||
def riga(nome: str, s: pd.Series, scala_tp: float, atcap: float, w_eff: str) -> None:
|
||||
v = s.values.astype(float)
|
||||
eq = np.cumprod(1 + v)
|
||||
dd = float((1 - eq / np.maximum.accumulate(eq)).max())
|
||||
print(f" {nome:>28}{scala_tp:>12.2f}{v.std()*365**.5:>11.2%}{v.mean()*365:>9.2%}"
|
||||
f"{v.mean()/v.std()*365**.5:>8.2f}{dd:>8.1%}{w_eff:>26}{atcap:>14.1%}")
|
||||
|
||||
riga("k=1.00, target_vol=20% (LIBRO)", base_s, 1.00, 0.0, "75 / 25")
|
||||
for tv in (0.25, 0.30, 0.40, 0.60):
|
||||
s, tpv, ac = libro(tv)
|
||||
sc = tv / 0.20
|
||||
w = W_TP * sc / (W_TP * sc + W_SKH)
|
||||
riga(f"target_vol={tv:.0%} (solo TP01)", s, sc, ac, f"{w*100:.0f} / {(1-w)*100:.0f}")
|
||||
for k in (1.25, 1.50, 2.00, 3.00):
|
||||
riga(f"k={k:.2f} (tutto il libro)", base_s * k, k, 0.0, "75 / 25")
|
||||
|
||||
print("\n ❌ NON SONO LA STESSA LEVA, e la ragione NON e' il leverage cap (che infatti non")
|
||||
print(" morde: le barre al cap restano sotto qualche punto percentuale). E':")
|
||||
print(" **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> e'")
|
||||
print(" simultaneamente una LEVA e un TILT DI PESI. A target_vol=30% il libro non e'")
|
||||
print(" piu' 75/25 ma ~82/18, e a 60% e' ~90/10.")
|
||||
print(" Un tilt di pesi in questo progetto ha un gate suo, e non e' opzionale:")
|
||||
|
||||
dcols = {"TP01": SL._tp01_returns(), "SKH01": skh}
|
||||
for nm, s in dcols.items():
|
||||
if s.index.tz is None:
|
||||
dcols[nm] = s.tz_localize("UTC")
|
||||
for tv in (0.30, 0.60):
|
||||
sc = tv / 0.20
|
||||
w = W_TP * sc / (W_TP * sc + W_SKH)
|
||||
try:
|
||||
g = weights_tilt_null(dcols, {"TP01": W_TP, "SKH01": W_SKH},
|
||||
{"TP01": round(w, 4), "SKH01": round(1 - w, 4)}, n=300)
|
||||
print(f" weights_tilt_null 75/25 -> {w*100:.0f}/{(1-w)*100:.0f} "
|
||||
f"(target_vol {tv:.0%}): gate_pass={g.get('gate_pass')} "
|
||||
f"delta_insample={g.get('delta_insample'):+.4f} pctl_hold={g.get('pctl_hold'):.0f}")
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" weights_tilt_null non girato per tv={tv:.0%}: {type(e).__name__}: {e}")
|
||||
|
||||
print("\n 📌 CONSEGUENZA CHE LO SCETTICATO NON POTEVA VEDERE dalla sua sezione (5b), che")
|
||||
print(" guardava TP01 DA SOLO: la sua raccomandazione implicita («il knob target_vol")
|
||||
print(" funziona, l'ostacolo e' la config») e' vera sullo sleeve e FALSA sul libro.")
|
||||
print(" Alzare `target_vol` NON e' la strada meno invasiva per alzare k: e' la strada")
|
||||
print(" che aggiunge un cambio di pesi mai giudicato al cambio di scala.")
|
||||
print(" La strada pulita per un puro cambio di SCALA e' il cap di config, che e'")
|
||||
print(" esattamente quella protetta dal test di guardia.")
|
||||
|
||||
|
||||
def guardia() -> None:
|
||||
sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia")
|
||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||
frac = cfg["max_notional_per_asset_frac"]
|
||||
print(f"""
|
||||
`config/live.json`: max_notional_per_asset_frac = {frac} su 2 asset (BTC, ETH)
|
||||
-> nozionale LORDO massimo = {frac*2:.2f}x l'equity. k > {frac*2:.2f}x oggi NON e' eseguibile.
|
||||
`tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x`
|
||||
asserisce `frac * len(ASSETS) <= 1.0` E `disaster_sl_pct > 0`.
|
||||
⚠️ Cio' che protegge NON e' "la leva e' pericolosa": e' la CONCLUSIONE PUBBLICATA del
|
||||
26/07 secondo cui la **liquidation fee 1%** introdotta da Deribit dal 2026-08-01 e'
|
||||
irrilevante per questo libro. Quella conclusione poggia interamente sul fatto che a
|
||||
nozionale lordo <= 1x servirebbe un movimento avverso ~100% per liquidare. Se il cap
|
||||
sale, la liquidation fee torna nel conto e `r0726_fee_sensitivity` va rifatto.
|
||||
Il test e' quindi un promemoria di DIPENDENZA, non un divieto: chi alza il cap deve
|
||||
rifare quell'analisi, non aggirare l'assert.
|
||||
Nota di taglia, che vale piu' del cap: il conto e' ~$635 e `min_order_usd` = {cfg['min_order_usd']}.
|
||||
Alzare k moltiplica anche il TURNOVER, e il progetto ha gia' misurato (02/07) che a
|
||||
questo capitale il min-order $5 e' GIA' la banda di ribilanciamento ottimale.""")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
attacco2()
|
||||
guardia()
|
||||
print(f"\n▸ totale: consegna nel messaggio dell'agente.")
|
||||
Reference in New Issue
Block a user