"""SEA05 — Intraday Momentum (1h)

HYPOTHESIS: On 1h bars, the sign of the morning session (00:00-11:59 UTC cumulative return)
predicts the afternoon session (12:00-23:59 UTC). Position taken at bar open of 12:00 UTC
and held through 23:59 UTC. Causal: morning return is fully known before entering at 12:00 close.

Implementation:
- Use 1h data only (the hypothesis requires intraday structure)
- For each day, compute morning cumulative return: product of (1+r) from 00:00 to 11:00 UTC (12 bars)
- At 12:00 UTC bar (i), compute signal from morning session (known at close[i-1] or earlier)
- Position = sign(morning_return) held for 12 bars (12:00 to 23:00 UTC)
- Vol-targeted continuous weights with vol_target(signal, df)

Grid: try 2 variants:
  A) raw sign (morning ret sign -> afternoon position)
  B) z-score of morning returns (magnitude matters -> stronger signal -> larger position)
"""
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 make_intraday_mom_signal(df: pd.DataFrame, use_zscore: bool = False, lookback_z: int = 20) -> np.ndarray:
    """
    For each 1h bar, compute an intraday momentum signal.

    Logic (causal):
    - Morning session = hours 0..11 UTC (12 bars per day)
    - At hour 12 (bar index where hour==12), the morning is complete
    - Signal = sign of morning cumulative return
    - Held for bars where hour in [12..23]
    - At hour 0 next day: flat (we re-evaluate)

    target[i] is set for bar i, evaluated with data up to close[i-1] for the morning.
    Actually: at bar with hour==12, close[i-1] is 11:00 close = last morning bar close.
    Morning return = close[11:00] / open[00:00] - 1 (for that day).
    """
    dt = df["datetime"]
    hour = dt.dt.hour

    # Compute log returns for each bar
    close = df["close"].values
    log_ret = np.zeros(len(df))
    log_ret[1:] = np.log(close[1:] / close[:-1])

    # Build daily morning cumulative return
    # For each bar at hour==12, sum log returns from hours 1..11 of same day
    # (hour 0 bar's return is from previous day's close to 00:00 close, we include it too)

    n = len(df)
    target = np.zeros(n)

    # We'll track morning cum-ret per day
    # Iterate bar by bar: accumulate morning, set signal at 12:00

    day_morning_cumret = 0.0
    morning_rets_history = []  # for z-score
    in_morning = False

    for i in range(n):
        h = hour.iloc[i]

        if h == 0:
            # Start of a new day: reset morning accumulator
            day_morning_cumret = 0.0
            in_morning = True

        if in_morning and h < 12:
            # Accumulate morning log return
            day_morning_cumret += log_ret[i]

        elif h == 12:
            # Morning complete, set position for afternoon
            in_morning = False

            if use_zscore and len(morning_rets_history) >= lookback_z:
                hist = np.array(morning_rets_history[-lookback_z:])
                mu = hist.mean()
                sigma = hist.std()
                if sigma > 1e-8:
                    z = (day_morning_cumret - mu) / sigma
                    # Clip to [-3, 3] and normalize
                    pos = np.clip(z / 2.0, -1.0, 1.0)
                else:
                    pos = 0.0
            else:
                # Simple sign
                pos = np.sign(day_morning_cumret)

            # Set target for this bar (12:00) and keep for afternoon
            # But we need to be careful: target[i] uses data up to close[i]
            # which is close of 12:00 bar. Actually we want to position DURING 12:00-23:00.
            # al.study_weights holds target[i] during bar i+1.
            # So: at bar i-1 (11:00), we know morning is done (close[i-1] = 11:00 close).
            # We should set target[i-1] to the signal so it's held during bar i (12:00 bar).
            # But that's complex. Instead: set target at i=12:00 bar using morning already
            # computed (morning is 00:00 to 11:00, all known before 12:00 bar opens).
            # The lib holds target[i] for bar i+1. So we set it at bar h==11 (last morning bar).
            # But we compute it here at h==12 for simplicity — let's adjust:
            # Actually set at h==11 (previous bar). We'll do a post-pass.

            # Store for z-score history
            morning_rets_history.append(day_morning_cumret)

            # We mark this as "12h signal" to be applied starting from 12:00 bar
            # Since lib shifts: target[i] held during bar i+1, we need target at i where h==11
            # We'll fix this in a second pass below; for now store in target[i]
            target[i] = pos

        elif h > 12:
            # Carry afternoon position forward
            target[i] = target[i-1]
        # else h in [1..11] or h==0: flat (0)

    # Shift the signal: target[i] where h==12 should be moved to h==11 bar
    # so that lib holds it during h==12 bar (bar i+1 from lib's perspective)
    # Find all bars where h==12, move signal to i-1 (h==11)
    afternoon_signal = np.zeros(n)
    i = 0
    while i < n:
        h = hour.iloc[i]
        if h == 12 and target[i] != 0:
            sig = target[i]
            # Set signal starting at i-1 (11:00 bar), lib holds it for bar i (12:00)
            # Actually we want to hold signal for bars 12..23
            # target[i-1] -> held during bar i (12:00) ✓
            # target[i]   -> held during bar i+1 (13:00) ✓
            # ...
            # target[i+10] -> held during bar i+11 (23:00) ✓
            # total: 12 bars (12:00-23:00)
            if i - 1 >= 0:
                afternoon_signal[i-1] = sig  # held during bar i (12:00)
            for k in range(i, min(i+11, n)):  # bars 12:00..22:00 -> held during 13:00..23:00
                afternoon_signal[k] = sig
            i += 12
        else:
            i += 1

    return afternoon_signal


def make_vol_targeted(df: pd.DataFrame, use_zscore: bool = False, lookback_z: int = 20) -> np.ndarray:
    """Intraday momentum with vol targeting."""
    raw_signal = make_intraday_mom_signal(df, use_zscore=use_zscore, lookback_z=lookback_z)
    # Vol-target: direction = sign(raw_signal), magnitude from vol_target
    direction = np.sign(raw_signal)
    w = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
    return w


# Run the study with 2 variants on 1h only
print("=" * 60)
print("SEA05 — Intraday Momentum (1h)")
print("=" * 60)

# Variant A: simple sign, vol-targeted
print("\n--- Variant A: sign(morning_ret), vol-targeted ---")
rep_a = al.study_weights(
    "SEA05-A-sign",
    lambda df: make_vol_targeted(df, use_zscore=False),
    tfs=("1h",)
)
print(al.fmt(rep_a))
print("JSON:", al.as_json(rep_a))

# Variant B: z-score based magnitude, vol-targeted
print("\n--- Variant B: zscore(morning_ret), vol-targeted ---")
rep_b = al.study_weights(
    "SEA05-B-zscore",
    lambda df: make_vol_targeted(df, use_zscore=True, lookback_z=20),
    tfs=("1h",)
)
print(al.fmt(rep_b))
print("JSON:", al.as_json(rep_b))

# Pick best by min_asset_full_sharpe
best = rep_a if rep_a["verdict"]["best_full_sharpe"] >= rep_b["verdict"]["best_full_sharpe"] else rep_b
print("\n=== BEST CONFIG ===")
print(al.fmt(best))
print("JSON:", al.as_json(best))
