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>
106 lines
3.8 KiB
Python
106 lines
3.8 KiB
Python
"""MIC03 — Volume-spike breakout
|
|
Hypothesis: Breakout of prior high CONFIRMED by volume z-score > threshold -> enter long at close.
|
|
Exit: TP, SL, or max_bars timeout.
|
|
|
|
Implementation:
|
|
- Breakout: close[i] > donchian_high(win)[i] (prior win-bar high, shifted by 1 so fully causal)
|
|
- Volume confirmation: volume z-score over vol_win bars > vol_thresh
|
|
- Entry at close[i], direction = long only (breakouts on the upside)
|
|
- TP = entry * (1 + tp_pct), SL = entry * (1 - sl_pct), max_bars timeout
|
|
|
|
Grid (<=4 param sets, 1d only -> total backtests = 4 * 2 assets = 8 <= 6... actually 8.
|
|
Reduce to 2 configs to stay within ~6 backtests and avoid slow fee sweeps):
|
|
Config A: donchian 20, vol_win 20, vol_thresh 2.0, tp 3%, sl 1.5%, max_bars 10
|
|
Config B: donchian 30, vol_win 30, vol_thresh 1.5, tp 4%, sl 2.0%, max_bars 15
|
|
|
|
Pick the best config by min_asset_holdout_sharpe.
|
|
"""
|
|
|
|
import sys
|
|
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
|
import altlib as al
|
|
import numpy as np
|
|
|
|
|
|
def make_entries_fn(don_win: int, vol_win: int, vol_thresh: float,
|
|
tp_pct: float, sl_pct: float, max_bars: int):
|
|
def entries_fn(df):
|
|
close = df["close"].values.astype(float)
|
|
volume = df["volume"].values.astype(float)
|
|
n = len(close)
|
|
|
|
# Donchian upper channel: prior don_win-bar HIGH (shifted, causal)
|
|
# Using high prices for breakout reference (breakout above prior high is more meaningful)
|
|
high = df["high"].values.astype(float)
|
|
don_hi = np.full(n, np.nan)
|
|
# rolling max of high over don_win bars, then shift by 1 (prior bar)
|
|
for i in range(don_win, n):
|
|
don_hi[i] = np.max(high[i - don_win: i]) # excludes bar i -> causal
|
|
|
|
# Volume z-score (causal): zscore of current volume vs rolling mean/std
|
|
vol_mean = np.full(n, np.nan)
|
|
vol_std = np.full(n, np.nan)
|
|
for i in range(vol_win, n):
|
|
v_window = volume[i - vol_win: i] # excludes current bar
|
|
vol_mean[i] = np.mean(v_window)
|
|
vol_std[i] = np.std(v_window)
|
|
|
|
vol_z = np.full(n, np.nan)
|
|
mask = (vol_std > 0) & np.isfinite(vol_std)
|
|
vol_z[mask] = (volume[mask] - vol_mean[mask]) / vol_std[mask]
|
|
|
|
# Build entry list
|
|
entries = [None] * n
|
|
for i in range(don_win + vol_win, n):
|
|
# Breakout condition: close breaks above prior don_win-bar high
|
|
breakout = (np.isfinite(don_hi[i]) and close[i] > don_hi[i])
|
|
# Volume confirmation
|
|
vol_confirmed = (np.isfinite(vol_z[i]) and vol_z[i] > vol_thresh)
|
|
|
|
if breakout and vol_confirmed:
|
|
entry_px = close[i] # fill at close[i]
|
|
tp_px = entry_px * (1.0 + tp_pct)
|
|
sl_px = entry_px * (1.0 - sl_pct)
|
|
entries[i] = {
|
|
"dir": +1,
|
|
"tp": tp_px,
|
|
"sl": sl_px,
|
|
"max_bars": max_bars,
|
|
}
|
|
|
|
return entries
|
|
|
|
return entries_fn
|
|
|
|
|
|
# Config A: tighter params
|
|
config_a = dict(don_win=20, vol_win=20, vol_thresh=2.0, tp_pct=0.03, sl_pct=0.015, max_bars=10)
|
|
# Config B: wider params
|
|
config_b = dict(don_win=30, vol_win=30, vol_thresh=1.5, tp_pct=0.04, sl_pct=0.02, max_bars=15)
|
|
|
|
configs = [
|
|
("MIC03-A", config_a),
|
|
("MIC03-B", config_b),
|
|
]
|
|
|
|
best_rep = None
|
|
best_score = -999.0
|
|
|
|
for cfg_name, cfg in configs:
|
|
print(f"\n--- Running {cfg_name}: {cfg} ---")
|
|
fn = make_entries_fn(**cfg)
|
|
rep = al.study_signals(cfg_name, fn, tfs=("1d",))
|
|
print(al.fmt(rep))
|
|
print("JSON:", al.as_json(rep))
|
|
|
|
score = rep["verdict"].get("best_holdout_sharpe", -999) or -999
|
|
if score > best_score:
|
|
best_score = score
|
|
best_rep = rep
|
|
best_rep["_config"] = cfg
|
|
best_rep["_config_name"] = cfg_name
|
|
|
|
print("\n\n=== BEST CONFIG ===")
|
|
print(al.fmt(best_rep))
|
|
print("JSON:", al.as_json(best_rep))
|