"""SEA03 — Weekend Effect HYPOTHESIS: Crypto prices behave differently on weekend bars vs weekday bars. We test long/flat (and long/short) positions on weekend bars only, with the direction chosen by expanding in-sample sign of weekend vs weekday returns. VARIANTS tested (<=4 param sets, <=6 total backtests with 2 TFs): V1: Fixed long on weekends (Sat/Sun bars), flat on weekdays V2: Expanding-sign direction on weekends (long or short), flat on weekdays V3: V2 + vol-targeting Best config selected by min_asset_holdout_sharpe. We use 1d bars (weekend = day_of_week in {5,6}: Saturday, Sunday). On hourly bars there may not be a clean weekend partition, so we use 1d only. """ 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 _is_weekend(df: pd.DataFrame) -> np.ndarray: """Return boolean array: True if this bar is a weekend bar (Sat or Sun).""" dt = pd.to_datetime(df["datetime"], utc=True) return (dt.dt.dayofweek >= 5).values # 5=Sat, 6=Sun def _expanding_weekend_sign(df: pd.DataFrame) -> np.ndarray: """For each bar, compute expanding-mean return on weekend bars vs weekday bars. Return +1 if weekend historically outperforms weekday, else -1. This is causal: at bar i we use only returns from bars 0..i-1. Returns array of +1/-1 (same sign for all bars on the same day as rolling expands). """ c = df["close"].values.astype(float) r = al.simple_returns(c) is_wk = _is_weekend(df) # Expanding cumulative mean of weekend returns and weekday returns up to bar i-1 # We look at sign(mean_wkend - mean_wkday) to decide direction for bar i sign_arr = np.ones(len(r)) # default +1 (long) cum_wkend_sum = 0.0 cum_wkend_n = 0 cum_wkday_sum = 0.0 cum_wkday_n = 0 for i in range(1, len(r)): # Use return of bar i-1 if is_wk[i - 1]: cum_wkend_sum += r[i - 1] cum_wkend_n += 1 else: cum_wkday_sum += r[i - 1] cum_wkday_n += 1 if cum_wkend_n >= 5 and cum_wkday_n >= 5: mean_wk = cum_wkend_sum / cum_wkend_n mean_wd = cum_wkday_sum / cum_wkday_n sign_arr[i] = 1.0 if mean_wk >= mean_wd else -1.0 # else: not enough history, default +1 return sign_arr # ---- Variant 1: Fixed long on weekends, flat on weekdays ---- def v1_fixed_long(df: pd.DataFrame) -> np.ndarray: is_wk = _is_weekend(df) # position: +1 on weekend bars, 0 on weekday bars return is_wk.astype(float) # ---- Variant 2: Expanding-sign direction on weekends, flat on weekdays ---- def v2_expanding_sign(df: pd.DataFrame) -> np.ndarray: is_wk = _is_weekend(df) sign = _expanding_weekend_sign(df) # Long or short on weekend depending on expanding sign, flat on weekdays return np.where(is_wk, sign, 0.0) # ---- Variant 3: V2 + vol targeting ---- def v3_voltarget(df: pd.DataFrame) -> np.ndarray: direction = v2_expanding_sign(df) return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) # ---- Variant 4: Long weekdays (inverse hypothesis) ---- def v4_fixed_long_weekday(df: pd.DataFrame) -> np.ndarray: is_wk = _is_weekend(df) return (~is_wk).astype(float) if __name__ == "__main__": variants = [ ("SEA03-V1-weekend-long", v1_fixed_long), ("SEA03-V2-expanding-sign", v2_expanding_sign), ("SEA03-V3-voltarget", v3_voltarget), ("SEA03-V4-weekday-long", v4_fixed_long_weekday), ] results = [] for name, fn in variants: print(f"\nRunning {name}...") rep = al.study_weights(name, fn, tfs=("1d",)) print(al.fmt(rep)) results.append(rep) # Pick best config by min_asset_holdout_sharpe across all cells best_rep = max(results, key=lambda r: r["verdict"].get("best_holdout_sharpe", -99)) print("\n\n=== BEST CONFIG ===") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))