Files
PythagorasGoal/scripts/research/alt/runs/CMB06.py
T
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

166 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CMB06 — Trend + Seasonality Combo
IDEA: TSMOM long-flat (multi-horizon, vol-targeted, like TP01) but scale the
exposure UP in historically strong calendar windows (day-of-week + month-of-year
expanding expanding expectancy). Causal only: expectancy estimated on expanding window
using data BEFORE the current bar.
Design:
- Base signal: TSMOM multi-horizon (1M / 3M / 6M), long-flat, vote-then-sign
- Volatility targeting: 20% target, 2x lev cap (same as TP01)
- Seasonality multiplier: expand-window daily/monthly return expectancy,
normalised to [scale_min, scale_max] so it's a scalar boost, not a flip.
The multiplier is always >= 0 (never inverts the trend).
Causal guarantee:
- Day-of-week expectancy at bar i uses only past bars (strict shift: computed on
data up to bar i-1, applied at bar i).
- Month-of-year same.
- Both use EXPANDING window (not rolling) -> no future-data leak, and it
gradually stabilises as history accumulates.
Grid (4 params): 2 scale ranges × 2 TFs = 4 cells total.
"""
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 _expanding_dow_expectancy(df: pd.DataFrame) -> np.ndarray:
"""For each bar, return the expanding-window mean return of the same day-of-week,
computed on PAST bars only (shift 1). Returns NaN until at least 4 samples exist."""
c = df["close"].values.astype(float)
r = al.simple_returns(c) # r[i] = return realized at bar i
dt = pd.to_datetime(df["datetime"], utc=True)
dow = dt.dt.dayofweek.values # 0=Mon..6=Sun
exp = np.full(len(r), np.nan)
# For each bar i, compute mean return of same DOW for all bars j < i
# Use expanding sum by DOW category
dow_sum = np.zeros(7, dtype=float)
dow_cnt = np.zeros(7, dtype=int)
for i in range(1, len(r)):
# update with bar i-1 (strictly past)
d_prev = dow[i - 1]
dow_sum[d_prev] += r[i - 1]
dow_cnt[d_prev] += 1
d = dow[i]
if dow_cnt[d] >= 4:
exp[i] = dow_sum[d] / dow_cnt[d]
return exp
def _expanding_month_expectancy(df: pd.DataFrame) -> np.ndarray:
"""Same but for month-of-year (1..12). Requires >= 4 past bars in same month."""
c = df["close"].values.astype(float)
r = al.simple_returns(c)
dt = pd.to_datetime(df["datetime"], utc=True)
moy = dt.dt.month.values # 1..12
exp = np.full(len(r), np.nan)
mo_sum = np.zeros(13, dtype=float)
mo_cnt = np.zeros(13, dtype=int)
for i in range(1, len(r)):
m_prev = moy[i - 1]
mo_sum[m_prev] += r[i - 1]
mo_cnt[m_prev] += 1
m = moy[i]
if mo_cnt[m] >= 4:
exp[i] = mo_sum[m] / mo_cnt[m]
return exp
def _seasonality_multiplier(df: pd.DataFrame, scale_min: float, scale_max: float) -> np.ndarray:
"""Combine DOW + month expanding expectancy into a [scale_min, scale_max] multiplier.
When either is NaN (early history), default to 1.0 (neutral)."""
dow_exp = _expanding_dow_expectancy(df)
mon_exp = _expanding_month_expectancy(df)
# Normalise each to [-1, +1] range using the expanding-window min/max seen so far.
# We use a causal expanding percentile: zscore is simpler and avoids percentile loop.
# Use zscore over an expanding window instead (pandas expanding).
dow_s = pd.Series(dow_exp)
mon_s = pd.Series(mon_exp)
# Causal z-score (expanding)
dow_z = (dow_s - dow_s.expanding().mean()) / dow_s.expanding().std().replace(0, np.nan)
mon_z = (mon_s - mon_s.expanding().mean()) / mon_s.expanding().std().replace(0, np.nan)
# Blend (equal weight)
combined = (dow_z.fillna(0.0) + mon_z.fillna(0.0)).values / 2.0
# Map to [scale_min, scale_max] via sigmoid-like clamp
# clip to [-2, 2] sigma, then linearly map
combined_clipped = np.clip(combined, -2.0, 2.0)
mid = (scale_min + scale_max) / 2.0
half_range = (scale_max - scale_min) / 2.0
mult = mid + half_range * (combined_clipped / 2.0)
# Where both were NaN (very early bars), use neutral = 1.0
both_nan = dow_s.isna().values & mon_s.isna().values
mult[both_nan] = 1.0
return mult
def _tsmom_base(df: pd.DataFrame) -> np.ndarray:
"""Multi-horizon TSMOM: 1M/3M/6M vote, long-flat, vol-targeted."""
c = df["close"].values.astype(float)
bpd = al.bars_per_day(df)
d = np.zeros(len(c))
for months in (1, 3, 6):
h = int(months * 30 * bpd)
if h >= len(c):
continue
s = np.full(len(c), np.nan)
s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
d = d + np.nan_to_num(s)
direction = np.clip(np.sign(d), 0, None) # long-flat only
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
def make_target(scale_min: float, scale_max: float):
"""Return a target_fn that applies the seasonality multiplier."""
def target_fn(df: pd.DataFrame) -> np.ndarray:
base = _tsmom_base(df)
mult = _seasonality_multiplier(df, scale_min, scale_max)
combined = base * mult
# Keep within leverage cap
combined = np.clip(combined, 0.0, 2.0)
combined = np.nan_to_num(combined, nan=0.0)
return combined
return target_fn
if __name__ == "__main__":
# Grid: 2 scale ranges × 2 TFs = 4 cells
# scale_min/max: how much to reduce/boost position in weak/strong seasons
# (0.5, 1.5) = modest 50% swing; (0.25, 1.75) = aggressive 150% swing
configs = [
("CMB06-modest", 0.5, 1.5),
("CMB06-aggr", 0.25, 1.75),
]
all_reps = []
for name, smin, smax in configs:
print(f"\n=== Running {name} (scale [{smin},{smax}]) ===")
rep = al.study_weights(name, make_target(smin, smax), tfs=("1d", "12h"))
print(al.fmt(rep))
all_reps.append((name, rep))
# Pick best by min_asset_holdout_sharpe at best TF
def best_holdout(rep):
return max(c["min_asset_holdout_sharpe"] for c in rep["cells"])
best_name, best_rep = max(all_reps, key=lambda x: best_holdout(x[1]))
print(f"\n>>> BEST CONFIG: {best_name}")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))