"""SEA07 — Monday Effect (expanding Monday expectancy). IDEA: On 1d bars, use the expanding-window mean Monday return as a directional signal. - Compute an expanding (causal) mean of Monday returns seen so far. - If the expanding Monday mean > 0 (continuation): go long (+1) on Mondays, flat otherwise. - If the expanding Monday mean < 0 (reversal): go short (-1) on Mondays, flat otherwise. - Also try "Friday signal": what happened last Friday may predict the Monday direction. We track expanding Friday return mean and use its sign to predict the following Monday. Signal styles tested (4 configs, 1 TF = 1d, 2 assets = <=8 cells total): 1. Monday continuation: long on Mondays when expanding E[Monday ret] > 0, else flat 2. Monday always long: always long on Mondays regardless of prior expectancy (baseline) 3. Friday-to-Monday: on Monday, go in the direction of last Friday's expanding mean 4. Monday vol-adjusted: same as #1 but NOT vol-targeted (raw position, to isolate the signal) All signals are on 1d only (as required). Causal guarantee: - Expanding Monday mean at bar i uses only Monday returns j < i (causal). - Friday-to-Monday: expanding Friday mean uses only Friday returns j < i (causal). - lib shifts position by 1 bar automatically (decided at close[i], held during bar i+1). WAIT: Monday bar i means we hold on Monday. close[i] of a Monday is ALREADY the end of Monday. So to hold DURING Monday, we must decide at close[i-1] (Sunday or prior day). Implementation: set target[i] = 0 always; set target[i-1] = signal for Monday i. But altlib shifts target[i] -> held at bar i+1. So to be in position DURING bar i: we need target[i-1] != 0, which becomes pos[i] = target[i-1]. Correct approach: for each Monday bar at index i, we set target[i-1] = signal. This means at close of Sunday (i-1), we enter; held during bar i (Monday). Since 1d bars, Sunday doesn't exist: previous bar is Friday at i-1. So: at close of Friday (i-1), we set the position to be held on Monday (i). This is the natural way: target[i-1] = signal, lib shifts to pos[i] = target[i-1]. Expanding stats use only data BEFORE the current Monday being evaluated: - When setting target[i-1] for Monday i: we have seen all Monday returns up to i-1 (none of which are Mondays in typical weeks; so effectively all Mondays before this one). """ 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 sea07_monday_continuation(df: pd.DataFrame, min_samples: int = 10, use_friday: bool = False, vol_tgt: bool = True) -> np.ndarray: """ Monday-effect signal on daily bars. Parameters ---------- min_samples : int Minimum Monday (or Friday) samples needed before trusting the expectancy. use_friday : bool If True, use the expanding mean of Friday returns to predict Monday direction. If False, use the expanding mean of Monday returns (continuation/reversal). vol_tgt : bool Whether to apply vol-targeting to the direction signal. """ dt = pd.to_datetime(df["datetime"]) c = df["close"].values.astype(float) r = al.simple_returns(c) n = len(df) # Day of week: 0=Monday, 1=Tuesday, ..., 4=Friday, 5=Saturday, 6=Sunday dow = dt.dt.dayofweek.values # 0=Mon, 4=Fri # Expanding stats for Monday and Friday returns mon_sum = 0.0 mon_cnt = 0 fri_sum = 0.0 fri_cnt = 0 # target[i]: position decided at close[i], held during bar i+1 # To be in position DURING Monday bar i, we set target[i-1]. # target is indexed by bar where decision is made. target = np.zeros(n, dtype=float) for i in range(1, n): # Update stats with bar i-1 (the bar we just closed) prev_dow = dow[i - 1] prev_r = r[i - 1] if prev_dow == 0: # previous bar was a Monday # We accumulate Monday return AFTER using it for the next decision # (this bar i is Tuesday or later; the Monday return r[i-1] is now known) pass # will update after computing signal for i # Current bar i: what day is it? curr_dow = dow[i] if curr_dow == 0: # Bar i is a Monday. We want to be in position during this bar. # Decision must be made at close[i-1] (Friday or whatever preceded it). # So we set target[i-1] based on stats available BEFORE bar i. if use_friday: # Use expanding Friday expectancy to decide Monday direction if fri_cnt >= min_samples and fri_sum != 0: fri_mean = fri_sum / fri_cnt direction = 1.0 if fri_mean > 0 else -1.0 else: direction = 0.0 else: # Use expanding Monday expectancy: continuation or reversal if mon_cnt >= min_samples and mon_sum != 0: mon_mean = mon_sum / mon_cnt direction = 1.0 if mon_mean > 0 else -1.0 else: direction = 0.0 target[i - 1] = direction # Now update the expanding stats with bar i-1's return (after using stats for bar i) # This ensures we never use r[i-1] to decide signal for bar i if prev_dow == 0: mon_sum += prev_r mon_cnt += 1 elif prev_dow == 4: fri_sum += prev_r fri_cnt += 1 if vol_tgt: return al.vol_target(target, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) else: return target if __name__ == "__main__": results = [] # Grid: 4 configs on 1d only grid = [ # (name_suffix, min_samples, use_friday, vol_tgt) ("mon-cont-ms10-vt", 10, False, True), # Monday continuation, vol-targeted ("mon-cont-ms20-vt", 20, False, True), # Monday continuation, more samples ("fri2mon-ms10-vt", 10, True, True), # Friday->Monday, vol-targeted ("fri2mon-ms20-vt", 20, True, True), # Friday->Monday, more samples ] # Use study_weights (continuous position style is appropriate for "hold on Mondays") for suffix, min_s, use_fri, vt in grid: name = f"SEA07-{suffix}" rep = al.study_weights( name, lambda df, ms=min_s, uf=use_fri, v=vt: sea07_monday_continuation( df, min_samples=ms, use_friday=uf, vol_tgt=v ), tfs=("1d",), ) 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 best config by hold-out Sharpe (tie-break: full 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))