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

188 lines
7.5 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.
"""SEA08 — US-session momentum on 1h bars.
HYPOTHESIS: On 1h: go long during 13-21 UTC when the prior (Asian+London) session
was positive; otherwise flat. Idea: captures US risk-on drift when prior price
action was constructive.
CAUSALITY CHECK:
- "Prior session" = we look at the cumulative return of bars from the prior day's
Asian+London window (00-12 UTC) that CLOSED before bar[i].
- We compute the prior-session return as the log return from close[previous_day_00:00 UTC]
to close[current_day_12:00 UTC], decided at bar[i] open (i.e., at close[i-1]).
- Actually, we'll compute it simpler: the bar that ENDS at 12:00 UTC (the last
Asian/London bar), and compare vs the bar that started the day (00:00 UTC).
- For each hourly bar[i], at close[i-1] (= open of bar[i]), we know:
* current UTC hour of bar[i]
* the close at 12:00 UTC of today (if past 12:00)
* the open at 00:00 UTC of today
- Implementation: for each bar ending at time t (with UTC hour h):
* If h in [13,21]: session is active
* prior_session_return = (close at 12:00 of the current day / close at 00:00 of current day) - 1
* We read close[i-1] with hour h (0-indexed, bar closes at h:00 UTC = bar represents h-1:00 to h:00)
* Position at bar i = long (1.0) if h in [14..22] (bars DURING 13-21 UTC) AND prior session positive
Wait - let me be precise about 1h bar labeling:
- A bar timestamped at "13:00 UTC" represents the candle from 12:00 to 13:00 UTC.
- "close[13:00]" = price at end of 13:00 bar = price at 13:00 UTC.
For US session: we want to be long FROM 13:00 UTC TO 21:00 UTC.
- We want to hold during bars whose close times are 14:00, 15:00, ..., 21:00 UTC
(i.e., the bar from 13:00-14:00, ..., 20:00-21:00).
CAUSAL DECISION AT close[i]:
- For each bar[i], we compute target[i] (what position to hold during bar i+1).
- Bar i+1 closes at hour h+1.
- We want to be long during bar i+1 if h+1 in {14,15,...,21}.
- So target[i] = 1 if h in {13,...,20} AND prior_session_ret > 0.
- prior_session_ret: from close at midnight (00:00 UTC) to close at noon (12:00 UTC) of the same day.
- At close[i] with h in [13..20], we already know close[12:00] of today (it's in the past).
GRID: 3 variants tested to find best config:
1. Pure time filter (no prior session condition)
2. Prior session > 0 (baseline hypothesis)
3. Prior session + vol-target scaling
We keep TF = 1h only (the hypothesis is inherently intraday on 1h bars).
Total backtests: 1 tf × 3 variants × 2 assets = 6. Within budget.
"""
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 _build_session_features(df: pd.DataFrame):
"""
For each 1h bar at index i:
- dt[i] = the UTC datetime when this bar closes (label of bar)
- hour[i] = UTC hour of bar close
- prior_session_ret[i] = return from close at 00:00 UTC to close at 12:00 UTC
of the same day as bar[i], computed CAUSALLY (only available if bar[i] closes after 12:00 UTC).
Returns (hour_arr, prior_session_ret_arr).
"""
dt = pd.to_datetime(df["datetime"], utc=True)
close = df["close"].values.astype(float)
n = len(df)
hour_arr = dt.dt.hour.values # UTC hour of bar close
# Build a lookup: for each (date, hour_target) -> close price
# We need close at 00:00 UTC and close at 12:00 UTC for each date.
#
# The bar timestamped/labeled at 00:00 UTC closes at midnight = end of prior day.
# So "open of day" price = close of the 23:00 bar (previous day) or close of 00:00 bar.
#
# Let's use simpler: close at 12:00 UTC bar (hour==12) as end of prior session.
# Anchor = close at 00:00 UTC bar (hour==0) as start of day.
# prior_session_ret = close[12:00] / close[00:00] - 1, for the same calendar date.
#
# To be causal at bar[i] with hour[i] >= 13: we need close[12:00] of same day,
# which was available since 12:00 UTC (in the past).
# Build date -> index of 00:00 and 12:00 bars
dates = dt.dt.date.values
# For each bar, find the closest prior-session data
prior_ret = np.full(n, np.nan)
# Create a series indexed by datetime for easy lookup
close_series = pd.Series(close, index=dt)
# Group by date to find the 00:00 and 12:00 anchors per day
date_anchors = {} # date -> (close_00, close_12)
for i in range(n):
d = dates[i]
h = hour_arr[i]
if d not in date_anchors:
date_anchors[d] = [np.nan, np.nan] # [close_00, close_12]
if h == 0:
date_anchors[d][0] = close[i]
elif h == 12:
date_anchors[d][1] = close[i]
# Now fill prior_ret for each bar
for i in range(n):
d = dates[i]
h = hour_arr[i]
# Only compute if bar is in US session window and after 12:00 UTC
if h >= 13 and d in date_anchors:
c00, c12 = date_anchors[d]
if np.isfinite(c00) and np.isfinite(c12) and c00 > 0:
prior_ret[i] = c12 / c00 - 1.0
return hour_arr, prior_ret
def target_time_only(df: pd.DataFrame) -> np.ndarray:
"""
Variant 1: Pure US-session time filter (13-21 UTC), no prior-session condition.
Long during US session hours, flat otherwise.
target[i] = 1.0 if bar[i+1] is in US session, else 0.0
= 1.0 if hour[i] in {13,...,20} (so bar i+1 closes at 14..21 UTC).
"""
hour_arr, _ = _build_session_features(df)
# target[i] = position held during bar i+1
# bar i+1 closes at hour (hour_arr[i] + 1) % 24 approximately,
# but let's use: hold long if hour[i] in 13..20 so we're long during 13:00->21:00 window
target = np.where((hour_arr >= 13) & (hour_arr <= 20), 1.0, 0.0)
return target
def target_prior_session_momentum(df: pd.DataFrame) -> np.ndarray:
"""
Variant 2: Long during US session (13-21 UTC) ONLY IF prior session (00-12 UTC) was positive.
"""
hour_arr, prior_ret = _build_session_features(df)
# Propagate prior_ret within the US session of the same day
# For bars in 13-21 UTC, prior_ret should already be set.
# For continuity: once we set prior_ret at h=13, keep it for h=14..20 of same day.
# Actually our loop sets it for all h>=13 of each day already.
us_session = (hour_arr >= 13) & (hour_arr <= 20)
prior_positive = np.isfinite(prior_ret) & (prior_ret > 0)
target = np.where(us_session & prior_positive, 1.0, 0.0)
return target
def target_prior_session_vol_targeted(df: pd.DataFrame) -> np.ndarray:
"""
Variant 3: Like Variant 2 but with vol-targeting (20% annualized vol, cap 2x).
"""
direction = target_prior_session_momentum(df)
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
if __name__ == "__main__":
print("SEA08 — US-session momentum on 1h bars")
print("Testing 3 variants on 1h TF...")
print()
# Variant 1: pure time filter
rep1 = al.study_weights("SEA08-v1-time-only", target_time_only, tfs=("1h",))
print(al.fmt(rep1))
print()
# Variant 2: prior session momentum condition
rep2 = al.study_weights("SEA08-v2-prior-session", target_prior_session_momentum, tfs=("1h",))
print(al.fmt(rep2))
print()
# Variant 3: vol-targeted version
rep3 = al.study_weights("SEA08-v3-vol-target", target_prior_session_vol_targeted, tfs=("1h",))
print(al.fmt(rep3))
print()
# Pick the best config by holdout Sharpe
reps = [rep1, rep2, rep3]
best = max(reps, key=lambda r: r["verdict"].get("best_holdout_sharpe", -9))
print("=== BEST CONFIG ===")
print(al.fmt(best))
print()
print("JSON:", al.as_json(best))