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))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
import numpy as np, pandas as pd
|
import numpy as np, pandas as pd
|
||||||
from src.portfolio.portfolio import StrategyPortfolio, metrics, HOLDOUT
|
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.portfolio.gtaa import gtaa_weights
|
||||||
from src.live.shadow import shadow_report, tp01_trades
|
from src.live.shadow import shadow_report, tp01_trades
|
||||||
from src.version import APP_VERSION
|
from src.version import APP_VERSION
|
||||||
@@ -32,6 +32,21 @@ def build():
|
|||||||
pf = StrategyPortfolio(active_sleeves(), capital=2000.0)
|
pf = StrategyPortfolio(active_sleeves(), capital=2000.0)
|
||||||
bt = pf.backtest()
|
bt = pf.backtest()
|
||||||
eq = bt["equity"]; idx = bt["index"]
|
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
|
# sparkline: subsample ~400 punti
|
||||||
step = max(1, len(eq) // 400)
|
step = max(1, len(eq) // 400)
|
||||||
spark = [(str(idx[i].date()), float(eq[i])) for i in range(0, len(eq), step)]
|
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
|
trades = tp01_trades(limit=15) # entry/exit TP01 dal segnale causale
|
||||||
except Exception:
|
except Exception:
|
||||||
trades = []
|
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(
|
data = dict(
|
||||||
version=APP_VERSION,
|
version=APP_VERSION,
|
||||||
last_data=str(idx[-1].date()),
|
last_data=str(idx[-1].date()),
|
||||||
full=bt["full"], holdout=bt["holdout"], weights=bt["weights"],
|
full=bt["full"], holdout=bt["holdout"], weights=bt["weights"],
|
||||||
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
|
per_sleeve=bt["per_sleeve"], yearly=bt["yearly"],
|
||||||
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
|
positions=pf.current_positions(), spark=spark, paper=paper, prevday=prevday,
|
||||||
combo=combo, gtaa_weights=gtaa_w,
|
combo=combo, gtaa_weights=gtaa_w, deribit=deribit,
|
||||||
shadow=shadow, trades=trades, bh=None,
|
shadow=shadow, trades=trades, freq=freq, bh=None,
|
||||||
)
|
)
|
||||||
_CACHE.update(t=time.time(), data=data)
|
_CACHE.update(t=time.time(), data=data)
|
||||||
return data
|
return data
|
||||||
@@ -146,6 +165,18 @@ def html():
|
|||||||
+ f" <span style='color:#8a93a0'>(asof {asof})</span>")
|
+ f" <span style='color:#8a93a0'>(asof {asof})</span>")
|
||||||
else:
|
else:
|
||||||
gw_html = "n/d (cache ETF assente — gira fetch_ib_equities.py)"
|
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")
|
sh = d.get("shadow")
|
||||||
if sh and "error" not in sh:
|
if sh and "error" not in sh:
|
||||||
bits = " · ".join(
|
bits = " · ".join(
|
||||||
@@ -153,13 +184,13 @@ def html():
|
|||||||
f"{a['target']:+.2f}x" for a in sh["assets"])
|
f"{a['target']:+.2f}x" for a in sh["assets"])
|
||||||
if sh.get("online"):
|
if sh.get("online"):
|
||||||
eq = f"${sh['real_equity']:,.2f}" if sh.get("real_equity") else sh.get("eq_basis", "?")
|
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"])
|
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)")
|
if sh.get("orders") else "nessuno (target flat / gia' a target)")
|
||||||
dsl = sh.get("disaster_sls") or []
|
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)
|
dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl)
|
||||||
if dsl else "nessuno (flat)")
|
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"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>"
|
||||||
f"🛡️ disaster-SL attivi (−30%): {dsl_txt}")
|
f"🛡️ disaster-SL attivi (−30%): {dsl_txt}")
|
||||||
else:
|
else:
|
||||||
@@ -185,6 +216,24 @@ def html():
|
|||||||
f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>")
|
f"<td>{x['amount']:.4f}</td><td>${x['price']:,.1f}</td><td>{x['fee']:.5f}</td></tr>")
|
||||||
if not live_trows:
|
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>"
|
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>
|
return f"""<!doctype html><html><head><meta charset=utf-8>
|
||||||
<meta http-equiv=refresh content=300><title>PythagorasGoal — Portafoglio</title>
|
<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}}
|
<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{{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>
|
.section.live{{border-left-color:#e74c3c;background:#1c1316;color:#f0c4c4}}</style></head><body>
|
||||||
<h1>PythagorasGoal — Portafoglio attivo (TP01 + XS01 + VRP01 + SKH01)</h1>
|
<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=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">PAPER — simulato (backtest + forward virtuale)</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=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>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>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>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>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>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>
|
||||||
<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><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>
|
<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>
|
<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>
|
<h3 style="font-size:14px;color:#8a93a0">Posizioni correnti (ultima barra chiusa)</h3>
|
||||||
<table>{pos}</table>
|
<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>
|
<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 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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
||||||
<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>
|
|
||||||
</body></html>"""
|
</body></html>"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,44 @@ def yearly(daily: pd.Series) -> dict:
|
|||||||
return out
|
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:
|
class StrategyPortfolio:
|
||||||
def __init__(self, sleeves: list[Sleeve], capital: float = 2000.0):
|
def __init__(self, sleeves: list[Sleeve], capital: float = 2000.0):
|
||||||
if not sleeves:
|
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)
|
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:
|
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 -----------------------------
|
# ----------------------------- 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)
|
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)
|
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 pandas as pd
|
||||||
import pytest
|
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):
|
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))
|
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():
|
def test_single_sleeve_equals_itself():
|
||||||
s = _const_sleeve("A", 1.0, 0.001)
|
s = _const_sleeve("A", 1.0, 0.001)
|
||||||
pf = StrategyPortfolio([s])
|
pf = StrategyPortfolio([s])
|
||||||
|
|||||||
@@ -129,6 +129,16 @@ def test_short_override_changes_only_shorts():
|
|||||||
assert longs_same > 0 and shorts_diff > 0
|
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():
|
def test_v2dd_robust_both_assets():
|
||||||
"""SKH01-V2-DD: PASS netto fee su BTCÐ, hold-out forte, e maxDD standalone <30%."""
|
"""SKH01-V2-DD: PASS netto fee su BTCÐ, hold-out forte, e maxDD standalone <30%."""
|
||||||
import skyhooklib as sk
|
import skyhooklib as sk
|
||||||
|
|||||||
Reference in New Issue
Block a user