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>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"""SEA05 — Intraday Momentum (1h)
|
||||
|
||||
HYPOTHESIS: On 1h bars, the sign of the morning session (00:00-11:59 UTC cumulative return)
|
||||
predicts the afternoon session (12:00-23:59 UTC). Position taken at bar open of 12:00 UTC
|
||||
and held through 23:59 UTC. Causal: morning return is fully known before entering at 12:00 close.
|
||||
|
||||
Implementation:
|
||||
- Use 1h data only (the hypothesis requires intraday structure)
|
||||
- For each day, compute morning cumulative return: product of (1+r) from 00:00 to 11:00 UTC (12 bars)
|
||||
- At 12:00 UTC bar (i), compute signal from morning session (known at close[i-1] or earlier)
|
||||
- Position = sign(morning_return) held for 12 bars (12:00 to 23:00 UTC)
|
||||
- Vol-targeted continuous weights with vol_target(signal, df)
|
||||
|
||||
Grid: try 2 variants:
|
||||
A) raw sign (morning ret sign -> afternoon position)
|
||||
B) z-score of morning returns (magnitude matters -> stronger signal -> larger position)
|
||||
"""
|
||||
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 make_intraday_mom_signal(df: pd.DataFrame, use_zscore: bool = False, lookback_z: int = 20) -> np.ndarray:
|
||||
"""
|
||||
For each 1h bar, compute an intraday momentum signal.
|
||||
|
||||
Logic (causal):
|
||||
- Morning session = hours 0..11 UTC (12 bars per day)
|
||||
- At hour 12 (bar index where hour==12), the morning is complete
|
||||
- Signal = sign of morning cumulative return
|
||||
- Held for bars where hour in [12..23]
|
||||
- At hour 0 next day: flat (we re-evaluate)
|
||||
|
||||
target[i] is set for bar i, evaluated with data up to close[i-1] for the morning.
|
||||
Actually: at bar with hour==12, close[i-1] is 11:00 close = last morning bar close.
|
||||
Morning return = close[11:00] / open[00:00] - 1 (for that day).
|
||||
"""
|
||||
dt = df["datetime"]
|
||||
hour = dt.dt.hour
|
||||
|
||||
# Compute log returns for each bar
|
||||
close = df["close"].values
|
||||
log_ret = np.zeros(len(df))
|
||||
log_ret[1:] = np.log(close[1:] / close[:-1])
|
||||
|
||||
# Build daily morning cumulative return
|
||||
# For each bar at hour==12, sum log returns from hours 1..11 of same day
|
||||
# (hour 0 bar's return is from previous day's close to 00:00 close, we include it too)
|
||||
|
||||
n = len(df)
|
||||
target = np.zeros(n)
|
||||
|
||||
# We'll track morning cum-ret per day
|
||||
# Iterate bar by bar: accumulate morning, set signal at 12:00
|
||||
|
||||
day_morning_cumret = 0.0
|
||||
morning_rets_history = [] # for z-score
|
||||
in_morning = False
|
||||
|
||||
for i in range(n):
|
||||
h = hour.iloc[i]
|
||||
|
||||
if h == 0:
|
||||
# Start of a new day: reset morning accumulator
|
||||
day_morning_cumret = 0.0
|
||||
in_morning = True
|
||||
|
||||
if in_morning and h < 12:
|
||||
# Accumulate morning log return
|
||||
day_morning_cumret += log_ret[i]
|
||||
|
||||
elif h == 12:
|
||||
# Morning complete, set position for afternoon
|
||||
in_morning = False
|
||||
|
||||
if use_zscore and len(morning_rets_history) >= lookback_z:
|
||||
hist = np.array(morning_rets_history[-lookback_z:])
|
||||
mu = hist.mean()
|
||||
sigma = hist.std()
|
||||
if sigma > 1e-8:
|
||||
z = (day_morning_cumret - mu) / sigma
|
||||
# Clip to [-3, 3] and normalize
|
||||
pos = np.clip(z / 2.0, -1.0, 1.0)
|
||||
else:
|
||||
pos = 0.0
|
||||
else:
|
||||
# Simple sign
|
||||
pos = np.sign(day_morning_cumret)
|
||||
|
||||
# Set target for this bar (12:00) and keep for afternoon
|
||||
# But we need to be careful: target[i] uses data up to close[i]
|
||||
# which is close of 12:00 bar. Actually we want to position DURING 12:00-23:00.
|
||||
# al.study_weights holds target[i] during bar i+1.
|
||||
# So: at bar i-1 (11:00), we know morning is done (close[i-1] = 11:00 close).
|
||||
# We should set target[i-1] to the signal so it's held during bar i (12:00 bar).
|
||||
# But that's complex. Instead: set target at i=12:00 bar using morning already
|
||||
# computed (morning is 00:00 to 11:00, all known before 12:00 bar opens).
|
||||
# The lib holds target[i] for bar i+1. So we set it at bar h==11 (last morning bar).
|
||||
# But we compute it here at h==12 for simplicity — let's adjust:
|
||||
# Actually set at h==11 (previous bar). We'll do a post-pass.
|
||||
|
||||
# Store for z-score history
|
||||
morning_rets_history.append(day_morning_cumret)
|
||||
|
||||
# We mark this as "12h signal" to be applied starting from 12:00 bar
|
||||
# Since lib shifts: target[i] held during bar i+1, we need target at i where h==11
|
||||
# We'll fix this in a second pass below; for now store in target[i]
|
||||
target[i] = pos
|
||||
|
||||
elif h > 12:
|
||||
# Carry afternoon position forward
|
||||
target[i] = target[i-1]
|
||||
# else h in [1..11] or h==0: flat (0)
|
||||
|
||||
# Shift the signal: target[i] where h==12 should be moved to h==11 bar
|
||||
# so that lib holds it during h==12 bar (bar i+1 from lib's perspective)
|
||||
# Find all bars where h==12, move signal to i-1 (h==11)
|
||||
afternoon_signal = np.zeros(n)
|
||||
i = 0
|
||||
while i < n:
|
||||
h = hour.iloc[i]
|
||||
if h == 12 and target[i] != 0:
|
||||
sig = target[i]
|
||||
# Set signal starting at i-1 (11:00 bar), lib holds it for bar i (12:00)
|
||||
# Actually we want to hold signal for bars 12..23
|
||||
# target[i-1] -> held during bar i (12:00) ✓
|
||||
# target[i] -> held during bar i+1 (13:00) ✓
|
||||
# ...
|
||||
# target[i+10] -> held during bar i+11 (23:00) ✓
|
||||
# total: 12 bars (12:00-23:00)
|
||||
if i - 1 >= 0:
|
||||
afternoon_signal[i-1] = sig # held during bar i (12:00)
|
||||
for k in range(i, min(i+11, n)): # bars 12:00..22:00 -> held during 13:00..23:00
|
||||
afternoon_signal[k] = sig
|
||||
i += 12
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return afternoon_signal
|
||||
|
||||
|
||||
def make_vol_targeted(df: pd.DataFrame, use_zscore: bool = False, lookback_z: int = 20) -> np.ndarray:
|
||||
"""Intraday momentum with vol targeting."""
|
||||
raw_signal = make_intraday_mom_signal(df, use_zscore=use_zscore, lookback_z=lookback_z)
|
||||
# Vol-target: direction = sign(raw_signal), magnitude from vol_target
|
||||
direction = np.sign(raw_signal)
|
||||
w = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
|
||||
return w
|
||||
|
||||
|
||||
# Run the study with 2 variants on 1h only
|
||||
print("=" * 60)
|
||||
print("SEA05 — Intraday Momentum (1h)")
|
||||
print("=" * 60)
|
||||
|
||||
# Variant A: simple sign, vol-targeted
|
||||
print("\n--- Variant A: sign(morning_ret), vol-targeted ---")
|
||||
rep_a = al.study_weights(
|
||||
"SEA05-A-sign",
|
||||
lambda df: make_vol_targeted(df, use_zscore=False),
|
||||
tfs=("1h",)
|
||||
)
|
||||
print(al.fmt(rep_a))
|
||||
print("JSON:", al.as_json(rep_a))
|
||||
|
||||
# Variant B: z-score based magnitude, vol-targeted
|
||||
print("\n--- Variant B: zscore(morning_ret), vol-targeted ---")
|
||||
rep_b = al.study_weights(
|
||||
"SEA05-B-zscore",
|
||||
lambda df: make_vol_targeted(df, use_zscore=True, lookback_z=20),
|
||||
tfs=("1h",)
|
||||
)
|
||||
print(al.fmt(rep_b))
|
||||
print("JSON:", al.as_json(rep_b))
|
||||
|
||||
# Pick best by min_asset_full_sharpe
|
||||
best = rep_a if rep_a["verdict"]["best_full_sharpe"] >= rep_b["verdict"]["best_full_sharpe"] else rep_b
|
||||
print("\n=== BEST CONFIG ===")
|
||||
print(al.fmt(best))
|
||||
print("JSON:", al.as_json(best))
|
||||
Reference in New Issue
Block a user