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

199 lines
8.3 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.
"""SEA09 — Asia-session mean-reversion on 1h bars.
HYPOTHESIS: During the Asian session (00-08 UTC), fade extreme moves back toward
the session open. If price has moved far up from the session open, go short
(expecting reversion); if far down, go long. Session mean-reversion idea.
BAR LABELING (1h bars):
- A bar labeled/timestamped at "01:00 UTC" closes at 01:00 UTC (covers 00:00-01:00).
- Close[00:00 UTC] = the midnight bar close = prior day's last bar.
- Close[08:00 UTC] = end of the Asia window.
CAUSAL DECISION:
target[i] = position to hold DURING bar i+1 (decided with data <= close[i]).
Asian session window: we want to hold a position during the bars from
01:00 UTC to 08:00 UTC (bars closing at those hours cover 00:00-01:00 ... 07:00-08:00).
To hold during the bar closing at h+1 UTC, we set target at bar closing at h UTC.
So to be active during hours 01..08 UTC, we set target at hours 00..07 UTC.
At bar[i] closing at h (00..07):
- We know the session open = close of the bar at h=00 of the current day (midnight).
If h > 0, this is already in the past and known. If h == 0, we use the current bar's
close as the session open (we'll be entering the next bar at h=1 anyway,
and we don't know the overnight move yet — so for h=0 we set target=0 to avoid
a contamination: we'd be computing signal from the same bar we're deciding on).
Actually at h=0 (midnight), we just know close[00:00] but don't yet know if there
will be an extreme move — so the target for bar(h=1) set at bar(h=0) should compare
close[00:00] vs itself = 0 move. We'll mark target=0 for this bar.
- For h in {1..7}: session_open = close of the 00:00 bar of the same day.
session_move = (close[i] - session_open) / session_open
z-score of session_move vs historical distribution (rolling 30d) -> signal strength.
target[i] = -sign(session_move) * |z| if |z| > threshold -> fade the move.
GRID (4 variants, 1 TF each = 4 * 2 assets = 8 backtests — within budget):
A: simple sign-fade, no z-threshold (fade any move, binary direction)
B: z-score fade, threshold=1.0 (only fade "significant" moves)
C: z-score proportional (continuous weight proportional to -z)
D: z-score proportional + vol-target
We only test 1h (this is an intraday hourly hypothesis).
Total: 4 variants × 1 TF × 2 assets = 8 backtests. 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_asia_features(df: pd.DataFrame, z_win_days: int = 30):
"""
For each 1h bar at index i:
- Compute session_move[i] = (close[i] - session_open) / session_open
where session_open = close of the 00:00 UTC bar of the SAME day.
- Causal: session_open for day D is known from bar(h=0, day D) onward.
- z-score of session_move vs rolling historical moves (causal).
Returns (hour_arr, session_move_arr, z_arr).
"""
dt = pd.to_datetime(df["datetime"], utc=True)
close = df["close"].values.astype(float)
n = len(df)
hour_arr = dt.dt.hour.values
date_arr = dt.dt.date.values
# Build date -> index of the 00:00 bar (the "session open" for that date)
# The 00:00 UTC bar closes at midnight, so date is the same calendar date.
session_open_by_date = {} # date -> close at 00:00 UTC
for i in range(n):
if hour_arr[i] == 0:
session_open_by_date[date_arr[i]] = close[i]
# Compute session_move for each bar in Asian session (h in 0..7)
session_move = np.full(n, np.nan)
for i in range(n):
h = hour_arr[i]
d = date_arr[i]
if h in range(1, 8): # h=1..7 (h=0 excluded: move relative to itself = 0, no signal)
so = session_open_by_date.get(d, np.nan)
if np.isfinite(so) and so > 0:
session_move[i] = (close[i] - so) / so
# Compute rolling z-score of session_move (causal, only using past observations)
# We compute it only for the non-NaN values (within-session bars), treating them
# as a time series. For z-scoring we use a rolling window of z_win_days * ~7 (bars per day
# in session = 7 bars at h=1..7).
session_move_series = pd.Series(session_move)
roll_mean = session_move_series.rolling(z_win_days * 7, min_periods=14).mean()
roll_std = session_move_series.rolling(z_win_days * 7, min_periods=14).std()
z_arr = ((session_move_series - roll_mean) / roll_std.replace(0, np.nan)).values
z_arr = np.nan_to_num(z_arr, nan=0.0)
return hour_arr, session_move, z_arr
def target_simple_fade(df: pd.DataFrame) -> np.ndarray:
"""
Variant A: Fade any Asia-session move (binary sign-based).
target[i] = -sign(session_move[i]) if h in [1..7], else 0.
Holds the position during bar i+1 (so exposure hours = 02..09 UTC closes).
We restrict to h in [0..6] so we hold during [1..7] UTC.
"""
hour_arr, session_move, _ = _build_asia_features(df)
n = len(df)
target = np.zeros(n)
for i in range(n):
h = hour_arr[i]
# Set target at h=0..6 -> holds during h+1=1..7 UTC bar
if h in range(0, 7) and np.isfinite(session_move[i]):
target[i] = -np.sign(session_move[i]) if session_move[i] != 0 else 0.0
# h=0: session_move is NaN (no move yet), so target stays 0 — flat at bar(h=1)
# Actually let's re-check: session_move[h=0] is NaN (excluded range(1,8) above).
# So for h=0, target=0 (flat) -> we don't take a position at the very first bar.
return target
def target_zscore_threshold(df: pd.DataFrame) -> np.ndarray:
"""
Variant B: Fade only when z-score of move exceeds 1.0 (i.e., "significant" extremes).
target[i] = -sign(z) if |z| > 1.0 and h in [0..6], else 0.
"""
hour_arr, _, z_arr = _build_asia_features(df)
n = len(df)
target = np.zeros(n)
THRESHOLD = 1.0
for i in range(n):
h = hour_arr[i]
if h in range(0, 7):
z = z_arr[i]
if abs(z) > THRESHOLD:
target[i] = -np.sign(z)
return target
def target_zscore_proportional(df: pd.DataFrame) -> np.ndarray:
"""
Variant C: Continuous fade proportional to -z (clipped to [-1, 1]).
target[i] = clip(-z / 2.0, -1, 1) for h in [0..6], else 0.
Dividing by 2.0 so that a z=2 sigma move gives full unit position.
"""
hour_arr, _, z_arr = _build_asia_features(df)
n = len(df)
target = np.zeros(n)
for i in range(n):
h = hour_arr[i]
if h in range(0, 7):
target[i] = float(np.clip(-z_arr[i] / 2.0, -1.0, 1.0))
return target
def target_zscore_vol_targeted(df: pd.DataFrame) -> np.ndarray:
"""
Variant D: Proportional z-score fade + vol-targeting (20% annual vol, 2x cap).
"""
direction = target_zscore_proportional(df)
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
if __name__ == "__main__":
print("SEA09 — Asia-session mean-reversion on 1h bars")
print("Grid: 4 variants × 1 TF (1h) × 2 assets = 8 backtests")
print()
# Variant A: simple sign fade
rep_a = al.study_weights("SEA09-A-simple-fade", target_simple_fade, tfs=("1h",))
print("=== Variant A: simple sign fade ===")
print(al.fmt(rep_a))
print()
# Variant B: z-score threshold
rep_b = al.study_weights("SEA09-B-zscore-threshold", target_zscore_threshold, tfs=("1h",))
print("=== Variant B: z-score threshold (|z|>1.0) ===")
print(al.fmt(rep_b))
print()
# Variant C: z-score proportional
rep_c = al.study_weights("SEA09-C-zscore-proportional", target_zscore_proportional, tfs=("1h",))
print("=== Variant C: z-score proportional ===")
print(al.fmt(rep_c))
print()
# Variant D: z-score vol-targeted
rep_d = al.study_weights("SEA09-D-zscore-vol-target", target_zscore_vol_targeted, tfs=("1h",))
print("=== Variant D: z-score proportional + vol-target ===")
print(al.fmt(rep_d))
print()
# Pick best by holdout Sharpe
reps = [rep_a, rep_b, rep_c, rep_d]
labels = ["A-simple-fade", "B-zscore-threshold", "C-zscore-proportional", "D-zscore-vol-target"]
best = max(reps, key=lambda r: r["verdict"].get("best_holdout_sharpe", -9))
best_label = labels[reps.index(best)]
print(f"=== BEST CONFIG: {best_label} ===")
print(al.fmt(best))
print()
print("JSON:", al.as_json(best))