729 lines
37 KiB
Python
729 lines
37 KiB
Python
#!/usr/bin/env python
|
|
"""r0822_vol_size.py — CONTROLLO DEL RISCHIO SU SKH01 e VOL-TARGET A LIVELLO DI LIBRO.
|
|
|
|
DOMANDA (ondata 2026-08-22, filone VOL-SIZE). Il libro live Deribit e' TP01 75% + SKH01 25%,
|
|
nettati su un solo strumento. TP01 e' vol-targeted al 20% sulla vol REALIZZATA dell'asset;
|
|
**SKH01 non ha alcun controllo del rischio** — `backtest_signals(..., leverage=1.0,
|
|
position_size=1.0)`, verificato il 26/07: il suo 20,7% di vol realizzata e' un PRODOTTO della
|
|
strategia (uscite % asimmetriche + poco tempo a mercato), non un parametro. E' l'unica delle
|
|
cinque gambe senza dimensionamento, la piu' fee-sensibile (~4x TP01) e la piu' fragile
|
|
all'ancora (LOO 26/07).
|
|
|
|
Q1 dare a SKH01 un dimensionamento del rischio (inverse-vol per-trade / rischio costante per
|
|
trade / vol-target sulla sua vol di strategia) migliora il LIBRO, o e' de-levering?
|
|
Q2 il vol-target va applicato al LIBRO gia' nettato invece che al singolo sleeve? Oggi ogni
|
|
sleeve si dimensiona da solo e poi si nettano: la versione book-level non e' mai stata
|
|
misurata.
|
|
|
|
COSA NON SI RIFA' (gia' misurato): TP01 x DVOL (26/06, de-levering); il peso 75/25 (confermato
|
|
3 volte). I pesi NOMINALI non si toccano; una modulazione di size e' pero' un cambio di pesi nel
|
|
tempo, quindi `weights_tilt_null` si riporta lo stesso (§7).
|
|
|
|
IPOTESI A PRIORI, REGISTRATA PRIMA DI MISURARE: **de-levering su quasi tutto**. Ragione
|
|
dichiarata prima: lo Sharpe e' INVARIANTE a una costante moltiplicativa, quindi una variante che
|
|
si limiti ad abbassare la leva media non puo' cambiare lo Sharpe e puo' solo abbassare il DD —
|
|
la firma esatta del null del de-levering (5 occorrenze nel progetto). Per VINCERE una variante
|
|
deve cambiare la FORMA (quale trade pesa quanto), e la forma si vede solo sullo Sharpe.
|
|
Sotto-ipotesi: l'inverse-vol per-trade e' l'unica con una ragione strutturale (lo stop di SKH01
|
|
e' una PERCENTUALE FISSA 4%/2%, quindi il rischio in unita' di sigma varia col regime); il
|
|
vol-target sulla serie giornaliera dello sleeve e' quello che ha piu' probabilita' di rompersi
|
|
(SKH01 e' ~88% di zeri: la vol trailing di quella serie misura l'ATTIVITA', non il rischio).
|
|
|
|
⚠️ DIFETTO TROVATO NELLA MIA PRIMA STESURA, E MISURATO INVECE CHE CANCELLATO (§4). Il modo
|
|
ovvio di scrivere un vol-target su uno sleeve — moltiplicare la sua serie GIORNALIERA per
|
|
L_t = tv/rv_{t-1} — e' **non causale su SKH01**, perche' la sua equity e' a GRADINO: tutto il
|
|
P&L di un trade e' contabilizzato il giorno di CHIUSURA. Scalare quel giorno per L_t significa
|
|
aver tenuto size L_t dall'INGRESSO, che e' fino a ~4 giorni prima (24 barre da 230m). L_t usa
|
|
solo dati <= t-1 e quindi passa qualunque controllo di causalita' scritto sulla serie
|
|
giornaliera: il look-ahead sta nella CONTABILITA', non nella formula. La versione causale fissa
|
|
la size all'INGRESSO. Entrambe sono misurate qui, e la differenza e' un risultato.
|
|
|
|
LENTE E ANCORA. Path CANONICO (fill al livello, entry a chiusura di bin): e' la lente di tutti i
|
|
numeri pubblicati di SKH01 ed e' identica nelle due braccia di ogni confronto — il Delta e'
|
|
pulito anche se il livello assoluto e' pessimistico verso il path live (26/07: il live e'
|
|
migliore su ingressi E uscite). Banda d'ancora = **23 offset**, la griglia a priori dell'audit
|
|
02/07 (ogni 30m su [0,690)); statistica = **mediana delle DIFFERENZE APPAIATE**
|
|
(`A.anchor_luck_delta`), mai differenza delle mediane. L'ancora di TP01 e' tenuta canonica in
|
|
entrambe le braccia — §6 verifica che il segno non dipenda da lei.
|
|
|
|
PRESENTAZIONE. Lo Sharpe e' invariante alla scala, maxDD e CAGR no: ogni variante e' riportata
|
|
anche a **ISO-VOL** (costante che pareggia la deviazione standard piena a quella del baseline).
|
|
E' una scelta di presentazione, post-hoc e dichiarata; non cambia lo Sharpe, che e' cio' che
|
|
decide. Il null del de-levering (`k_iso_dd`, importato da r0822_sol_leg, non riscritto) e'
|
|
calcolato per ogni variante che riduce il DD.
|
|
|
|
USO: nice -n 19 timeout 900 uv run python scripts/research/r0822_vol_size.py
|
|
[--every K] 1 offset ogni K dei 23 (K=1 = tutti; pilota: --every 8)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
|
|
|
|
import altlib as A # noqa: E402
|
|
import r0702_anchor_skh01 as R # noqa: E402 (run_asset/resample_off riusati)
|
|
import r0702_tp01_offset as TO # noqa: E402 (TP01 per ancora + target 1h)
|
|
from r0822_sol_leg import k_iso_dd # noqa: E402 (null de-levering, non riscritto)
|
|
from src.portfolio.portfolio import ( # noqa: E402
|
|
HOLDOUT, combine_outer, weights_tilt_null,
|
|
)
|
|
|
|
ASSETS = ("BTC", "ETH")
|
|
OFFSETS_FULL = tuple(range(0, 690, 30)) # 23 a priori: griglia dell'audit 02/07
|
|
LTF_MIN = 230
|
|
BARS_YR = 365.25 * 24 * 60 / LTF_MIN
|
|
DPY = 365.25
|
|
FEE_RT = 0.001
|
|
W_TP, W_SKH = 0.75, 0.25 # libro live Deribit
|
|
CAPITAL = 635.0 # conto reale dichiarato nel brief
|
|
MIN_ORDER = 5.0
|
|
CAP_ASSET = 0.5 # cap $318/asset su equity $635
|
|
SIG_WARM = 2000 # barre 230m (~1 anno) prima di dimensionare
|
|
VT_WARM = 250 # giorni prima di dimensionare su vol di strategia
|
|
|
|
|
|
# --------------------------------------------------------------------------- metriche
|
|
def sh(s) -> float:
|
|
r = np.asarray(pd.Series(s).dropna().values, float)
|
|
return float(r.mean() / r.std() * np.sqrt(DPY)) if len(r) > 2 and r.std() > 0 else 0.0
|
|
|
|
|
|
def maxdd(s) -> float:
|
|
r = np.asarray(pd.Series(s).dropna().values, float)
|
|
if len(r) < 2:
|
|
return 0.0
|
|
eq = np.cumprod(1.0 + r)
|
|
pk = np.maximum.accumulate(eq)
|
|
return float(np.max((pk - eq) / pk))
|
|
|
|
|
|
def cagr(s) -> float:
|
|
r = np.asarray(pd.Series(s).dropna().values, float)
|
|
if len(r) < 2:
|
|
return 0.0
|
|
eq = float(np.prod(1.0 + r))
|
|
yrs = len(r) / DPY
|
|
return float(eq ** (1 / yrs) - 1) if eq > 0 and yrs > 0 else -1.0
|
|
|
|
|
|
def vol(s) -> float:
|
|
r = np.asarray(pd.Series(s).dropna().values, float)
|
|
return float(r.std() * np.sqrt(DPY)) if len(r) > 2 else 0.0
|
|
|
|
|
|
def hold(s: pd.Series) -> pd.Series:
|
|
return s[s.index >= HOLDOUT]
|
|
|
|
|
|
def ins(s: pd.Series) -> pd.Series:
|
|
return s[s.index < HOLDOUT]
|
|
|
|
|
|
# --------------------------------------------------------------------------- estrazione trade
|
|
def extract(asset: str, off: int) -> dict:
|
|
"""Trade + contesto per (asset, offset). Replica il loop di `backtest_signals` con
|
|
leverage=1: stessa detection, stesso non-overlap, stessi fill al livello. Cio' che
|
|
aggiunge e' la SIZE per-trade, che l'harness non espone (ha un solo `position_size`
|
|
scalare). La §0 verifica bit-exact che con size=1 si ritrovi la serie ufficiale."""
|
|
_, _, ltf, ent = R.run_asset(asset, off)
|
|
c = ltf["close"].values.astype(float)
|
|
h = ltf["high"].values.astype(float)
|
|
lo = ltf["low"].values.astype(float)
|
|
n = len(c)
|
|
|
|
i_ent, i_ex, dirs, nets, stops = [], [], [], [], []
|
|
busy = -1
|
|
for i in range(n):
|
|
e = ent[i] if i < len(ent) else None
|
|
if e is None or e.get("dir", 0) == 0 or i <= busy:
|
|
continue
|
|
d = int(e["dir"])
|
|
entry = c[i]
|
|
tp, sl = e.get("tp"), e.get("sl")
|
|
mb = int(e.get("max_bars") or 24)
|
|
ex_i = min(i + mb, n - 1)
|
|
ex_p = c[ex_i]
|
|
for j in range(i + 1, min(i + mb + 1, n)):
|
|
hit_sl = sl is not None and ((d == 1 and lo[j] <= sl) or (d == -1 and h[j] >= sl))
|
|
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and lo[j] <= tp))
|
|
if hit_sl:
|
|
ex_p, ex_i = sl, j
|
|
break
|
|
if hit_tp:
|
|
ex_p, ex_i = tp, j
|
|
break
|
|
ex_p, ex_i = c[j], j
|
|
i_ent.append(i); i_ex.append(ex_i); dirs.append(d)
|
|
nets.append((ex_p - entry) / entry * d - FEE_RT)
|
|
stops.append(abs(float(sl) - entry) / entry) # DERIVATO dal segnale, non ridichiarato
|
|
busy = ex_i
|
|
|
|
idxE = np.asarray(i_ent, int)
|
|
lr = A.log_returns(c)
|
|
sig, sigref = {}, {}
|
|
for w in (30, 90):
|
|
sv = A.realized_vol(lr, w, BARS_YR) # finestra che termina in i: causale
|
|
sig[w] = sv[idxE] if len(idxE) else np.array([])
|
|
sigref[w] = _expanding_median_at(sv, idxE)
|
|
idx = pd.DatetimeIndex(pd.to_datetime(ltf["timestamp"].values, unit="ms", utc=True))
|
|
out = dict(
|
|
n=n, idx=idx, i_ent=idxE, i_ex=np.asarray(i_ex, int), dir=np.asarray(dirs, int),
|
|
net=np.asarray(nets, float), stop=np.asarray(stops, float), sig=sig, sigref=sigref,
|
|
ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000,
|
|
day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"),
|
|
)
|
|
out["base_daily"] = equity_daily(out, size_flat(out)) # riusata da VTL/§9: calcolarla 1 volta
|
|
R._CACHE.clear() # ent = 18k dict per chiave: non accumulare
|
|
return out
|
|
|
|
|
|
def _expanding_median_at(v: np.ndarray, idxE: np.ndarray) -> np.ndarray:
|
|
"""Mediana ESPANDENTE di v fino a i incluso, valutata solo agli indici d'ingresso.
|
|
Causale per costruzione. NaN prima del warm-up."""
|
|
out = np.full(len(idxE), np.nan)
|
|
for k, i in enumerate(idxE):
|
|
if i < SIG_WARM:
|
|
continue
|
|
x = v[: i + 1]
|
|
x = x[np.isfinite(x)]
|
|
if len(x) >= SIG_WARM // 2:
|
|
out[k] = float(np.median(x))
|
|
return out
|
|
|
|
|
|
def equity_daily(ex: dict, sizes: np.ndarray) -> pd.Series:
|
|
"""Serie giornaliera dei rendimenti dalla tabella trade, con size per-trade. Convenzione
|
|
IDENTICA a `backtest_signals` + `_skyhook_returns`: equity a gradino a fine trade,
|
|
resample 1D last, ffill, pct_change."""
|
|
n = ex["n"]
|
|
eq = np.full(n, 1000.0, float)
|
|
cap = 1000.0
|
|
for k in range(len(ex["i_ent"])):
|
|
cap += cap * float(sizes[k]) * float(ex["net"][k])
|
|
cap = max(cap, 1.0)
|
|
eq[ex["i_ent"][k]: ex["i_ex"][k] + 1] = cap
|
|
for k in range(1, n): # stessi due riempimenti dell'harness
|
|
if eq[k] == 1000.0 and eq[k - 1] != 1000.0:
|
|
eq[k] = eq[k - 1]
|
|
last = 1000.0
|
|
for k in range(n):
|
|
if eq[k] != last and eq[k] != 1000.0:
|
|
last = eq[k]
|
|
else:
|
|
eq[k] = last
|
|
return pd.Series(eq, index=ex["idx"]).resample("1D").last().ffill().pct_change().dropna()
|
|
|
|
|
|
def leg_daily(exs: dict, sizes: dict) -> pd.Series:
|
|
"""Sleeve SKH01 50/50 BTC+ETH (convenzione di `_skyhook_returns`: inner join)."""
|
|
ser = {a: equity_daily(exs[a], sizes[a]) for a in ASSETS}
|
|
J = pd.concat(ser, axis=1, join="inner").fillna(0.0)
|
|
return pd.Series(0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values, index=J.index)
|
|
|
|
|
|
# --------------------------------------------------------------------------- famiglie di size
|
|
def size_flat(ex: dict) -> np.ndarray:
|
|
return np.ones(len(ex["i_ent"]))
|
|
|
|
|
|
def size_iv(p: float, w: int, cap: float):
|
|
"""INVERSE-VOL per-trade sulla vol dell'ASSET: size = clip((sig_rif/sig_ingresso)^p, 1/cap, cap).
|
|
sig_rif = mediana ESPANDENTE causale -> size media ~1: la variante e' neutra alla leva,
|
|
quindi qualunque effetto sullo Sharpe e' FORMA, non de-levering."""
|
|
def f(ex: dict) -> np.ndarray:
|
|
s, r = ex["sig"][w], ex["sigref"][w]
|
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
raw = np.where((s > 0) & np.isfinite(r), (r / np.maximum(s, 1e-12)) ** p, 1.0)
|
|
return np.clip(np.nan_to_num(raw, nan=1.0, posinf=cap, neginf=1.0), 1.0 / cap, cap)
|
|
return f
|
|
|
|
|
|
def size_risk(p: float):
|
|
"""RISCHIO COSTANTE PER TRADE: size ~ 1/distanza-dallo-stop. Lo stop di SKH01 e' una
|
|
percentuale FISSA (4% long / 2% short) -> questa famiglia e' di fatto 'gli short pesano
|
|
2x i long'. La costante e' irrilevante (lo Sharpe e' scale-invariante)."""
|
|
def f(ex: dict) -> np.ndarray:
|
|
st = np.maximum(ex["stop"], 1e-9)
|
|
return np.where(ex["stop"] > 0, (0.04 / st) ** p, 1.0)
|
|
return f
|
|
|
|
|
|
def _rv_daily(s: pd.Series, w: int, active_only: bool) -> pd.Series:
|
|
"""Vol realizzata trailing di una serie GIORNALIERA, SFASATA di 1 giorno (causale al giorno t).
|
|
active_only=True la calcola sulle sole barre ATTIVE e la riporta sulla griglia di calendario:
|
|
SKH01 e' ~88% di zeri, e la versione 'tutte le barre' misura l'ATTIVITA', non il rischio."""
|
|
if not active_only:
|
|
return (s.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)).shift(1)
|
|
act = s[s != 0.0]
|
|
rv = act.rolling(w, min_periods=max(5, w // 3)).std() * np.sqrt(DPY)
|
|
return rv.reindex(s.index).ffill().shift(1)
|
|
|
|
|
|
def leverage_series(base: pd.Series, tv: float | None, w: int, cap: float,
|
|
active_only: bool = False) -> pd.Series:
|
|
"""L_t da una serie giornaliera di riferimento. tv=None -> versione NEUTRA ALLA LEVA
|
|
(rif = mediana espandente causale di rv), tv=float -> vol-target assoluto."""
|
|
rv = _rv_daily(base, w, active_only).replace(0.0, np.nan)
|
|
if tv is None:
|
|
ref = rv.expanding(min_periods=VT_WARM).median()
|
|
L = (ref / rv).clip(lower=1.0 / cap, upper=cap)
|
|
else:
|
|
L = (tv / rv).clip(lower=0.0, upper=cap)
|
|
return L.fillna(1.0)
|
|
|
|
|
|
def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray:
|
|
"""La size di ogni trade = L al giorno d'INGRESSO. E' la versione CAUSALE del vol-target su
|
|
uno sleeve a equity a gradino: L al giorno di CHIUSURA non era nota all'ingresso."""
|
|
if not len(ex["i_ent"]):
|
|
return np.array([])
|
|
v = L.reindex(ex["day_ent"]).values.astype(float)
|
|
return np.nan_to_num(v, nan=1.0)
|
|
|
|
|
|
# --------------------------------------------------------------------------- libro
|
|
def book(tp: pd.Series, skh: pd.Series) -> pd.Series:
|
|
return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH})
|
|
|
|
|
|
def iso_vol(s: pd.Series, ref: pd.Series) -> pd.Series:
|
|
v = vol(s)
|
|
return s * (vol(ref) / v) if v > 0 else s
|
|
|
|
|
|
HEAD = (f" {'variante':<28}{'ShFULL':>8}{'ShHOLD':>9}{'ShIS':>8}"
|
|
f"{'vol':>9}{'maxDD':>9}{'DDisovol':>10}{'CAGRiso':>9}{'size med':>10}")
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--every", type=int, default=1)
|
|
args = ap.parse_args()
|
|
offs = OFFSETS_FULL[:: max(1, args.every)]
|
|
t0 = time.time()
|
|
|
|
print("=" * 112)
|
|
print(" r0822 VOL-SIZE — controllo del rischio su SKH01 (Q1) e vol-target di LIBRO (Q2)")
|
|
print("=" * 112)
|
|
print(f" ancore: {len(offs)}/{len(OFFSETS_FULL)} offset a priori {list(offs)}")
|
|
print(" lente: path CANONICO. Ancora TP01: canonica in ENTRAMBE le braccia (§6 la varia).")
|
|
|
|
print("\n estrazione tabelle trade ...", flush=True)
|
|
EX = {o: {a: extract(a, o) for a in ASSETS} for o in offs}
|
|
print(f" fatto in {time.time()-t0:.0f}s — trade medi/ancora: " +
|
|
", ".join(f"{a} {int(np.mean([len(EX[o][a]['i_ent']) for o in offs]))}" for a in ASSETS))
|
|
|
|
# ------------------------------------------------------------------ §0 SANITY
|
|
print("\n" + "-" * 112)
|
|
print(" 0. SANITY — con size=1 la ricomposizione DEVE riprodurre la serie ufficiale bit-exact")
|
|
print(" (se non e' 0, ogni numero sotto e' di un'altra strategia)")
|
|
print("-" * 112)
|
|
worst = 0.0
|
|
for o in offs[: min(3, len(offs))]:
|
|
for a in ASSETS:
|
|
mine = equity_daily(EX[o][a], size_flat(EX[o][a]))
|
|
ref = R.run_asset(a, o)[0]
|
|
R._CACHE.clear()
|
|
assert len(mine) == len(ref), f"len {len(mine)} vs {len(ref)} ({a},{o})"
|
|
d = float(np.max(np.abs(mine.values - ref.values)))
|
|
worst = max(worst, d)
|
|
print(f" {a} off{o:>3}: max|dif| = {d:.2e} ({len(mine)} giorni)")
|
|
assert worst < 1e-15, f"ricomposizione NON bit-exact: {worst:.2e}"
|
|
if 0 in EX:
|
|
from src.portfolio.sleeves import _skyhook_returns
|
|
s0 = leg_daily(EX[0], {a: size_flat(EX[0][a]) for a in ASSETS})
|
|
d0 = float(np.max(np.abs(s0.values - _skyhook_returns().values)))
|
|
print(f" sleeve 50/50 off0 vs sleeves._skyhook_returns(): max|dif| = {d0:.2e}")
|
|
assert d0 < 1e-15
|
|
|
|
# ------------------------------------------------------------------ §1 CAUSALITA'
|
|
print("\n" + "-" * 112)
|
|
print(" 1. CAUSALITA' — ogni size ricalcolata TRONCANDO i dati alla barra d'ingresso")
|
|
print("-" * 112)
|
|
ex = EX[offs[0]]["BTC"]
|
|
_, _, ltf, _ = R.run_asset("BTC", offs[0])
|
|
c_full = ltf["close"].values.astype(float)
|
|
R._CACHE.clear()
|
|
full_sz = size_iv(1.0, 30, 3.0)(ex)
|
|
sel = [k for k in range(len(ex["i_ent"])) if ex["i_ent"][k] >= SIG_WARM][:40]
|
|
bad = 0
|
|
for k in sel:
|
|
i = ex["i_ent"][k]
|
|
sv_t = A.realized_vol(A.log_returns(c_full[: i + 1]), 30, BARS_YR)
|
|
s_t = sv_t[i]
|
|
r_t = float(np.median(sv_t[np.isfinite(sv_t)]))
|
|
sz_t = np.clip((r_t / s_t) if s_t > 0 else 1.0, 1 / 3.0, 3.0)
|
|
bad += int(abs(sz_t - full_sz[k]) > 1e-9)
|
|
print(f" IV: {len(sel)-bad}/{len(sel)} size identiche a dati troncati (divergenze {bad})")
|
|
assert bad == 0, "la size IV usa dati futuri"
|
|
lag = np.array([(ex["idx"][ex["i_ex"][k]] - ex["idx"][ex["i_ent"][k]]).total_seconds() / 86400
|
|
for k in range(len(ex["i_ent"]))])
|
|
print(f" durata dei trade (giorni): mediana {np.median(lag):.2f}, p90 {np.percentile(lag,90):.2f}, "
|
|
f"max {lag.max():.2f} <- e' l'ampiezza del look-ahead della versione NAIVE (§4)")
|
|
|
|
# ------------------------------------------------------------------ griglia
|
|
SZ: dict = {}
|
|
for p in (0.5, 1.0):
|
|
for w in (30, 90):
|
|
for cp in (2.0, 4.0):
|
|
SZ[f"IV p{p} w{w} cap{cp:.0f}"] = size_iv(p, w, cp)
|
|
for p in (0.5, 1.0):
|
|
SZ[f"RISK p{p}"] = size_risk(p)
|
|
VTL = {} # vol-target sulla vol di STRATEGIA, causale
|
|
for w in (30, 90):
|
|
VTL[f"VTL neutro w{w}"] = dict(tv=None, w=w, cap=3.0, active_only=False)
|
|
VTL[f"VTL tv20 w{w}"] = dict(tv=0.20, w=w, cap=3.0, active_only=False)
|
|
VTL["VTL neutro w90 attive"] = dict(tv=None, w=90, cap=3.0, active_only=True)
|
|
VTL["VTL tv20 w90 attive"] = dict(tv=0.20, w=90, cap=3.0, active_only=True)
|
|
BVT = {} # Q2: vol-target del LIBRO
|
|
for w in (30, 90, 180):
|
|
for cp in (2.0, 3.0):
|
|
BVT[f"BOOKVT w{w} cap{cp:.0f}"] = dict(tv=0.10, w=w, cap=cp)
|
|
NAIVE = {f"NAIVE-VT w{w}": dict(tv=0.20, w=w, cap=3.0, active_only=False) for w in (30, 90)}
|
|
ALL = list(SZ) + list(VTL) + list(BVT) + list(NAIVE)
|
|
print(f"\n GRIGLIA DICHIARATA: {len(SZ)} size per-trade + {len(VTL)} vol-target di gamba + "
|
|
f"{len(BVT)} vol-target di libro + {len(NAIVE)} controlli non-causali = {len(ALL)} celle "
|
|
f"(+ baseline).")
|
|
|
|
# ------------------------------------------------------------------ serie
|
|
TP = A.tp01_baseline_daily()
|
|
SLE = {"BASE": {}}
|
|
BK = {"BASE": {}}
|
|
MTP = {"BASE": {o: 1.0 for o in offs}} # rapporto di vol della gamba TP01
|
|
SIZEMED = {}
|
|
for o in offs:
|
|
SLE["BASE"][o] = leg_daily(EX[o], {a: size_flat(EX[o][a]) for a in ASSETS})
|
|
BK["BASE"][o] = book(TP, SLE["BASE"][o])
|
|
for nm, fn in SZ.items():
|
|
SLE[nm] = {o: leg_daily(EX[o], {a: fn(EX[o][a]) for a in ASSETS}) for o in offs}
|
|
BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs}
|
|
MTP[nm] = {o: 1.0 for o in offs}
|
|
SIZEMED[nm] = float(np.median(np.concatenate([fn(EX[offs[0]][a]) for a in ASSETS])))
|
|
for nm, sp in VTL.items():
|
|
SLE[nm], BK[nm], MTP[nm] = {}, {}, {}
|
|
for o in offs:
|
|
sz = {}
|
|
for a in ASSETS:
|
|
sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp))
|
|
SLE[nm][o] = leg_daily(EX[o], sz)
|
|
BK[nm][o] = book(TP, SLE[nm][o])
|
|
MTP[nm][o] = 1.0
|
|
if o == offs[0]:
|
|
SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values()))))
|
|
for nm, sp in BVT.items():
|
|
SLE[nm], BK[nm], MTP[nm] = {}, {}, {}
|
|
for o in offs:
|
|
L = leverage_series(BK["BASE"][o], sp["tv"], sp["w"], sp["cap"])
|
|
sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS}
|
|
SLE[nm][o] = leg_daily(EX[o], sz)
|
|
tp_s = TP * L.reindex(TP.index).ffill().fillna(1.0)
|
|
BK[nm][o] = book(tp_s, SLE[nm][o])
|
|
MTP[nm][o] = vol(tp_s) / vol(TP)
|
|
if o == offs[0]:
|
|
SIZEMED[nm] = float(np.median(np.concatenate(list(sz.values()))))
|
|
for nm, sp in NAIVE.items(): # controllo NON causale (vedi §4)
|
|
SLE[nm] = {o: SLE["BASE"][o] * leverage_series(SLE["BASE"][o], **sp) for o in offs}
|
|
BK[nm] = {o: book(TP, SLE[nm][o]) for o in offs}
|
|
MTP[nm] = {o: 1.0 for o in offs}
|
|
SIZEMED[nm] = float(np.median(leverage_series(SLE["BASE"][offs[0]], **sp).values))
|
|
|
|
# CONTROLLO ISO-PESO: ogni variante che alza la vol della gamba SKH alza il suo peso EFFETTIVO
|
|
# nel libro — e "alzare il peso di SKH01" e' gia' stato misurato (26/07: argmax w=0.35, ma
|
|
# `weights_tilt_null` FALLITO e proposta respinta). Quindi il confronto che conta NON e' vs il
|
|
# baseline nudo ma vs il baseline RI-SCALATO agli stessi rapporti di vol: cosi' resta solo la
|
|
# FORMA (quale trade pesa quanto). E' la regola dell'ISO-RISCHIO gia' codificata (25/07 §3).
|
|
CTRL, WEFF = {}, {}
|
|
for nm in ALL:
|
|
CTRL[nm], WEFF[nm] = {}, {}
|
|
for o in offs:
|
|
m_s = vol(SLE[nm][o]) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0
|
|
m_t = MTP[nm][o]
|
|
CTRL[nm][o] = book(TP * m_t, SLE["BASE"][o] * m_s)
|
|
WEFF[nm][o] = W_SKH * m_s / (W_TP * m_t + W_SKH * m_s)
|
|
|
|
# ------------------------------------------------------------------ §2 sleeve
|
|
can = offs[0]
|
|
print("\n" + "-" * 112)
|
|
print(f" 2. SLEEVE SKH01 all'ancora canonica (off {can}). 'DDisovol'/'CAGRiso' a vol pareggiata")
|
|
print(" col baseline (post-hoc, dichiarato: lo Sharpe non ne dipende).")
|
|
print("-" * 112)
|
|
print(HEAD)
|
|
base_s = SLE["BASE"][can]
|
|
|
|
def prow(nm, s, med=None):
|
|
iv = iso_vol(s, base_s)
|
|
m = f"{med:>10.2f}" if med is not None else f"{'-':>10}"
|
|
print(f" {nm:<28}{sh(s):>8.3f}{sh(hold(s)):>9.3f}{sh(ins(s)):>8.3f}{vol(s)*100:>8.1f}%"
|
|
f"{maxdd(s)*100:>8.1f}%{maxdd(iv)*100:>9.1f}%{cagr(iv)*100:>8.1f}%{m}")
|
|
|
|
prow("BASELINE (size fissa)", base_s, 1.00)
|
|
for nm in list(SZ) + list(VTL) + list(NAIVE):
|
|
prow(nm, SLE[nm][can], SIZEMED.get(nm))
|
|
|
|
# ------------------------------------------------------------------ §3 libro, banda appaiata
|
|
print("\n" + "-" * 112)
|
|
print(f" 3. LIBRO 75/25 — mediana delle DIFFERENZE APPAIATE su {len(offs)} ancore, + null del")
|
|
print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)")
|
|
print("-" * 112)
|
|
print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}"
|
|
f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}")
|
|
base_b = BK["BASE"]
|
|
RES = {}
|
|
for nm in ALL:
|
|
dF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o: base_b[o], offs, metric=sh)
|
|
dH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]), lambda o: hold(base_b[o]),
|
|
offs, metric=sh)
|
|
iF = A.anchor_luck_delta(lambda o, n=nm: BK[n][o], lambda o, n=nm: CTRL[n][o],
|
|
offs, metric=sh)
|
|
iH = A.anchor_luck_delta(lambda o, n=nm: hold(BK[n][o]),
|
|
lambda o, n=nm: hold(CTRL[n][o]), offs, metric=sh)
|
|
dd = float(np.median([maxdd(BK[nm][o]) - maxdd(base_b[o]) for o in offs])) * 100
|
|
if maxdd(BK[nm][can]) < maxdd(base_b[can]) - 1e-6:
|
|
k = k_iso_dd(base_b[can], BK[nm][can])
|
|
shk = sh(k * base_b[can])
|
|
ks = f"{k:.3f}"
|
|
vd = "DE-LEVERING" if shk >= sh(BK[nm][can]) else "oltre-null"
|
|
else:
|
|
ks, vd = "n/a", "DD peggiore"
|
|
we = float(np.median([WEFF[nm][o] for o in offs]))
|
|
RES[nm] = dict(dF=dF["median_paired"], nF=dF["n_positive"], dH=dH["median_paired"],
|
|
nH=dH["n_positive"], iF=iF["median_paired"], niF=iF["n_positive"],
|
|
iH=iH["median_paired"], niH=iH["n_positive"], dd=dd, k=ks,
|
|
verdict=vd, weff=we)
|
|
print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}"
|
|
f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}"
|
|
f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}"
|
|
f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}")
|
|
print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo")
|
|
print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:")
|
|
print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.")
|
|
med_F = float(np.median([sh(base_b[o]) for o in offs]))
|
|
med_H = float(np.median([sh(hold(base_b[o])) for o in offs]))
|
|
med_D = float(np.median([maxdd(base_b[o]) for o in offs]))
|
|
print(f"\n libro BASE, ancora canonica: ShFULL {sh(base_b[can]):.3f} / ShHOLD "
|
|
f"{sh(hold(base_b[can])):.3f} / maxDD {maxdd(base_b[can])*100:.1f}% / "
|
|
f"CAGR {cagr(base_b[can])*100:.1f}%")
|
|
print(f" libro BASE, stima ONESTA (mediana della banda): ShFULL {med_F:.3f} / "
|
|
f"ShHOLD {med_H:.3f} / maxDD {med_D*100:.1f}%")
|
|
|
|
# ------------------------------------------------------------------ §4 il difetto misurato
|
|
print("\n" + "-" * 112)
|
|
print(" 4. IL DIFETTO MISURATO — vol-target NAIVE (scala il giorno di CHIUSURA) vs CAUSALE")
|
|
print(" (size fissata all'INGRESSO). Stessa formula di L, stessa finestra: cambia solo")
|
|
print(" QUANDO la si applica. La differenza e' il valore del look-ahead di contabilita'.")
|
|
print("-" * 112)
|
|
print(f" {'coppia':<34}{'dShFULL naive':>15}{'dShFULL causale':>17}{'phantom':>10}")
|
|
for w in (30, 90):
|
|
n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}"
|
|
a1, a2 = RES[n_nm]["dF"], RES[c_nm]["dF"]
|
|
print(f" {'vol-target tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}")
|
|
for w in (30, 90):
|
|
n_nm, c_nm = f"NAIVE-VT w{w}", f"VTL tv20 w{w}"
|
|
a1, a2 = RES[n_nm]["dH"], RES[c_nm]["dH"]
|
|
print(f" {' (hold-out) tv20 w%d' % w:<34}{a1:>+15.3f}{a2:>+17.3f}{a1-a2:>+10.3f}")
|
|
|
|
# ------------------------------------------------------------------ §5 selezione + DSR
|
|
print("\n" + "-" * 112)
|
|
print(" 5. SELEZIONE AL BUIO (solo pre-2025, mediana di banda) + deflated-Sharpe")
|
|
print("-" * 112)
|
|
CAUSAL = [n for n in ALL if n not in NAIVE]
|
|
is_med = {n: float(np.median([sh(ins(BK[n][o])) for o in offs])) for n in CAUSAL}
|
|
ho_med = {n: float(np.median([sh(hold(BK[n][o])) for o in offs])) for n in CAUSAL}
|
|
fu_med = {n: float(np.median([sh(BK[n][o]) for o in offs])) for n in CAUSAL}
|
|
base_is = float(np.median([sh(ins(base_b[o])) for o in offs]))
|
|
print(f" baseline in-sample (mediana banda): {base_is:.3f}")
|
|
for n in sorted(CAUSAL, key=lambda x: -is_med[x])[:6]:
|
|
print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}")
|
|
best = max(CAUSAL, key=lambda x: is_med[x])
|
|
print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})")
|
|
iso_is = {n: float(np.median([sh(ins(BK[n][o])) - sh(ins(CTRL[n][o])) for o in offs]))
|
|
for n in CAUSAL}
|
|
best_iso = max(CAUSAL, key=lambda x: iso_is[x])
|
|
print(f" cella scelta AL BUIO **a ISO-PESO**: {best_iso} (dIS iso {iso_is[best_iso]:+.3f})")
|
|
print(f" la stessa cella a iso-peso: dShFULL {RES[best]['iF']:+.3f} "
|
|
f"({RES[best]['niF']}/{len(offs)}), dShHOLD {RES[best]['iH']:+.3f} "
|
|
f"({RES[best]['niH']}/{len(offs)})")
|
|
trials = [fu_med[n] for n in CAUSAL] + [med_F]
|
|
dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can])
|
|
dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can])
|
|
print(f" DSR candidato {dsr:.3f} / DSR BASELINE {dsr_b:.3f} (null max-Sharpe {sr0:.3f}, "
|
|
f"N={len(trials)})")
|
|
print(f" dispersione degli {len(trials)} trial: sd {np.std(trials, ddof=1):.4f} -> "
|
|
f"⚠️ il DSR e' QUASI VACUO su una famiglia di perturbazioni della stessa strategia")
|
|
print(" (penalizza in proporzione alla varianza fra i trial: qui il baseline stesso lo")
|
|
print(" passa). Il gate che decide e' la banda appaiata del §3, non questo.")
|
|
|
|
# ------------------------------------------------------------------ §6 ancora TP01
|
|
print("\n" + "-" * 112)
|
|
print(" 6. CONTROLLO — l'ancora di TP01 e' tenuta fissa: il segno del Delta dipende da lei?")
|
|
print("-" * 112)
|
|
cand6 = [best] + [n for n in (list(SZ)[:0] + ["RISK p1.0"] + list(BVT)[:2]) if n != best]
|
|
print(f" {'variante':<28}" + "".join(f"{'TP h=%d' % h:>12}" for h in (0, 8, 16)))
|
|
for nm in cand6:
|
|
cells = []
|
|
for h in (0, 8, 16):
|
|
tph = TO.port_daily_native(h) if h else TP
|
|
if nm in BVT:
|
|
arm = {}
|
|
for o in offs:
|
|
L = leverage_series(book(tph, SLE["BASE"][o]), BVT[nm]["tv"], BVT[nm]["w"],
|
|
BVT[nm]["cap"])
|
|
sz = {a: sizes_from_L(EX[o][a], L) for a in ASSETS}
|
|
arm[o] = book(tph * L.reindex(tph.index).ffill().fillna(1.0),
|
|
leg_daily(EX[o], sz))
|
|
else:
|
|
arm = {o: book(tph, SLE[nm][o]) for o in offs}
|
|
bse = {o: book(tph, SLE["BASE"][o]) for o in offs}
|
|
d = A.anchor_luck_delta(lambda o, x=arm: x[o], lambda o, y=bse: y[o], offs, metric=sh)
|
|
cells.append(d["median_paired"])
|
|
print(f" {nm:<28}" + "".join(f"{v:>+12.3f}" for v in cells))
|
|
|
|
# ------------------------------------------------------------------ §7 gate
|
|
print("\n" + "-" * 112)
|
|
print(" 7. GATE — marginal_vs_tp01 / implausible_sharpe / weights_tilt_null")
|
|
print("-" * 112)
|
|
best_sleeve = max([n for n in list(SZ) + list(VTL)], key=lambda x: is_med[x])
|
|
for nm in ("BASE", best_sleeve):
|
|
m = A.marginal_vs_tp01(SLE[nm][can])
|
|
b25 = m.get("blends", {}).get("w25", {})
|
|
print(f" marginal {nm:<26} {m.get('marginal_verdict')} corr {m.get('corr_full')} "
|
|
f"uplift w25 full {b25.get('uplift_full')} / hold {b25.get('uplift_hold')}")
|
|
nlos = int((EX[can]["BTC"]["net"] < 0).sum() + (EX[can]["ETH"]["net"] < 0).sum())
|
|
ntot = int(len(EX[can]["BTC"]["net"]) + len(EX[can]["ETH"]["net"]))
|
|
for nm in ("BASE", best_sleeve):
|
|
imp = A.implausible_sharpe(SLE[nm][can], n_trades=ntot, n_losing_trades=nlos)
|
|
print(f" implausible {nm:<25} implausible={imp['implausible']} "
|
|
f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive "
|
|
f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}")
|
|
cols = {"TP01": TP, "SKH01": SLE["BASE"][can]}
|
|
for nm in [best, best_sleeve] + list(BVT)[:2]:
|
|
m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can])
|
|
wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s}
|
|
g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp,
|
|
caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL))
|
|
print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} "
|
|
f"d_IS {g['delta_insample']:+.4f} d_HOLD {g['delta_hold']:+.4f} "
|
|
f"pctl {g['pctl_hold']:.1f} gate_pass={g['gate_pass']}")
|
|
print(" LIMITE DICHIARATO: il gate confronta vettori di pesi STATICI; della modulazione")
|
|
print(" cattura la sola componente di SCALA. La FORMA la giudica la banda appaiata (§3).")
|
|
|
|
# ------------------------------------------------------------------ §8 eseguibilita'
|
|
print("\n" + "-" * 112)
|
|
print(f" 8. ESEGUIBILITA' a ${CAPITAL:.0f} — min-order ${MIN_ORDER:.0f}, cap {CAP_ASSET:.0%}"
|
|
f" equity/asset, target NETTATO 0.75*TP01 + 0.25*SKH sul grid 1h (cio' che il cron manda)")
|
|
print("-" * 112)
|
|
print(f" {'configurazione':<28}{'Sh model':>10}{'Sh reale':>10}{'haircut':>9}"
|
|
f"{'ordini eseg':>13}{'sotto-min':>11}{'turnover/a':>12}")
|
|
todo = [("BASE (size fissa)", None), (best_sleeve, best_sleeve), (list(BVT)[0], list(BVT)[0])]
|
|
for label, nm in todo:
|
|
tm = tr = tu = 0.0
|
|
no = ns = 0
|
|
for a in ASSETS:
|
|
df1h = TO.get1h(a)
|
|
tgt = netted_target(a, EX[can][a], nm, SZ, VTL, BVT, SLE, TP, can, df1h)
|
|
ev = A.eval_weights_smallcap(df1h, tgt, capital=CAPITAL, min_order=MIN_ORDER)
|
|
tm += 0.5 * ev["modeled"]["sharpe"]
|
|
tr += 0.5 * ev["realistic"]["sharpe"]
|
|
tu += ev["executed_turnover_per_year"]
|
|
no += ev["n_executed_trades"]
|
|
dw = np.abs(np.diff(np.nan_to_num(tgt), prepend=0.0))
|
|
ns += int(((dw > 1e-12) & (dw * CAPITAL < MIN_ORDER)).sum())
|
|
print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}")
|
|
print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.")
|
|
|
|
# ------------------------------------------------------------------ §9 per anno + deriva
|
|
print("\n" + "-" * 112)
|
|
print(" 9. SCOMPOSIZIONE PER ANNO del dSharpe a ISO-PESO (regola codificata dopo SOL, 22/08:")
|
|
print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size")
|
|
print("-" * 112)
|
|
yrs = sorted({int(y) for y in BK["BASE"][can].index.year})
|
|
top = [best, best_iso] + [max(BVT, key=lambda x: RES[x]["iF"])]
|
|
top = list(dict.fromkeys(top))
|
|
print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs))
|
|
for nm in top:
|
|
cells = []
|
|
for y in yrs:
|
|
v = []
|
|
for o in offs:
|
|
a_ = BK[nm][o][BK[nm][o].index.year == y]
|
|
c_ = CTRL[nm][o][CTRL[nm][o].index.year == y]
|
|
if len(a_) > 20:
|
|
v.append(sh(a_) - sh(c_))
|
|
cells.append(float(np.median(v)) if v else float("nan"))
|
|
npos = int(np.nansum(np.asarray(cells) > 0))
|
|
print(f" {nm:<28}" + "".join(f"{c:>+8.2f}" for c in cells) +
|
|
f" anni positivi {npos}/{len(yrs)}")
|
|
print()
|
|
for nm in top:
|
|
if nm in SZ:
|
|
szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS])
|
|
dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS]))
|
|
elif nm in VTL:
|
|
szs, dys = [], []
|
|
for a in ASSETS:
|
|
szs.append(sizes_from_L(EX[can][a],
|
|
leverage_series(EX[can][a]["base_daily"], **VTL[nm])))
|
|
dys.append(EX[can][a]["day_ent"].values)
|
|
szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys))
|
|
else:
|
|
L = leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
|
|
szs, dys = [], []
|
|
for a in ASSETS:
|
|
szs.append(sizes_from_L(EX[can][a], L))
|
|
dys.append(EX[can][a]["day_ent"].values)
|
|
szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys))
|
|
s = pd.Series(szs, index=dys)
|
|
med = s.groupby(s.index.year).median()
|
|
rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan")
|
|
print(f" size mediana per anno — {nm:<22}" +
|
|
"".join(f"{med.get(y, float('nan')):>8.2f}" for y in yrs) +
|
|
f" corr col tempo {rho:+.2f}")
|
|
print(" ⚠️ una size che DERIVA col tempo non e' controllo del rischio: e' un tilt temporale")
|
|
print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il")
|
|
print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.")
|
|
|
|
print("\n" + "=" * 112)
|
|
print(f" fatto in {time.time()-t0:.0f}s")
|
|
print("=" * 112)
|
|
|
|
|
|
def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) -> np.ndarray:
|
|
"""Peso NETTO per asset sul grid 1h: 0.75*target TP01 + 0.25*posizione SKH (dir*size), poi
|
|
l'eventuale leva di libro sulla gamba TP01. E' cio' che il cron manda a Deribit come UNA
|
|
posizione netta sullo stesso strumento."""
|
|
tp = np.asarray(TO.hourly_target(asset, 0), float)
|
|
ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000
|
|
pos = np.zeros(len(ts1))
|
|
L1h = np.ones(len(ts1))
|
|
if nm is None:
|
|
sizes = size_flat(ex)
|
|
elif nm in SZ:
|
|
sizes = SZ[nm](ex)
|
|
elif nm in VTL:
|
|
sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
|
|
else: # BOOKVT
|
|
L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
|
|
sizes = sizes_from_L(ex, L)
|
|
idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True))
|
|
L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values
|
|
t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]]
|
|
for k in range(len(t_in)):
|
|
j0 = int(np.searchsorted(ts1, t_in[k], side="left"))
|
|
j1 = int(np.searchsorted(ts1, t_out[k], side="left"))
|
|
pos[j0:j1] = ex["dir"][k] * sizes[k]
|
|
return np.clip(W_TP * tp * L1h + W_SKH * pos, -CAP_ASSET, CAP_ASSET)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|