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>
183 lines
7.1 KiB
Python
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))
|