"""TRD08 — Hull MA slope strategy.

HYPOTHESIS: HMA(n); long when HMA rising (slope > 0), flat when falling.
Grid: n in {20, 50, 100}.

Hull Moving Average (causal):
  WMA(n) = weighted moving average with linear weights
  HMA(n) = WMA(sqrt(n), 2*WMA(n//2) - WMA(n))

Position sizing: vol-targeted (20% target, 2x cap), long-flat only.
"""

import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
from numpy.lib.stride_tricks import as_strided


def wma_vectorized(x: np.ndarray, win: int) -> np.ndarray:
    """Causal weighted moving average — vectorized via cumsum trick."""
    n = len(x)
    # Use pandas for clean rolling WMA: sum(w_i * x_i) / sum(w_i)
    # weights = 1, 2, ..., win
    # We can compute via cumsum: WMA = (sum(i * x[t-i]) for i=1..win) / (win*(win+1)/2)
    # Use a numerator via weighted cumsum
    weights = np.arange(1, win + 1, dtype=float)
    total_w = weights.sum()

    result = np.full(n, np.nan)

    # Efficient: build a 2D sliding window using stride tricks, then dot with weights
    if n < win:
        return result

    # pad at start for alignment
    # shape: (n - win + 1, win)
    shape = (n - win + 1, win)
    strides = (x.strides[0], x.strides[0])
    windows = as_strided(x, shape=shape, strides=strides)
    result[win - 1:] = windows @ weights / total_w
    return result


def hma(x: np.ndarray, n: int) -> np.ndarray:
    """Causal Hull Moving Average."""
    half_n = max(2, n // 2)
    sqrt_n = max(2, int(round(np.sqrt(n))))

    wma_full = wma_vectorized(x, n)
    wma_half = wma_vectorized(x, half_n)

    # 2 * WMA(n//2) - WMA(n)
    raw = 2.0 * wma_half - wma_full

    # Apply WMA(sqrt(n)) to the raw series
    return wma_vectorized(raw, sqrt_n)


def make_target(n: int):
    """Return a lambda that computes vol-targeted HMA slope signal."""
    def target(df):
        c = df["close"].values.astype(float)
        h = hma(c, n)
        # slope: hma[i] > hma[i-1] => rising => long
        slope = np.zeros(len(h))
        slope[1:] = np.where(h[1:] > h[:-1], 1.0, 0.0)
        # NaN protection: flat when HMA not yet valid or slope undefined
        nan_mask = np.isnan(h) | np.isnan(np.concatenate([[np.nan], h[:-1]]))
        slope[nan_mask] = 0.0
        # Vol-target
        return al.vol_target(slope, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
    return target


# Grid: n in {20, 50, 100} across timeframes {1d, 12h}
# 3 param sets × 2 TFs = 6 total backtests (within limit)
tfs = ("1d", "12h")
grid_n = [20, 50, 100]

best_rep = None
best_score = -999.0
best_n = grid_n[0]

for n in grid_n:
    name = f"TRD08-HMA{n}"
    rep = al.study_weights(name, make_target(n), tfs=tfs)
    print(al.fmt(rep))
    print("JSON:", al.as_json(rep))

    # Score by best_holdout_sharpe
    score = rep["verdict"].get("best_holdout_sharpe", rep["verdict"].get("min_asset_holdout_sharpe", -999))
    if score > best_score:
        best_score = score
        best_rep = rep
        best_n = n

print("\n" + "="*60)
print(f"BEST CONFIG: n={best_n}")
print(al.fmt(best_rep))
print("JSON:", al.as_json(best_rep))
