5ac4e16af8
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>
159 lines
5.6 KiB
Python
159 lines
5.6 KiB
Python
"""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))
|