Files
Adriano Dal Pastro 5ac4e16af8 research(alt): sweep 104 strategie alternative su Deribit (153 agenti) + marginal scorer
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>
2026-06-20 19:50:39 +00:00

151 lines
5.0 KiB
Python

"""TRD05 — ADX-filtered EMA crossover.
Hypothesis: EMA(fast, slow) cross provides directional signal ONLY when ADX(14) > threshold
(trending regime). When ADX is below the threshold (chop), position goes flat.
Grid (<=4 param sets, total backtests = 4 params * 2 assets * 2 tfs = 16, but we limit to 2 TFs):
(fast_ema, slow_ema, adx_period, adx_thresh)
- (20, 100, 14, 25) — canonical from hypothesis
- (10, 50, 14, 25) — faster cross
- (20, 100, 14, 20) — more lenient ADX gate
- (5, 20, 14, 25) — short-term cross with ADX filter
We run 4 configs but only 1 TF at a time to stay within 2-CPU budget.
Best config selected by min-asset holdout Sharpe across 2 TFs (1d, 12h).
ADX calculation (causal):
+DM[i] = max(high[i]-high[i-1], 0) if > (low[i-1]-low[i]) else 0
-DM[i] = max(low[i-1]-low[i], 0) if > (high[i]-high[i-1]) else 0
TR[i] = max(high[i]-low[i], |high[i]-close[i-1]|, |low[i]-close[i-1]|)
Smooth over `period` with Wilder's EMA (alpha=1/period)
+DI = 100 * smooth(+DM) / smooth(TR)
-DI = 100 * smooth(-DM) / smooth(TR)
DX = 100 * |+DI - -DI| / (+DI + -DI)
ADX = Wilder EMA(DX, period)
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
import pandas as pd
def _wilder_ema(x: np.ndarray, period: int) -> np.ndarray:
"""Wilder smoothing (EMA with alpha=1/period, adjust=False)."""
alpha = 1.0 / period
out = np.empty(len(x), dtype=float)
out[0] = x[0]
for i in range(1, len(x)):
out[i] = out[i - 1] * (1.0 - alpha) + x[i] * alpha
return out
def _adx(df: pd.DataFrame, period: int = 14) -> np.ndarray:
"""Compute causal ADX(period). Returns array len(df), NaN for first ~2*period bars."""
h = df["high"].values.astype(float)
l = df["low"].values.astype(float)
c = df["close"].values.astype(float)
n = len(h)
# True Range
pc = np.roll(c, 1)
pc[0] = c[0]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
# Directional Movements
up = h - np.roll(h, 1)
dn = np.roll(l, 1) - l
up[0] = 0.0
dn[0] = 0.0
pos_dm = np.where((up > dn) & (up > 0), up, 0.0)
neg_dm = np.where((dn > up) & (dn > 0), dn, 0.0)
# Wilder smooth
str_ = _wilder_ema(tr, period)
spdm = _wilder_ema(pos_dm, period)
sndm = _wilder_ema(neg_dm, period)
# DI lines
pdi = 100.0 * np.where(str_ > 0, spdm / str_, 0.0)
ndi = 100.0 * np.where(str_ > 0, sndm / str_, 0.0)
# DX and ADX
denom = pdi + ndi
dx = np.where(denom > 0, 100.0 * np.abs(pdi - ndi) / denom, 0.0)
adx = _wilder_ema(dx, period)
# First 2*period bars are warm-up — NaN them
adx[:2 * period] = np.nan
return adx
def make_target(fast: int, slow: int, adx_period: int, adx_thresh: float,
vol_target: bool = True):
"""Return a target_fn for study_weights that implements ADX-filtered EMA cross."""
def target_fn(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
ema_fast = al.ema(c, fast)
ema_slow = al.ema(c, slow)
adx_vals = _adx(df, adx_period)
# Signal: +1 if fast > slow (bullish trend), -1 if fast < slow (bearish)
# Flat when ADX < threshold (choppy) or ADX is NaN (warmup)
cross_signal = np.where(ema_fast > ema_slow, 1.0, -1.0)
trending = np.where(
np.isfinite(adx_vals) & (adx_vals > adx_thresh),
1.0, 0.0
)
direction = cross_signal * trending
# Long-flat only (like TP01, we don't short crypto)
# Actually let's try L/S first since hypothesis doesn't restrict
direction_lf = np.clip(direction, 0, 1) # long-flat version
if vol_target:
return al.vol_target(direction_lf, df, target_vol=0.20, vol_win_days=30,
leverage_cap=2.0)
else:
return direction_lf
return target_fn
# --- Grid of configs ---------------------------------------------------------
CONFIGS = [
dict(fast=20, slow=100, adx_period=14, adx_thresh=25), # canonical
dict(fast=10, slow=50, adx_period=14, adx_thresh=25), # faster cross
dict(fast=20, slow=100, adx_period=14, adx_thresh=20), # relaxed gate
dict(fast=5, slow=20, adx_period=14, adx_thresh=25), # short-term
]
# We test 2 timeframes: 1d and 12h (within 2-CPU budget constraint)
TFS = ("1d", "12h")
best_rep = None
best_score = -999.0
print("=== TRD05: ADX-filtered EMA crossover ===\n")
for cfg in CONFIGS:
label = f"TRD05(ema{cfg['fast']}/{cfg['slow']},adx{cfg['adx_period']}>{cfg['adx_thresh']})"
fn = make_target(**cfg)
rep = al.study_weights(label, fn, tfs=TFS)
print(al.fmt(rep))
print()
# Score = min holdout sharpe across cells
score = rep["verdict"].get("best_holdout_sharpe", -999.0) or -999.0
if score > best_score:
best_score = score
best_rep = rep
best_cfg = cfg
print("\n" + "=" * 60)
print(f"BEST CONFIG: {best_cfg}")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))