research(wave-0822): XSR-REPRO trova un difetto di produzione (i monitor forward registrano ~41 min/giorno); SKEW scompone il f di VRP01 — il 42% e' struttura a termine
This commit is contained in:
@@ -283,24 +283,29 @@ def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFr
|
||||
if b in fb.index:
|
||||
print(f" {b:9s} flat={fb.loc[b,'flat']*100:5.1f}% barre={int(fb.loc[b,'n']):,}")
|
||||
|
||||
# (b) volume orario mediano per bucket (il flat da solo non basta)
|
||||
# (b) volume orario per bucket (il flat da solo non basta).
|
||||
# ATTENZIONE: il `volume` di get_tradingview_chart_data e' in VALUTA BASE (BTC/ETH),
|
||||
# non in USD -> va moltiplicato per il prezzo, o si sbaglia di ~5 ordini di grandezza.
|
||||
vrows = []
|
||||
for nm in vwide.columns:
|
||||
exp = ok.loc[ok.contract == nm, "exp"]
|
||||
if exp.empty:
|
||||
continue
|
||||
s = vwide[nm].dropna()
|
||||
dte = (exp.iloc[0] - s.index).total_seconds() / 86400.0
|
||||
v = vwide[nm].dropna()
|
||||
px = wide[nm].reindex(v.index)
|
||||
usd = (v * px).dropna() # <- conversione in USD
|
||||
dte = (exp.iloc[0] - usd.index).total_seconds() / 86400.0
|
||||
for lo, hi, lab in [(0, 30, "0-30g"), (30, 90, "30-90g"),
|
||||
(90, 180, "90-180g"), (180, 400, ">180g")]:
|
||||
m = (dte >= lo) & (dte < hi)
|
||||
if m.sum() > 50:
|
||||
vrows.append(dict(bucket=lab, med=float(s[m].median()), zero=float((s[m] == 0).mean())))
|
||||
mm = (dte >= lo) & (dte < hi)
|
||||
if mm.sum() > 50:
|
||||
vrows.append(dict(bucket=lab, med=float(usd[mm].median()),
|
||||
zero=float((usd[mm] == 0).mean())))
|
||||
vb = pd.DataFrame(vrows).groupby("bucket").agg(med=("med", "median"), zero=("zero", "mean"))
|
||||
print(" volume orario mediano (USD di nozionale) e quota ore a volume ZERO:")
|
||||
print(" volume orario mediano in USD (volume base x prezzo) e quota ore a volume ZERO:")
|
||||
for b in order:
|
||||
if b in vb.index:
|
||||
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f} ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
|
||||
print(f" {b:9s} mediano=${vb.loc[b,'med']:>12,.0f}/ora ore a vol 0 = {vb.loc[b,'zero']*100:5.1f}%")
|
||||
|
||||
# (c) accordo con l'INDICE alla scadenza: il datato DEVE convergere
|
||||
conv = []
|
||||
@@ -385,34 +390,36 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame,
|
||||
ok = meta[meta.status == "ok"].sort_values("exp")
|
||||
exps = {r.contract: r.exp for r in ok.itertuples()}
|
||||
idx = wide.index
|
||||
F = fund.set_index("ts").reindex(idx)
|
||||
F = fund.set_index("ts").reindex(idx).ffill(limit=2)
|
||||
n = len(idx)
|
||||
front = np.full(n, "", dtype=object)
|
||||
back = np.full(n, "", dtype=object)
|
||||
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}
|
||||
# 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 = []
|
||||
for c in order:
|
||||
if avail[c][i] and tau[c][i] >= roll_dte * 24:
|
||||
picked.append(c)
|
||||
if len(picked) == 2:
|
||||
break
|
||||
if len(picked) == 2:
|
||||
front[i], back[i] = picked
|
||||
order = [c for c in ok.contract if c in wide.columns] # ordine di SCADENZA
|
||||
if len(order) < 2:
|
||||
raise RuntimeError("meno di due contratti utilizzabili")
|
||||
|
||||
# ALIVE[i, k] = il k-esimo contratto (per scadenza) e' quotato a i E ha dte >= roll
|
||||
TAU = np.column_stack([(exps[c] - idx).total_seconds().values / 3600.0 for c in order])
|
||||
AV = np.column_stack([wide[c].notna().values for c in order])
|
||||
ALIVE = AV & (TAU >= roll_dte * 24)
|
||||
|
||||
any1 = ALIVE.any(axis=1)
|
||||
k1 = np.argmax(ALIVE, axis=1) # primo vivo = FRONT
|
||||
A2 = ALIVE.copy()
|
||||
A2[np.arange(n), k1] = False
|
||||
any2 = A2.any(axis=1)
|
||||
k2 = np.argmax(A2, axis=1) # secondo vivo = BACK
|
||||
m = any1 & any2
|
||||
|
||||
front = np.where(m, np.array(order, dtype=object)[k1], "")
|
||||
back = np.where(m, np.array(order, dtype=object)[k2], "")
|
||||
rows = np.arange(n)
|
||||
Wv = wide[order].values.astype(float)
|
||||
Ff = np.where(m, Wv[rows, k1], np.nan)
|
||||
Fb = np.where(m, Wv[rows, k2], np.nan)
|
||||
tf = np.where(m, TAU[rows, k1], np.nan)
|
||||
tb = np.where(m, TAU[rows, k2], np.nan)
|
||||
|
||||
out = pd.DataFrame(index=idx)
|
||||
out["front"], out["back"] = front, back
|
||||
m = (front != "") & (back != "")
|
||||
Ff = np.full(n, np.nan); Fb = np.full(n, np.nan)
|
||||
tf = np.full(n, np.nan); tb = np.full(n, np.nan)
|
||||
W = wide.values
|
||||
colpos = {c: k for k, c in enumerate(wide.columns)}
|
||||
for i in np.where(m)[0]:
|
||||
Ff[i] = W[i, colpos[front[i]]]; Fb[i] = W[i, colpos[back[i]]]
|
||||
tf[i] = tau[front[i]][i]; tb[i] = tau[back[i]][i]
|
||||
out["F_front"], out["F_back"] = Ff, Fb
|
||||
out["tau_front_h"], out["tau_back_h"] = tf, tb
|
||||
out["index"] = F["index"].values
|
||||
@@ -514,7 +521,8 @@ def signal_w(cv: pd.DataFrame, family: str, sig: str, win_h: int, thr: float) ->
|
||||
|
||||
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, lag: int = 1) -> pd.Series:
|
||||
fee_side: float = FEE_SIDE, fi=None, bi=None, lag: int = 1,
|
||||
dec_hour: int | None = 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
|
||||
@@ -529,6 +537,11 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
|
||||
if fi is None or bi is None:
|
||||
fi, bi = pair_idx(cv, ctx)
|
||||
w = signal_w(cv, family, sig, win_h, thr)
|
||||
if dec_hour is not None:
|
||||
# ANCORA: la posizione si aggiorna SOLO a quell'ora del giorno e si TIENE il resto
|
||||
# (non "si guadagna solo a quell'ora", che sarebbe un'altra strategia).
|
||||
ws = pd.Series(w, index=idx).where(idx.hour == dec_hour)
|
||||
w = ws.ffill().fillna(0.0).values
|
||||
|
||||
P = np.zeros((n, ncol))
|
||||
rows = np.arange(n)
|
||||
@@ -564,6 +577,12 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str,
|
||||
# ==========================================================================
|
||||
# 4. METRICHE / GATE
|
||||
# ==========================================================================
|
||||
def combine(hs: list[pd.Series]) -> pd.Series:
|
||||
"""50/50 BTC+ETH sulle ore COMUNI (inner join, come tp01_baseline_daily)."""
|
||||
J = pd.concat(hs, axis=1, join="inner").fillna(0.0)
|
||||
return J.mean(axis=1)
|
||||
|
||||
|
||||
def to_daily(h: pd.Series) -> pd.Series:
|
||||
return ((1.0 + h.fillna(0.0)).resample("1D").prod() - 1.0).dropna()
|
||||
|
||||
@@ -698,9 +717,7 @@ def main() -> None:
|
||||
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
|
||||
d = to_daily(comb)
|
||||
d = to_daily(combine(hs))
|
||||
st = stats(d)
|
||||
rows.append(dict(family=fam, sig=sig, win=win, roll=rl, thr=th,
|
||||
daily=d, **st))
|
||||
@@ -768,13 +785,10 @@ def main() -> None:
|
||||
hs = []
|
||||
for a in ASSETS:
|
||||
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
|
||||
hs.append(hh)
|
||||
return to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1))
|
||||
hs.append(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, dec_hour=off))
|
||||
return to_daily(combine(hs))
|
||||
try:
|
||||
ab = A.anchor_luck_band(by_off, list(range(24)), canonical=0)
|
||||
if "median" in ab:
|
||||
@@ -807,7 +821,7 @@ def main() -> None:
|
||||
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), slip_est, fi=f_i, bi=b_i, lag=lg))
|
||||
sl_ = stats(to_daily(pd.concat(hs, axis=1).fillna(0.0).mean(axis=1)))
|
||||
sl_ = stats(to_daily(combine(hs)))
|
||||
lag_tab.append((lg, sl_["sharpe"], sl_["cagr"]))
|
||||
print(f" ritardo {lg:2d}h -> Sharpe {sl_['sharpe']:+6.2f} CAGR {sl_['cagr']*100:+6.2f}%")
|
||||
s1 = lag_tab[0][1]; s2 = lag_tab[1][1]
|
||||
@@ -827,7 +841,7 @@ def main() -> None:
|
||||
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)))
|
||||
s2 = stats(to_daily(combine(hs)))
|
||||
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} "
|
||||
f"CAGR {s2['cagr']*100:+6.2f}%{mark}")
|
||||
|
||||
Reference in New Issue
Block a user