713 lines
37 KiB
Python
713 lines
37 KiB
Python
"""r0822_prop_alloc — PROGETTARE l'allocazione per la BARRIERA, non per lo Sharpe (2026-08-22).
|
|
|
|
IL BUCO CHE CHIUDE
|
|
------------------
|
|
Il canale prop/funded e' l'unico misurato che moltiplica il nozionale senza possedere capitale.
|
|
Il progetto lo ha gia' VALUTATO (24-25/07: `r0725_prop_ladder.py`, `r0725_prop_coupled.py`,
|
|
`r0725_hyro.py`) ma non lo ha mai **PROGETTATO**: ha sempre preso il libro ottimizzato per lo
|
|
Sharpe (config A = live 75/25, config B = 55/20/25 scelta a mano) e poi MISURATO P(pass) del
|
|
risultato. Due punti nello spazio dei pesi, tre leve, nessuna ottimizzazione, nessun null.
|
|
|
|
Qui la funzione obiettivo cambia: sotto una **barriera di maxDD STATICA per-conto** conta la coda
|
|
sinistra e la DURATA, non il rapporto rendimento/vol. Si ottimizza
|
|
|
|
J(w, k) = P(passare l'eval) x P(essere vivo dopo 12 mesi da funded)
|
|
|
|
su pesi w e leva k, con la lente WICK ACCOPPIATA (la `close-only` e' CIECA: 0% di breach da
|
|
daily-loss su ogni configurazione, la regola non scatta mai sulle chiusure).
|
|
|
|
LA PREVISIONE, REGISTRATA PRIMA DI MISURARE
|
|
-------------------------------------------
|
|
Nel limite gaussiano con leva LIBERA l'obiettivo si riduce allo Sharpe, e si vede in due righe.
|
|
Per un moto browniano con drift mu e vol sigma a leva k, la probabilita' di toccare +T prima di
|
|
-D vale P = (1-e^{-theta*D})/(1-e^{-theta*(T+D)}) con **theta = 2*mu/(k*sigma^2)**: la leva entra
|
|
SOLO dividendo theta. Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) si ottiene
|
|
theta ~ 2*mu^2/(T*sigma^2) = 2*S^2/T, con S = Sharpe: **la vol sparisce e resta lo Sharpe al
|
|
quadrato**. Quindi mi aspetto PRIMA di misurare:
|
|
(a) l'argmax di J vicino all'argmax di Sharpe -> la premessa "obiettivo diverso" e' in gran
|
|
parte FALSA sui PESI;
|
|
(b) il contenuto vero nella **LEVA**, che lo Sharpe non determina affatto, e nella deviazione
|
|
dal gaussiano (skew, wick, daily-loss: una soglia ASSOLUTA non e' scale-invariante);
|
|
(c) leva ottima per J **piu' bassa** di quella ottima per il payout (0.75x, 25/07): J e' una
|
|
probabilita' di sopravvivenza, il payout e' un flusso.
|
|
Se (a) risulta falsa, e' un risultato; se risulta vera, il filone "riallocare per la barriera"
|
|
si chiude e si smette di pagarlo.
|
|
|
|
CONVENZIONI DICHIARATE
|
|
----------------------
|
|
* Macchineria del wick ACCOPPIATO **riusata** da `r0725_prop_coupled.py` (import, non riscritta):
|
|
`_hourly_legs` (recon MTM orario TP01/SKH01), `crypto_daily_tuples` (minimo ESATTO sul path
|
|
orario condiviso), `xsec_daily_tuples` (XS01 dagli OHLC 1d, ordine condiviso avverso).
|
|
* VRP01 e GTAA01 sono AGGIUNTI qui (non c'erano): GTAA01 col wick dagli OHLC dei 6 ETF (long-only
|
|
co-moventi -> ogni gamba al proprio minimo = convenzione severa dichiarata); VRP01 con
|
|
**gap = 0**, cioe' CIECO, perche' il suo mark infra-settimana non e' nel dataset dello sleeve
|
|
(il rendimento settimanale e' lumpato sul giorno di scadenza). E' una lente GENEROSA per VRP01:
|
|
la misura del 30/07 sulle quote reali dice mark peggiore infra-settimana mediano -6.9% e minimo
|
|
**-81.7%** del capitale su un trade finito in UTILE. Se VRP01 perde sotto una lente generosa,
|
|
perde.
|
|
* de-luck sul DRIFT **x0.89** (fattore MISURATO il 26/07 sul libro live, `r0726_deluck_factor.py`),
|
|
non x0.6 come nel 25/07 (li' era stimato a occhio e ri-misurato troppo severo del 31-34%).
|
|
Sensibilita' a 1.00 e 0.60 girata: cio' che conta e' se l'ORDINE fra le configurazioni tiene.
|
|
* bootstrap a blocchi (20g) con **indici CONDIVISI fra tutte le configurazioni** -> le differenze
|
|
fra configurazioni sono APPAIATE e il loro errore MC e' molto minore di quello dei livelli.
|
|
* finestra comune **2024+** (XS01 nasce li'): ~2.6 anni, ed e' la finestra in cui XS01 e' stato
|
|
scoperto -> le configurazioni che lo contengono sono FAVORITE per costruzione. Dichiarato.
|
|
* fisco 33% sui payout, split 80%, regole vere delle due firm (max-loss STATICO dal saldo
|
|
iniziale, base ripristinata dopo il prelievo — bug del 25/07 gia' corretto a monte).
|
|
|
|
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822_prop_alloc.py`
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from functools import lru_cache
|
|
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"))
|
|
|
|
from src.data.eq_splits import repair_splits # noqa: E402
|
|
from src.portfolio.gtaa import ( # noqa: E402
|
|
ANN, EQ_UNIVERSE, GTAA_DEFAULT_CAPITAL, HORIZONS, IB_MIN_TRADE_USD, REBAL_BAND_USD,
|
|
REBAL_EVERY, TARGET_VOL, ib_commission,
|
|
)
|
|
from src.portfolio.portfolio import metrics, weights_tilt_null # noqa: E402
|
|
from src.portfolio.sleeves import ( # noqa: E402
|
|
GTAA_BOOK_ACTIVATION, _gtaa_daily_returns, _skyhook_returns, _tp01_returns,
|
|
_vrp_combo_returns, _xsec_returns,
|
|
)
|
|
|
|
import r0725_prop_coupled as pc # noqa: E402
|
|
import r0725_prop_ladder as pl # noqa: E402
|
|
from r0725_capcurve import EURUSD, TAX_RATE # noqa: E402
|
|
|
|
RAW = ROOT / "data" / "raw"
|
|
|
|
# ------------------------------------------------------------------ costanti dichiarate
|
|
DELUCK = 0.89 # fattore d'ancora MISURATO sul drift (26/07), non stimato a occhio
|
|
START_WIN = "2024-01-01" # finestra comune ai 5 sleeve (XS01 nasce qui)
|
|
BLOCK = 20 # blocco del bootstrap, in giorni
|
|
N_PATHS = 4000
|
|
SPLIT = pl.SPLIT # 80% al trader
|
|
TARGET_EUR_DAY = 50.0
|
|
FIRMS = {f["name"]: f for f in pl.FIRMS}
|
|
SLEEVES = ("TP01", "SKH01", "XS01", "VRP01", "GTAA01")
|
|
CRYPTO_PROP = ("TP01", "SKH01", "XS01") # cio' che una firm CRYPTO puo' davvero far eseguire
|
|
|
|
|
|
# ================================================================== §0. PANNELLO (R, wick)
|
|
|
|
@lru_cache(maxsize=8)
|
|
def _eq_ohlc(sym: str) -> pd.DataFrame:
|
|
p = RAW / f"eq_{sym.lower()}_1d.parquet"
|
|
d = pd.read_parquet(p)
|
|
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
|
d = d.sort_index()
|
|
d, _ = repair_splits(d) # split IWM/EFA non aggiustati (25/07)
|
|
return d[["open", "high", "low", "close"]].astype(float)
|
|
|
|
|
|
def _gtaa_exposure(close: pd.Series) -> np.ndarray:
|
|
"""Copia FEDELE di gtaa._exposure (che prende una Series di soli close)."""
|
|
px = close.values
|
|
n = len(px)
|
|
tgt = np.zeros(n)
|
|
mh = max(HORIZONS)
|
|
for i in range(mh, n):
|
|
tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS])
|
|
s = pd.Series(tgt, index=close.index)
|
|
rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN
|
|
scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0)
|
|
return (s * scale).clip(0, 1.0).values
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def gtaa_daily_tuples() -> pd.DataFrame:
|
|
"""(R, m) giornalieri di GTAA01: chiusura replicata da gtaa.gtaa_returns e minimo intraday
|
|
dagli OHLC delle 6 gambe. Long-only e co-moventi -> convenzione **ogni gamba al proprio
|
|
minimo insieme** (severa, la stessa famiglia del bound 'perleg' di XS01).
|
|
Weekend/festivi: R = 0 e m = 0 (capitale IB fermo — convenzione del book)."""
|
|
cap_leg = GTAA_DEFAULT_CAPITAL / len(EQ_UNIVERSE)
|
|
legs_r, legs_m = {}, {}
|
|
for sym in EQ_UNIVERSE:
|
|
d = _eq_ohlc(sym)
|
|
close = d["close"]
|
|
ex = np.nan_to_num(_gtaa_exposure(close))
|
|
px = close.values
|
|
prev = np.concatenate(([np.nan], px[:-1]))
|
|
ret = np.zeros(len(px))
|
|
ret[1:] = px[1:] / px[:-1] - 1.0
|
|
ret_low = d["low"].values / prev - 1.0 # long-only: il peggio e' il minimo
|
|
held = np.empty(len(ex))
|
|
comm = np.zeros(len(ex))
|
|
cur = 0.0
|
|
for i in range(len(ex)): # identico a gtaa._gated_returns
|
|
if i % REBAL_EVERY == 0:
|
|
notional = abs(ex[i] - cur) * cap_leg
|
|
if notional >= max(REBAL_BAND_USD, IB_MIN_TRADE_USD):
|
|
comm[i] = ib_commission(notional, px[i]) / cap_leg
|
|
cur = ex[i]
|
|
held[i] = cur
|
|
pos = np.zeros(len(held))
|
|
pos[1:] = held[:-1]
|
|
legs_r[sym] = pd.Series(pos * ret - comm, index=close.index)
|
|
legs_m[sym] = pd.Series(pos * np.nan_to_num(ret_low) - comm, index=close.index)
|
|
R = pd.concat(legs_r, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
|
|
M = pd.concat(legs_m, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
|
|
days = pd.date_range(R.index.min().normalize(), R.index.max().normalize(), freq="1D", tz="UTC")
|
|
R = R.reindex(days).fillna(0.0)
|
|
M = M.reindex(days).fillna(0.0)
|
|
out = pd.DataFrame({"R": R, "m": np.minimum(M, R)})
|
|
return out[out.index >= GTAA_BOOK_ACTIVATION]
|
|
|
|
|
|
@lru_cache(maxsize=2)
|
|
def vrp_daily_tuples() -> pd.DataFrame:
|
|
"""(R, m) di VRP01 con **m = R**: il mark infra-settimana NON e' nel dataset dello sleeve
|
|
(rendimento settimanale lumpato sul giorno di scadenza). Lente CIECA e GENEROSA — dichiarata."""
|
|
r = _vrp_combo_returns().dropna().sort_index()
|
|
if r.index.tz is None:
|
|
r.index = r.index.tz_localize("UTC")
|
|
return pd.DataFrame({"R": r.values, "m": r.values}, index=r.index)
|
|
|
|
|
|
def _d(s: pd.Series) -> pd.Series:
|
|
s = s.dropna().sort_index()
|
|
if s.index.tz is None:
|
|
s.index = s.index.tz_localize("UTC")
|
|
return s
|
|
|
|
|
|
_CRY_CACHE: dict = {}
|
|
|
|
|
|
def _crypto(w_tp: float, w_skh: float) -> pd.DataFrame:
|
|
key = (round(w_tp, 6), round(w_skh, 6))
|
|
if key not in _CRY_CACHE:
|
|
_CRY_CACHE[key] = pc.crypto_daily_tuples(w_tp, w_skh)
|
|
return _CRY_CACHE[key]
|
|
|
|
|
|
@lru_cache(maxsize=4)
|
|
def _panel_index(start: str) -> pd.DatetimeIndex:
|
|
"""Griglia comune ai 5 sleeve (intersezione), dal `start` in poi."""
|
|
idx = _crypto(1.0, 0.0).index
|
|
for f in (pc.xsec_daily_tuples(), vrp_daily_tuples(), gtaa_daily_tuples()):
|
|
idx = idx.intersection(f.index)
|
|
return idx[idx >= pd.Timestamp(start, tz="UTC")]
|
|
|
|
|
|
_RG_CACHE: dict = {}
|
|
|
|
|
|
def book_RG(w: dict, start: str = START_WIN) -> tuple[np.ndarray, np.ndarray]:
|
|
"""(R, gap) giornalieri di un libro a pesi arbitrari sui 5 sleeve.
|
|
Crypto TP01+SKH01: minimo ESATTO sul path orario condiviso (r0725_prop_coupled).
|
|
XS01/VRP01/GTAA01: il loro minimo si SOMMA (worst simultaneo — convenzione del 25/07)."""
|
|
key = (start,) + tuple(round(float(w.get(nm, 0.0)), 6) for nm in SLEEVES)
|
|
if key in _RG_CACHE:
|
|
return _RG_CACHE[key]
|
|
idx = _panel_index(start)
|
|
w_tp, w_skh = w.get("TP01", 0.0), w.get("SKH01", 0.0)
|
|
R = np.zeros(len(idx))
|
|
M = np.zeros(len(idx))
|
|
if w_tp > 0 or w_skh > 0:
|
|
c = _crypto(w_tp, w_skh).reindex(idx)
|
|
R += c["R"].values
|
|
M += c["m"].values
|
|
for nm, fr in (("XS01", pc.xsec_daily_tuples()), ("VRP01", vrp_daily_tuples()),
|
|
("GTAA01", gtaa_daily_tuples())):
|
|
wt = w.get(nm, 0.0)
|
|
if wt > 0:
|
|
f = fr.reindex(idx)
|
|
R += wt * f["R"].values
|
|
M += wt * f["m"].values
|
|
R = np.nan_to_num(R)
|
|
M = np.nan_to_num(M)
|
|
_RG_CACHE[key] = (R, np.minimum(M, R) - R)
|
|
return _RG_CACHE[key]
|
|
|
|
|
|
def deluck(R: np.ndarray, factor: float = DELUCK) -> np.ndarray:
|
|
return R - (1.0 - factor) * R.mean()
|
|
|
|
|
|
# ================================================================== §1. SIMULATORE
|
|
|
|
def boot_idx(n_hist: int, n_days: int, n_paths: int, seed: int, block: int = BLOCK) -> np.ndarray:
|
|
rng = np.random.default_rng(seed)
|
|
n_blocks = int(np.ceil(n_days / block))
|
|
st = rng.integers(0, n_hist - block, size=(n_paths, n_blocks))
|
|
return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
|
|
|
|
|
|
def eval_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str,
|
|
lens: str = "coupled") -> dict:
|
|
"""P(passare l'eval) entro l'orizzonte del bootstrap + giorni al pass."""
|
|
tgt, dd, dl = FIRMS[firm]["ev"]
|
|
r = R[idx] * lev
|
|
g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r)
|
|
P, D = r.shape
|
|
eq = np.ones(P)
|
|
alive = np.ones(P, bool)
|
|
passed = np.zeros(P, bool)
|
|
tpass = np.full(P, D, int)
|
|
for t in range(D):
|
|
act = alive & ~passed
|
|
if not act.any():
|
|
break
|
|
rt, gt = r[:, t], g[:, t]
|
|
eqp = eq
|
|
eq = np.where(act, eq * (1.0 + rt), eq)
|
|
eq_low = np.where(act, eqp * (1.0 + rt + gt), eq)
|
|
bust = act & ((eq_low < 1.0 - dd) | (rt + gt < -dl))
|
|
alive &= ~bust
|
|
ok = act & ~bust & (eq >= 1.0 + tgt)
|
|
tpass[ok] = t
|
|
passed |= ok
|
|
return dict(p_pass=float(passed.mean()), passed=passed,
|
|
t_pass=float(np.median(tpass[passed])) if passed.any() else np.nan)
|
|
|
|
|
|
def funded_sim(R: np.ndarray, G: np.ndarray, idx: np.ndarray, lev: float, firm: str,
|
|
notional: float, lens: str = "coupled") -> dict:
|
|
"""P(vivo a fine orizzonte) + payout netto cumulato (prelievo mensile, base ripristinata)."""
|
|
ml, dl = FIRMS[firm]["fu"]
|
|
r = R[idx] * lev
|
|
g = (G[idx] * lev) if lens == "coupled" else np.zeros_like(r)
|
|
P, D = r.shape
|
|
eq = np.ones(P)
|
|
alive = np.ones(P, bool)
|
|
payout = np.zeros(P)
|
|
alive_at = {}
|
|
for t in range(D):
|
|
rt, gt = r[:, t], g[:, t]
|
|
eqp = eq
|
|
eq = np.where(alive, eq * (1.0 + rt), eq)
|
|
eq_low = np.where(alive, eqp * (1.0 + rt + gt), eq)
|
|
bust = alive & ((eq_low < 1.0 - ml) | (rt + gt < -dl))
|
|
alive &= ~bust
|
|
if (t + 1) % 30 == 0:
|
|
take = alive & (eq > 1.0)
|
|
payout += np.where(take, (eq - 1.0) * notional, 0.0) * SPLIT * (1.0 - TAX_RATE)
|
|
eq = np.where(take, 1.0, eq)
|
|
if (t + 1) % 365 == 0:
|
|
alive_at[(t + 1) // 365] = float(alive.mean())
|
|
return dict(p_alive=float(alive.mean()), alive_at=alive_at,
|
|
e_payout=float(payout.mean()), payout=payout)
|
|
|
|
|
|
def objective(w: dict, lev: float, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
|
|
lens: str = "coupled", factor: float = DELUCK,
|
|
notional: float = 100_000.0) -> dict:
|
|
R, G = book_RG(w)
|
|
R = deluck(R, factor)
|
|
e = eval_sim(R, G, ev_idx, lev, firm, lens)
|
|
f = funded_sim(R, G, fu_idx, lev, firm, notional, lens)
|
|
ann = float(R.mean() * 365.0)
|
|
vol = float(R.std() * np.sqrt(365.0))
|
|
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
|
|
e_payout=f["e_payout"], sharpe=(ann / vol if vol > 0 else 0.0),
|
|
vol=vol, drift=ann, t_pass=e["t_pass"])
|
|
|
|
|
|
# ================================================================== §2. IL CONTO ANALITICO
|
|
|
|
def p_hit_target_first(mu: float, sig: float, lev: float, T: float, D: float) -> float:
|
|
"""Gambler's ruin per moto browniano con drift: P(toccare +T prima di -D), SENZA limite di
|
|
tempo. theta = 2*mu/(k*sigma^2): la leva entra SOLO dividendo theta -> con leva -> inf la
|
|
probabilita' tende a D/(T+D) (37.5% con 10%/6%), qualunque sia lo Sharpe."""
|
|
if sig <= 0:
|
|
return float(mu > 0)
|
|
theta = 2.0 * mu / (lev * sig * sig)
|
|
if abs(theta) < 1e-12:
|
|
return D / (T + D)
|
|
return float((1.0 - np.exp(-theta * D)) / (1.0 - np.exp(-theta * (T + D))))
|
|
|
|
|
|
# ================================================================== §3. GRIGLIA / NULL
|
|
|
|
def simplex_grid(names: tuple, step_den: int) -> list[dict]:
|
|
"""Tutti i vettori di peso a passo 1/step_den sul simplesso di `names` (somma 1)."""
|
|
out = []
|
|
n = len(names)
|
|
|
|
def rec(prefix, rem, k):
|
|
if k == n - 1:
|
|
out.append(dict(zip(names, [p / step_den for p in prefix] + [rem / step_den])))
|
|
return
|
|
for v in range(rem + 1):
|
|
rec(prefix + [v], rem - v, k + 1)
|
|
|
|
rec([], step_den, 0)
|
|
return out
|
|
|
|
|
|
def wkey(w: dict) -> str:
|
|
return "/".join(f"{int(round(100 * w.get(nm, 0.0))):d}" for nm in CRYPTO_PROP)
|
|
|
|
|
|
def grid_scan(grid: list[dict], levs: tuple, firm: str, ev_idx: np.ndarray, fu_idx: np.ndarray,
|
|
lens: str = "coupled", factor: float = DELUCK) -> pd.DataFrame:
|
|
rows = []
|
|
for w in grid:
|
|
for lev in levs:
|
|
o = objective(w, lev, firm, ev_idx, fu_idx, lens, factor)
|
|
rows.append(dict(w=wkey(w), lev=lev, **{k: v for k, v in o.items()}))
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def random_weights(n: int, names: tuple, seed: int, floor: float = 0.0,
|
|
caps: dict | None = None) -> list[dict]:
|
|
"""Stesso campionatore di `portfolio.weights_tilt_null` (dirichlet uniforme sul simplesso +
|
|
rejection su floor/caps), ma qui serve a costruire il NULL della funzione obiettivo J."""
|
|
rng = np.random.default_rng(seed)
|
|
caps_v = np.array([(caps or {}).get(nm, 1.0) for nm in names], float)
|
|
out = []
|
|
while len(out) < n:
|
|
b = rng.dirichlet(np.ones(len(names)), size=4 * n + 64)
|
|
ok = (b >= floor).all(axis=1) & (b <= caps_v).all(axis=1)
|
|
out.extend(b[ok])
|
|
return [dict(zip(names, v)) for v in out[:n]]
|
|
|
|
|
|
# ================================================================== main
|
|
|
|
def hr(t: str = "") -> None:
|
|
print("\n" + "-" * 104)
|
|
if t:
|
|
print(f" {t}")
|
|
print("-" * 104)
|
|
|
|
|
|
def main() -> None:
|
|
t0 = time.time()
|
|
print("=" * 104)
|
|
print(" PROP-ALLOC — allocare per la BARRIERA (P(pass) x P(vivo 12m)) invece che per lo Sharpe")
|
|
print("=" * 104)
|
|
|
|
# ---------------------------------------------------------------- A. pannello + sanity
|
|
idx = _panel_index(START_WIN)
|
|
print(f"\n finestra comune ai 5 sleeve: {len(idx)} giorni {idx[0].date()} -> {idx[-1].date()}"
|
|
f" (XS01 nasce nel 2024: e' anche la sua finestra di SCOPERTA -> lo favorisce)")
|
|
print(f" de-luck sul drift x{DELUCK} (misurato 26/07) | bootstrap a blocchi di {BLOCK}g, "
|
|
f"indici CONDIVISI fra configurazioni | {N_PATHS} percorsi")
|
|
|
|
off = {"TP01": _d(_tp01_returns()), "SKH01": _d(_skyhook_returns()), "XS01": _d(_xsec_returns()),
|
|
"VRP01": _d(_vrp_combo_returns()), "GTAA01": _d(_gtaa_daily_returns())}
|
|
rec = {"TP01": _crypto(1.0, 0.0)["R"], "SKH01": _crypto(0.0, 1.0)["R"],
|
|
"XS01": pc.xsec_daily_tuples()["R"], "VRP01": vrp_daily_tuples()["R"],
|
|
"GTAA01": gtaa_daily_tuples()["R"]}
|
|
print("\n sanity — la ricostruzione (R) deve coincidere con lo sleeve ufficiale sulla chiusura:")
|
|
for nm in SLEEVES:
|
|
com = off[nm].index.intersection(rec[nm].index)
|
|
dmax = float(np.abs(off[nm].reindex(com).values - rec[nm].reindex(com).values).max())
|
|
note = "" if nm not in ("TP01", "SKH01") else " (recon MTM orario: lente diversa per costruzione)"
|
|
print(f" {nm:>7}: max|delta| = {dmax:.2e} {'OK' if dmax < 1e-9 else 'DIVERGE'}{note}")
|
|
|
|
hr("A. PROFILO DEGLI SLEEVE SOTTO BARRIERA — cio' che conta non e' lo Sharpe, e si vede qui")
|
|
print(f" {'sleeve':>8} {'Sharpe':>7} {'vol a':>7} {'drift a':>8} {'skew':>7} "
|
|
f"{'giorno peggiore':>16} {'gap p90':>9} {'gap peggiore':>13} {'g/vol':>7}")
|
|
prof = {}
|
|
for nm in SLEEVES:
|
|
R, G = book_RG({nm: 1.0})
|
|
Rd = deluck(R)
|
|
vol = Rd.std() * np.sqrt(365.0)
|
|
dr = Rd.mean() * 365.0
|
|
sk = float(pd.Series(Rd).skew())
|
|
prof[nm] = dict(sharpe=dr / vol if vol > 0 else 0.0, vol=vol, drift=dr, skew=sk,
|
|
worst=Rd.min(), gap90=np.percentile(-G, 90), gapw=G.min())
|
|
print(f" {nm:>8} {prof[nm]['sharpe']:>7.2f} {vol:>6.1%} {dr:>8.1%} {sk:>7.2f} "
|
|
f"{Rd.min():>15.2%} {-np.percentile(-G, 90)*100:>8.2f}pp {G.min()*100:>12.2f}pp "
|
|
f"{(-G.min()/vol) if vol > 0 else 0:>7.2f}")
|
|
print("\n lettura: `gap peggiore` e' l'escursione intraday che la lente close-only NON vede;")
|
|
print(" `g/vol` la misura in unita' di vol annua. VRP01 ha gap 0 **per cecita' del dato**,")
|
|
print(" non per assenza di rischio (mark infra-settimana misurato il 30/07: mediana -6.9%,")
|
|
print(" minimo -81.7% del capitale su un trade finito in UTILE).")
|
|
|
|
hr("B. IL CONTO ANALITICO — perche' con leva LIBERA la barriera premia lo SHARPE, non la vol")
|
|
tgt, dd, _dl = FIRMS["HYRO"]["ev"]
|
|
print(f" P(toccare +{tgt:.0%} prima di -{dd:.0%}), gambler's ruin senza limite di tempo, "
|
|
f"theta = 2*mu/(k*sigma^2):")
|
|
print(f" {'sleeve':>8} {'Sharpe':>7} | " + " ".join(f"{f'k={k:g}':>8}" for k in (0.25, 0.5, 1.0, 2.0, 4.0)))
|
|
for nm in SLEEVES:
|
|
p = prof[nm]
|
|
cells = " ".join(f"{p_hit_target_first(p['drift'], p['vol'], k, tgt, dd):>8.1%}"
|
|
for k in (0.25, 0.5, 1.0, 2.0, 4.0))
|
|
print(f" {nm:>8} {p['sharpe']:>7.2f} | {cells}")
|
|
print(f" {'limite k->inf':>16} | " + " ".join([f"{dd/(tgt+dd):>8.1%}"] * 5)
|
|
+ " <- D/(T+D): la leva alta CANCELLA l'edge")
|
|
print("\n Alla leva minima che porta a +T dentro l'orizzonte (k ~ T/mu) resta theta ~ 2*S^2/T:")
|
|
print(" la VOL sparisce e conta lo Sharpe AL QUADRATO. Percio' la previsione registrata prima")
|
|
print(" di misurare e': l'argmax di J sui PESI ~ l'argmax di Sharpe, e il contenuto vero e'")
|
|
print(" nella LEVA (che lo Sharpe non determina) e nella deviazione dal gaussiano.")
|
|
|
|
# ---------------------------------------------------------------- C. griglia
|
|
n_hist = len(idx)
|
|
EV_H, FU_H = 365, 365
|
|
ev_idx = boot_idx(n_hist, EV_H, N_PATHS, seed=8220)
|
|
fu_idx = boot_idx(n_hist, FU_H, N_PATHS, seed=8221)
|
|
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
|
|
STEP = 8
|
|
grid = simplex_grid(CRYPTO_PROP, STEP)
|
|
|
|
hr(f"C. GRIGLIA — {len(grid)} vettori di peso (passo 1/{STEP}) x {len(LEVS)} leve "
|
|
f"= {len(grid)*len(LEVS)} celle, firm HYRO (eval +10%/-6%/-4%, funded -6%/-4%)")
|
|
print(f" orizzonte eval {EV_H}g, funded {FU_H}g. Lente ACCOPPIATA. "
|
|
f"J = P(pass) x P(vivo 12m).")
|
|
t = time.time()
|
|
G3 = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx)
|
|
print(f" ({time.time()-t:.0f}s)")
|
|
|
|
best = G3.loc[G3["J"].idxmax()]
|
|
# argmax di SHARPE: lo Sharpe non dipende dalla leva -> si sceglie il peso, poi la leva
|
|
# migliore PER QUEL PESO secondo J (altrimenti non e' un confronto onesto).
|
|
w_sh = G3.loc[G3["sharpe"].idxmax(), "w"]
|
|
sub_sh = G3[G3["w"] == w_sh]
|
|
best_sh = sub_sh.loc[sub_sh["J"].idxmax()]
|
|
live = G3[(G3["w"] == "75/25/0")]
|
|
best_live = live.loc[live["J"].idxmax()]
|
|
|
|
print(f"\n {'':>22} {'pesi TP/SKH/XS':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} "
|
|
f"{'P(vivo12m)':>11} {'Sharpe':>7} {'vol':>6} {'E[pay/a]':>10} {'g.al pass':>10}")
|
|
for lab, row in (("argmax J", best), ("argmax Sharpe", best_sh),
|
|
("libro LIVE 75/25", best_live)):
|
|
print(f" {lab:>22} {row['w']:>16} {row['lev']:>6.3f} {row['J']:>7.3f} "
|
|
f"{row['p_pass']:>8.1%} {row['p_alive']:>11.1%} {row['sharpe']:>7.2f} "
|
|
f"{row['vol']:>6.1%} {row['e_payout']:>9,.0f}$ {row['t_pass']:>10.0f}")
|
|
|
|
print("\n Le prime 8 celle per J:")
|
|
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8} "
|
|
f"{'Sharpe':>7} {'E[pay/a]':>10}")
|
|
for i, (_, r) in enumerate(G3.sort_values("J", ascending=False).head(8).iterrows(), 1):
|
|
print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['J']:>7.3f} {r['p_pass']:>8.1%} "
|
|
f"{r['p_alive']:>8.1%} {r['sharpe']:>7.2f} {r['e_payout']:>9,.0f}$")
|
|
|
|
print("\n E per E[payout] (obiettivo DIVERSO: un flusso, non una sopravvivenza):")
|
|
print(f" {'#':>3} {'pesi':>16} {'leva':>6} {'E[pay/a]':>10} {'J':>7} {'P(pass)':>8} {'P(vivo)':>8}")
|
|
for i, (_, r) in enumerate(G3.sort_values("e_payout", ascending=False).head(4).iterrows(), 1):
|
|
print(f" {i:>3} {r['w']:>16} {r['lev']:>6.3f} {r['e_payout']:>9,.0f}$ {r['J']:>7.3f} "
|
|
f"{r['p_pass']:>8.1%} {r['p_alive']:>8.1%}")
|
|
|
|
hr("C-bis. LA LEVA — la variabile che lo Sharpe non determina e che decide tutto")
|
|
print(f" pesi = argmax J ({best['w']}):")
|
|
print(f" {'leva':>7} {'J':>7} {'P(pass) 365g':>13} {'P(pass) 1095g':>14} {'P(vivo 12m)':>12} "
|
|
f"{'E[pay/a]':>10}")
|
|
ev_long = boot_idx(n_hist, 1095, N_PATHS, seed=8222)
|
|
w_best = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in best["w"].split("/")]))
|
|
Rb, Gb = book_RG(w_best)
|
|
Rb = deluck(Rb)
|
|
for lev in LEVS:
|
|
o = objective(w_best, lev, "HYRO", ev_idx, fu_idx)
|
|
pl_long = eval_sim(Rb, Gb, ev_long, lev, "HYRO")["p_pass"]
|
|
print(f" {lev:>7.3f} {o['J']:>7.3f} {o['p_pass']:>12.1%} {pl_long:>13.1%} "
|
|
f"{o['p_alive']:>11.1%} {o['e_payout']:>9,.0f}$")
|
|
print("\n A 365 giorni P(pass) e' a CAMPANA nella leva (a leva bassa il drift non arriva a +10%")
|
|
print(" in tempo); senza limite di tempo — che e' la regola vera di HyroTrader — e' MONOTONA")
|
|
print(" DECRESCENTE, come dice il conto analitico. Le due colonne dicono cose diverse e vanno")
|
|
print(" lette insieme: la leva alta non aumenta la probabilita' di farcela, aumenta la FRETTA.")
|
|
|
|
# ---------------------------------------------------------------- D. i null
|
|
hr("D. NULL 1 — l'argmax e' speciale, o e' solo il MASSIMO DI 45 ESTRAZIONI?")
|
|
rw = random_weights(300, CRYPTO_PROP, seed=8223)
|
|
lev_star = float(best["lev"])
|
|
Jr = np.array([objective(w, lev_star, "HYRO", ev_idx, fu_idx)["J"] for w in rw])
|
|
pctl = float((Jr < best["J"]).mean() * 100.0)
|
|
bestofk = 100.0 * len(grid) / (len(grid) + 1)
|
|
print(f" J dell'argmax = {best['J']:.3f} alla leva {lev_star:g}")
|
|
print(f" null: 300 vettori di peso casuali uniformi sul simplesso (stesso campionatore di")
|
|
print(f" `portfolio.weights_tilt_null`), valutati alla STESSA leva")
|
|
print(f" mediana {np.median(Jr):.3f} | p90 {np.percentile(Jr, 90):.3f} | "
|
|
f"max {Jr.max():.3f} | frazione sopra il LIVE 75/25 = {(Jr > best_live['J']).mean():.0%}")
|
|
print(f" percentile dell'argmax fra i casuali = {pctl:.1f}° contro la firma best-of-"
|
|
f"{len(grid)} = {bestofk:.1f}°")
|
|
print(f" -> {'DENTRO' if pctl <= bestofk else 'SOPRA'} la firma del massimo-di-k: "
|
|
f"{'nessuna evidenza che quel vettore sia speciale' if pctl <= bestofk else 'vettore particolare'}")
|
|
print(f" (la meta' bassa del null e' pero' informativa: il LIVE 75/25 e' battuto dal "
|
|
f"{(Jr > best_live['J']).mean():.0%} dei pesi CASUALI)")
|
|
|
|
hr("D-bis. NULL 2 — selezione IN-SAMPLE-ONLY (la scelta guardando J e' selezione come ogni altra)")
|
|
half = n_hist // 2
|
|
R_all = {}
|
|
for w in grid:
|
|
R_, G_ = book_RG(w)
|
|
R_all[wkey(w)] = (deluck(R_), G_)
|
|
ev_h = boot_idx(half, EV_H, N_PATHS, seed=8224)
|
|
fu_h = boot_idx(half, FU_H, N_PATHS, seed=8225)
|
|
|
|
def _J(key, lev, sl):
|
|
R_, G_ = R_all[key]
|
|
e = eval_sim(R_[sl], G_[sl], ev_h, lev, "HYRO")["p_pass"]
|
|
f = funded_sim(R_[sl], G_[sl], fu_h, lev, "HYRO", 100_000.0)["p_alive"]
|
|
return e * f
|
|
|
|
IS, OOS = slice(0, half), slice(half, n_hist)
|
|
rows = []
|
|
for w in grid:
|
|
k = wkey(w)
|
|
for lev in LEVS:
|
|
rows.append((k, lev, _J(k, lev, IS), _J(k, lev, OOS)))
|
|
S = pd.DataFrame(rows, columns=["w", "lev", "J_is", "J_oos"])
|
|
pick = S.loc[S["J_is"].idxmax()]
|
|
oracle = S.loc[S["J_oos"].idxmax()]
|
|
live_oos = S[(S["w"] == "75/25/0")].sort_values("J_oos", ascending=False).iloc[0]
|
|
print(f" prima meta' {idx[0].date()}->{idx[half-1].date()} | seconda {idx[half].date()}->{idx[-1].date()}")
|
|
print(f" {'':>28} {'pesi':>16} {'leva':>6} {'J in-sample':>12} {'J out-of-sample':>16}")
|
|
print(f" {'scelto SUL SOLO in-sample':>28} {pick['w']:>16} {pick['lev']:>6.3f} "
|
|
f"{pick['J_is']:>12.3f} {S[(S['w']==pick['w'])&(S['lev']==pick['lev'])]['J_oos'].iloc[0]:>16.3f}")
|
|
print(f" {'oracolo (max sull OOS)':>28} {oracle['w']:>16} {oracle['lev']:>6.3f} "
|
|
f"{oracle['J_is']:>12.3f} {oracle['J_oos']:>16.3f}")
|
|
print(f" {'libro LIVE 75/25 (baseline)':>28} {live_oos['w']:>16} {live_oos['lev']:>6.3f} "
|
|
f"{live_oos['J_is']:>12.3f} {live_oos['J_oos']:>16.3f}")
|
|
rho = float(S["J_is"].corr(S["J_oos"], method="spearman"))
|
|
print(f" Spearman(J in-sample, J out-of-sample) su {len(S)} celle = {rho:+.2f}")
|
|
print(" -> se rho e' alto la mappa e' stabile e la scelta si trasferisce; se e' ~0 l'argmax")
|
|
print(" e' rumore e la decisione va presa sul MECCANISMO (la leva), non sulla cella.")
|
|
|
|
hr("D-ter. NULL DEL DE-LEVERING — la leva e' GIA' dentro l'ottimizzazione, ma va detto")
|
|
print(" Il null classico ('esiste k<1 che da' lo stesso DD con Sharpe migliore?') qui e'")
|
|
print(" strutturale: ogni configurazione e' valutata a OTTO leve, quindi nessuna puo' vincere")
|
|
print(" solo perche' e' meno esposta. La domanda che resta e': la MIGLIOR leva del libro LIVE")
|
|
print(" raggiunge il J dell'argmax?")
|
|
print(f" argmax J : {best['w']:>10} @ {best['lev']:g}x -> J = {best['J']:.3f}")
|
|
print(f" LIVE 75/25 al suo : {best_live['w']:>10} @ {best_live['lev']:g}x -> J = {best_live['J']:.3f}")
|
|
dJ = best["J"] - best_live["J"]
|
|
se = np.sqrt(0.25 / N_PATHS) * 2
|
|
print(f" delta = {dJ:+.3f} (errore MC su livelli ~+-{se:.3f}; le due celle condividono")
|
|
print(f" gli indici di bootstrap -> il delta e' APPAIATO e il suo errore e' piu' piccolo)")
|
|
|
|
# ---------------------------------------------------------------- E. banda d'ancora
|
|
hr("E. BANDA D'ANCORA — non ri-misurata sotto questa lente (costo); si usa il FATTORE misurato")
|
|
print(" Ri-fare i 24x23x10 offset col recon MTM orario e' fuori budget. Si applica il fattore")
|
|
print(" di drift MISURATO il 26/07 (x0.89 sul libro live) e si controlla cio' che conta: se")
|
|
print(" l'ORDINE fra le configurazioni sopravvive alla banda 0.60 (stima 25/07, ri-misurata")
|
|
print(" troppo severa) - 1.00 (nessuna correzione).")
|
|
cands = [best["w"], best_sh["w"], best_live["w"]]
|
|
print(f"\n {'fattore':>9} " + " ".join(f"{('J ' + c):>14}" for c in dict.fromkeys(cands))
|
|
+ f" {'argmax pesi':>14} {'argmax leva':>11}")
|
|
for fct in (1.00, 0.89, 0.60):
|
|
cells = []
|
|
for c in dict.fromkeys(cands):
|
|
wc = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in c.split("/")]))
|
|
sub = [objective(wc, lv, "HYRO", ev_idx, fu_idx, factor=fct)["J"] for lv in LEVS]
|
|
cells.append(max(sub))
|
|
g = grid_scan(grid, LEVS, "HYRO", ev_idx, fu_idx, factor=fct)
|
|
b = g.loc[g["J"].idxmax()]
|
|
print(f" {fct:>9.2f} " + " ".join(f"{v:>14.3f}" for v in cells)
|
|
+ f" {b['w']:>14} {b['lev']:>11.3f}")
|
|
|
|
# ---------------------------------------------------------------- F. VRP01 / GTAA01
|
|
hr("F. E GLI SLEEVE 'INUTILI A $600'? — l'asimmetria del 25/07 §4 vale per XS01, non per tutti")
|
|
print(" Su un conto funded da $100k gli sleeve fuori portata a $600 diventano ESEGUIBILI per")
|
|
print(" TAGLIA. Ma la taglia non e' l'unico vincolo: una firm CRYPTO non lista opzioni Deribit")
|
|
print(" (VRP01) ne' ETF azionari USA (GTAA01, che al retail UE e' pure bloccato dal PRIIPs).")
|
|
print(" Qui si misura comunque il CONTROFATTUALE: se fossero disponibili, aiuterebbero?")
|
|
print(f"\n {'aggiunta al ' + best['w']:>34} {'leva':>6} {'J':>7} {'P(pass)':>8} "
|
|
f"{'P(vivo)':>8} {'Sharpe':>7} {'E[pay/a]':>10}")
|
|
for extra, wx in [(None, 0.0), ("VRP01", 0.10), ("VRP01", 0.20),
|
|
("GTAA01", 0.10), ("GTAA01", 0.20)]:
|
|
w2 = {k: v * (1.0 - wx) for k, v in w_best.items()}
|
|
if extra:
|
|
w2[extra] = wx
|
|
rows = [(lv, objective(w2, lv, "HYRO", ev_idx, fu_idx)) for lv in LEVS]
|
|
lv, o = max(rows, key=lambda x: x[1]["J"])
|
|
lab = "nessuna (base)" if extra is None else f"{extra} @ {wx:.0%}"
|
|
print(f" {lab:>34} {lv:>6.3f} {o['J']:>7.3f} {o['p_pass']:>8.1%} {o['p_alive']:>8.1%} "
|
|
f"{o['sharpe']:>7.2f} {o['e_payout']:>9,.0f}$")
|
|
print("\n ATTENZIONE: la riga VRP01 e' calcolata con **gap = 0** (il mark infra-settimana non")
|
|
print(" esiste nel dataset dello sleeve): e' un TETTO, non una stima. Con il mark misurato il")
|
|
print(" 30/07 (peggiore infra-settimana mediano -6.9%, minimo -81.7%) una barriera STATICA al")
|
|
print(" 6% verrebbe sfondata dal mark, non dal risultato.")
|
|
|
|
# ---------------------------------------------------------------- G. FTMO
|
|
hr("G. LA BARRIERA CAMBIA LA RISPOSTA? — HYRO (-6% statico) vs FTMO (-10% statico)")
|
|
print(f" {'firm':>6} {'argmax pesi':>16} {'leva':>6} {'J':>7} {'P(pass)':>8} {'P(vivo12m)':>11} "
|
|
f"{'E[pay/a]':>10} | {'LIVE 75/25: leva':>17} {'J':>7}")
|
|
for fm in ("HYRO", "FTMO"):
|
|
g = grid_scan(grid, LEVS, fm, ev_idx, fu_idx)
|
|
b = g.loc[g["J"].idxmax()]
|
|
lv2 = g[g["w"] == "75/25/0"]
|
|
b2 = lv2.loc[lv2["J"].idxmax()]
|
|
print(f" {fm:>6} {b['w']:>16} {b['lev']:>6.3f} {b['J']:>7.3f} {b['p_pass']:>8.1%} "
|
|
f"{b['p_alive']:>11.1%} {b['e_payout']:>9,.0f}$ | {b2['lev']:>17.3f} {b2['J']:>7.3f}")
|
|
print("\n La barriera piu' larga di FTMO sposta l'ottimo di leva verso l'ALTO: il vincolo e' la")
|
|
print(" barriera, non il libro. E' la stessa lettura del 25/07 §4 ('sul canale funded il")
|
|
print(" vincolo binding e' la regola di DD, non il capitale'), qui misurata sull'ottimo.")
|
|
|
|
# ---------------------------------------------------------------- H. gate ufficiale sui pesi
|
|
hr("H. GATE UFFICIALE SUI PESI (`portfolio.weights_tilt_null`) — sullo SHARPE, come previsto")
|
|
cols = {nm: off[nm] for nm in CRYPTO_PROP}
|
|
try:
|
|
res = weights_tilt_null(cols, {"TP01": 0.75, "SKH01": 0.25, "XS01": 0.0001},
|
|
{nm: max(w_best.get(nm, 0.0), 1e-4) for nm in CRYPTO_PROP},
|
|
floor=0.0, n=300, k_seen=len(grid) * len(LEVS))
|
|
print(f" proposta {best['w']} vs LIVE 75/25 : delta_insample {res['delta_insample']:+.4f} | "
|
|
f"delta_hold {res['delta_hold']:+.4f} | pctl_hold {res['pctl_hold']:.1f}° "
|
|
f"(best-of-k {res['bestofk_pctl']:.1f}°) | gate_pass = {res['gate_pass']}")
|
|
except Exception as exc: # pragma: no cover
|
|
print(f" non girato: {exc}")
|
|
print(" NB: questo gate giudica lo SHARPE del book di ricerca, non J. Un cambio dei pesi del")
|
|
print(" LIBRO LIVE resta soggetto a lui; qui si progetta un conto SEPARATO (funded), che non")
|
|
print(" e' il libro live e non passa da questo gate.")
|
|
|
|
# ---------------------------------------------------------------- I. tavola operativa
|
|
hr("I. TAVOLA OPERATIVA — le 3 configurazioni migliori, e quanti conti servono per 50 EUR/g")
|
|
top = G3.sort_values("J", ascending=False).drop_duplicates("w").head(3)
|
|
rows_ops = []
|
|
fu3 = boot_idx(n_hist, 1095, N_PATHS, seed=8226)
|
|
print(f" {'pesi TP/SKH/XS':>16} {'leva':>6} {'P(pass)':>8} {'P(vivo12m)':>11} "
|
|
f"{'E[payout 3a]':>13} {'EUR/g per conto':>16} {'conti per 50/g':>15}")
|
|
for _, r in top.iterrows():
|
|
w2 = dict(zip(CRYPTO_PROP, [int(x) / 100 for x in r["w"].split("/")]))
|
|
R_, G_ = book_RG(w2)
|
|
R_ = deluck(R_)
|
|
f3 = funded_sim(R_, G_, fu3, float(r["lev"]), "HYRO", 100_000.0)
|
|
eur_day = f3["e_payout"] / EURUSD / 3.0 / 365.0
|
|
n_acct = (TARGET_EUR_DAY / eur_day) if eur_day > 0 else np.inf
|
|
rows_ops.append((r["w"], float(r["lev"]), f3["e_payout"], eur_day, n_acct))
|
|
print(f" {r['w']:>16} {r['lev']:>6.3f} {r['p_pass']:>8.1%} {r['p_alive']:>11.1%} "
|
|
f"{f3['e_payout']:>12,.0f}$ {eur_day:>15.2f} {n_acct:>15.1f}")
|
|
print("\n E[payout 3a] e' su UN conto funded da $100k, gia' netto split 80% e fisco 33%, e")
|
|
print(" MEDIA su tutti i percorsi (i morti contano zero). 'conti per 50/g' e' quindi un numero")
|
|
print(" ATTESO, non una garanzia: la distribuzione e' bimodale (vivo / bustato).")
|
|
cap_tot = sum(f["cap"] for f in pl.FIRMS)
|
|
print(f" ⚠ TETTO STRUTTURALE: cap per trader ${FIRMS['HYRO']['cap']:,.0f} (HYRO) + "
|
|
f"${FIRMS['FTMO']['cap']:,.0f} (FTMO) = ${cap_tot:,.0f} di nozionale su due firm")
|
|
print(f" -> al massimo {cap_tot/100_000:.0f} conti da $100k. Se ne servono di piu', servono "
|
|
f"ALTRE FIRM (e ognuna aggiunge rischio di controparte).")
|
|
|
|
hr("I-bis. LA SCALA DI CONTI — macchineria del 25/07 (`r0725_prop_coupled.simulate`), "
|
|
"de-luck portato a x0.89")
|
|
print(f" 36 mesi, EUR {pl.START_EUR:.0f} di cassa iniziale, max {pl.MAX_CONCURRENT} conti, "
|
|
f"morte-firm {pl.FIRM_DEATH_PER_YEAR:.0%}/anno, lente ACCOPPIATA")
|
|
pc.DELUCK = DELUCK # il 25/07 girava a 0.6 (poi ri-misurato troppo severo)
|
|
cfg_best = tuple(w_best.get(nm, 0.0) for nm in CRYPTO_PROP)
|
|
cfg_live = (0.75, 0.25, 0.0)
|
|
cfg_B = (0.55, 0.20, 0.25)
|
|
pols = {
|
|
f"argmax-J {best['w']} su tutti i conti": ([cfg_best], [0] * pl.MAX_CONCURRENT),
|
|
"LIVE 75/25 su tutti i conti (baseline)": ([cfg_live], [0] * pl.MAX_CONCURRENT),
|
|
"config B 55/20/25 (scelta a mano 25/07)": ([cfg_B], [0] * pl.MAX_CONCURRENT),
|
|
}
|
|
print(f"\n {'politica':>44} {'leva':>6} {'EUR/g med':>10} {'p90':>8} {'P(>=10/g)':>10} "
|
|
f"{'P(>=50/g)':>10} {'P(zero)':>8}")
|
|
for pname, (cfgs, slots) in pols.items():
|
|
for lev in (float(best["lev"]), 0.75):
|
|
r = pc.simulate(cfgs, slots, n_paths=2000, lev=lev, seed=20260822,
|
|
lens="coupled", start=START_WIN)
|
|
print(f" {pname:>44} {lev:>6.3f} {r['med']:>10.2f} {r['p90']:>8.2f} "
|
|
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
|
|
|
|
print("\n" + "=" * 104)
|
|
print(f" fatto in {time.time()-t0:.0f}s")
|
|
print("=" * 104)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|