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,198 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user