research(capitale): il lump-sum vale 2.45x, e la protezione dalla rovina non passa da GTAA01

Quattro filoni chiesti dall'operatore ("proposte"). Book, pesi, config: INVARIATI.

1. LUMP-SUM + VENUE (r0727_lumpsum_split.py). Tutte le traiettorie del 25-26/07 avevano
   START=600 cablato: mai misurato un versamento iniziale, mentre ~10k EUR stanno fermi
   altrove. Macchineria validata: con lump 0 riproduce IDENTICI i numeri del 26/07.
   - 10k EUR oggi e mai piu' nulla -> traguardo 17.2a, P 62%, rendita 61.58 EUR/g
   - equivalenza onesta: +154 EUR/mese per 13 anni = 24.523 EUR, cioe' 2.45x
     (la prima stesura misurava i versamenti risparmiati: numero giusto, domanda sbagliata)
   - col rischio venue: a 11.500$ lo split e' possibile (quota IB 26%, non 25%) e taglia
     P(perso tutto) da 18.4% a 3.5% a p=1%, costando 1.9-2.6pp di P(arrivare)
   - SPLIT-CASSA: seconda gamba ferma costa altri 0.6-0.8pp e protegge IDENTICO
     -> la protezione non e' bloccata dal PRIIPs: serve un CONTO, non uno sleeve

2. FEE WATCH (scripts/live/fee_watch.py). Nuovo schema Deribit dal 1 agosto senza numeri
   pubblicati -> sorvegliante invece di promemoria. Legge il tier base dall'endpoint
   pubblico (oggi taker 5.00 bps), applica la regola congelata e allerta sui cambiamenti.

3. MONITOR HEALTH (src/live/monitor_health.py). Tre gate pre-registrati si decidono su
   serie forward di cui una sola era sorvegliata. Misura coda E buchi interni: una serie
   bucata ma fresca passa qualunque guardia di freschezza.

4. BANDA GTAA01 25% VALIDATA (r0727_gtaa_band_gate.py). 30 celle, 29.9 anni, dpy=252.
   Non e' selection-on-holdout (4/30 IS, 5/30 OOS), DSR 0.999, tracking OK ma AL BORDO.
   Il modo di fallire non e' il de-levering (la vol non scende) ma la perdita di tracking.
   Impatto sul book: zero -> REBAL_BAND_USD non toccato, si applica al deploy.

Aggiunto anche il bullet edge_watch, cablato il 26/07 e mai finito in CLAUDE.md.

Test: 56 nuovi, 504/504 verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-27 14:14:39 +00:00
parent 3f0812c7fc
commit fd05307c96
13 changed files with 1963 additions and 0 deletions
+7
View File
@@ -28,5 +28,12 @@ mkdir -p logs
# Sorveglianza dell'EDGE del book live (criteri di kill pre-registrati,
# tarati in scripts/research/r0726_edge_death.py). Riporta e allerta, non spegne nulla.
uv run python scripts/live/edge_watch.py --quiet || true
# Schema fee Deribit (nuovo dal 2026-08-01, annunciato senza numeri): legge il tier BASE
# dall'endpoint pubblico e applica la regola decisa in anticipo (<=5bps nulla, >10bps peso SKH01).
uv run python scripts/live/fee_watch.py --quiet || true
# I forward-monitor stanno registrando? Tre gate pre-registrati (27/09, 23/10, 24/10) si
# decidono su queste serie: un monitor fermo produce silenzio, e il silenzio sembra uno zero.
# Va DOPO tutti i monitor, altrimenti misura lo stato di ieri.
uv run python scripts/live/monitor_health.py --quiet || true
echo "===== done $(date -u '+%H:%M:%SZ') ====="
} >> logs/cron_daily.log 2>&1
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env python
"""fee_watch.py — sorveglia lo schema fee di Deribit e applica la regola DECISA IN ANTICIPO.
PERCHE' ESISTE. Il 2026-07-26 Deribit ha annunciato un nuovo schema fee dal **2026-08-01** senza
pubblicare i numeri (la tabella nell'articolo Insights e' un'immagine, fonte secondaria). La
risposta del progetto e' stata misurare la CURVA invece di aspettare il numero
(`scripts/research/r0726_fee_sensitivity.py`):
bps/lato %RT TP01 Sh SKH01 Sh BOOK Sh BOOK CAGR
0 0.00% 1.322 1.567 1.849 21.69%
3 0.06% 1.303 1.495 1.799 20.99%
5 0.10% 1.290 1.446 1.766 20.53% <- assunzione di TUTTI i backtest
10 0.20% 1.258 1.324 1.682 19.39%
15 0.30% 1.226 1.200 1.597 18.26%
sensibilita' marginale del BOOK: -0.017 Sharpe/bps, -0.23% CAGR/bps
SKH01 e' ~4x piu' sensibile di TP01 (-0.69% vs -0.09% CAGR/bps): round-trip discreti contro
posizione continua vol-targeted -> se il taker sale, il primo parametro da rivedere e' il
PESO 75/25, non altro.
REGOLA CONGELATA (decisa PRIMA di vedere il numero, per non deciderla col numero davanti):
* taker <= 5 bps/lato -> non si tocca nulla (i backtest restano conservativi o esatti)
* 5 < taker <= 10 bps -> si riporta il costo, nessuna azione dovuta
* taker > 10 bps/lato -> si rivede il PESO di SKH01 (`r0724_skh_live_weight` /
`r0726_reeval_live_weight`), passando da `weights_tilt_null`
COSA LEGGE. `public/get_instrument` (endpoint PUBBLICO, nessuna chiave): `taker_commission` e
`maker_commission` sono il tier BASE dell'istrumento, cioe' quello che paga questo conto — a $600
il volume 30g e' trascurabile e ogni soglia VIP e' fuori portata. Sorveglia anche
`max_liquidation_commission`, che l'annuncio dice diventera' 1% su tutti i prodotti.
Cross-check autorevole (best-effort): la fee REALMENTE pagata, dai trade del conto.
⚠️ Il tier VIP di un conto ad alto volume NON e' leggibile qui. La guardia
`test_fee_watch_assume_tier_base` congela questa assunzione: se il conto smettesse di essere al
tier base, questo script misurerebbe la cosa sbagliata.
uv run python scripts/live/fee_watch.py # report
uv run python scripts/live/fee_watch.py --quiet # stampa/allerta solo se qualcosa cambia
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import requests
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.notifier import notify # noqa: E402
STATE = ROOT / "data" / "fee_watch" / "state.json"
INSTRUMENTS = ("BTC-PERPETUAL", "ETH-PERPETUAL")
API = "https://www.deribit.com/api/v2/public/get_instrument"
# --- riferimenti CONGELATI (cambiarli invalida la curva di r0726_fee_sensitivity.py) ---
BASELINE_TAKER_BPS = 5.0 # 0.10% RT: l'assunzione di OGNI backtest del progetto
BASELINE_MAKER_BPS = 0.0
SOGLIA_OK = 5.0 # <= -> nessuna azione
SOGLIA_AZIONE = 10.0 # > -> rivedere il peso di SKH01
D_SHARPE_PER_BPS = -0.017 # sensibilita' marginale del BOOK, misurata
D_CAGR_PER_BPS = -0.0023
def fetch_fees(instrument: str) -> dict:
"""Commissioni correnti dell'istrumento (frazione di nozionale -> bps)."""
r = requests.get(API, params={"instrument_name": instrument}, timeout=15)
r.raise_for_status()
res = r.json()["result"]
return dict(
instrument=instrument,
taker_bps=float(res["taker_commission"]) * 1e4,
maker_bps=float(res["maker_commission"]) * 1e4,
liq_bps=float(res.get("max_liquidation_commission", 0.0)) * 1e4,
)
def verdict(taker_bps: float) -> tuple[str, str]:
"""Applica la regola congelata. Ritorna (livello, motivazione)."""
if taker_bps <= SOGLIA_OK:
return "OK", f"taker {taker_bps:.1f}bps <= {SOGLIA_OK:.0f}: non si tocca nulla"
if taker_bps <= SOGLIA_AZIONE:
d = (taker_bps - BASELINE_TAKER_BPS)
return "NOTA", (f"taker {taker_bps:.1f}bps: costo ~{d * D_SHARPE_PER_BPS:+.3f} Sharpe / "
f"{d * D_CAGR_PER_BPS:+.2%} CAGR di book, nessuna azione dovuta")
return "AZIONE", (f"taker {taker_bps:.1f}bps > {SOGLIA_AZIONE:.0f}: rivedere il PESO di SKH01 "
f"(4x piu' fee-sensibile di TP01) via weights_tilt_null")
def realized_fee_bps(limit: int = 20) -> dict:
"""Fee REALMENTE pagata sui trade del conto, in bps di nozionale. Fonte autorevole, ma
disponibile solo se il book ha tradato di recente: {} non e' 'zero', e' 'non misurata'."""
try:
from src.live.deribit import DeribitRead
d = DeribitRead()
except Exception:
return {}
out: dict[str, float] = {}
for ins in INSTRUMENTS:
tot_fee_usd = tot_notional = 0.0
try:
trades = d.trade_history(ins, limit=limit)
except Exception:
continue
for t in trades:
try:
notional = abs(float(t.get("amount") or 0.0)) # perp: nozionale in USD
price = float(t.get("price") or 0.0)
fee = abs(float(t.get("fee") or 0.0)) # in valuta di settlement
if notional <= 0 or price <= 0:
continue
tot_fee_usd += fee * price
tot_notional += notional
except (TypeError, ValueError):
continue
if tot_notional > 0:
out[ins] = tot_fee_usd / tot_notional * 1e4
return out
def load_state() -> dict:
if STATE.exists():
try:
return json.loads(STATE.read_text())
except Exception:
return {}
return {}
def save_state(cur: dict) -> None:
STATE.parent.mkdir(parents=True, exist_ok=True)
STATE.write_text(json.dumps(cur, indent=2, sort_keys=True))
def diff_vs(prev: dict, cur: dict) -> list[str]:
"""Cambiamenti rispetto all'ultima lettura. Prima lettura = nessun cambiamento (non e'
un evento: e' l'inizializzazione)."""
if not prev:
return []
ch = []
for ins, c in cur.items():
p = prev.get(ins)
if not p:
ch.append(f"{ins}: strumento nuovo nella sorveglianza")
continue
for k, lab in (("taker_bps", "taker"), ("maker_bps", "maker"), ("liq_bps", "liquidazione")):
if abs(float(p.get(k, -1)) - float(c[k])) > 1e-9:
ch.append(f"{ins} {lab}: {float(p.get(k, float('nan'))):.2f} -> {c[k]:.2f} bps")
return ch
def run() -> dict:
cur = {ins: fetch_fees(ins) for ins in INSTRUMENTS}
prev = load_state()
changes = diff_vs(prev, cur)
worst = max(c["taker_bps"] for c in cur.values())
lvl, why = verdict(worst)
return dict(current=cur, changes=changes, worst_taker_bps=worst, level=lvl, reason=why,
first_read=not prev)
def main() -> int:
r = run()
quiet = "--quiet" in sys.argv
interessante = bool(r["changes"]) or r["level"] != "OK"
if not quiet or interessante:
print("=" * 86)
print(" FEE WATCH — schema fee Deribit vs la regola decisa in anticipo")
print("=" * 86)
print(f"\n {'strumento':<16}{'taker':>10}{'maker':>10}{'liquidaz.':>12}")
for ins, c in r["current"].items():
print(f" {ins:<16}{c['taker_bps']:>9.2f}{c['maker_bps']:>10.2f}{c['liq_bps']:>11.2f}")
print(f"\n baseline dei backtest: taker {BASELINE_TAKER_BPS:.1f} / "
f"maker {BASELINE_MAKER_BPS:.1f} bps (0.10% RT)")
print(f" verdetto: [{r['level']}] {r['reason']}")
if r["first_read"]:
print(" (prima lettura: stato inizializzato, nessun confronto possibile)")
for c in r["changes"]:
print(f" CAMBIO: {c}")
real = realized_fee_bps()
if real:
print("\n cross-check sui trade REALI del conto (fonte autorevole):")
for ins, bps in real.items():
print(f" {ins:<16}{bps:>9.2f} bps/lato effettivi")
else:
print("\n cross-check sui trade reali: NON MISURATO (nessun trade recente leggibile)")
if r["changes"] or r["level"] == "AZIONE":
notify("💸 FEE WATCH — schema fee Deribit", {
"verdetto": f"[{r['level']}] {r['reason']}",
**{f"cambio {i+1}": c for i, c in enumerate(r["changes"])},
})
save_state(r["current"])
return 2 if r["level"] == "AZIONE" else 0
if __name__ == "__main__":
raise SystemExit(main())
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python
"""monitor_health.py — i forward-monitor stanno registrando? Riporta e allerta, non tocca nulla.
Tre gate pre-registrati (STATARB 27/09, XSR01 23/10, DVOLSPREAD 24/10) si decideranno leggendo
serie forward che, tranne una, **nessuno sorvegliava**. Un monitor fermo produce silenzio, e il
silenzio in una serie di ritorni si legge come "zero", cioe' come una misura. Non lo e'.
Vedi `src/live/monitor_health.py` per le due misure (coda + buchi interni), le cadenze per
monitor e la soglia di copertura 0.80 (la stessa del veto d'integrita' di DVOLSPREAD).
uv run python scripts/live/monitor_health.py # report
uv run python scripts/live/monitor_health.py --quiet # stampa/allerta solo se scatta
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.live.monitor_health import MIN_COVERAGE, alerts_from, check_all # noqa: E402
from src.live.notifier import notify # noqa: E402
def main() -> int:
rows = check_all(ROOT)
alerts = alerts_from(rows)
if "--quiet" not in sys.argv or alerts:
print("=" * 92)
print(" MONITOR HEALTH — i forward-monitor stanno ancora registrando?")
print("=" * 92)
print(f"\n {'monitor':<20}{'stato':>10}{'barre':>8}{'attese':>8}"
f"{'copert.':>10}{'eta ultima':>13} gate che ne dipende")
for r in rows:
cov = f"{r['coverage']:.0%}" if r["coverage"] is not None else "-"
exp = f"{r['expected']}" if r["expected"] is not None else "-"
age = f"{r['age_h']:.0f}h" if r["age_h"] is not None else "-"
gate = (r["gate"] or "").split("")[0]
print(f" {r['name']:<20}{r['status']:>10}{r['n_bars']:>8}{exp:>8}"
f"{cov:>10}{age:>13} {gate}")
print(f"\n soglia di copertura {MIN_COVERAGE:.0%} (= veto d'integrita' di DVOLSPREAD) · "
f"'NUOVO' = troppo giovane per un giudizio, non 'OK'")
for r in rows:
if r["why"] and r["status"] != "OK":
print(f" {r['name']}: {r['why']}")
if alerts:
notify("🩺 MONITOR HEALTH — un forward-monitor non sta registrando",
{f"alert {i+1}": a for i, a in enumerate(alerts)})
for a in alerts:
print(f" ALERT: {a}")
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python
"""r0727_gtaa_band_gate.py — la banda al 25% della gamba passa i gate, o e' una cella scelta bene?
CONTESTO. Il 27/07 e' stata proposta una configurazione d'esecuzione nuova per GTAA01 — banda =
**25% della gamba** invece dei $50 assoluti, cadenza settimanale invariata — perche' la banda in
dollari assoluti degenera al variare del capitale (a $3.000 una banda da $400 e' l'80% della
gamba: 3 ordini l'anno e a mercato il 45% del tempo invece del 66%). La proposta e' stata
dichiarata esplicitamente **non validata**: "tarata su questa finestra, non passata per
`study_family_honest` ne' per un deflated-Sharpe".
Questo script fa quella validazione. NON e' un gate sui pesi (quello resta `weights_tilt_null`):
la banda e' un parametro di **esecuzione**, non di allocazione. Ma un parametro d'esecuzione
scelto guardando il risultato e' selezione come qualunque altra, e va trattato come tale.
TRE GATE, e uno e' specifico di questo tipo di parametro:
(A) **SELEZIONE IN-SAMPLE** — la cella si sceglie con i soli dati PRE-2015 e si legge sul
2015+, l'hold-out equity documentato di GTAA01 (validazione 2026-06-22) e indipendente
dall'hold-out crypto. Se la cella scelta al buio non e' quella proposta, la proposta e'
selection-on-holdout.
(B) **DEFLATED SHARPE** su TUTTA la griglia cercata (Bailey & Lopez de Prado, PASS >= 0.95),
con `dpy=252`: e' una serie su giorni di BORSA, e passarla a 365 gonfia lo Sharpe di
×1.20 (l'errore di annualizzazione del 25/07).
(C) **NULL DE-LEVERING**, che qui indossa la veste "meno costi" invece di "meno drawdown":
una banda larga puo' migliorare i numeri semplicemente smettendo di seguire il target.
Il controllo non e' lo Sharpe ma quanto la serie bandata **assomiglia ancora** a quella
non bandata (correlazione e rapporto di volatilita' contro il riferimento banda=0,
cadenza=1). E' la 4a occorrenza dello stesso null in questo progetto.
⚠️ Il controllo (C) qui e' in coordinate diverse da quello del 27/07 (che misurava la quota di
tempo a mercato sulla finestra UCITS corta, 66% vs 45%): la API pubblica di `gtaa.py` non espone
le posizioni tenute, e la correlazione col riferimento misura la stessa cosa — se lo sleeve si
congela, smette di somigliare a se' stesso. Le due letture sono complementari, non alternative.
uv run python scripts/research/r0727_gtaa_band_gate.py
"""
from __future__ import annotations
import sys
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" / "alt"))
from altlib import deflated_sharpe # noqa: E402
from src.portfolio.gtaa import ( # noqa: E402
EQ_UNIVERSE, GTAA_MIN_CAPITAL, REBAL_BAND_USD, REBAL_EVERY, gtaa_returns,
)
DPY = 252.0 # giorni di BORSA: annualizzare a 365 gonfia Sharpe ×1.20
HOLDOUT = "2015-01-01" # hold-out equity documentato di GTAA01 (validazione 22/06)
CAPITALS = (3_000.0, 10_000.0, 50_000.0)
CAP_REF = 10_000.0
EVERY_GRID = (1, 5, 10, 21, 42)
FRAC_GRID = (0.0, 0.05, 0.10, 0.25, 0.40, 0.60)
PROPOSTA = (5, 0.25) # cadenza settimanale, banda = 25% della gamba
CANONICO = (REBAL_EVERY, REBAL_BAND_USD)
MIN_CORR = 0.95 # sotto: la banda non filtra piu', congela
def met(s: pd.Series) -> dict:
v = s.dropna().values.astype(float)
if len(v) < 30 or v.std() == 0:
return dict(sharpe=float("nan"), cagr=float("nan"), maxdd=float("nan"), vol=float("nan"))
eq = (1.0 + v).cumprod()
return dict(sharpe=float(v.mean() / v.std() * np.sqrt(DPY)),
cagr=float(eq[-1] ** (DPY / len(v)) - 1.0),
maxdd=float((1.0 - eq / np.maximum.accumulate(eq)).max()),
vol=float(v.std() * np.sqrt(DPY)))
def run_cell(every: int, frac: float, capital: float) -> pd.Series:
"""La banda e' una FRAZIONE della gamba: cosi' il parametro non dipende dal capitale."""
band = frac * capital / len(EQ_UNIVERSE)
return gtaa_returns(capital=capital, band_usd=band, every=every)
def orders_per_year(s: pd.Series, capital: float, every: int, frac: float) -> float:
"""Ordini l'anno stimati: il conteggio esatto vive dentro `_gated_returns`, qui si usa il
piano pubblico su una griglia di controlli — proporzionale e sufficiente al confronto."""
from src.portfolio.gtaa import _close, _exposure
band = frac * capital / len(EQ_UNIVERSE)
n = 0
for sym in EQ_UNIVERSE:
e = _exposure(_close(sym)).dropna()
held = 0.0
for i, tgt in enumerate(e.values):
if i % every:
continue
if abs(tgt - held) * (capital / len(EQ_UNIVERSE)) >= max(band, 1.0):
held = float(tgt)
n += 1
span = max((s.index[-1] - s.index[0]).days / 365.25, 1e-9)
return n / span
def main() -> None:
print("=" * 104)
print(" r0727 — GATE ONESTO SULLA BANDA GTAA01 (proposta: 25% della gamba, settimanale)")
print("=" * 104)
ref = run_cell(1, 0.0, CAP_REF) # riferimento: nessuna banda, controllo giornaliero
print(f" storia {ref.index[0].date()} -> {ref.index[-1].date()} "
f"({len(ref) / DPY:.1f} anni di borsa) · annualizzazione √{DPY:.0f} · "
f"capitale di riferimento ${CAP_REF:,.0f}")
print(f" canonico in produzione: ogni {CANONICO[0]} barre, banda ${CANONICO[1]:.0f} "
f"(= {CANONICO[1] / (CAP_REF / 6):.0%} della gamba a ${CAP_REF:,.0f})")
# ---------------------------------------------------------------- griglia
cells: dict[tuple[int, float], pd.Series] = {}
rows = []
for every in EVERY_GRID:
for frac in FRAC_GRID:
s = run_cell(every, frac, CAP_REF)
cells[(every, frac)] = s
full = met(s)
ins = met(s.loc[: HOLDOUT])
oos = met(s.loc[HOLDOUT:])
j = pd.concat([s, ref], axis=1, join="inner").dropna()
corr = float(j.iloc[:, 0].corr(j.iloc[:, 1]))
rows.append(dict(every=every, frac=frac, sh_full=full["sharpe"], sh_in=ins["sharpe"],
sh_oos=oos["sharpe"], cagr=full["cagr"], vol=full["vol"],
corr=corr, volratio=full["vol"] / met(ref)["vol"]))
G = pd.DataFrame(rows)
print("\n" + "=" * 104)
print(" GRIGLIA — Sharpe in-sample (pre-2015) / hold-out (2015+) / full, e somiglianza al riferimento")
print("=" * 104)
print(f"\n {'cadenza':>8}{'banda%':>8}{'Sh IS':>8}{'Sh OOS':>9}{'Sh FULL':>9}"
f"{'CAGR':>8}{'vol':>7}{'corr rif':>10}{'vol/rif':>9}")
for _, r in G.iterrows():
star = " <- proposta" if (r['every'], r['frac']) == PROPOSTA else ""
print(f" {int(r['every']):>8}{r['frac']:>8.0%}{r['sh_in']:>8.2f}{r['sh_oos']:>9.2f}{r['sh_full']:>9.2f}"
f"{r['cagr']:>8.2%}{r['vol']:>7.1%}{r['corr']:>10.3f}{r['volratio']:>9.2f}{star}")
# ---------------------------------------------------------------- (A) selezione in-sample
print("\n" + "=" * 104)
print(" (A) SELEZIONE IN-SAMPLE — quale cella si sceglie guardando SOLO il pre-2015?")
print("=" * 104)
best = G.sort_values("sh_in", ascending=False).iloc[0]
prop = G[(G["every"] == PROPOSTA[0]) & (G["frac"] == PROPOSTA[1])].iloc[0]
print(f"\n cella scelta al buio : cadenza {int(best['every'])}, banda {best['frac']:.0%} "
f"-> IS {best['sh_in']:.2f} · OOS {best['sh_oos']:.2f} · FULL {best['sh_full']:.2f}")
print(f" cella proposta 27/07 : cadenza {PROPOSTA[0]}, banda {PROPOSTA[1]:.0%} "
f"-> IS {prop['sh_in']:.2f} · OOS {prop['sh_oos']:.2f} · FULL {prop['sh_full']:.2f}")
rank_in = int((G["sh_in"] > prop['sh_in']).sum()) + 1
rank_oos = int((G["sh_oos"] > prop['sh_oos']).sum()) + 1
print(f" rango della proposta : {rank_in}/{len(G)} in-sample · {rank_oos}/{len(G)} sull'hold-out")
if rank_oos < rank_in:
print(" ⚠️ La proposta sta MEGLIO sull'hold-out che in-sample: e' la firma della")
print(" selezione-sull'hold-out. Il gate si giudica su cosa succede scegliendo al buio.")
print(f"\n Costo di scegliere al buio invece che a posteriori (hold-out): "
f"{best['sh_oos'] - prop['sh_oos']:+.2f} di Sharpe")
n_best = orders_per_year(cells[(int(best["every"]), float(best["frac"]))], CAP_REF,
int(best["every"]), float(best["frac"]))
n_prop = orders_per_year(cells[PROPOSTA], CAP_REF, *PROPOSTA)
print(f"\n ⚠️ La cella scelta al buio controlla il target OGNI GIORNO ({n_best:.0f} ordini/anno)")
print(f" contro la cadenza settimanale della proposta ({n_prop:.0f} ordini/anno). Il conto")
print(f" di esecuzione (Degiro/Revolut) NON ha API: 250 controlli manuali l'anno non sono")
print(f" una configurazione, sono un'ipotesi. La proposta costa "
f"{prop['sh_full'] - best['sh_full']:+.2f} di Sharpe FULL e")
print(" compra una cadenza eseguibile a mano. E' un vincolo dichiarato, non una scoperta.")
# ---------------------------------------------------------------- (B) deflated Sharpe
print("\n" + "=" * 104)
print(f" (B) DEFLATED SHARPE sulla griglia intera ({len(G)} celle), dpy={DPY:.0f}")
print("=" * 104)
all_sr = list(G["sh_full"].values)
for label, cell in (("proposta 27/07", PROPOSTA),
("scelta in-sample", (int(best['every']), float(best['frac'])))):
s = cells[cell]
dsr, null_max = deflated_sharpe(met(s)["sharpe"], all_sr, s.dropna().values, dpy=DPY)
print(f"\n {label:<18} cadenza {cell[0]}, banda {cell[1]:.0%} · "
f"Sharpe {met(s)['sharpe']:.2f} vs massimo atteso sotto il nullo {null_max:.2f}")
print(f" {'':<18} DSR = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0.95)")
# ---------------------------------------------------------------- (C) null de-levering
print("\n" + "=" * 104)
print(" (C) NULL DE-LEVERING — la banda filtra, o congela?")
print("=" * 104)
print(f"\n Riferimento = banda 0%, controllo giornaliero. Una banda che FILTRA lascia la")
print(f" serie quasi identica (corr >= {MIN_CORR}); una che CONGELA la stacca.")
print(f"\n {'banda%':>8}{'corr rif':>11}{'vol/rif':>10}{'Sh FULL':>10} verdetto")
for frac in FRAC_GRID:
r = G[(G["every"] == PROPOSTA[0]) & (G["frac"] == frac)].iloc[0]
v = "OK" if r['corr'] >= MIN_CORR else "❌ FUORI TRACCIA"
print(f" {frac:>8.0%}{r['corr']:>11.3f}{r['volratio']:>10.2f}{r['sh_full']:>10.2f} {v}")
print("\n ⚠️ Il modo di fallire NON e' quello atteso, e va detto: allargando la banda la")
print(" volatilita' NON scende (vol/rif resta 0.99-1.10), quindi qui il null de-levering")
print(" classico non morde. Cio' che si rompe e' il TRACKING: la serie smette di")
print(" somigliare a se' stessa perche' tiene posizioni vecchie. E i due segnali")
print(" CONCORDANO — oltre il 25% la correlazione cala E lo Sharpe smette di migliorare")
print(" (0.70 -> 0.67 -> 0.59): non c'e' una zona in cui il numero premia il congelamento.")
print(f" Ma il 25% e' AL BORDO della regione (corr {G[(G['every'] == 5) & (G['frac'] == 0.25)].iloc[0]['corr']:.3f} "
f"contro una soglia di {MIN_CORR}): non e' un plateau centrale.")
# ---------------------------------------------------------------- invarianza
print("\n" + "=" * 104)
print(" INVARIANZA AL CAPITALE — il motivo per cui la proposta esiste")
print("=" * 104)
print(f"\n {'capitale':>10}{'banda 25%':>12}{'ord/anno':>11}{'Sharpe':>9}{'corr rif':>10}"
f" | {'banda $50 fissa':>16}{'ord/anno':>11}{'Sharpe':>9}")
for cap in CAPITALS:
s_f = run_cell(PROPOSTA[0], PROPOSTA[1], cap)
s_a = gtaa_returns(capital=cap, band_usd=CANONICO[1], every=CANONICO[0])
n_f = orders_per_year(s_f, cap, PROPOSTA[0], PROPOSTA[1])
n_a = orders_per_year(s_a, cap, CANONICO[0], CANONICO[1] / (cap / 6))
j = pd.concat([s_f, ref], axis=1, join="inner").dropna()
print(f" ${cap:>9,.0f}{PROPOSTA[1] * cap / 6:>11,.0f}${n_f:>11.0f}{met(s_f)['sharpe']:>9.2f}"
f"{float(j.iloc[:, 0].corr(j.iloc[:, 1])):>10.3f} | "
f"{CANONICO[1]:>15,.0f}${n_a:>11.0f}{met(s_a)['sharpe']:>9.2f}")
print(f"\n (sotto ${GTAA_MIN_CAPITAL:,.0f} lo sleeve non e' deployabile comunque: "
"GTAA_MIN_CAPITAL)")
# ---------------------------------------------------------------- impatto sul book
print("\n" + "=" * 104)
print(" IMPATTO SUL BOOK — il numero che decide se toccare la produzione")
print("=" * 104)
from src.portfolio.portfolio import combine_outer
from src.portfolio.sleeves import GTAA_BOOK_ACTIVATION, active_sleeves
def as_book(gtaa_series: pd.Series | None) -> pd.Series:
cols, w = {}, {}
for sl in active_sleeves():
s = sl.daily()
if sl.name == "GTAA01_eq_trend" and gtaa_series is not None:
g = gtaa_series.dropna().sort_index()
if g.index.tz is None:
g.index = g.index.tz_localize("UTC")
days = pd.date_range(g.index.min().normalize(), g.index.max().normalize(),
freq="1D", tz="UTC")
s = g.reindex(days).fillna(0.0)
s = s[s.index >= GTAA_BOOK_ACTIVATION]
cols[sl.name], w[sl.name] = s, sl.weight
return combine_outer(cols, w)
def bk(s: pd.Series) -> dict:
v = s.dropna().values.astype(float)
eq = (1.0 + v).cumprod()
return dict(sharpe=float(v.mean() / v.std() * np.sqrt(365.0)),
maxdd=float((1.0 - eq / np.maximum.accumulate(eq)).max()))
b_can = bk(as_book(None))
b_new = bk(as_book(run_cell(PROPOSTA[0], PROPOSTA[1], CAP_REF)))
print(f"\n book 5-sleeve, banda CANONICA $50 : Sharpe FULL {b_can['sharpe']:.3f} · "
f"maxDD {b_can['maxdd']:.2%}")
print(f" book 5-sleeve, banda PROPOSTA 25% : Sharpe FULL {b_new['sharpe']:.3f} · "
f"maxDD {b_new['maxdd']:.2%}")
print(f" delta : {b_new['sharpe'] - b_can['sharpe']:+.3f} Sharpe · "
f"{(b_new['maxdd'] - b_can['maxdd']) * 100:+.2f} pt di maxDD")
print("\n ⚠️ Il book si annualizza a 365 (griglia giornaliera piena, weekend = 0), lo sleeve")
print(" a 252 (giorni di borsa): i due Sharpe NON sono confrontabili fra loro, solo")
print(" ciascuno con se' stesso fra le due configurazioni.")
if __name__ == "__main__":
main()
+291
View File
@@ -0,0 +1,291 @@
#!/usr/bin/env python
"""r0727_lumpsum_split.py — il capitale che e' GIA' fermo, e cosa fa alla decisione di venue.
LA DOMANDA. Sul conto che gira ci sono **$600**. Fermi altrove ce ne sono ~**€10.000** (Revolut:
€6.043 in XEON, che rende ~0% reale netto dopo inflazione e tasse, piu' VWCE/SPYM/PPFB) e €329 su
Degiro. Tutte le traiettorie pubblicate (25/07 e 26/07) partono da `START = 600.0` **cablato**: il
progetto ha misurato con cura il CALENDARIO dei versamenti (fattore 6) e non ha mai misurato un
**versamento iniziale**.
PERCHE' LE DUE DOMANDE SI MISURANO INSIEME. Un lump-sum e' il front-loading estremo, e il 26/07
aveva gia' misurato che il front-loading *mette piu' capitale sull'exchange prima* — cioe' proprio
dove vive il rischio di venue. E la decisione del 26/07 ("100% Deribit fino a $20k") e' stata
presa con **$600 sul conto, dove lo split e' impossibile per costruzione**. CLAUDE.md registra
l'impegno esplicito: *"Se cambia il piano (orizzonte, importo dei versamenti) si riapre PRIMA."*
Un lump-sum da €5-10k e' esattamente quel cambiamento. Misurare il lump-sum senza il rischio di
venue risponderebbe alla meta' comoda della domanda.
COSA NON DECIDE QUESTO SCRIPT. Quanto di quei €6.043 sia un vero fondo d'emergenza e quanto
capitale in attesa: e' una decisione dell'operatore, e un fondo d'emergenza NON e' capitale
disponibile. Qui si misura **cosa compra ogni euro che entra**, per ogni taglia, cosi' che la
decisione si prenda sul prezzo giusto.
ASSUNZIONI DICHIARATE
* Il costo-opportunita' del lump-sum e' ~0% reale (XEON: ~2% lordo, ~0 netto dopo inflazione e
26%). Tutti i numeri sono NOMINALI in USD, come ovunque nel progetto: il confronto e' quindi
fra "fermo a ~0 reale" e "dentro il book", non fra due investimenti rischiosi.
* Book = quello LIVE (TP01+SKH01 su Deribit), de-luckato ×0.89 (fattore MISURATO,
r0726_deluck_factor). E' lo stesso book di ogni traiettoria pubblicata -> i numeri sono
confrontabili con la tabella dei versamenti del 26/07.
* Bersaglio = $272.061, il muro del 26/07 (rendita perpetua a €50/g netti). Identico per CONC e
SPLIT: e' CONSERVATIVO contro lo split, che avrebbe un muro leggermente diverso.
* `p` (probabilita' annua di fallimento di un venue) NON e' stimata: e' una sensibilita'.
La sceglie l'operatore.
uv run python scripts/research/r0727_lumpsum_split.py
"""
from __future__ import annotations
import sys
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
import r0726_capwall_refresh as WR # noqa: E402
import r0726_venue_risk as VR # noqa: E402
from src.portfolio.gtaa import GTAA_MIN_CAPITAL # noqa: E402
DELUCK = 0.89
START = 600.0 # il conto Deribit di oggi
YEARS = 20
N_PATHS = 4000
BLOCK = 20
SEED = 726 # lo stesso di r0726_venue_risk: consente la replica esatta
TARGET = VR.TARGET # $272.061
LUMPS_EUR = (0.0, 2_000.0, 5_000.0, 10_000.0)
P_GRID = (0.0, 0.005, 0.01, 0.02, 0.05)
# Split: la quota su IB parte dal 25% raccomandato da r0725_ib10k (oltre, la rendita cala:
# 0% -> €2.16/g, 50% -> €1.54/g su $11.5k). Ma la quota MINIMA e' imposta dall'eseguibilita'
# della gamba equity: sotto GTAA_MIN_CAPITAL non c'e' uno sleeve, c'e' cash su un secondo conto.
# Si accetta di salire fino a IB_SHARE_MAX pur di rendere lo split possibile; oltre, no.
CONC = {"Deribit": 1.0}
IB_SHARE = 0.25
IB_SHARE_MAX = 0.35
# Haircut sulla gamba IB nella variante realistica: costi a taglia piccola (CAGR 3.8% -> 3.0% a
# $3k, misurato il 25/07) + drag del veicolo UCITS (+0.10%/anno, misurato il 26/07).
IB_HAIRCUT_YR = 0.008 + 0.001
def simulate(cfg: dict, series: dict, p_annual: float, lump_usd: float = 0.0,
dep_eur: float = 250.0, n_paths: int = N_PATHS, years: int = YEARS,
seed: int = SEED) -> dict:
"""Accumulo con capitale iniziale `START + lump_usd` e jump di venue.
Generalizza `r0726_venue_risk.simulate` sul solo capitale iniziale e sul deposito: la
struttura (ordine di consumo dell'RNG compreso) e' identica, cosi' che con lump=0 e
dep=250 i numeri debbano coincidere ESATTAMENTE con quelli pubblicati il 26/07.
"""
rng = np.random.default_rng(seed)
n_days = years * 365
names = list(cfg)
J = pd.concat({k: series[k] for k in names}, axis=1, join="inner").fillna(0.0)
R = J.values.astype(float)
idx = CC._boot_paths(np.arange(len(R), dtype=float), n_paths, n_days, BLOCK, rng).astype(int)
dep = dep_eur * CC.EURUSD
w0 = np.array([cfg[k] for k in names], float)
cap = np.outer(np.full(n_paths, START + lump_usd), w0)
alive = np.ones((n_paths, len(names)), bool)
haz = 1.0 - (1.0 - p_annual) ** (1.0 / 365.0)
hit = np.zeros(n_paths, bool)
hit_day = np.full(n_paths, n_days + 1, int)
for t in range(n_days):
cap *= (1.0 + R[idx[:, t], :])
if haz > 0:
dead = (rng.random((n_paths, len(names))) < haz) & alive
cap = np.where(dead, 0.0, cap)
alive &= ~dead
if t % 30 == 0 and t > 0 and dep > 0:
wa = alive * w0
tot = wa.sum(axis=1, keepdims=True)
share = np.divide(wa, tot, out=np.zeros_like(wa), where=tot > 0)
cap += dep * share
tot_cap = cap.sum(axis=1)
newly = (~hit) & (tot_cap >= TARGET)
hit_day[newly] = t
hit |= newly
yrs = np.where(hit, hit_day / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)]
return dict(p_reach=float(hit.mean()),
median_yrs=float(np.median(fin)) if len(fin) > 50 else float("nan"),
p_all_dead=float((~alive.any(axis=1)).mean()),
median_end=float(np.median(cap.sum(axis=1))),
paid=START + lump_usd + dep * (n_days // 30))
def haircut(s: pd.Series, per_year: float) -> pd.Series:
"""Abbassa il drift di una serie giornaliera di `per_year` all'anno (costi non modellati)."""
return s - per_year / 365.0
def split_config(total_usd: float) -> dict | None:
"""Configurazione di split possibile a quel capitale, o None.
La quota IB non e' libera: dal basso la vincola l'ESEGUIBILITA' (sotto GTAA_MIN_CAPITAL la
gamba equity non esiste, e lo 'split' sarebbe cash fermo su un secondo conto), dall'alto la
vincola la RENDITA (r0725_ib10k: oltre il 25% si paga, oltre il 50% si dimezza). Fra i due
vincoli si prende la quota minima che rende la gamba eseguibile — mai meno del 25%
raccomandato, mai piu' di IB_SHARE_MAX.
"""
need = GTAA_MIN_CAPITAL / total_usd if total_usd > 0 else float("inf")
share = max(IB_SHARE, need)
if share > IB_SHARE_MAX:
return None
return {"Deribit": 1.0 - share, "IB": share}
def main() -> None:
print("=" * 104)
print(" r0727 — IL CAPITALE GIA' FERMO: cosa compra un versamento iniziale, e cosa fa al venue")
print("=" * 104)
series = VR.venue_series()
r_deribit = series["Deribit"].values.astype(float)
gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
_, perp, wall = WR.perp_and_wall(r_deribit, 1.0, gross)
print(f" book live de-luckato ×{DELUCK} | rendita perpetua {perp:.2%} | bersaglio ${TARGET:,.0f}")
print(f" {N_PATHS} path, blocchi {BLOCK}g, orizzonte {YEARS}a, conto di partenza ${START:.0f}, "
f"EURUSD {CC.EURUSD}")
# ---------------------------------------------------------------- validazione
print("\n" + "-" * 104)
print(" VALIDAZIONE — con lump 0 e €250/mese si devono riprodurre i numeri del 26/07")
print("-" * 104)
for p in (0.0, 0.01):
mine = simulate(CONC, series, p, lump_usd=0.0, dep_eur=250.0)
theirs = VR.simulate({"Deribit": 1.0}, series, p, n_paths=N_PATHS, years=YEARS, seed=SEED)
ok = (abs(mine["p_reach"] - theirs["p_reach"]) < 1e-12
and abs(mine["p_all_dead"] - theirs["p_all_dead"]) < 1e-12)
print(f" p={p:.1%} P(arrivare) {mine['p_reach']:.4f} vs {theirs['p_reach']:.4f} · "
f"P(perso tutto) {mine['p_all_dead']:.4f} vs {theirs['p_all_dead']:.4f} "
f"-> {'IDENTICO' if ok else 'DIVERGE (la generalizzazione ha cambiato qualcosa)'}")
# ---------------------------------------------------------------- (1) cosa compra il lump
print("\n" + "=" * 104)
print(" (1) COSA COMPRA UN VERSAMENTO INIZIALE — senza rischio di venue (p=0), 100% Deribit")
print("=" * 104)
print(f"\n {'lump':>8}{'dep/mese':>10}{'tot versato':>14}{'anni al muro':>14}"
f"{'P(entro 20a)':>14}{'cap. mediano':>15}{'rendita a 20a':>16}")
grid = {}
for dep in (0.0, 250.0):
for L in LUMPS_EUR:
r = simulate(CONC, series, 0.0, lump_usd=L * CC.EURUSD, dep_eur=dep)
grid[(L, dep)] = r
rent = r["median_end"] * perp * (1 - CC.TAX_RATE) / 365.0 / CC.EURUSD
yrs = f"{r['median_yrs']:.1f}a" if not np.isnan(r["median_yrs"]) else "mai"
print(f"{L:>7,.0f}{dep:>10,.0f}{r['paid']:>13,.0f}${yrs:>14}"
f"{r['p_reach']:>13.1%}{r['median_end']:>14,.0f}${rent:>13.2f} €/g")
# equivalenza: a quale versamento mensile equivale il lump, a parita' di traguardo mediano.
# NB: NON si confronta il lump col totale dei versamenti risparmiati — sarebbe la domanda
# sbagliata (il valore e' arrivare prima, non versare meno), e darebbe un rapporto <1 che
# invita alla conclusione opposta.
print("\n EQUIVALENZA — a quanti €/mese IN PIU', per sempre, equivale il lump-sum?")
base = grid[(0.0, 250.0)]["median_yrs"]
for L in LUMPS_EUR[1:]:
got = grid[(L, 250.0)]["median_yrs"]
lo, hi = 250.0, 3_000.0
for _ in range(12): # bisezione sul deposito a pari traguardo
mid = 0.5 * (lo + hi)
y = simulate(CONC, series, 0.0, lump_usd=0.0, dep_eur=mid)["median_yrs"]
if np.isnan(y) or y > got:
lo = mid
else:
hi = mid
eq = 0.5 * (lo + hi)
print(f"{L:>6,.0f} oggi -> traguardo {got:.1f}a (invece di {base:.1f}a) "
f"= come versare €{eq:,.0f}/mese invece di €250, cioe'{eq - 250:,.0f}/mese in piu' "
f"per {got:.0f} anni (€{(eq - 250) * 12 * got:,.0f})")
# ---------------------------------------------------------------- (2) venue
print("\n" + "=" * 104)
print(" (2) LO STESSO LUMP, COL RISCHIO DI VENUE DENTRO — concentrato vs split")
print(f" SPLIT = Deribit / IB, quota IB scelta dal CAPITALE (minimo eseguibile "
f"${GTAA_MIN_CAPITAL:,.0f}, tetto {IB_SHARE_MAX:.0%})")
print("=" * 104)
ib_real = {**series, "IB": haircut(series["IB"], IB_HAIRCUT_YR)}
for L in LUMPS_EUR:
tot = START + L * CC.EURUSD
cfg = split_config(tot)
if cfg:
why = (f"POSSIBILE a quota IB {cfg['IB']:.0%} (${tot * cfg['IB']:,.0f} sulla gamba equity; "
f"il minimo eseguibile e' ${GTAA_MIN_CAPITAL:,.0f})")
else:
why = (f"IMPOSSIBILE: servirebbe una quota IB del {GTAA_MIN_CAPITAL / tot:.0%} "
f"per fare ${GTAA_MIN_CAPITAL:,.0f}, oltre il tetto {IB_SHARE_MAX:.0%}")
print(f"\n lump €{L:,.0f} -> capitale iniziale ${tot:,.0f} · split {why}")
print(f" {'p annua':>9}{'P(arrivare) CONC':>19}{'SPLIT':>9}{'SPLIT+costi':>13}"
f"{'P(perso tutto) CONC':>22}{'SPLIT':>9}")
for p in P_GRID:
c = simulate(CONC, series, p, lump_usd=L * CC.EURUSD)
if cfg:
s = simulate(cfg, series, p, lump_usd=L * CC.EURUSD)
sr = simulate(cfg, ib_real, p, lump_usd=L * CC.EURUSD)
print(f" {p:>8.1%}{c['p_reach']:>18.1%}{s['p_reach']:>9.1%}{sr['p_reach']:>13.1%}"
f"{c['p_all_dead']:>21.1%}{s['p_all_dead']:>9.1%}")
else:
print(f" {p:>8.1%}{c['p_reach']:>18.1%}{'-':>9}{'-':>13}"
f"{c['p_all_dead']:>21.1%}{'-':>9}")
# --- lo split che si puo' fare OGGI: la gamba equity e' bloccata dal PRIIPs -------------
print("\n" + "-" * 104)
print(" (2-bis) E SE LA SECONDA GAMBA FOSSE SOLO LIQUIDITA'?")
print(" GTAA01 oggi NON e' deployabile (blocco PRIIPs sugli ETF USA; la via UCITS su Degiro")
print(" e' verificata ma in preparazione). Lo split disponibile SUBITO e' Deribit + un conto")
print(" fermo (XEON o simile, ~0% reale). Quanta protezione viene dalla DIVERSIFICAZIONE DI")
print(" VENUE e quanta dai ritorni della gamba equity?")
print("-" * 104)
L = 10_000.0
tot = START + L * CC.EURUSD
cfg = split_config(tot)
zero = {**series, "IB": series["IB"] * 0.0} # stessa griglia temporale, drift nullo
print(f"\n lump €{L:,.0f} (${tot:,.0f}), quota seconda gamba {cfg['IB']:.0%}")
print(f" {'p annua':>9}{'P(arrivare) CONC':>19}{'SPLIT-GTAA':>13}{'SPLIT-CASSA':>13}"
f"{'P(perso tutto) CONC':>22}{'SPLIT':>9}")
for p in P_GRID:
c = simulate(CONC, series, p, lump_usd=L * CC.EURUSD)
g = simulate(cfg, series, p, lump_usd=L * CC.EURUSD)
z = simulate(cfg, zero, p, lump_usd=L * CC.EURUSD)
print(f" {p:>8.1%}{c['p_reach']:>18.1%}{g['p_reach']:>13.1%}{z['p_reach']:>13.1%}"
f"{c['p_all_dead']:>21.1%}{g['p_all_dead']:>9.1%}")
print("\n (P(perso tutto) e' identica fra SPLIT-GTAA e SPLIT-CASSA: dipende da QUANTI conti")
print(" falliscono, non da cosa ci sta sopra. La differenza fra le due colonne di")
print(" P(arrivare) e' il prezzo di tenere ferma la seconda gamba invece di investirla.)")
print("\n ⚠️ La colonna P(perso tutto) sotto CONC non dipende dal lump: con UN conto solo,")
print(" 'almeno un fallimento' COINCIDE con 'perso tutto', e quella probabilita' e' una")
print(" proprieta' del tempo di esposizione, non del capitale. Il lump non la peggiora —")
print(" ma moltiplica cio' che quella probabilita' porta via.")
# ---------------------------------------------------------------- (3) la soglia
print("\n" + "=" * 104)
print(" (3) DA QUALE CAPITALE LO SPLIT E' POSSIBILE SENZA DISTORCERE I PESI")
print("=" * 104)
ideale = GTAA_MIN_CAPITAL / IB_SHARE
minimo = GTAA_MIN_CAPITAL / IB_SHARE_MAX
tot10 = START + 10_000 * CC.EURUSD
print(f"\n soglia di eseguibilita' della gamba equity (GTAA_MIN_CAPITAL): ${GTAA_MIN_CAPITAL:,.0f}")
print(f" quota IB raccomandata da r0725_ib10k (oltre, cala la rendita): {IB_SHARE:.0%}")
print(f" -> split a quota RACCOMANDATA possibile da : ${ideale:,.0f}")
print(f" -> split possibile forzando la quota fino al {IB_SHARE_MAX:.0%} da : ${minimo:,.0f}")
print(f"\n Con un lump da €10.000 il conto sarebbe ${tot10:,.0f}: "
f"{'SOPRA' if tot10 >= ideale else 'SOTTO'} la soglia a quota")
print(f" raccomandata (${ideale:,.0f}) e {'sopra' if tot10 >= minimo else 'sotto'} "
f"quella a quota forzata (${minimo:,.0f}) -> lo split e' possibile, ma a")
print(f" quota IB {max(IB_SHARE, GTAA_MIN_CAPITAL / tot10):.0%}, non {IB_SHARE:.0%}: "
"e' il capitale a scegliere la quota, non la preferenza.")
print(f"\n La riapertura della decisione di venue e' fissata a ${20_000:,.0f}. Un lump da")
print(f" €10k porta il conto a ${tot10:,.0f} — sotto quella data ma sopra la fattibilita'")
print(" tecnica, ed e' un cambiamento del piano: CLAUDE.md dice di riaprire PRIMA in")
print(" questo caso, e questa tabella e' il materiale per farlo.")
if __name__ == "__main__":
main()