5ac4e16af8
Ondata di ricerca onesta a largo spettro su BTC/ETH+DVOL certificati: 104 ipotesi distinte (11 famiglie), un agente-finder per ipotesi, verifica avversariale a 3 scettici sui promettenti, sintesi (153 agenti totali). Esito: NIENTE di nuovo regge -> conferma del soffitto strutturale ~1.3 BTC/ETH-direzionale; lo stack TP01+XS01+VRP01 resta imbattuto. - altlib.py: harness condiviso vettoriale leak-free (eval_weights/study_weights, fee-sweep, both-asset + hold-out 2025+). Riproduce i numeri canonici di TP01. - MARGINAL SCORER (study_marginal/marginal_vs_tp01): Sharpe INCREMENTALE vs baseline TP01 (corr, blend uplift OOS, alpha residua) + jackknife OOS (clean-year + drop-best-month). earns_slot = abs!=FAIL & ADDS & robust_oos. Smaschera gli overlay su TSMOM con PASS assoluti fasulli (CMB04, VOL11, ...) e il falso positivo KAMA (ADDS ma muore al jackknife). - runs/*.py (104) script riproducibili per ipotesi; wf_altstrat.js workflow. - Verdetto: 0 candidati deployabili; 2 LEAD fragili (VOL08, STA05_LS) da forward-monitor. - test_marginal_scorer.py blocca baseline + invarianti. Suite: 32 verde. Diario: docs/diary/2026-06-20-alt-strategies-100agent-sweep.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
169 lines
6.8 KiB
Python
169 lines
6.8 KiB
Python
"""RSK03 — Inverse-vol Risk Parity (2-asset blend BTC+ETH).
|
|
|
|
IDEA: Scale each asset's exposure by the inverse of its realized volatility,
|
|
normalized so the blended portfolio targets a fixed volatility (20%).
|
|
This is risk-parity weighting: assets contribute equally to portfolio risk
|
|
rather than receiving equal capital. Compare to fixed 50/50 exposure.
|
|
|
|
TWO sub-configs tested (small grid, <=4 param sets total over 2 TFs):
|
|
Config A: vol_win=30d, leverage_cap=2.0 (standard)
|
|
Config B: vol_win=60d, leverage_cap=2.0 (smoother vol estimate)
|
|
|
|
Approach:
|
|
- For each bar, compute realized vol for BTC and ETH
|
|
- Assign each an inverse-vol weight, normalize so sum of weights = 1
|
|
- Scale combined weight to target_vol=20% using blended portfolio vol
|
|
- Both assets always long (long-flat risk parity proxy)
|
|
- Result is a single "blended" return series; reported per-asset for consistency,
|
|
but the real edge is the BTC/ETH blend with risk-parity weighting
|
|
|
|
Since study_weights evaluates per-asset independently, we test two approaches:
|
|
1. Per-asset vol-targeted weights (each asset gets its own vol-targeting)
|
|
2. Cross-asset: for the combined report, we show the blend explicitly
|
|
|
|
For the per-asset evaluation compatible with altlib, we use vol_target per asset
|
|
(which IS inverse-vol risk parity when both assets are long) and let the library
|
|
evaluate each independently. The cross-asset blend is computed separately and
|
|
printed as the "combined" result.
|
|
"""
|
|
|
|
import sys
|
|
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
|
import altlib as al
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
# ── Config grid ─────────────────────────────────────────────────────────────
|
|
# vol_win_days, leverage_cap
|
|
CONFIGS = [
|
|
(30, 2.0), # A: standard 30d window
|
|
(60, 2.0), # B: smoother 60d window
|
|
]
|
|
|
|
|
|
def make_target(vol_win_days: int, leverage_cap: float):
|
|
"""Returns a target_fn: df -> per-bar position.
|
|
Long-only, vol-targeted using inverse realized vol.
|
|
This is the per-asset component of inverse-vol RP.
|
|
direction=+1 always (long-flat), then scaled by target_vol/realized_vol.
|
|
"""
|
|
def target_fn(df):
|
|
direction = np.ones(len(df)) # always long
|
|
return al.vol_target(direction, df,
|
|
target_vol=0.20,
|
|
vol_win_days=vol_win_days,
|
|
leverage_cap=leverage_cap)
|
|
return target_fn
|
|
|
|
|
|
def combined_rp_report(vol_win_days: int, leverage_cap: float, tf: str):
|
|
"""Compute blended BTC+ETH inverse-vol risk-parity returns.
|
|
At each bar, blend BTC and ETH using inverse-vol weights normalized to 1,
|
|
then apply an overall vol-target to the combined portfolio.
|
|
Returns (sharpe_full, maxdd_full, sharpe_holdout, ret_full, ret_holdout).
|
|
"""
|
|
df_btc = al.get("BTC", tf)
|
|
df_eth = al.get("ETH", tf)
|
|
|
|
# Align BTC and ETH by timestamp (BTC starts 2018, ETH 2019)
|
|
df_btc = df_btc.set_index("datetime")
|
|
df_eth = df_eth.set_index("datetime")
|
|
common_idx = df_btc.index.intersection(df_eth.index)
|
|
df_btc = df_btc.loc[common_idx].reset_index()
|
|
df_eth = df_eth.loc[common_idx].reset_index()
|
|
|
|
c_btc = df_btc["close"].values.astype(float)
|
|
c_eth = df_eth["close"].values.astype(float)
|
|
|
|
bpd = al.bars_per_day(df_btc)
|
|
bpy = bpd * 365.25
|
|
vol_win = max(2, vol_win_days * bpd)
|
|
|
|
r_btc = al.simple_returns(c_btc)
|
|
r_eth = al.simple_returns(c_eth)
|
|
|
|
vol_btc = al.realized_vol(r_btc, vol_win, bpy)
|
|
vol_eth = al.realized_vol(r_eth, vol_win, bpy)
|
|
|
|
# Inverse-vol weights (causal: at i, vol computed using data<=i)
|
|
# weight_i = (1/vol_i) / (1/vol_btc + 1/vol_eth)
|
|
inv_btc = np.where((vol_btc > 0) & np.isfinite(vol_btc), 1.0 / vol_btc, np.nan)
|
|
inv_eth = np.where((vol_eth > 0) & np.isfinite(vol_eth), 1.0 / vol_eth, np.nan)
|
|
inv_sum = inv_btc + inv_eth
|
|
w_btc = np.where(np.isfinite(inv_sum) & (inv_sum > 0), inv_btc / inv_sum, 0.5)
|
|
w_eth = np.where(np.isfinite(inv_sum) & (inv_sum > 0), inv_eth / inv_sum, 0.5)
|
|
|
|
# Blended portfolio return (before vol-targeting)
|
|
r_blend = w_btc * r_btc + w_eth * r_eth
|
|
|
|
# Now vol-target the blended return to 20%
|
|
vol_blend = al.realized_vol(r_blend, vol_win, bpy)
|
|
scal = np.where((vol_blend > 0) & np.isfinite(vol_blend), 0.20 / vol_blend, 0.0)
|
|
pos = np.clip(scal, 0, leverage_cap) # long-flat only
|
|
pos = np.nan_to_num(pos, nan=0.0)
|
|
|
|
# Honest shift: pos[i] decided at close[i], held during bar i+1
|
|
pos_held = np.zeros(len(pos))
|
|
pos_held[1:] = pos[:-1]
|
|
|
|
gross = pos_held * r_blend
|
|
turn = np.abs(np.diff(pos_held, prepend=0.0))
|
|
fee_side = al.FEE_SIDE
|
|
net = gross - fee_side * turn
|
|
net[0] = 0.0
|
|
|
|
# Use BTC index for timestamps (both aligned)
|
|
idx = pd.DatetimeIndex(pd.to_datetime(df_btc["datetime"], utc=True))
|
|
|
|
full = al._metrics_from_net(net, idx)
|
|
hmask = idx >= al.HOLDOUT
|
|
if hmask.sum() > 3:
|
|
hold = al._metrics_from_net(net[hmask], idx[hmask])
|
|
else:
|
|
hold = dict(sharpe=0.0, ret=0.0, n=0)
|
|
|
|
yearly = al._yearly(net, idx)
|
|
return full, hold, yearly
|
|
|
|
|
|
# ── Main ────────────────────────────────────────────────────────────────────
|
|
if __name__ == "__main__":
|
|
# Run per-asset study (vol-targeted, long-flat per asset)
|
|
# This is equivalent to inverse-vol RP: each asset separately scaled by 1/vol
|
|
TFS = ("1d", "12h")
|
|
|
|
best_rep = None
|
|
best_holdout = -999
|
|
|
|
for (vol_win, lev_cap) in CONFIGS:
|
|
name = f"RSK03-InvVol-vw{vol_win}d"
|
|
fn = make_target(vol_win, lev_cap)
|
|
rep = al.study_weights(name, fn, tfs=TFS)
|
|
|
|
verdict = rep["verdict"]
|
|
hold_sh = verdict.get("best_holdout_sharpe", -999) or -999
|
|
print(al.fmt(rep))
|
|
print()
|
|
|
|
if hold_sh > best_holdout:
|
|
best_holdout = hold_sh
|
|
best_rep = rep
|
|
|
|
# Also print the combined BTC+ETH blend for the best config
|
|
best_vw = CONFIGS[0][0] if best_rep is None else (
|
|
int(best_rep["name"].split("vw")[1].replace("d", ""))
|
|
)
|
|
best_lev = CONFIGS[0][1]
|
|
|
|
print("\n=== COMBINED BTC+ETH Blend (Inverse-Vol Risk Parity) ===")
|
|
for tf in TFS:
|
|
full, hold, yearly = combined_rp_report(best_vw, best_lev, tf)
|
|
yr_str = " ".join(f"{y}:{d['ret']*100:+.0f}%" for y, d in list(yearly.items()))
|
|
print(f" TF {tf}: FULL Sh={full['sharpe']:+.2f} DD={full['maxdd']*100:.0f}% "
|
|
f"ret={full['ret']*100:+.0f}% | HOLD Sh={hold.get('sharpe',0):+.2f} "
|
|
f"ret={hold.get('ret',0)*100:+.0f}% | {yr_str}")
|
|
|
|
print()
|
|
print(al.fmt(best_rep))
|
|
print("JSON:", al.as_json(best_rep))
|