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>
126 lines
4.7 KiB
Python
126 lines
4.7 KiB
Python
"""MRV05 — Williams %R Mean-Reversion
|
|
|
|
HYPOTHESIS: Buy when %R(14) < -90 (oversold) with trend filter (close > SMA200);
|
|
exit (go flat) when %R > -50 (momentum restored). Long-flat only.
|
|
|
|
Williams %R = (Highest High(n) - Close) / (Highest High(n) - Lowest Low(n)) * -100
|
|
Range: -100 (most oversold) to 0 (most overbought).
|
|
%R < -80 = oversold zone; %R > -20 = overbought zone.
|
|
|
|
The exit condition (%R > -50) is causal: we check %R[i] and decide position for bar i+1.
|
|
This maps naturally to study_weights (continuous hold logic):
|
|
- position[i] = 1 if %R[i] < -90 AND close[i] > SMA200[i] (buy signal)
|
|
- position[i] = 0 if %R[i] > -50 (exit signal)
|
|
- else hold previous position
|
|
|
|
Variants (small grid, 4 configs):
|
|
V1: %R entry -90, exit -50, SMA200 trend filter, long-flat
|
|
V2: %R entry -85, exit -50, SMA200 trend filter, long-flat (slightly less oversold entry)
|
|
V3: %R entry -90, exit -50, SMA50 trend filter, long-flat (shorter trend filter)
|
|
V4: %R entry -90, exit -40, SMA200 trend filter, long-flat (later exit)
|
|
|
|
Best variant selected by min-asset hold-out Sharpe.
|
|
All positions are vol-targeted (20% annualized, 2x leverage cap).
|
|
"""
|
|
|
|
import sys
|
|
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
|
import altlib as al
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Williams %R calculation (causal: uses data <= bar i)
|
|
# ---------------------------------------------------------------------------
|
|
def williams_r(df: pd.DataFrame, win: int = 14) -> np.ndarray:
|
|
"""Causal Williams %R. Value at i uses data[i-win+1 .. i].
|
|
%R = (HH - Close) / (HH - LL) * -100
|
|
Range: -100 (oversold) to 0 (overbought).
|
|
"""
|
|
h = df["high"].values.astype(float)
|
|
l = df["low"].values.astype(float)
|
|
c = df["close"].values.astype(float)
|
|
n = len(c)
|
|
wr = np.full(n, np.nan)
|
|
# Vectorized rolling using pandas
|
|
hh = pd.Series(h).rolling(win, min_periods=win).max().values
|
|
ll = pd.Series(l).rolling(win, min_periods=win).min().values
|
|
rng = hh - ll
|
|
# Avoid division by zero
|
|
valid = rng > 0
|
|
wr[valid] = (hh[valid] - c[valid]) / rng[valid] * -100.0
|
|
return wr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Strategy factory
|
|
# ---------------------------------------------------------------------------
|
|
def make_wrpct_target(wr_entry: float = -90.0, wr_exit: float = -50.0,
|
|
sma_win: int = 200, wr_win: int = 14):
|
|
"""Williams %R long-flat mean-reversion with trend filter.
|
|
|
|
Entry: %R[i] < wr_entry AND close[i] > SMA(sma_win)[i] -> go long
|
|
Exit: %R[i] > wr_exit -> go flat
|
|
Hold: otherwise, maintain current position
|
|
|
|
Causal: position decided using data <= close[i], held during bar i+1.
|
|
Vol-targeted: 20% annualized, 2x leverage cap.
|
|
"""
|
|
def target_fn(df):
|
|
c = df["close"].values.astype(float)
|
|
wr = williams_r(df, wr_win)
|
|
sma_trend = al.sma(c, sma_win)
|
|
|
|
# Vectorized state machine using ffill
|
|
# Signal: 1 = enter long, 0 = exit to flat, NaN = hold
|
|
# Priority: exit takes precedence over entry
|
|
sig = np.where(
|
|
wr > wr_exit, # exit condition
|
|
0.0,
|
|
np.where(
|
|
(wr < wr_entry) & (c > sma_trend), # entry condition
|
|
1.0,
|
|
np.nan # hold
|
|
)
|
|
)
|
|
|
|
# Start flat
|
|
sig[0] = 0.0
|
|
|
|
# Forward-fill NaN (hold previous position)
|
|
pos = pd.Series(sig).ffill().fillna(0.0).values
|
|
|
|
# Vol-target
|
|
return al.vol_target(pos, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
|
|
|
|
return target_fn
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Run all variants and pick best
|
|
# ---------------------------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
TFS = ("1d",)
|
|
|
|
variants = [
|
|
("MRV05-V1-WR90-exit50-SMA200", make_wrpct_target(-90.0, -50.0, 200, 14)),
|
|
("MRV05-V2-WR85-exit50-SMA200", make_wrpct_target(-85.0, -50.0, 200, 14)),
|
|
("MRV05-V3-WR90-exit50-SMA50", make_wrpct_target(-90.0, -50.0, 50, 14)),
|
|
("MRV05-V4-WR90-exit40-SMA200", make_wrpct_target(-90.0, -40.0, 200, 14)),
|
|
]
|
|
|
|
results = []
|
|
for name, fn in variants:
|
|
print(f"\nRunning {name} ...")
|
|
rep = al.study_weights(name, fn, tfs=TFS)
|
|
print(al.fmt(rep))
|
|
results.append(rep)
|
|
|
|
# Pick best by min_asset_holdout_sharpe
|
|
best = max(results, key=lambda r: r["verdict"].get("best_holdout_sharpe", -99))
|
|
print("\n" + "=" * 60)
|
|
print(f"BEST VARIANT: {best['name']}")
|
|
print(al.fmt(best))
|
|
print("JSON:", al.as_json(best))
|