research: porta artefatti da strategy-research-calendar (tracks F-I + eval crypto_backtest + lead OPZIONI/VRP)
Dal branch parallelo strategy-research-calendar (continuazione della linea TP01). Porta su main il record di ricerca + la fondazione del lead opzioni (NIENTE blob dati, niente codice in conflitto): - Tracks F/G/H/I (seasonality/calendar, prior-levels, volume-vol, momentum-reversal): tutti NEGATIVI/spurii -> confermano il soffitto Sharpe ~1.3 su BTC/ETH direzionale (calendar = buy&hold travestito; mean-reversion morta anche a fee 0). Diari + script. - trackD_lookahead_audit.py: audit anti-look-ahead (stesso esito del nostro fix >=12h). - eval-crypto-backtest-options.md: valutazione strategia esterna crypto_backtest. Cross-valida TP01 (il loro sleeve spot 12h ~ TP01: due ricerche indipendenti, stessa conclusione). Identifica il LEAD: sleeve income OPZIONI (vendita put settimanali delta-0.28, VRP IV>RV), scorrelato ~0.22 al trend -> via per superare il soffitto ~1.3. - options_real_quote_check.py + cerbero-bite-mainnet-verified.md: VERIFICATO su QUOTE REALI Deribit mainnet (cerbero-bite/MCP = mainnet, bit-identico a ccxt.deribit). Premio reale (BID, con skew) = 1.29x il modellato -> il backtest SOTTOSTIMA il premio; il rischio vero e' la CODA (short-vol) + liquidita' di roll in stress, non la magnitudine. NB: lo sleeve opzioni e' un LEAD, NON deployato: prezzato da modello (BS su DVOL) + 1 snapshot in regime calmo. Serve validazione real-chain multi-regime + stress crash + paper su testnet prima di aggiungerlo al portafoglio. Portafoglio attivo invariato: TP01 70% + XS01 30%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
"""TRACK I — ALTERNATIVE MOMENTUM FORMULATIONS + LONG-HORIZON REVERSAL (BTC & ETH, >=12h).
|
||||
|
||||
Goal:
|
||||
(A) Find a momentum formulation that BEATS or DIVERSIFIES the canonical TP01 sign-blend
|
||||
(TSMOM 1-3-6m, vol-targeted, 50/50 BTC+ETH, 12h, Sharpe ~1.32).
|
||||
(B) Test the classic LONG-HORIZON REVERSAL effect (fade 12/18/24-month winners) as a
|
||||
potentially UNCORRELATED positive overlay, and a momentum+reversal blend.
|
||||
|
||||
Honest harness (mirrors src/strategies/trend_portfolio.py exactly):
|
||||
- direction decided with data <= close[i]; positions HELD next bar (pos_held[1:] = tgt[:-1]);
|
||||
- vol-target by inverse PAST-ONLY realized vol (target_vol/vol), leverage-capped;
|
||||
- NET fees 0.10% RT (0.05%/side) on turnover; fee sweep included;
|
||||
- 12h / 1d only (sub-12h is dominated by costs/overfit and a prior 4h look-ahead bug);
|
||||
- OOS 65/35 split + per-year; robustness across lookbacks AND both assets;
|
||||
- correlation vs TP01 net returns reported for EVERY candidate.
|
||||
|
||||
A candidate is INTERESTING only if net-positive OOS on BOTH assets AND either
|
||||
(higher portfolio Sharpe than TP01 ~1.32) OR (|corr to TP01| < ~0.3 and positive).
|
||||
|
||||
Run: uv run python scripts/research/trackI_momentum_reversal.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from src.backtest.harness import load
|
||||
from src.strategies.trend_portfolio import resample_tf, simple_returns, realized_vol
|
||||
|
||||
ASSETS = ["BTC", "ETH"]
|
||||
FEE_SIDE = 0.0005 # 0.05%/side = 0.10% RT
|
||||
TARGET_VOL = 0.20
|
||||
LEVERAGE = 2.0
|
||||
VOL_WIN_DAYS = 30
|
||||
OOS_FRAC = 0.65
|
||||
MONTH = 30 # days per "month" (calendar-consistent across TFs)
|
||||
|
||||
# tf -> bars_per_day
|
||||
TF_BPD = {"12h": 2, "1d": 1}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# data
|
||||
# ---------------------------------------------------------------------------
|
||||
def get_df(asset: str, tf: str) -> pd.DataFrame:
|
||||
df = load(asset, "1h")
|
||||
rule = {"12h": "12h", "1d": "1D"}[tf]
|
||||
return resample_tf(df, rule)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# vol-target machinery (identical convention to TP01)
|
||||
# ---------------------------------------------------------------------------
|
||||
def build_target(direction, vol, long_only):
|
||||
d = np.clip(direction, 0, None) if long_only else direction
|
||||
scal = np.where((vol > 0) & np.isfinite(vol), TARGET_VOL / vol, 0.0)
|
||||
tgt = np.clip(d * scal, -LEVERAGE, LEVERAGE)
|
||||
tgt[~np.isfinite(tgt)] = 0.0
|
||||
return tgt
|
||||
|
||||
|
||||
def net_from_target(tgt, r, fee_side=FEE_SIDE):
|
||||
pos_held = np.zeros(len(tgt))
|
||||
pos_held[1:] = tgt[:-1]
|
||||
gross = pos_held * r
|
||||
turn = np.abs(np.diff(pos_held, prepend=0.0))
|
||||
net = gross - fee_side * turn
|
||||
net[0] = 0.0
|
||||
return np.clip(net, -0.99, None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DIRECTION FORMULATIONS (each returns array in roughly [-1, 1], causal, decided <= close[i])
|
||||
# ---------------------------------------------------------------------------
|
||||
def _log_mom(c, h):
|
||||
"""log return over h bars; nan before h."""
|
||||
m = np.full(len(c), np.nan)
|
||||
m[h:] = np.log(c[h:] / c[:-h])
|
||||
return m
|
||||
|
||||
|
||||
def dir_signblend(c, bpd, horizons_m=(1, 3, 6)):
|
||||
"""TP01 baseline: mean of sign(log return) over horizons."""
|
||||
n = len(c)
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for hm in horizons_m:
|
||||
h = hm * MONTH * bpd
|
||||
s = np.full(n, np.nan)
|
||||
s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||
v = np.isfinite(s); acc[v] += s[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def dir_zscore(c, bpd, horizons_m=(1, 3, 6), std_win_m=12):
|
||||
"""(i) Continuous momentum: z-scored cumulative log-return, tanh-bounded, multi-horizon avg."""
|
||||
n = len(c); w = std_win_m * MONTH * bpd
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for hm in horizons_m:
|
||||
h = hm * MONTH * bpd
|
||||
m = _log_mom(c, h)
|
||||
s = pd.Series(m)
|
||||
sd = s.rolling(w, min_periods=w // 3).std().values
|
||||
z = np.where((sd > 0) & np.isfinite(sd), m / sd, np.nan)
|
||||
d = np.tanh(z)
|
||||
v = np.isfinite(d); acc[v] += d[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def dir_riskadj(c, bpd, horizons_m=(1, 3, 6)):
|
||||
"""(ii) Risk-adjusted momentum: h-horizon return / vol-of-that-horizon, tanh, multi-horizon."""
|
||||
n = len(c); r = simple_returns(c)
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for hm in horizons_m:
|
||||
h = hm * MONTH * bpd
|
||||
ret = np.full(n, np.nan); ret[h:] = c[h:] / c[:-h] - 1.0
|
||||
# vol of the h-bar return = per-bar std over last h bars * sqrt(h)
|
||||
sd = pd.Series(r).rolling(h, min_periods=h // 2).std().values * np.sqrt(h)
|
||||
ra = np.where((sd > 0) & np.isfinite(sd), ret / sd, np.nan)
|
||||
d = np.tanh(ra)
|
||||
v = np.isfinite(d); acc[v] += d[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def _ema(c, span):
|
||||
return pd.Series(c).ewm(span=span, adjust=False).mean().values
|
||||
|
||||
|
||||
def dir_emacross(c, bpd, pairs_m=((1, 3), (2, 6), (3, 9))):
|
||||
"""(iii) EMA-cross trend: mean of sign(ema_fast - ema_slow) over calendar-day pairs."""
|
||||
n = len(c)
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for fm, sm in pairs_m:
|
||||
ef = _ema(c, fm * MONTH * bpd)
|
||||
es = _ema(c, sm * MONTH * bpd)
|
||||
warm = sm * MONTH * bpd
|
||||
d = np.sign(ef - es)
|
||||
d[:warm] = np.nan
|
||||
v = np.isfinite(d); acc[v] += d[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def dir_macd(c, bpd):
|
||||
"""(iii-b) Classic MACD with calendar spans (fast~1m, slow~2m, signal~0.75m): sign(macd-signal)."""
|
||||
n = len(c)
|
||||
fast = int(round(1.0 * MONTH * bpd)); slow = int(round(2.0 * MONTH * bpd))
|
||||
sig = int(round(0.75 * MONTH * bpd))
|
||||
macd = _ema(c, fast) - _ema(c, slow)
|
||||
signal = pd.Series(macd).ewm(span=sig, adjust=False).mean().values
|
||||
d = np.sign(macd - signal)
|
||||
d[:slow] = 0.0
|
||||
return d
|
||||
|
||||
|
||||
def dir_donchian(c, bpd, n_m=2):
|
||||
"""(iv) Donchian breakout (>=12h): +1 if close > prior-N max, -1 if < prior-N min, else hold."""
|
||||
n = len(c); N = n_m * MONTH * bpd
|
||||
hi = pd.Series(c).rolling(N, min_periods=N).max().shift(1).values
|
||||
lo = pd.Series(c).rolling(N, min_periods=N).min().shift(1).values
|
||||
d = np.zeros(n); state = 0.0
|
||||
for i in range(n):
|
||||
if np.isfinite(hi[i]) and c[i] >= hi[i]:
|
||||
state = 1.0
|
||||
elif np.isfinite(lo[i]) and c[i] <= lo[i]:
|
||||
state = -1.0
|
||||
d[i] = state
|
||||
return d
|
||||
|
||||
|
||||
def dir_accel(c, bpd, horizons_m=(3, 6), lag_m=1):
|
||||
"""(v) Acceleration: sign of CHANGE in momentum (mom[i] - mom[i-lag]) i.e. 2nd derivative."""
|
||||
n = len(c); lag = lag_m * MONTH * bpd
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for hm in horizons_m:
|
||||
h = hm * MONTH * bpd
|
||||
m = _log_mom(c, h)
|
||||
dm = np.full(n, np.nan)
|
||||
dm[lag:] = m[lag:] - m[:-lag]
|
||||
d = np.sign(dm)
|
||||
v = np.isfinite(d); acc[v] += d[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def dir_mom12_1(c, bpd, lookbacks_m=(6, 12), skip_m=1):
|
||||
"""(vi) 12-1 momentum: return from (i-L) to (i-skip), skipping the most-recent `skip` month.
|
||||
For index i (>=L): sign( c[i-skip] / c[i-L] - 1 ). Causal (uses data <= close[i-skip])."""
|
||||
n = len(c); skip = skip_m * MONTH * bpd
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for Lm in lookbacks_m:
|
||||
L = Lm * MONTH * bpd
|
||||
s = np.full(n, np.nan)
|
||||
# i runs L..n-1: c[i-skip] = c[L-skip : n-skip], c[i-L] = c[0 : n-L]
|
||||
s[L:] = np.sign(c[L - skip:n - skip] / c[:n - L] - 1.0)
|
||||
v = np.isfinite(s); acc[v] += s[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
|
||||
|
||||
def make_reversal(lookbacks_m):
|
||||
"""(B) long-horizon reversal: -sign of long-horizon return (short past winners)."""
|
||||
def fn(c, bpd):
|
||||
n = len(c)
|
||||
acc = np.zeros(n); cnt = np.zeros(n)
|
||||
for Lm in lookbacks_m:
|
||||
L = Lm * MONTH * bpd
|
||||
s = np.full(n, np.nan)
|
||||
s[L:] = -np.sign(c[L:] / c[:-L] - 1.0)
|
||||
v = np.isfinite(s); acc[v] += s[v]; cnt[v] += 1
|
||||
out = np.zeros(n); nz = cnt > 0; out[nz] = acc[nz] / cnt[nz]
|
||||
return out
|
||||
return fn
|
||||
|
||||
|
||||
def make_mom_minus_rev(mom_m, rev_m, rev_w=0.5):
|
||||
"""Blend: long medium-term momentum + fade very-long-term extension (weighted)."""
|
||||
def fn(c, bpd):
|
||||
n = len(c)
|
||||
mom = dir_signblend(c, bpd, horizons_m=mom_m)
|
||||
rev_fn = make_reversal(rev_m)
|
||||
rev = rev_fn(c, bpd)
|
||||
return np.clip(mom + rev_w * rev, -1.0, 1.0)
|
||||
return fn
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# run a formulation -> per-asset net series, combined portfolio series, metrics
|
||||
# ---------------------------------------------------------------------------
|
||||
def asset_net_series(asset, tf, dir_fn, long_only, fee_side=FEE_SIDE):
|
||||
df = get_df(asset, tf); bpd = TF_BPD[tf]
|
||||
c = df["close"].values.astype(float)
|
||||
r = simple_returns(c)
|
||||
bpy = bpd * 365.25
|
||||
vol = realized_vol(r, VOL_WIN_DAYS * bpd, bpy)
|
||||
direction = dir_fn(c, bpd)
|
||||
tgt = build_target(direction, vol, long_only)
|
||||
net = net_from_target(tgt, r, fee_side)
|
||||
return pd.Series(net, index=pd.to_datetime(df["datetime"].values))
|
||||
|
||||
|
||||
def portfolio_combo(tf, dir_fn, long_only, fee_side=FEE_SIDE):
|
||||
s = {a: asset_net_series(a, tf, dir_fn, long_only, fee_side) for a in ASSETS}
|
||||
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
|
||||
combo = 0.5 * J[ASSETS[0]].values + 0.5 * J[ASSETS[1]].values
|
||||
return pd.Series(combo, index=J.index), s
|
||||
|
||||
|
||||
def sharpe_of(series, bpy):
|
||||
r = series.values[np.isfinite(series.values)]
|
||||
return float(np.mean(r) / np.std(r) * np.sqrt(bpy)) if len(r) and np.std(r) > 0 else 0.0
|
||||
|
||||
|
||||
def metrics_of(combo: pd.Series, bpy):
|
||||
idx = combo.index
|
||||
equity = np.cumprod(1.0 + np.clip(combo.values, -0.99, None))
|
||||
sharpe = sharpe_of(combo, bpy)
|
||||
peak = np.maximum.accumulate(equity)
|
||||
dd = float(np.max((peak - equity) / peak))
|
||||
years = (idx[-1] - idx[0]).total_seconds() / 86400 / 365.25
|
||||
total = equity[-1] / equity[0]
|
||||
cagr = total ** (1 / years) - 1 if years > 0 and total > 0 else -1.0
|
||||
eq = pd.Series(equity, index=idx)
|
||||
yearly = {}
|
||||
for y, g in eq.groupby(eq.index.year):
|
||||
if len(g) > 1 and g.iloc[0] > 0:
|
||||
v = g.values; pk = np.maximum.accumulate(v)
|
||||
yearly[int(y)] = (float(g.iloc[-1] / g.iloc[0] - 1), float(np.max((pk - v) / pk)))
|
||||
# OOS split
|
||||
k = int(len(combo) * OOS_FRAC)
|
||||
is_sh = sharpe_of(combo.iloc[:k], bpy)
|
||||
oos_sh = sharpe_of(combo.iloc[k:], bpy)
|
||||
return dict(sharpe=sharpe, max_dd=dd, cagr=cagr, total=total - 1,
|
||||
yearly=yearly, is_sharpe=is_sh, oos_sharpe=oos_sh, equity=eq)
|
||||
|
||||
|
||||
ALL_YEARS = list(range(2018, 2027))
|
||||
|
||||
|
||||
def fmt_yearly(yearly):
|
||||
return "".join((" . " if y not in yearly else f"{yearly[y][0]*100:>+6.0f}") for y in ALL_YEARS)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# main
|
||||
# ---------------------------------------------------------------------------
|
||||
PART_A = [
|
||||
("baseline signblend 1-3-6m", dir_signblend),
|
||||
("(i) z-score cum-ret", dir_zscore),
|
||||
("(ii) risk-adj momentum", dir_riskadj),
|
||||
("(iii) EMA-cross trend", dir_emacross),
|
||||
("(iii-b) MACD", dir_macd),
|
||||
("(iv) Donchian breakout", dir_donchian),
|
||||
("(v) acceleration", dir_accel),
|
||||
("(vi) 12-1 skip momentum", dir_mom12_1),
|
||||
]
|
||||
|
||||
|
||||
def report_block(title, items, tf, long_only, tp_combo, bpy):
|
||||
mode = "LONG-FLAT" if long_only else "LONG-SHORT"
|
||||
print(f"\n{'='*112}\n {title} | TF={tf} mode={mode}\n{'='*112}")
|
||||
print(f" {'formulation':<26s} {'Shrp':>5s} {'IS':>5s} {'OOS':>5s} {'CAGR':>6s} "
|
||||
f"{'maxDD':>6s} {'corrTP':>7s} {'aBTC':>5s} {'aETH':>5s} per-year PnL%")
|
||||
print(f" {'':<26s} {'':>5s} {'':>5s} {'':>5s} {'':>6s} {'':>6s} {'':>7s} {'':>5s} {'':>5s} "
|
||||
+ "".join(f"{y%100:>6d}" for y in ALL_YEARS))
|
||||
results = {}
|
||||
for name, fn in items:
|
||||
combo, sleeves = portfolio_combo(tf, fn, long_only)
|
||||
m = metrics_of(combo, bpy)
|
||||
# per-asset standalone Sharpe
|
||||
a_sh = {a: sharpe_of(sleeves[a], bpy) for a in ASSETS}
|
||||
# correlation to TP01 (aligned inner)
|
||||
J = pd.concat([combo.rename("x"), tp_combo.rename("t")], axis=1, join="inner").dropna()
|
||||
corr = float(np.corrcoef(J["x"], J["t"])[0, 1]) if len(J) > 2 else float("nan")
|
||||
print(f" {name:<26s} {m['sharpe']:>5.2f} {m['is_sharpe']:>5.2f} {m['oos_sharpe']:>5.2f} "
|
||||
f"{m['cagr']*100:>+5.0f}% {m['max_dd']*100:>5.1f}% {corr:>7.2f} "
|
||||
f"{a_sh['BTC']:>5.2f} {a_sh['ETH']:>5.2f} {fmt_yearly(m['yearly'])}")
|
||||
results[name] = dict(metrics=m, corr=corr, combo=combo, a_sh=a_sh)
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
print("#" * 112)
|
||||
print("# TRACK I — alternative momentum formulations + long-horizon reversal (BTCÐ, >=12h)")
|
||||
print("# vol-target 20%, lev cap 2x, fee 0.10% RT, positions +1 bar, 50/50 BTC+ETH. OOS 65/35.")
|
||||
print("#" * 112)
|
||||
|
||||
for tf in ("12h", "1d"):
|
||||
bpy = TF_BPD[tf] * 365.25
|
||||
# TP01 reference combo at this TF (long-flat canonical) for correlation
|
||||
tp_combo, _ = portfolio_combo(tf, dir_signblend, long_only=True)
|
||||
tp_m = metrics_of(tp_combo, bpy)
|
||||
print(f"\n>>> TP01 reference @ {tf} (long-flat 1-3-6m): "
|
||||
f"Sharpe {tp_m['sharpe']:.2f} IS {tp_m['is_sharpe']:.2f} OOS {tp_m['oos_sharpe']:.2f} "
|
||||
f"CAGR {tp_m['cagr']*100:+.0f}% maxDD {tp_m['max_dd']*100:.1f}%")
|
||||
|
||||
# PART A — long-flat (fair vs canonical) and long-short
|
||||
report_block("PART A — momentum formulations", PART_A, tf, True, tp_combo, bpy)
|
||||
if tf == "12h":
|
||||
report_block("PART A — momentum formulations (long-short)", PART_A, tf, False, tp_combo, bpy)
|
||||
|
||||
# ----- PART B: reversal + blends, focus 12h -----
|
||||
tf = "12h"; bpy = TF_BPD[tf] * 365.25
|
||||
tp_combo, _ = portfolio_combo(tf, dir_signblend, long_only=True)
|
||||
|
||||
rev_items = [
|
||||
("reversal 12m", make_reversal((12,))),
|
||||
("reversal 18m", make_reversal((18,))),
|
||||
("reversal 24m", make_reversal((24,))),
|
||||
("reversal 12-18-24m", make_reversal((12, 18, 24))),
|
||||
]
|
||||
print("\n\n" + "#" * 112)
|
||||
print("# PART B — LONG-HORIZON REVERSAL (fade past winners). Must be net-positive AND uncorrelated.")
|
||||
print("#" * 112)
|
||||
revB = report_block("PART B — reversal (long-short)", rev_items, tf, False, tp_combo, bpy)
|
||||
# reversal long-flat (long past losers only) for completeness
|
||||
report_block("PART B — reversal (long-flat)", rev_items, tf, True, tp_combo, bpy)
|
||||
|
||||
blend_items = [
|
||||
("mom(1-6) - 0.5*rev(12-24)", make_mom_minus_rev((1, 3, 6), (12, 24), 0.5)),
|
||||
("mom(1-6) - 1.0*rev(12-24)", make_mom_minus_rev((1, 3, 6), (12, 24), 1.0)),
|
||||
("mom(1-3) - 0.5*rev(18-24)", make_mom_minus_rev((1, 3), (18, 24), 0.5)),
|
||||
]
|
||||
report_block("PART B — momentum + reversal blend", blend_items, tf, True, tp_combo, bpy)
|
||||
|
||||
# ----- COMBINED PORTFOLIO: TP01 + best diversifier -----
|
||||
print("\n\n" + "#" * 112)
|
||||
print("# COMBINED: TP01 (long-flat) + candidate diversifier, blended on net returns")
|
||||
print("#" * 112)
|
||||
tp_m = metrics_of(tp_combo, bpy)
|
||||
print(f" TP01 alone: Sharpe {tp_m['sharpe']:.3f} CAGR {tp_m['cagr']*100:+.0f}% maxDD {tp_m['max_dd']*100:.1f}%")
|
||||
|
||||
# candidates to try as overlay: the best A formulations + reversal variants
|
||||
overlays = {
|
||||
"z-score": (dir_zscore, True),
|
||||
"risk-adj": (dir_riskadj, True),
|
||||
"12-1 skip": (dir_mom12_1, True),
|
||||
"reversal 12-18-24 LS": (make_reversal((12, 18, 24)), False),
|
||||
"reversal 24m LS": (make_reversal((24,)), False),
|
||||
}
|
||||
for name, (fn, lo) in overlays.items():
|
||||
cand, _ = portfolio_combo(tf, fn, lo)
|
||||
J = pd.concat([tp_combo.rename("t"), cand.rename("c")], axis=1, join="inner").fillna(0.0)
|
||||
corr = float(np.corrcoef(J["t"], J["c"])[0, 1])
|
||||
for w in (0.5, 0.3, 0.2):
|
||||
mix = pd.Series((1 - w) * J["t"].values + w * J["c"].values, index=J.index)
|
||||
mm = metrics_of(mix, bpy)
|
||||
tag = f"TP01 + {w:.0%} {name}"
|
||||
print(f" {tag:<30s} Sharpe {mm['sharpe']:.3f} CAGR {mm['cagr']*100:+5.0f}% "
|
||||
f"maxDD {mm['max_dd']*100:4.1f}% OOS {mm['oos_sharpe']:.2f} (corr={corr:+.2f})")
|
||||
|
||||
# ----- FEE SWEEP (robustness): 0.00 .. 0.40% RT -----
|
||||
print("\n\n" + "#" * 112)
|
||||
print("# FEE SWEEP — portfolio Sharpe @12h across round-trip fees (0.00-0.40% RT)")
|
||||
print("#" * 112)
|
||||
sweep = [
|
||||
("baseline 1-3-6m (LF)", dir_signblend, True),
|
||||
("z-score cum-ret (LF)", dir_zscore, True),
|
||||
("MACD (LF)", dir_macd, True),
|
||||
("mom(1-6)-0.5rev(12-24)(LF)", make_mom_minus_rev((1, 3, 6), (12, 24), 0.5), True),
|
||||
("reversal 24m (LS)", make_reversal((24,)), False),
|
||||
]
|
||||
rts = [0.0, 0.0005, 0.0010, 0.0020, 0.0040]
|
||||
print(f" {'formulation':<28s}" + "".join(f"{rt*100:>7.2f}%" for rt in rts) + " (RT)")
|
||||
for name, fn, lo in sweep:
|
||||
row = [sharpe_of(portfolio_combo(tf, fn, lo, fee_side=rt / 2)[0], bpy) for rt in rts]
|
||||
print(f" {name:<28s}" + "".join(f"{v:>8.2f}" for v in row))
|
||||
|
||||
print("\nDone. See verdict in the script docstring / diary.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user