"""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))