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

183 lines
7.1 KiB
Python

"""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))