Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 25a22fc7c1 | |||
| eeac97dde4 | |||
| c8a390d6b7 | |||
| 384b9cb0af | |||
| 160ad300be | |||
| 50e2adf837 |
@@ -0,0 +1,106 @@
|
||||
"""PROIEZIONE ACCUMULO del book DERIBIT-ONLY (TP01 + SKH01) — compounding puro (reinvesti le
|
||||
vincite), allineamento MENSILE, NESSUN versamento esterno (non e' un PAC).
|
||||
|
||||
Base: rendimenti mensili del book Deribit-only (rebal mensile, netto costo turnover). Proietta in
|
||||
avanti l'equity da un capitale iniziale:
|
||||
- DETERMINISTICO @CAGR storico e @CAGR conservativo (= storico × cons_frac, default metà);
|
||||
- MONTE CARLO block-bootstrap dei rendimenti mensili storici (mediana + banda p10/p90);
|
||||
- €/giorno run-rate (cresce col capitale, perche' si rigiocano le vincite).
|
||||
|
||||
⚠️ ONESTA': lo storico e' un BULL crypto ~2019-26 -> il futuro sara' quasi certamente piu' magro.
|
||||
Pianificare sulla colonna conservativa; il MC non contiene un vero bear pluriennale (anche il p10
|
||||
e' ottimista). Nessuna leva. SKH01 e' research/forward-monitor (solo TP01 e' armato live). Non e'
|
||||
una garanzia: e' una proiezione condizionata "se il futuro somigliasse al passato".
|
||||
|
||||
uv run python scripts/portfolio/forecast_deribit_book.py
|
||||
uv run python scripts/portfolio/forecast_deribit_book.py --capital 5000 --years 1,3,5,10 --cons-frac 0.5
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.portfolio.portfolio import StrategyPortfolio, rebalance_sim
|
||||
from src.portfolio.sleeves import deribit_book_sleeves
|
||||
|
||||
|
||||
def book_monthly_returns(rebal_days: int, cost_rate: float) -> pd.Series:
|
||||
"""Rendimenti MENSILI del book Deribit-only (rebal periodico, netto costo)."""
|
||||
sl = deribit_book_sleeves()
|
||||
w = StrategyPortfolio(sl).weights()
|
||||
cols = {s.name: s.daily() for s in sl}
|
||||
r = rebalance_sim(cols, w, period_days=rebal_days, cost_rate=cost_rate)["daily"]
|
||||
return ((1.0 + r).resample("ME").prod() - 1.0).dropna()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Proiezione accumulo book Deribit-only (compounding, allineamento mensile)")
|
||||
ap.add_argument("--capital", type=float, default=5000.0)
|
||||
ap.add_argument("--years", type=str, default="1,2,3,5,8,10")
|
||||
ap.add_argument("--cons-frac", type=float, default=0.5, help="CAGR conservativo = storico × questo (default 0.5)")
|
||||
ap.add_argument("--sims", type=int, default=4000)
|
||||
ap.add_argument("--block-months", type=int, default=3, help="blocco del bootstrap (mesi)")
|
||||
ap.add_argument("--rebal-days", type=int, default=30)
|
||||
ap.add_argument("--cost-rate", type=float, default=0.0005, help="fee per-lato sul turnover (Deribit taker)")
|
||||
ap.add_argument("--seed", type=int, default=7)
|
||||
a = ap.parse_args()
|
||||
|
||||
cap = a.capital
|
||||
HY = [int(x) for x in a.years.split(",") if x.strip()]
|
||||
m = book_monthly_returns(a.rebal_days, a.cost_rate)
|
||||
mv = m.values
|
||||
nm = len(mv)
|
||||
g_month = float(np.prod(1.0 + mv) ** (1.0 / nm) - 1.0)
|
||||
cagr = (1.0 + g_month) ** 12 - 1.0
|
||||
vol_ann = float(mv.std() * np.sqrt(12))
|
||||
cons_cagr = cagr * a.cons_frac
|
||||
|
||||
print("=" * 90)
|
||||
print(f" PROIEZIONE ACCUMULO — book Deribit-only (TP01+SKH01) | compounding, allineamento mensile, no versamenti")
|
||||
print(f" storico: {nm} mesi · CAGR {cagr*100:.1f}% · vol annua {vol_ann*100:.0f}% (bull crypto, no leva) | capitale €{cap:,.0f}")
|
||||
print("=" * 90)
|
||||
|
||||
# Monte Carlo: block-bootstrap dei rendimenti mensili
|
||||
rng = np.random.default_rng(a.seed)
|
||||
blk = a.block_months
|
||||
maxM = max(HY) * 12
|
||||
nb = maxM // blk + 1
|
||||
starts = rng.integers(0, nm - blk, size=(a.sims, nb))
|
||||
paths = np.concatenate([mv[starts[:, k][:, None] + np.arange(blk)[None, :]] for k in range(nb)], axis=1)[:, :maxM]
|
||||
eqMC = cap * np.cumprod(1.0 + paths, axis=1)
|
||||
|
||||
cons_m = (1.0 + cons_cagr) ** (1.0 / 12) - 1.0
|
||||
print(f"\n ACCUMULO (reinvesti le vincite):")
|
||||
print(f" {'oriz.':>6} | {'det @storico':>13} | {'det @conserv.':>14} | {'MC mediana':>11} | {'MC p10':>9} | {'MC p90':>10}")
|
||||
print(f" {'':>6} | {'('+format(cagr*100,'.0f')+'%)':>13} | {'('+format(cons_cagr*100,'.0f')+'%)':>14} |")
|
||||
print(" " + "-" * 80)
|
||||
for y in HY:
|
||||
mo = y * 12
|
||||
det = cap * (1.0 + g_month) ** mo
|
||||
detc = cap * (1.0 + cons_m) ** mo
|
||||
c = eqMC[:, mo - 1]
|
||||
print(f" {y:>4}a | €{det:>11,.0f} | €{detc:>12,.0f} | €{np.median(c):>9,.0f} | €{np.percentile(c,10):>7,.0f} | €{np.percentile(c,90):>8,.0f}")
|
||||
|
||||
# €/giorno run-rate @conservativo (cresce col capitale)
|
||||
rd = (1.0 + cons_cagr) ** (1.0 / 365.0) - 1.0
|
||||
print(f"\n €/GIORNO run-rate @conservativo ({cons_cagr*100:.1f}% CAGR) — cresce col capitale (rigiochi le vincite):")
|
||||
print(f" {'oriz.':>6} | {'equity':>9} | {'€/giorno':>10} | {'€/mese':>8}")
|
||||
print(" " + "-" * 42)
|
||||
for y in [0] + HY:
|
||||
E = cap * (1.0 + cons_cagr) ** y
|
||||
print(f" {('oggi' if y==0 else str(y)+'a'):>6} | €{E:>7,.0f} | €{E*rd:>7,.2f} | €{E*rd*30:>6,.0f}")
|
||||
E_end = cap * (1.0 + cons_cagr) ** max(HY)
|
||||
print(f" media €/giorno su {max(HY)} anni: €{(E_end-cap)/(max(HY)*365):.2f}/g (profitto €{E_end-cap:,.0f})")
|
||||
need = 50 * 365 / cons_cagr if cons_cagr > 0 else float('inf')
|
||||
print(f" capitale per ~€50/giorno @{cons_cagr*100:.1f}%: ≈ €{need:,.0f}")
|
||||
|
||||
print(f"\n ⚠️ Proiezione condizionata (storico = bull crypto); pianifica sul conservativo. Nessuna leva.")
|
||||
print(f" SKH01 = research/forward-monitor; solo TP01 e' armato live. Non e' una garanzia.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""REPORT del BOOK DERIBIT-ONLY realmente eseguibile = TP01 + SKH01 (75/25).
|
||||
|
||||
Le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), entrambe dal 2019. Esclude XS01
|
||||
(Hyperliquid, stat-mode) e VRP01 (opzioni modellate). Mostra:
|
||||
1. metriche oneste continuo (rebalance-continuo) vs RIBILANCIAMENTO PERIODICO realistico
|
||||
(settimanale/mensile) con costo turnover Deribit-taker;
|
||||
2. per-anno, accumulo da €2k (e nota €600 reale + min-order $5);
|
||||
3. posizioni correnti per gamba.
|
||||
|
||||
uv run python scripts/portfolio/run_deribit_book.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np
|
||||
|
||||
from src.portfolio.portfolio import StrategyPortfolio, metrics, yearly, rebalance_sim, HOLDOUT
|
||||
|
||||
CAP = 2000.0
|
||||
REAL = 600.0 # capitale reale (vedi CLAUDE.md), min-order Deribit $5
|
||||
COST_RATE = 0.0005 # Deribit taker per-lato (~0.10% RT sul turnover netto)
|
||||
|
||||
|
||||
def line(tag, daily, extra=""):
|
||||
m = metrics(daily); h = metrics(daily[daily.index >= HOLDOUT])
|
||||
eqf = CAP * float(np.prod(1.0 + daily.values))
|
||||
print(f" {tag:<26} FULL Sh {m['sharpe']:.2f} ret {m['ret']*100:+.0f}% DD {m['maxdd']*100:.1f}% "
|
||||
f"| HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.1f}% | €2k→€{eqf:,.0f} {extra}")
|
||||
return m, h
|
||||
|
||||
|
||||
def main():
|
||||
from src.portfolio.sleeves import deribit_book_sleeves
|
||||
sleeves = deribit_book_sleeves()
|
||||
pf = StrategyPortfolio(sleeves, capital=CAP)
|
||||
w = pf.weights()
|
||||
cols = {s.name: s.daily() for s in sleeves}
|
||||
|
||||
print("=" * 100)
|
||||
print(f" BOOK DERIBIT-ONLY (eseguibile) — {' + '.join(f'{k} {v*100:.0f}%' for k, v in w.items())} "
|
||||
f"| capitale €{CAP:,.0f} (reale ≈ ${REAL:,.0f}) | hold-out {HOLDOUT.date()}+")
|
||||
print("=" * 100)
|
||||
|
||||
# standalone per-gamba
|
||||
print("\n PER-GAMBA (standalone):")
|
||||
for s in sleeves:
|
||||
d = s.daily(); m = metrics(d); h = metrics(d[d.index >= HOLDOUT])
|
||||
print(f" {s.name:<16} [{w[s.name]*100:>3.0f}%] FULL Sh {m['sharpe']:.2f} DD {m['maxdd']*100:.0f}% "
|
||||
f"| HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.0f}%")
|
||||
|
||||
print("\n COMBINATO — rebalance-CONTINUO (idealizzato, no costi) vs PERIODICO (reale, costo turnover):")
|
||||
cont = pf.combined_daily()
|
||||
line("continuo (no costo)", cont)
|
||||
sims = {}
|
||||
for tag, period in (("settimanale (7g)", 7), ("bisettimanale (14g)", 14), ("mensile (30g)", 30)):
|
||||
sim = rebalance_sim(cols, w, period_days=period, cost_rate=COST_RATE)
|
||||
sims[tag] = sim
|
||||
line(f"rebal {tag}", sim["daily"], extra=f"| turnover {sim['turnover_per_year']:.1f}×/anno, {sim['n_rebalances']} ribilanci")
|
||||
|
||||
# raccomandato = mensile
|
||||
rec = sims["mensile (30g)"]["daily"]
|
||||
print("\n PER ANNO (rebal mensile, netto costo):")
|
||||
for y, d in yearly(rec).items():
|
||||
print(f" {y}: ret {d['ret']*100:>+7.1f}% DD {d['dd']*100:>5.1f}%")
|
||||
|
||||
print("\n ACCUMULO (rebal mensile):")
|
||||
for cap, lbl in ((CAP, "€2k nominale"), (REAL, "$600 reale")):
|
||||
eq = cap * np.cumprod(1.0 + rec.values)
|
||||
yrs = len(rec) / 365.25
|
||||
print(f" {lbl:<14}: {cap:,.0f} → {eq[-1]:,.0f} (×{eq[-1]/cap:.1f}, ~{(eq[-1]-cap)/(yrs*365.25):+,.2f}/g)")
|
||||
|
||||
print("\n POSIZIONI CORRENTI (ultima barra chiusa):")
|
||||
for name, pos in pf.current_positions().items():
|
||||
print(f" {name}: {pos if pos is not None else 'segnale dual-TF (no pos-fn) — vedi engine'}")
|
||||
|
||||
print("\n NOTE ONESTE:")
|
||||
print(" · TP01 = unico armato live su Deribit (flat=risk-off). SKH01 = 2a gamba candidata (perp BTC/ETH).")
|
||||
print(" · SKH01 equity daily-step (Sharpe lens). A $600 il min-order è $5: un ribilancio mensile")
|
||||
print(" muove abbastanza nozionale da eseguirsi; il giornaliero NO (Δ sub-$5 = finzione) → usa ≥ settimanale.")
|
||||
print(" · Prima del deploy 2a gamba: validare causalità sul CODICE D'ESECUZIONE reale e costi del book a 230m.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+76
-15
@@ -14,7 +14,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np, pandas as pd
|
||||
from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT
|
||||
from src.portfolio.sleeves import active_sleeves
|
||||
from src.portfolio.sleeves import active_sleeves, book_trade_frequency
|
||||
from src.portfolio.gtaa import gtaa_weights
|
||||
from src.live.shadow import shadow_report, tp01_trades
|
||||
from src.version import APP_VERSION
|
||||
@@ -32,6 +32,21 @@ def build():
|
||||
pf = StrategyPortfolio(active_sleeves(), capital=2000.0)
|
||||
bt = pf.backtest()
|
||||
eq = bt["equity"]; idx = bt["index"]
|
||||
# BOOK DERIBIT-ONLY eseguibile (TP01 75% + SKH01 25%), riusando i daily gia' calcolati
|
||||
try:
|
||||
sl_by = {s.name: s for s in pf.sleeves}
|
||||
tp_d, sk_d = sl_by["TP01_trend_1d"].daily(), sl_by["SKH01_skyhook"].daily()
|
||||
Jd = pd.concat({"t": tp_d, "s": sk_d}, axis=1, join="inner").fillna(0.0)
|
||||
der = 0.75 * Jd["t"] + 0.25 * Jd["s"]
|
||||
mm = ((1.0 + der).resample("ME").prod() - 1.0).dropna()
|
||||
d_cagr = float(np.prod(1.0 + mm.values) ** (12.0 / len(mm)) - 1.0) if len(mm) else 0.0
|
||||
cons = d_cagr * 0.5
|
||||
rd = (1.0 + cons) ** (1.0 / 365.0) - 1.0 if cons > 0 else 0.0
|
||||
deribit = dict(full=metrics(der), hold=metrics(der[der.index >= HOLDOUT]),
|
||||
cagr=d_cagr, cons_cagr=cons, eday_5k=5000.0 * rd,
|
||||
eq5_5y=5000.0 * (1.0 + cons) ** 5, eq5_10y=5000.0 * (1.0 + cons) ** 10)
|
||||
except Exception as e:
|
||||
deribit = {"error": f"{type(e).__name__}: {e}"}
|
||||
# sparkline: subsample ~400 punti
|
||||
step = max(1, len(eq) // 400)
|
||||
spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)]
|
||||
@@ -50,14 +65,18 @@ def build():
|
||||
trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale
|
||||
except Exception:
|
||||
trades = []
|
||||
try:
|
||||
freq = book_trade_frequency() # trade/anno contati (SKH01 round-trip + TP01 turnover)
|
||||
except Exception as e:
|
||||
freq = {"error": f"{type(e).__name__}: {e}"}
|
||||
data = dict(
|
||||
version=APP_VERSION,
|
||||
last_data=str(idx[-1].date()),
|
||||
full=bt["full"], holdout=bt["holdout"], weights=bt["weights"],
|
||||
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
|
||||
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
|
||||
combo=combo, gtaa_weights=gtaa_w,
|
||||
shadow=shadow, trades=trades, bh=None,
|
||||
combo=combo, gtaa_weights=gtaa_w, deribit=deribit,
|
||||
shadow=shadow, trades=trades, freq=freq, bh=None,
|
||||
)
|
||||
_CACHE.update(t=time.time(), data=data)
|
||||
return data
|
||||
@@ -146,6 +165,18 @@ def html():
|
||||
+ f" <span style='color:#8a93a0'>(asof {asof})</span>")
|
||||
else:
|
||||
gw_html = "n/d (cache ETF assente — gira fetch_ib_equities.py)"
|
||||
db = d.get("deribit")
|
||||
if db and "error" not in db:
|
||||
f2, h2 = db["full"], db["hold"]
|
||||
deribit_html = (
|
||||
f"FULL <b>Sh {f2['sharpe']:.2f}</b> ret {f2['ret']*100:+.0f}% DD {f2['maxdd']*100:.1f}% · "
|
||||
f"HOLD-OUT <b>Sh {h2['sharpe']:.2f}</b> DD {h2['maxdd']*100:.1f}%<br>"
|
||||
f"<span style='color:#8a93a0;font-size:13px'>accumulo (reinvesti le vincite, no leva) — CAGR storico "
|
||||
f"<b>{db['cagr']*100:.0f}%</b>, conservativo <b>{db['cons_cagr']*100:.0f}%</b>: "
|
||||
f"da €5k → ~€{db['eq5_5y']:,.0f} (5a) / ~€{db['eq5_10y']:,.0f} (10a) · "
|
||||
f"run-rate oggi ~<b>€{db['eday_5k']:.2f}/g</b> @conservativo</span>")
|
||||
else:
|
||||
deribit_html = "n/d" + (f" — {db['error']}" if db and db.get("error") else "")
|
||||
sh = d.get("shadow")
|
||||
if sh and "error" not in sh:
|
||||
bits = " · ".join(
|
||||
@@ -153,13 +184,13 @@ def html():
|
||||
f"{a['target']:+.2f}x" for a in sh["assets"])
|
||||
if sh.get("online"):
|
||||
eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?")
|
||||
pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"])
|
||||
shpos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"])
|
||||
ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"])
|
||||
if sh.get("orders") else "nessuno (target flat / gia' a target)")
|
||||
dsl = sh.get("disaster_sls") or []
|
||||
dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl)
|
||||
if dsl else "nessuno (flat)")
|
||||
shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {pos} · dato {sh['last_data']}<br>"
|
||||
shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {shpos} · dato {sh['last_data']}<br>"
|
||||
f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>"
|
||||
f"🛡️ disaster-SL attivi (−30%): {dsl_txt}")
|
||||
else:
|
||||
@@ -185,6 +216,24 @@ def html():
|
||||
f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>")
|
||||
if not live_trows:
|
||||
live_trows = "<tr><td colspan=6 style='color:#8a93a0'>nessun trade reale eseguito (o conto non leggibile dal container)</td></tr>"
|
||||
fq = d.get("freq")
|
||||
if fq and "error" not in fq:
|
||||
skh = fq["skh"]
|
||||
by = lambda a: " ".join(f"{y}:{c}" for y, c in skh[a]["by_year"].items())
|
||||
tpy = fq["skh_combo_per_year"]
|
||||
freq_html = (
|
||||
f"<b>SKH01</b> (round-trip = apri+chiudi, no-overlap): BTC <b>~{fq['skh_per_year']['BTC']:.0f}/anno</b> · "
|
||||
f"ETH <b>~{fq['skh_per_year']['ETH']:.0f}/anno</b> → combinato <b>~{tpy:.0f}/anno</b> "
|
||||
f"(~{2*tpy:.0f} esecuzioni)<br>"
|
||||
f"<b>TP01</b> (posizione continua vol-target, NON trade discreti): turnover "
|
||||
f"<b>~{fq['tp01_turnover_per_year']:.0f}×/anno</b> per asset · zero quando flat/risk-off<br>"
|
||||
f"<span style='color:#8a93a0;font-size:13px'>→ book eseguibile ~{tpy:.0f}-{tpy+5:.0f} operazioni/anno: "
|
||||
f"poche per design (regime AND breakout). Fee ~$45-60/anno, gia' nette nei numeri storici · min-order $5 OK a $600.<br>"
|
||||
f"SKH01 round-trip/anno — BTC: {by('BTC')} · ETH: {by('ETH')}</span>")
|
||||
tpy_card = f"~{tpy:.0f}"
|
||||
else:
|
||||
freq_html = "n/d" + (f" — {fq['error']}" if fq and fq.get("error") else "")
|
||||
tpy_card = "~75"
|
||||
return f"""<!doctype html><html><head><meta charset=utf-8>
|
||||
<meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title>
|
||||
<style>body{{font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#0e1116;color:#e6e6e6;margin:0;padding:24px;max-width:980px;margin:auto}}
|
||||
@@ -199,14 +248,26 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
|
||||
.section{{font-size:15px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:34px 0 14px;padding:10px 14px;border-radius:9px;background:#12181f;border-left:5px solid #2ecc71;color:#d7dee6}}
|
||||
.section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body>
|
||||
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)</h1>
|
||||
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test)</div>
|
||||
<div class="section">PAPER — simulato (backtest + forward virtuale)</div>
|
||||
<div class=sub>monitor · v{d['version']} · ultimo dato {d['last_data']} · esecuzione REALE non attiva (solo micro-test) · ordine: LIVE → trades fatti → storico</div>
|
||||
|
||||
<div class="section live">① LIVE — Deribit mainnet (conto reale, sola lettura)</div>
|
||||
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
|
||||
<p class=warn>Solo TP01 e' armato live (cap $300/asset · disaster-SL on-book −30% · capitale reale ≈ $600). SKH01/XS01/VRP01 = paper/stat-mode. Lo "Shadow" mostra il target reale ma NON invia ordini.</p>
|
||||
|
||||
<div class="section">② TRADES ESEGUITI (reali) + frequenza operativa</div>
|
||||
<div class=box><b>Frequenza operativa</b> (contata sui dati certificati, non stimata):<br>{freq_html}</div>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
|
||||
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
|
||||
<p class=warn>Oggi TP01 e' in target TSMOM risk-off → flat: nessun ordine reale. La tabella si popola quando il segnale arma una posizione e il cron giornaliero la esegue.</p>
|
||||
|
||||
<div class="section">③ STORICO — simulato (backtest + forward virtuale)</div>
|
||||
<div class=cards>
|
||||
<div class=card><div class=k>FULL Sharpe</div><div class="v g">{f['sharpe']:.2f}</div></div>
|
||||
<div class=card><div class=k>HOLD-OUT Sharpe (2025-26)</div><div class="v g">{ho['sharpe']:.2f}</div></div>
|
||||
<div class=card><div class=k>maxDD</div><div class=v>{f['maxdd']*100:.1f}%</div></div>
|
||||
<div class=card><div class=k>CAGR</div><div class=v>{f['cagr']*100:.0f}%</div></div>
|
||||
<div class=card><div class=k>ret totale</div><div class=v>{f['ret']*100:+.0f}%</div></div>
|
||||
<div class=card><div class=k>trade/anno (book eseguibile)</div><div class=v>{tpy_card}</div></div>
|
||||
</div>
|
||||
<div class=box><div class=k style="color:#8a93a0;font-size:12px">EQUITY backtest (2019→oggi, €2k)</div>{svg_spark(d['spark'])}</div>
|
||||
<div class=box><b>Paper forward-only:</b> {paper_html}</div>
|
||||
@@ -214,21 +275,21 @@ th{{color:#8a93a0;font-weight:500}}.y{{display:inline-block;background:#161b22;b
|
||||
<table><tr><th>sleeve</th><th>peso</th><th>FULL Sh</th><th>DD</th><th>HOLD Sh</th></tr>{rows}</table>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3>
|
||||
<table>{pos}</table>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale, ultimi 15)</h3>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Trades TP01 — entry/exit (segnale causale SIMULATO, ultimi 15)</h3>
|
||||
<table><tr><th>data</th><th>asset</th><th>azione</th><th>posizione</th><th>prezzo</th></tr>{trows}</table>
|
||||
<div style="margin-top:10px">{yrs}</div>
|
||||
<div class="section">COMBO DEPLOYABLE — cross-venue (paper, forward-only)</div>
|
||||
<div class="section">③·a COMBO DEPLOYABLE — cross-venue (paper, forward-only)</div>
|
||||
<div class=box><b>TP01 (Deribit) + GTAA (IB)</b> — le DUE gambe ESEGUIBILI a basso capitale, scorrelate (corr ~0.21) → blend Sharpe ~1.5, drawdown dimezzato. <b>Nessuna esecuzione reale</b>:<br>{combo_html}<br>
|
||||
<span style="color:#8a93a0;font-size:13px">posizioni azionabili IB (GTAA, peso ETF):</span> {gw_html}</div>
|
||||
<p class=warn>⚠️ PAPER cross-venue: valida l'operativita' su due conti (Deribit + IB) a rischio zero. Lo Sharpe ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la diversificazione (corr 0.21, DD dimezzato), non il livello. XS01/VRP01 esclusi (STAT-MODE): qui solo TP01+GTAA.</p>
|
||||
<div class="section">FORWARD-MONITOR — lead paper (non deploy)</div>
|
||||
<div class="section">③·b BOOK DERIBIT-ONLY — eseguibile (TP01 + SKH01)</div>
|
||||
<div class=box><b>TP01 75% + SKH01 25%</b> — le due gambe direzionali BTC/ETH sullo STESSO venue (Deribit), ribilancio mensile. Sottoinsieme realmente armabile (XS01/VRP01 esclusi):<br>{deribit_html}<br>
|
||||
<span style="color:#8a93a0;font-size:13px">forecast: <code>scripts/portfolio/forecast_deribit_book.py</code> · report: <code>scripts/portfolio/run_deribit_book.py</code></span></div>
|
||||
<p class=warn>⚠️ Accumulo = proiezione condizionata (storico bull crypto → pianifica sul conservativo); nessuna leva; SKH01 research/forward-monitor (solo TP01 armato live). A €50/g servono ~€177k @conservativo: la via è capitale+tempo, non leva.</p>
|
||||
<div class="section">③·c FORWARD-MONITOR — lead paper (non deploy)</div>
|
||||
<div class=box><b>PREVDAY range-breakout</b> — lead ORTOGONALE a TP01 (corr ~0.15 full / ~0 hold; marginal ADDS, non-hedge, robusto allo shift del confine-giorno). Forward-only, <b>nessuna esecuzione reale</b>:<br>{prevday_html}</div>
|
||||
<p class=warn>⚠️ LEAD in osservazione, NON deployato. Sopravvissuto alla verifica avversariale dell'onda intraday; lo teniamo in paper per validarlo fuori-campione-vero. I due libri (modeled vs real-$600) mostrano l'haircut di fill che lo scettico aveva segnalato.</p>
|
||||
<div class="section live">LIVE — Deribit mainnet (conto reale, sola lettura)</div>
|
||||
<div class=box><b>Shadow TP01</b> (cosa farebbe ORA sul conto reale, nessun ordine inviato):<br>{shadow_html}</div>
|
||||
<h3 style="font-size:14px;color:#8a93a0">Trades REALI eseguiti su Deribit</h3>
|
||||
<table><tr><th>data/ora UTC</th><th>strum.</th><th>dir</th><th>amount</th><th>prezzo</th><th>fee USDC</th></tr>{live_trows}</table>
|
||||
<p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). SKH01 (Skyhook dual-TF regime+breakout, BTC/ETH) = diversificatore quasi-ortogonale (corr ~0.09) aggiunto @25%: alza il FULL Sharpe del portafoglio 1.68→2.13 e dimezza il DD (14→8%) — RESEARCH/forward-monitor (book a 230m, causalita' verificata su harness ma costi reali e codice d'esecuzione da validare prima del deploy). TP01 e' l'unico deployable pieno: lo "Shadow live" mostra cosa farebbe sul mainnet, ma NON invia ordini.</p>
|
||||
<p class=warn>⚠️ Paper/monitor. XS01 e' STAT-MODE (book a 19 gambe market-neutral, non eseguibile a €2k, storia ~2.5 anni). VRP01 = lead short-vol MODELLATO (non deploy pieno). SKH01 (Skyhook dual-TF regime+breakout, BTC/ETH) = diversificatore quasi-ortogonale (corr ~0.09) aggiunto @25%: alza il FULL Sharpe del portafoglio 1.68→2.13 e dimezza il DD (14→8%) — RESEARCH/forward-monitor (book a 230m, causalita' verificata su harness ma costi reali e codice d'esecuzione da validare prima del deploy). TP01 e' l'unico deployable pieno.</p>
|
||||
</body></html>"""
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,44 @@ def yearly(daily: pd.Series) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
def rebalance_sim(daily_cols: dict[str, pd.Series], weights: dict,
|
||||
period_days: int, cost_rate: float = 0.0005) -> dict:
|
||||
"""Ribilanciamento PERIODICO REALISTICO (vs il rebalance-continuo implicito di combined_daily).
|
||||
|
||||
Tra una data di ribilanciamento e l'altra ogni sleeve DERIVA col suo rendimento (i pesi si
|
||||
scostano dal target). Ogni `period_days` si riporta al target pagando il turnover:
|
||||
cost = cost_rate * sum_i |valore_i - target_i| (cost_rate = fee per-lato, Deribit taker ~0.0005)
|
||||
Modella l'attrito reale che il rebalance-continuo (combined_daily) ignora. period_days=1 con
|
||||
cost_rate=0 ricade sul rebalance-continuo. Ritorna serie netta + turnover annuo + n ribilanci."""
|
||||
J = pd.concat(daily_cols, axis=1, join="inner").sort_index().fillna(0.0)
|
||||
cols = list(J.columns)
|
||||
w = np.array([weights[c] for c in cols], float); w = w / w.sum()
|
||||
R = J.values
|
||||
n = len(J)
|
||||
E = 1.0
|
||||
v = w * E
|
||||
out = np.zeros(n)
|
||||
n_rebal = 0
|
||||
turn_tot = 0.0
|
||||
for t in range(n):
|
||||
Eprev = E
|
||||
v = v * (1.0 + R[t])
|
||||
E = float(v.sum())
|
||||
if (t + 1) % period_days == 0: # giorno di ribilanciamento
|
||||
target = w * E
|
||||
turn = float(np.abs(v - target).sum())
|
||||
cost = cost_rate * turn
|
||||
E -= cost
|
||||
v = w * E
|
||||
n_rebal += 1
|
||||
turn_tot += turn / max(Eprev, 1e-12)
|
||||
out[t] = E / Eprev - 1.0 if Eprev > 0 else 0.0
|
||||
years = n / DAYS_PER_YEAR
|
||||
return dict(daily=pd.Series(out, index=J.index),
|
||||
turnover_per_year=round(turn_tot / years, 2) if years > 0 else 0.0,
|
||||
n_rebalances=n_rebal, period_days=period_days, cost_rate=cost_rate)
|
||||
|
||||
|
||||
class StrategyPortfolio:
|
||||
def __init__(self, sleeves: list[Sleeve], capital: float = 2000.0):
|
||||
if not sleeves:
|
||||
|
||||
@@ -236,8 +236,82 @@ def _skyhook_returns() -> pd.Series:
|
||||
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||
|
||||
|
||||
def _skyhook_positions() -> dict:
|
||||
"""Stato corrente del book Skyhook per asset (introspezione live): se c'e' un trade APERTO ORA
|
||||
-> dir/entry/sl/tp/barre-trascorse; altrimenti 'flat'. Replica la logica non-overlap di
|
||||
entry+exit (TP/SL/max_bars) fino all'ultima barra 230m chiusa. Causale: usa solo barre chiuse."""
|
||||
out = {}
|
||||
for a in ASSETS:
|
||||
ltf, htf = build_frames(load_data(a, "5m"))
|
||||
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
|
||||
H = ltf["high"].values; L = ltf["low"].values; Cc = ltf["close"].values
|
||||
n = len(ltf); i = 0; open_pos = "flat"
|
||||
while i < n:
|
||||
e = ent[i]
|
||||
if e is None:
|
||||
i += 1; continue
|
||||
d, sl, tp, mb = e["dir"], e["sl"], e["tp"], e["max_bars"]
|
||||
exit_idx = None
|
||||
for s in range(1, mb + 1):
|
||||
j = i + s
|
||||
if j >= n: # non ancora uscito: posizione APERTA ora
|
||||
break
|
||||
hit = (L[j] <= sl or H[j] >= tp) if d == 1 else (H[j] >= sl or L[j] <= tp)
|
||||
if hit or s == mb:
|
||||
exit_idx = j; break
|
||||
if exit_idx is None:
|
||||
open_pos = dict(dir="LONG" if d == 1 else "SHORT", entry=round(float(Cc[i]), 2),
|
||||
sl=round(float(sl), 2), tp=round(float(tp), 2),
|
||||
bars_in=int((n - 1) - i), max_bars=int(mb))
|
||||
break
|
||||
i = exit_idx + 1
|
||||
out[a] = open_pos
|
||||
return out
|
||||
|
||||
|
||||
def skyhook_sleeve(weight: float = 0.25) -> Sleeve:
|
||||
return Sleeve("SKH01_skyhook", weight, _skyhook_returns)
|
||||
return Sleeve("SKH01_skyhook", weight, _skyhook_returns, pos_fn=_skyhook_positions)
|
||||
|
||||
|
||||
_TRADE_FREQ_CACHE = None
|
||||
|
||||
|
||||
def book_trade_frequency() -> dict:
|
||||
"""Frequenza operativa del book eseguibile (TP01+SKH01), conteggiata sui dati certificati.
|
||||
Strategia + storico fissi -> cache a livello di MODULO: calcolata una sola volta per processo
|
||||
(le nuove barre aggiungono pochissimo). Niente magic-number: e' contata, non stimata.
|
||||
- SKH01: round-trip DISCRETI (apri+chiudi) no-overlap dell'harness, per asset e per anno.
|
||||
- TP01: posizione CONTINUA vol-target -> 'turnover' annuo (somma |Δpeso|), non trade discreti."""
|
||||
global _TRADE_FREQ_CACHE
|
||||
if _TRADE_FREQ_CACHE is not None:
|
||||
return _TRADE_FREQ_CACHE
|
||||
skh = {}
|
||||
for a in ASSETS:
|
||||
ltf, htf = build_frames(load_data(a, "5m"))
|
||||
ent = skyhook_entries(ltf, htf, SKH01_V2_DD)
|
||||
yr = pd.to_datetime(pd.Series(ltf["timestamp"]).astype("int64"), unit="ms", utc=True).dt.year.values
|
||||
yc: dict[int, int] = {}
|
||||
for i, e in enumerate(ent):
|
||||
if e is not None:
|
||||
y = int(yr[i]); yc[y] = yc.get(y, 0) + 1
|
||||
skh[a] = dict(by_year={int(k): int(v) for k, v in sorted(yc.items())},
|
||||
total=int(sum(yc.values())), first=int(min(yc)), last=int(max(yc)))
|
||||
sf = min(v["first"] for v in skh.values()); sl = max(v["last"] for v in skh.values())
|
||||
skh_combo = sum(v["total"] for v in skh.values()) / (sl - sf + 1)
|
||||
tp = TrendPortfolio(**CANONICAL); turns = []
|
||||
for a in ASSETS:
|
||||
df = resample_1d(load_data(a, "1h"))
|
||||
tgt = np.asarray(tp.target_series(df), dtype=float)
|
||||
years = pd.DatetimeIndex(pd.to_datetime(df["datetime"])).year # ndarray-aligned key
|
||||
d = np.abs(np.diff(np.nan_to_num(tgt, nan=0.0), prepend=0.0)) # turnover = somma |Δpeso|
|
||||
ty = pd.Series(d).groupby(years).sum() # groupby posizionale (RangeIndex)
|
||||
turns.append(float(ty.mean()))
|
||||
_TRADE_FREQ_CACHE = dict(
|
||||
skh=skh,
|
||||
skh_per_year={a: skh[a]["total"] / (skh[a]["last"] - skh[a]["first"] + 1) for a in ASSETS},
|
||||
skh_combo_per_year=skh_combo,
|
||||
tp01_turnover_per_year=sum(turns) / len(turns))
|
||||
return _TRADE_FREQ_CACHE
|
||||
|
||||
|
||||
# ----------------------------- REGISTRY -----------------------------
|
||||
@@ -253,3 +327,16 @@ def active_sleeves() -> list[Sleeve]:
|
||||
vrp_sleeve(weight=0.15), # options short-vol (put credit spread + gate IV-rank), dal 2021 (lead modellato, scorrelato)
|
||||
skyhook_sleeve(weight=0.25), # dual-TF regime+breakout BTC/ETH, dal 2019 (quasi-ortogonale, exit %-asimmetrici, research)
|
||||
]
|
||||
|
||||
|
||||
def deribit_book_sleeves() -> list[Sleeve]:
|
||||
"""BOOK DERIBIT-ONLY realmente eseguibile (TP01 + SKH01, 75/25): le DUE gambe direzionali
|
||||
BTC/ETH sullo stesso venue (Deribit), entrambe dal 2019. Esclude XS01 (Hyperliquid, stat-mode)
|
||||
e VRP01 (opzioni modellate). FULL Sharpe ~1.78 / HOLD ~1.17 / DD ~9% (research; SKH01 daily-step).
|
||||
Pensato per il deploy reale a basso capitale: stesso conto, stesso feed, ribilanciamento
|
||||
periodico (vedi src.portfolio.portfolio.rebalance_sim + scripts/portfolio/run_deribit_book.py).
|
||||
TP01 e' gia' armato live; SKH01 e' il candidato 2a gamba (da validare codice d'esecuzione)."""
|
||||
return [
|
||||
tp01_sleeve(weight=0.75),
|
||||
skyhook_sleeve(weight=0.25),
|
||||
]
|
||||
|
||||
+32
-1
@@ -7,7 +7,7 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics
|
||||
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, rebalance_sim
|
||||
|
||||
|
||||
def _const_sleeve(name, weight, val, n=400):
|
||||
@@ -15,6 +15,37 @@ def _const_sleeve(name, weight, val, n=400):
|
||||
return Sleeve(name, weight, lambda: pd.Series(val, index=idx))
|
||||
|
||||
|
||||
def _ret_series(vals):
|
||||
idx = pd.date_range("2020-01-01", periods=len(vals), freq="1D", tz="UTC")
|
||||
return pd.Series(vals, index=idx)
|
||||
|
||||
|
||||
def test_rebalance_sim_no_cost_period1_matches_continuous():
|
||||
"""period=1 + cost=0 deve coincidere col rebalance-continuo (weighted-return giornaliero)."""
|
||||
rng = np.random.default_rng(0)
|
||||
A = _ret_series(rng.normal(0.001, 0.02, 300))
|
||||
B = _ret_series(rng.normal(0.000, 0.03, 300))
|
||||
w = {"A": 0.6, "B": 0.4}
|
||||
sim = rebalance_sim({"A": A, "B": B}, w, period_days=1, cost_rate=0.0)
|
||||
cont = 0.6 * A + 0.4 * B
|
||||
assert np.allclose(sim["daily"].values, cont.values, atol=1e-12)
|
||||
assert sim["n_rebalances"] == 300
|
||||
|
||||
|
||||
def test_rebalance_sim_cost_reduces_return_and_counts():
|
||||
"""Il costo del turnover abbassa il rendimento; ribilanci meno frequenti = meno costo."""
|
||||
rng = np.random.default_rng(1)
|
||||
A = _ret_series(rng.normal(0.001, 0.02, 360))
|
||||
B = _ret_series(rng.normal(0.001, 0.04, 360))
|
||||
w = {"A": 0.5, "B": 0.5}
|
||||
free = rebalance_sim({"A": A, "B": B}, w, period_days=7, cost_rate=0.0)["daily"]
|
||||
weekly = rebalance_sim({"A": A, "B": B}, w, period_days=7, cost_rate=0.001)
|
||||
monthly = rebalance_sim({"A": A, "B": B}, w, period_days=30, cost_rate=0.001)
|
||||
assert weekly["daily"].sum() < free.sum() # il costo morde
|
||||
assert monthly["n_rebalances"] < weekly["n_rebalances"] # mensile ribilancia meno
|
||||
assert weekly["turnover_per_year"] > 0
|
||||
|
||||
|
||||
def test_single_sleeve_equals_itself():
|
||||
s = _const_sleeve("A", 1.0, 0.001)
|
||||
pf = StrategyPortfolio([s])
|
||||
|
||||
@@ -129,6 +129,16 @@ def test_short_override_changes_only_shorts():
|
||||
assert longs_same > 0 and shorts_diff > 0
|
||||
|
||||
|
||||
def test_skyhook_pos_fn_structure():
|
||||
"""pos_fn introspettiva: dict BTC/ETH, ciascuno 'flat' o dict con dir/entry/sl/tp coerenti."""
|
||||
from src.portfolio.sleeves import _skyhook_positions
|
||||
pos = _skyhook_positions()
|
||||
assert set(pos.keys()) == {"BTC", "ETH"}
|
||||
for a, p in pos.items():
|
||||
assert p == "flat" or (isinstance(p, dict) and p["dir"] in ("LONG", "SHORT")
|
||||
and p["sl"] > 0 and p["tp"] > 0 and 0 <= p["bars_in"] <= p["max_bars"])
|
||||
|
||||
|
||||
def test_v2dd_robust_both_assets():
|
||||
"""SKH01-V2-DD: PASS netto fee su BTCÐ, hold-out forte, e maxDD standalone <30%."""
|
||||
import skyhooklib as sk
|
||||
|
||||
Reference in New Issue
Block a user