"""MRV10 — Stochastic Reversion in Range (ADX-gated)

IDEA: Stochastic(14,3) oversold (<20) → long entry. Only trade when ADX<25 (range/sideways
regime, not a trending market). Exit when Stochastic crosses back above 50, or TP/SL hit.

This is a DISCRETE signal strategy (study_signals, 1d only).

Stochastic %K = 100 * (C - L_n) / (H_n - L_n)   [n=14 default]
Stochastic %D = SMA(%K, 3)                         [smoothed signal line]
ADX = average directional index (non-directional trend strength)

Grid: 2 param sets (stoch_period, adx_period) x (oversold threshold, adx_threshold)
  - Config A: stoch=14, %D<20, ADX<25, hold max 10 bars
  - Config B: stoch=14, %D<25, ADX<20, hold max 8 bars (stricter ADX)
Total: 2 configs -> 2 backtests (each on BTC+ETH) = 4 runs total.
"""
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 stochastic_k(df: pd.DataFrame, period: int = 14) -> np.ndarray:
    """Raw %K = (Close - LowestLow_n) / (HighestHigh_n - LowestLow_n) * 100. Causal."""
    hi = df["high"].values
    lo = df["low"].values
    c = df["close"].values
    n = len(c)
    k = np.full(n, np.nan)
    for i in range(period - 1, n):
        h_max = np.max(hi[i - period + 1: i + 1])
        l_min = np.min(lo[i - period + 1: i + 1])
        denom = h_max - l_min
        if denom > 0:
            k[i] = 100.0 * (c[i] - l_min) / denom
        else:
            k[i] = 50.0  # flat candle
    return k


def stochastic_d(k: np.ndarray, smooth: int = 3) -> np.ndarray:
    """Stochastic %D = SMA(%K, smooth). Causal."""
    return pd.Series(k).rolling(smooth, min_periods=smooth).mean().values


def adx(df: pd.DataFrame, period: int = 14) -> np.ndarray:
    """ADX (Average Directional Index). Causal, EMA-smoothed."""
    hi = df["high"].values
    lo = df["low"].values
    c = df["close"].values
    n = len(c)

    pc = np.roll(c, 1)
    pc[0] = c[0]
    ph = np.roll(hi, 1)
    ph[0] = hi[0]
    pl = np.roll(lo, 1)
    pl[0] = lo[0]

    tr = np.maximum(hi - lo, np.maximum(np.abs(hi - pc), np.abs(lo - pc)))
    dm_plus = np.where((hi - ph) > (pl - lo), np.maximum(hi - ph, 0.0), 0.0)
    dm_minus = np.where((pl - lo) > (hi - ph), np.maximum(pl - lo, 0.0), 0.0)

    # Wilder smoothing (like EMA with alpha=1/period)
    atr_s = pd.Series(tr).ewm(alpha=1.0 / period, adjust=False).mean().values
    dmp_s = pd.Series(dm_plus).ewm(alpha=1.0 / period, adjust=False).mean().values
    dmm_s = pd.Series(dm_minus).ewm(alpha=1.0 / period, adjust=False).mean().values

    di_plus = np.where(atr_s > 0, 100.0 * dmp_s / atr_s, 0.0)
    di_minus = np.where(atr_s > 0, 100.0 * dmm_s / atr_s, 0.0)

    di_sum = di_plus + di_minus
    dx = np.where(di_sum > 0, 100.0 * np.abs(di_plus - di_minus) / di_sum, 0.0)
    adx_arr = pd.Series(dx).ewm(alpha=1.0 / period, adjust=False).mean().values
    return adx_arr


def make_entries_fn(stoch_period=14, stoch_smooth=3, os_thresh=20, adx_thresh=25, max_bars=10):
    """Returns an entries_fn(df) -> list[dict|None] for study_signals.

    Signal: go long when:
      - Stochastic %D crosses below os_thresh (oversold) from above
      - ADX < adx_thresh (range regime, not trending)

    Exit: when %D crosses back above 50 OR max_bars elapsed.
    TP: 2 * ATR above entry. SL: 1.5 * ATR below entry.
    """
    def entries_fn(df: pd.DataFrame):
        k = stochastic_k(df, stoch_period)
        d = stochastic_d(k, stoch_smooth)
        adx_vals = adx(df, stoch_period)
        atr_vals = al.atr(df, stoch_period)
        c = df["close"].values
        n = len(df)

        entries = [None] * n
        for i in range(2, n):
            if np.isnan(d[i]) or np.isnan(d[i - 1]) or np.isnan(adx_vals[i]):
                continue
            # Oversold cross: %D was above threshold, now crossed below
            crossed_oversold = (d[i - 1] >= os_thresh) and (d[i] < os_thresh)
            in_range = adx_vals[i] < adx_thresh

            if crossed_oversold and in_range:
                atr_i = atr_vals[i] if np.isfinite(atr_vals[i]) and atr_vals[i] > 0 else c[i] * 0.01
                tp = c[i] + 2.0 * atr_i
                sl = c[i] - 1.5 * atr_i
                entries[i] = {
                    "dir": +1,
                    "tp": tp,
                    "sl": sl,
                    "max_bars": max_bars,
                }
        return entries

    return entries_fn


# ── Grid (small: 2 configs only) ──────────────────────────────────────────────
CONFIGS = [
    dict(label="CFG-A", stoch_period=14, stoch_smooth=3, os_thresh=20, adx_thresh=25, max_bars=10),
    dict(label="CFG-B", stoch_period=14, stoch_smooth=3, os_thresh=25, adx_thresh=20, max_bars=8),
]

if __name__ == "__main__":
    best_rep = None
    best_hold = -99.0

    for cfg in CONFIGS:
        label = cfg.pop("label")
        fn = make_entries_fn(**cfg)
        name = f"MRV10-{label}"
        print(f"\n--- Running {name} ---")
        rep = al.study_signals(name, fn, tfs=("1d",))
        print(al.fmt(rep))
        hold = rep["verdict"].get("best_holdout_sharpe", -99.0) or -99.0
        if hold > best_hold:
            best_hold = hold
            best_rep = rep
        cfg["label"] = label  # restore for logging

    print("\n\n=== BEST CONFIG ===")
    print(al.fmt(best_rep))
    print("JSON:", al.as_json(best_rep))
