"""
post_close_signal_check_v1.py
==============================
Tests whether checking for a new signal IMMEDIATELY after a trade
closes improves results vs waiting for the next hourly check.

Scenario tested:
  Current (hourly only): After close, wait up to 59 min for next :01 check
  Enhanced: After close, immediately check for new signal

Key question: In the 22yr backtest, how many extra trades are caught
in the post-close window, and what is their win rate?

Run from project root on home machine:
  python src/research/post_close_signal_check_v1.py
"""

import pandas as pd
import numpy as np
from pathlib import Path
import sys

BASE_PATH  = Path(__file__).resolve().parents[2]
SRC_PATH   = BASE_PATH / "src"
TRADES_DIR = BASE_PATH / "data" / "processed" / "trades"

if str(SRC_PATH) not in sys.path:
    sys.path.append(str(SRC_PATH))

THRESHOLD     = 2.75
FIB           = 0.786
STOP_PCT      = 0.0025
TP_PCT        = 0.0020
ZSCORE_EXIT   = 1.5
SPREAD_COST   = 0.0001
ALLOWED_HOURS = set(range(5, 15))   # 05:00-14:00 UTC session
BASE_NOTIONAL = 300_000.0
YEARS         = 22.0
ACCOUNT_START = 100_000.0
HOLD_HOURS    = 52

ZSCORE_BANDS = [(2.75, 3.50, 1.0), (3.50, 4.50, 1.5), (4.50, 99.0, 2.0)]


def get_multiplier(z):
    for lo, hi, m in ZSCORE_BANDS:
        if lo <= z < hi:
            return m
    return 2.0


def main():
    print("=" * 70)
    print("POST-CLOSE IMMEDIATE SIGNAL CHECK BACKTEST")
    print("=" * 70)
    print("""
  Question: If we check for a new signal immediately after a trade
  closes (rather than waiting for next hourly :01), how many extra
  trades are caught and what is their quality?
""")

    # ── Load signal series ────────────────────────────────────────────────────
    try:
        from features.spot_lag_v3 import get_model_ready_spot_lag_v3
        signal_df = get_model_ready_spot_lag_v3().copy()
        signal_df["datetime"] = pd.to_datetime(signal_df["datetime"])
        signal_df = signal_df.sort_values("datetime").reset_index(drop=True)
        z_col = "lag_zscore_24h_v3"
        print(f"  Signal series loaded: {len(signal_df):,} hourly rows")
    except Exception as e:
        print(f"  ERROR: {e}")
        return

    # ── Load 15M price data for entry simulation ──────────────────────────────
    try:
        from ingestion.price_loader_15m import load_eurusd_15m
        m15 = load_eurusd_15m().sort_values("datetime").reset_index(drop=True)
        m15["datetime"] = pd.to_datetime(m15["datetime"])
        print(f"  15M bars loaded: {len(m15):,}")
    except Exception as e:
        print(f"  ERROR loading 15M: {e}")
        return

    # ── Run backtest with HOURLY only schedule ────────────────────────────────
    print("\n  Running hourly-only backtest...")
    hourly_trades = run_backtest(signal_df, m15, z_col,
                                  immediate_recheck=False)
    print(f"  Hourly only: {len(hourly_trades):,} trades")

    # ── Run backtest WITH immediate post-close check ──────────────────────────
    print("  Running immediate post-close backtest...")
    immediate_trades = run_backtest(signal_df, m15, z_col,
                                     immediate_recheck=True)
    print(f"  With immediate recheck: {len(immediate_trades):,} trades")

    # ── Compare results ───────────────────────────────────────────────────────
    print(f"\n{'='*70}")
    print("RESULTS COMPARISON")
    print(f"{'='*70}")

    for label, trades in [("Hourly only (current)", hourly_trades),
                           ("With immediate recheck", immediate_trades)]:
        if not trades:
            continue
        df = pd.DataFrame(trades)
        rets = df["net_ret"].values
        n = len(df)
        wins = (rets > 0).sum()
        equity = ACCOUNT_START + (rets * BASE_NOTIONAL).cumsum()
        peak = np.maximum.accumulate(equity)
        dd = (equity - peak) / peak * 100
        per_yr = n / YEARS
        rf = (1 + 0.04) ** (1 / per_yr) - 1
        excess = rets - rf
        sharpe = (excess.mean() / excess.std() * np.sqrt(per_yr)
                  if excess.std() > 0 else 0)
        gp = (rets[rets > 0] * BASE_NOTIONAL).sum()
        gl = abs((rets[rets < 0] * BASE_NOTIONAL).sum())
        pf = gp / gl if gl > 0 else 999

        print(f"\n  {label}")
        print(f"  {'─'*50}")
        print(f"  Trades      : {n:,}  ({per_yr:.1f}/yr)")
        print(f"  Win rate    : {wins/n*100:.1f}%")
        print(f"  Sharpe      : {sharpe:.2f}")
        print(f"  Profit factor: {pf:.2f}")
        print(f"  Max DD      : {dd.min():.2f}%")
        print(f"  Net PnL     : ${(rets * BASE_NOTIONAL).sum():,.0f}")

    # ── Extra trades analysis ─────────────────────────────────────────────────
    if hourly_trades and immediate_trades:
        extra = len(immediate_trades) - len(hourly_trades)
        print(f"\n{'─'*70}")
        print(f"EXTRA TRADES FROM IMMEDIATE RECHECK: {extra:+,}")
        print(f"{'─'*70}")

        if extra > 0:
            # Find the extra trades — those not in hourly set
            hourly_times = {t["entry_time"] for t in hourly_trades}
            extra_trades = [t for t in immediate_trades
                            if t["entry_time"] not in hourly_times]
            if extra_trades:
                df_extra = pd.DataFrame(extra_trades)
                extra_wr = (df_extra["net_ret"] > 0).mean() * 100
                extra_pnl = (df_extra["net_ret"] * BASE_NOTIONAL).sum()
                print(f"\n  Extra trade win rate : {extra_wr:.1f}%")
                print(f"  Extra trade net PnL  : ${extra_pnl:,.0f}")
                print(f"  Per year             : {len(extra_trades)/YEARS:.1f} extra trades")

                # Time gap distribution — how long after close did signal fire?
                print(f"\n  These trades fired within 59 min of a previous close")
                print(f"  Win rate vs baseline: {extra_wr:.1f}% vs "
                      f"{(pd.DataFrame(hourly_trades)['net_ret']>0).mean()*100:.1f}%")

        print(f"""
  CONCLUSION:
  {'Extra trades improve results' if extra > 0 and extra_wr > 74 else
   'Extra trades do not improve results' if extra > 0 else
   'No extra trades generated'}

  {'→ RECOMMEND implementing immediate post-close check' if extra > 0 and extra_wr > 74 else
   '→ KEEP hourly-only schedule (no meaningful benefit)'}
""")


def run_backtest(signal_df, m15, z_col, immediate_recheck=False):
    """
    Simulate trades using hourly signal checks.
    If immediate_recheck=True, also checks for signal at the exact
    minute a trade closes (simulating post-close immediate check).
    """
    trades = []
    last_exit_time = None
    last_exit_z    = None  # z-score at the moment of close (for recheck)

    for i, row in signal_df.iterrows():
        dt = row["datetime"]

        # Skip outside session
        if dt.hour not in range(5, 15):
            continue

        z = float(row[z_col])

        # If we are in a trade, check for exit conditions
        if last_exit_time is not None:
            # Normal hourly check — standard behaviour
            pass

        # Check if this is a valid new signal
        if abs(z) < THRESHOLD:
            continue

        # Skip if too soon after last exit (concurrent trade prevention)
        if last_exit_time is not None and dt <= last_exit_time:
            continue

        # Find Fibonacci entry
        signal_close = float(row.get("close", 0))
        if signal_close == 0:
            continue

        direction = 1 if z >= THRESHOLD else -1

        # Get high/low from signal bar in 15M data
        bar_15m = m15[m15["datetime"] == dt]
        if bar_15m.empty:
            # Approximate from nearby bars
            nearby = m15[(m15["datetime"] >= dt) &
                         (m15["datetime"] < dt + pd.Timedelta(hours=1))]
            if nearby.empty:
                continue
            bar_high = float(nearby["high"].max())
            bar_low  = float(nearby["low"].min())
            bar_close = float(nearby.iloc[-1]["close"])
        else:
            bar_high  = float(bar_15m.iloc[0]["high"])
            bar_low   = float(bar_15m.iloc[0]["low"])
            bar_close = float(bar_15m.iloc[0]["close"])

        if direction == 1:
            pullback = bar_close - bar_low
            if pullback <= 0.00005:
                continue
            target = bar_close - FIB * pullback
        else:
            pullback = bar_high - bar_close
            if pullback <= 0.00005:
                continue
            target = bar_close + FIB * pullback

        # Search for entry in next 6 hours of 15M bars
        window_end = dt + pd.Timedelta(hours=6)
        entry_bars = m15[
            (m15["datetime"] > dt) &
            (m15["datetime"] <= window_end)
        ]

        entry_price = None
        entry_time  = None
        for _, bar in entry_bars.iterrows():
            if direction == 1 and float(bar["low"]) <= target:
                entry_price = target
                entry_time  = bar["datetime"]
                break
            elif direction == -1 and float(bar["high"]) >= target:
                entry_price = target
                entry_time  = bar["datetime"]
                break

        if entry_price is None:
            continue

        # Simulate trade exit
        zscore_abs = abs(z)
        mult = get_multiplier(zscore_abs)
        tp_price = (entry_price * (1 + TP_PCT) if direction == 1
                    else entry_price * (1 - TP_PCT))
        sl_price = (entry_price * (1 - STOP_PCT) if direction == 1
                    else entry_price * (1 + STOP_PCT))

        exit_price  = None
        exit_time   = None
        hold_bars   = m15[
            (m15["datetime"] > entry_time) &
            (m15["datetime"] <= entry_time + pd.Timedelta(hours=HOLD_HOURS))
        ]

        for _, bar in hold_bars.iterrows():
            if direction == 1:
                if float(bar["high"]) >= tp_price:
                    exit_price = tp_price; exit_time = bar["datetime"]; break
                if float(bar["low"]) <= sl_price:
                    exit_price = sl_price; exit_time = bar["datetime"]; break
            else:
                if float(bar["low"]) <= tp_price:
                    exit_price = tp_price; exit_time = bar["datetime"]; break
                if float(bar["high"]) >= sl_price:
                    exit_price = sl_price; exit_time = bar["datetime"]; break

        if exit_price is None:
            exit_price = float(hold_bars.iloc[-1]["close"]) if not hold_bars.empty else entry_price
            exit_time  = hold_bars.iloc[-1]["datetime"] if not hold_bars.empty else entry_time

        raw_ret = ((exit_price - entry_price) / entry_price * direction
                   - SPREAD_COST)

        trades.append({
            "signal_time" : dt,
            "entry_time"  : entry_time,
            "exit_time"   : exit_time,
            "direction"   : direction,
            "zscore"      : z,
            "entry_price" : entry_price,
            "exit_price"  : exit_price,
            "net_ret"     : raw_ret * mult,
            "win"         : int(raw_ret > 0),
        })

        last_exit_time = exit_time

    return trades


if __name__ == "__main__":
    main()
