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,158 @@
|
||||
"""SEA06 — Overnight vs Intraday session capture.
|
||||
|
||||
IDEA: Split the 24h day into named trading sessions:
|
||||
- ASIA: UTC 00-08 (Tokyo, Hong Kong, Singapore)
|
||||
- EUROPE: UTC 08-16 (London open to US open)
|
||||
- US_INTRADAY: UTC 13-21 (NYSE hours, overlap with Europe 13-16)
|
||||
- US_OVERNIGHT: UTC 21-24 & 00-01 (NY close to Asia open)
|
||||
|
||||
For each 1h bar, we assign it to a session. We track the EXPANDING-WINDOW
|
||||
cumulative mean return per session (causal: uses only past bars).
|
||||
At bar i, we go long (+1) during the session that has had the best
|
||||
mean return so far (among those with enough samples >= min_samples).
|
||||
If no session qualifies, we stay flat.
|
||||
|
||||
This captures the historically positive session with a continuously
|
||||
updating, causal estimate — no look-ahead.
|
||||
|
||||
Vol-target applied to the direction signal.
|
||||
|
||||
Grid (4 configs total to stay <= 6 total backtests):
|
||||
- min_samples in [30, 90] x 1 TF (1h) = 2 calls (each covers BTC+ETH internally)
|
||||
- We also try the "best 2 sessions" variant: go long if session is in top-2
|
||||
|
||||
Causal guarantee:
|
||||
- session_mean[s] at bar i = mean of r[j] for all j < i in session s
|
||||
- direction[i] assigned from session_mean BEFORE updating with r[i]
|
||||
- lib shifts target by 1 bar before multiplying by returns
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
||||
import altlib as al
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
# Session definitions: list of UTC hours belonging to each session
|
||||
SESSIONS = {
|
||||
"ASIA": list(range(0, 8)), # 00:00-07:59 UTC
|
||||
"EUROPE": list(range(8, 16)), # 08:00-15:59 UTC
|
||||
"US_INTRADAY": list(range(13, 21)), # 13:00-20:59 UTC
|
||||
"US_OVERNIGHT": list(range(21, 24)) + list(range(0, 2)), # 21:00-01:59 UTC
|
||||
}
|
||||
|
||||
# Map each UTC hour (0-23) to its primary session
|
||||
# (some hours overlap; assign to highest-priority session)
|
||||
# Priority: US_INTRADAY > EUROPE > ASIA > US_OVERNIGHT for overlapping hours
|
||||
HOUR_TO_SESSION = {}
|
||||
for h in range(24):
|
||||
assigned = None
|
||||
for sess, hours in SESSIONS.items():
|
||||
if h in hours:
|
||||
if assigned is None:
|
||||
assigned = sess
|
||||
# Apply priority: prefer US_INTRADAY > EUROPE > ASIA > US_OVERNIGHT
|
||||
priority = {"US_INTRADAY": 4, "EUROPE": 3, "ASIA": 2, "US_OVERNIGHT": 1}
|
||||
if priority[sess] > priority.get(assigned, 0):
|
||||
assigned = sess
|
||||
HOUR_TO_SESSION[h] = assigned if assigned else "ASIA"
|
||||
|
||||
SESSION_NAMES = list(SESSIONS.keys())
|
||||
N_SESS = len(SESSION_NAMES)
|
||||
SESS_IDX = {s: i for i, s in enumerate(SESSION_NAMES)}
|
||||
|
||||
|
||||
def sea06_target(df: pd.DataFrame, min_samples: int = 30, top_n: int = 1) -> np.ndarray:
|
||||
"""
|
||||
Go long during bars that belong to the top-N sessions by expanding-window mean return.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
min_samples : int
|
||||
Minimum number of past bars in a session before we trust its mean.
|
||||
top_n : int
|
||||
Number of sessions to consider "good" (1 = only the best, 2 = best two).
|
||||
"""
|
||||
dt = pd.to_datetime(df["datetime"])
|
||||
c = df["close"].values.astype(float)
|
||||
r = al.simple_returns(c)
|
||||
n = len(df)
|
||||
|
||||
hours = dt.dt.hour.values # 0..23
|
||||
bar_session = np.array([SESS_IDX[HOUR_TO_SESSION[h]] for h in hours], dtype=int)
|
||||
|
||||
# Expanding cumulative stats per session
|
||||
sess_sum = np.zeros(N_SESS, dtype=float)
|
||||
sess_cnt = np.zeros(N_SESS, dtype=int)
|
||||
|
||||
direction = np.zeros(n, dtype=float)
|
||||
|
||||
for i in range(n):
|
||||
s = bar_session[i]
|
||||
|
||||
# Compute mean returns for all sessions that have enough samples
|
||||
means = np.full(N_SESS, np.nan)
|
||||
for si in range(N_SESS):
|
||||
if sess_cnt[si] >= min_samples:
|
||||
means[si] = sess_sum[si] / sess_cnt[si]
|
||||
|
||||
# Find top-N sessions by mean return (ignore NaN)
|
||||
valid_mask = np.isfinite(means)
|
||||
if valid_mask.sum() >= 1:
|
||||
valid_indices = np.where(valid_mask)[0]
|
||||
valid_means = means[valid_indices]
|
||||
# Sort descending by mean
|
||||
sorted_idx = valid_indices[np.argsort(-valid_means)]
|
||||
top_sessions = set(sorted_idx[:top_n].tolist())
|
||||
|
||||
# Only go long if current bar's session is in top-N AND its mean > 0
|
||||
if s in top_sessions and means[s] > 0:
|
||||
direction[i] = 1.0
|
||||
|
||||
# Update expanding window AFTER using it (causal)
|
||||
sess_sum[s] += r[i]
|
||||
sess_cnt[s] += 1
|
||||
|
||||
tgt = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
|
||||
return tgt
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = []
|
||||
|
||||
# Grid: min_samples x top_n — 4 configs, 1 TF, 2 assets = 4 calls to study_weights
|
||||
# (each study_weights call runs both BTC and ETH internally)
|
||||
grid = [
|
||||
(30, 1),
|
||||
(90, 1),
|
||||
(30, 2),
|
||||
(90, 2),
|
||||
]
|
||||
|
||||
for min_samples, top_n in grid:
|
||||
name = f"SEA06-ms{min_samples}-top{top_n}"
|
||||
rep = al.study_weights(
|
||||
name,
|
||||
lambda df, ms=min_samples, tn=top_n: sea06_target(df, min_samples=ms, top_n=tn),
|
||||
tfs=("1h",),
|
||||
)
|
||||
print(al.fmt(rep))
|
||||
print("JSON:", al.as_json(rep))
|
||||
print()
|
||||
|
||||
best_cell = max(rep["cells"], key=lambda c: c.get("min_asset_holdout_sharpe", -9))
|
||||
results.append((
|
||||
rep["verdict"].get("best_holdout_sharpe", best_cell.get("min_asset_holdout_sharpe", -9)),
|
||||
rep["verdict"].get("best_full_sharpe", best_cell.get("min_asset_full_sharpe", -9)),
|
||||
name,
|
||||
rep,
|
||||
))
|
||||
|
||||
# Pick the best config by hold-out Sharpe
|
||||
results.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
||||
best_hold, best_full, best_name, best_rep = results[0]
|
||||
|
||||
print("\n=== BEST CONFIG ===", best_name)
|
||||
print(al.fmt(best_rep))
|
||||
print("JSON:", al.as_json(best_rep))
|
||||
Reference in New Issue
Block a user