research(wave-0822): VOL-SIZE e' un LEAD — e un overlay giornaliero su equity a gradino e' look-ahead che nessun causality check vede
This commit is contained in:
@@ -383,11 +383,7 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame,
|
||||
contracts = list(wide.columns)
|
||||
tau = {c: (exps[c] - idx).total_seconds() / 3600.0 for c in contracts if c in exps}
|
||||
avail = {c: wide[c].notna().values for c in contracts}
|
||||
for c in contracts:
|
||||
if c not in exps:
|
||||
continue
|
||||
alive = avail[c] & (tau[c] >= roll_dte * 24)
|
||||
# front = il primo (per scadenza) ancora vivo; i contratti sono ordinati per exp
|
||||
# front = il primo (per scadenza) ancora vivo con dte >= roll_dte; back = il successivo
|
||||
order = [c for c in ok.contract if c in wide.columns]
|
||||
for i in range(n):
|
||||
picked = []
|
||||
@@ -462,70 +458,89 @@ def _z(x: pd.Series, win: int) -> pd.Series:
|
||||
return ((x - m) / s.replace(0, np.nan))
|
||||
|
||||
|
||||
def run_strategy(cv: pd.DataFrame, wide: pd.DataFrame, family: str, sig: str,
|
||||
win_h: int, thr: float, slip_bps: float,
|
||||
fee_side: float = FEE_SIDE) -> pd.Series:
|
||||
"""Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA.
|
||||
Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la
|
||||
variazione per contratto e' automaticamente piena => il roll paga il round-trip
|
||||
completo su entrambe le gambe. Nessuna serie di prezzi viene MAI incollata.
|
||||
Causalita': w deciso con dati <= t, TENUTO durante t -> t+1 (shift esplicito)."""
|
||||
idx = cv.index
|
||||
if family == "CAL":
|
||||
base = cv["slope"]
|
||||
else: # FvP: basis annualizzato del front
|
||||
base = cv["c_front"]
|
||||
def asset_ctx(wide: pd.DataFrame) -> dict:
|
||||
"""Precalcolo per asset: matrice dei rendimenti per contratto + mappa colonne.
|
||||
Fatto UNA volta e riusato da tutte le celle (altrimenti la griglia non sta nel budget)."""
|
||||
Wv = wide.values.astype(float)
|
||||
R = np.zeros_like(Wv)
|
||||
R[1:] = Wv[1:] / Wv[:-1] - 1.0
|
||||
R[~np.isfinite(R)] = 0.0
|
||||
return dict(R=R, cpos={c: k for k, c in enumerate(wide.columns)},
|
||||
ncol=Wv.shape[1], idx=wide.index)
|
||||
|
||||
|
||||
def pair_idx(cv: pd.DataFrame, ctx: dict) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Indici di colonna di front/back per ogni ora (-1 = coppia non disponibile)."""
|
||||
cp = ctx["cpos"]
|
||||
fi = np.array([cp.get(x, -1) for x in cv["front"].values], dtype=int)
|
||||
bi = np.array([cp.get(x, -1) for x in cv["back"].values], dtype=int)
|
||||
return fi, bi
|
||||
|
||||
|
||||
def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) -> np.ndarray:
|
||||
base = cv["slope"] if family == "CAL" else cv["c_front"]
|
||||
if sig == "static_long":
|
||||
w = pd.Series(1.0, index=idx)
|
||||
w = pd.Series(1.0, index=cv.index)
|
||||
elif sig == "static_short":
|
||||
w = pd.Series(-1.0, index=idx)
|
||||
elif sig == "carry": # segno del carry corrente (causale)
|
||||
w = -np.sign(base) # contango -> vendi il datato
|
||||
elif sig == "zrev":
|
||||
w = pd.Series(-1.0, index=cv.index)
|
||||
elif sig == "carry": # contango -> vendi il datato
|
||||
w = -np.sign(base)
|
||||
elif sig in ("zrev", "zmom"):
|
||||
z = _z(base, win_h)
|
||||
w = -np.tanh(z)
|
||||
w = w.where(z.abs() >= thr, 0.0)
|
||||
elif sig == "zmom":
|
||||
z = _z(base, win_h)
|
||||
w = np.tanh(z)
|
||||
w = (-np.tanh(z)) if sig == "zrev" else np.tanh(z)
|
||||
w = w.where(z.abs() >= thr, 0.0)
|
||||
else:
|
||||
raise ValueError(sig)
|
||||
w = pd.Series(np.asarray(w, float), index=idx).fillna(0.0).clip(-1, 1)
|
||||
return np.nan_to_num(np.asarray(w, float), nan=0.0).clip(-1, 1)
|
||||
|
||||
# --- posizioni per CONTRATTO ---
|
||||
cols = list(wide.columns)
|
||||
cpos = {c: k for k, c in enumerate(cols)}
|
||||
P = np.zeros((len(idx), len(cols)))
|
||||
fr = cv["front"].values; bk = cv["back"].values
|
||||
wv = w.values
|
||||
for i in range(len(idx)):
|
||||
if fr[i] == "" or bk[i] == "":
|
||||
continue
|
||||
if family == "CAL": # long spread = long back / short front
|
||||
P[i, cpos[bk[i]]] = wv[i]
|
||||
P[i, cpos[fr[i]]] = -wv[i]
|
||||
else: # FvP: gamba datata; il perp e' a parte
|
||||
P[i, cpos[fr[i]]] = wv[i]
|
||||
Pd = pd.DataFrame(P, index=idx, columns=cols)
|
||||
|
||||
held = Pd.shift(1).fillna(0.0) # decisa a t, TENUTA in t->t+1
|
||||
R = wide.reindex(idx).pct_change().replace([np.inf, -np.inf], np.nan).fillna(0.0)
|
||||
gross = (held * R).sum(axis=1)
|
||||
def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
|
||||
win_h: int, thr: float, slip_bps: float,
|
||||
fee_side: float = FEE_SIDE, fi=None, bi=None) -> pd.Series:
|
||||
"""Ritorno ORARIO netto per $1 di nozionale LORDO PER GAMBA.
|
||||
|
||||
Le posizioni vivono in spazio CONTRATTO: quando la coppia cambia (roll) la variazione
|
||||
per contratto e' automaticamente piena => il roll paga il round-trip completo su
|
||||
entrambe le gambe. Nessuna serie di prezzi viene MAI incollata: si incollano solo i
|
||||
RENDIMENTI, che e' l'unico posto in cui e' lecito.
|
||||
|
||||
Causalita': w e' deciso con dati <= t e TENUTO durante t -> t+1 (shift esplicito qui,
|
||||
non nel segnale) => non si puo' sbagliare per distrazione a valle."""
|
||||
idx = ctx["idx"]
|
||||
n, ncol = len(idx), ctx["ncol"]
|
||||
if fi is None or bi is None:
|
||||
fi, bi = pair_idx(cv, ctx)
|
||||
w = signal_w(cv, family, sig, win_h, thr)
|
||||
|
||||
P = np.zeros((n, ncol))
|
||||
rows = np.arange(n)
|
||||
if family == "CAL":
|
||||
m = (fi >= 0) & (bi >= 0)
|
||||
P[rows[m], bi[m]] = w[m] # long spread = long back
|
||||
P[rows[m], fi[m]] = -w[m] # / short front
|
||||
else: # FvP: solo la gamba datata
|
||||
m = fi >= 0
|
||||
P[rows[m], fi[m]] = w[m]
|
||||
|
||||
held = np.zeros_like(P)
|
||||
held[1:] = P[:-1] # deciso a t, tenuto in t->t+1
|
||||
gross = (held * ctx["R"]).sum(axis=1)
|
||||
|
||||
dP = np.zeros_like(P)
|
||||
dP[1:] = P[1:] - P[:-1]
|
||||
dP[0] = P[0]
|
||||
turn = np.abs(dP).sum(axis=1)
|
||||
|
||||
if family == "FvP":
|
||||
# gamba perp: nozionale opposto alla gamba datata + funding pagato/incassato
|
||||
perp_r = pd.Series(cv["index"].pct_change().fillna(0.0).values, index=idx)
|
||||
wheld = held.sum(axis=1) # = w shiftato (una sola gamba datata)
|
||||
gross = gross - wheld * perp_r - wheld * (-1.0) * cv["f1h"].fillna(0.0)
|
||||
# long datato (w>0) => short perp => INCASSA funding se f>0
|
||||
turn = (Pd.diff().abs().sum(axis=1) * 2.0) # datato + perp
|
||||
else:
|
||||
turn = Pd.diff().abs().sum(axis=1)
|
||||
perp_r = np.nan_to_num(cv["index"].pct_change().values, nan=0.0)
|
||||
f1h = np.nan_to_num(cv["f1h"].values, nan=0.0)
|
||||
wheld = held.sum(axis=1) # una sola gamba datata
|
||||
# long datato => short perp: guadagna -w*perp_r e INCASSA w*funding
|
||||
gross = gross - wheld * perp_r + wheld * f1h
|
||||
turn = turn * 2.0 # datato + gamba perp
|
||||
|
||||
cost = turn * (fee_side + slip_bps * 1e-4)
|
||||
return (gross - cost).fillna(0.0)
|
||||
return pd.Series(gross - cost, index=idx).fillna(0.0)
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
@@ -649,14 +664,18 @@ def main() -> None:
|
||||
print(f" = {len(cells)} celle valutate (le varianti statiche non usano W/soglia:"
|
||||
f" le conto lo stesso, al rialzo)")
|
||||
|
||||
ctxs = {a: asset_ctx(panels[a]) for a in ASSETS}
|
||||
cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl)
|
||||
for a in ASSETS for rl in ROLLS}
|
||||
pidx = {(a, rl): pair_idx(cvcache[(a, rl)], ctxs[a]) for a in ASSETS for rl in ROLLS}
|
||||
|
||||
rows = []
|
||||
for (fam, sig, win, rl, th) in cells:
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
h = run_strategy(cvcache[(a, rl)], panels[a], fam, sig, win, th, slip_est)
|
||||
f_i, b_i = pidx[(a, rl)]
|
||||
h = run_strategy(cvcache[(a, rl)], ctxs[a], fam, sig, win, th, slip_est,
|
||||
fi=f_i, bi=b_i)
|
||||
hs.append(h)
|
||||
J = pd.concat(hs, axis=1).fillna(0.0)
|
||||
comb = J.mean(axis=1) # 50/50 BTC+ETH, come il resto del progetto
|
||||
@@ -727,8 +746,9 @@ def main() -> None:
|
||||
def by_off(off):
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
cv = cvcache[(a, int(b.roll))]
|
||||
h = run_strategy(cv, panels[a], b.family, b.sig, int(b.win), float(b.thr), slip_est)
|
||||
f_i, b_i = pidx[(a, int(b.roll))]
|
||||
h = run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family, b.sig,
|
||||
int(b.win), float(b.thr), slip_est, fi=f_i, bi=b_i)
|
||||
hh = h.copy()
|
||||
mask = (hh.index.hour != off)
|
||||
hh[mask] = 0.0 # decide/agisce solo a quell'ora
|
||||
@@ -736,9 +756,13 @@ def main() -> None:
|
||||
return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))
|
||||
try:
|
||||
ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0)
|
||||
print(f" canonica {ab.get('canonical'):+.3f} MEDIANA(onesta) {ab.get('median'):+.3f} "
|
||||
f"banda [{ab.get('p10'):+.3f},{ab.get('p90'):+.3f}] "
|
||||
f"frazione>0 {ab.get('frac_positive'):.2f} gate_pass={ab.get('gate_pass')}")
|
||||
if "median" in ab:
|
||||
print(f" canonica {ab['canonical']:+.3f} (pctl {ab['canonical_pctl']*100:.0f}°) "
|
||||
f"MEDIANA(stima onesta) {ab['median']:+.3f} banda [{ab['lo']:+.3f},{ab['hi']:+.3f}] "
|
||||
f"frazione>0 {ab['frac_positive']:.2f} fortuna {ab['luck']:+.3f} "
|
||||
f"gate_pass={ab['gate_pass']}")
|
||||
else:
|
||||
print(f" {ab}")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f" NON GIRATO ({type(exc).__name__}: {exc})")
|
||||
|
||||
@@ -754,8 +778,9 @@ def main() -> None:
|
||||
for sl in [0.0, 1.0, 2.5, 5.0, 7.5, 10.0, 20.0]:
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
hs.append(run_strategy(cvcache[(a, int(b.roll))], panels[a], b.family,
|
||||
b.sig, int(b.win), float(b.thr), sl))
|
||||
f_i, b_i = pidx[(a, int(b.roll))]
|
||||
hs.append(run_strategy(cvcache[(a, int(b.roll))], ctxs[a], b.family,
|
||||
b.sig, int(b.win), float(b.thr), sl, fi=f_i, bi=b_i))
|
||||
s2 = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
|
||||
mark = " <= stima di oggi" if abs(sl - slip_est) < 0.6 else ""
|
||||
print(f" mezzo-spread {sl:5.1f} bps/lato -> Sharpe {s2['sharpe']:+6.2f} "
|
||||
|
||||
Reference in New Issue
Block a user