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:
Adriano Dal Pastro
2026-06-20 19:50:39 +00:00
parent bf84bc91e2
commit 5ac4e16af8
111 changed files with 16924 additions and 0 deletions
+158
View File
@@ -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))