"""
london_open_entry_v1.py
========================
Tests London open entry method vs the current 15M Fibonacci pullback
on EURUSD using identical signals, hold period, and stop.

Hypothesis
----------
The 15M Fibonacci pullback entry places trades at a precise intraday level
that gets immediately tested by normal market noise, causing 66% stop hit rate.
The macro signal operates on a 24-48h horizon. Entering at the next London open
(08:00 EET) after signal fires aligns entry with institutional execution timing,
reducing noise-driven stop-outs.

Methods tested
--------------
  A) Current baseline : Fibonacci 0.786 pullback on 15M within 6h of signal
  B) London open next : Enter at 08:00 EET bar on the NEXT calendar day

Both use:
  - Same frozen signals (threshold 2.75, London+NY session filter)
  - Same hold period (52h from entry)
  - Same stop (-0.25% hard stop)
  - Same spread cost (0.01%)
  - One trade at a time

Output
------
  Full comparison table
  Trade logs saved to data/processed/trades/

Place this file in:
  C:\\Users\\paul_\\OneDrive\\fx_macro_intraday\\src\\research\\london_open_entry_v1.py

Run from project root:
  python src/research/london_open_entry_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"
if str(SRC_PATH) not in sys.path:
    sys.path.append(str(SRC_PATH))

from research.combined_candidate_matrix_v1 import (
    build_frozen_signals,
    find_entry_pullback,
    get_first_m15_idx_at_or_after,
)
from ingestion.price_loader import load_eurusd
from ingestion.price_loader_15m import load_eurusd_15m

TRADES_DIR    = BASE_PATH / "data" / "processed" / "trades"
TRADES_DIR.mkdir(parents=True, exist_ok=True)

# ── Frozen validated parameters ───────────────────────────────────────────────
THRESHOLD     = 2.75
FIB           = 0.786
HOLD_HOURS    = 52
STOP          = 0.0025
SPREAD_COST   = 0.0001
ALLOWED_HOURS = set(range(7, 17))
LONDON_OPEN   = 8    # 08:00 EET — London open in EET timezone

N_SIMS        = 2000
RANDOM_SEED   = 42

ACCOUNT_START = 100_000.0
PROFIT_TARGET = ACCOUNT_START * 0.10
MAX_OVERALL   = ACCOUNT_START * 0.10
MAX_DAILY     = ACCOUNT_START * 0.05
BASE_RISK_PCT = 0.003
BASE_NOTIONAL = (ACCOUNT_START * BASE_RISK_PCT) / STOP   # $120,000

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

YEARS = 22.0


# ── Signal scaling ────────────────────────────────────────────────────────────
def get_multiplier(z: float) -> float:
    for lo, hi, mult in ZSCORE_BANDS:
        if lo <= z < hi:
            return mult
    return ZSCORE_BANDS[-1][2]


# ── Trade simulator (full diagnostics) ───────────────────────────────────────
def simulate_trade_full(
    price_df    : pd.DataFrame,
    entry_time  : pd.Timestamp,
    entry_price : float,
    signal      : int,
    hold_hours  : int = HOLD_HOURS,
    stop        : float = STOP,
    spread_cost : float = SPREAD_COST,
    is_1h       : bool = False,
) -> dict | None:
    """
    Simulates a single trade on either 1H or 15M data.
    is_1h: True = use 1H bars, False = use 15M bars.
    """
    bars_per_hour = 1 if is_1h else 4

    # Find entry bar index
    if is_1h:
        idx = price_df["datetime"].searchsorted(entry_time, side="left")
        if idx >= len(price_df):
            return None
        entry_idx = int(idx)
    else:
        entry_idx = get_first_m15_idx_at_or_after(price_df, entry_time)
        if entry_idx is None:
            return None

    hold_bars = hold_hours * bars_per_hour
    exit_idx  = min(entry_idx + hold_bars, len(price_df) - 1)
    path      = price_df.iloc[entry_idx : exit_idx + 1].copy()
    if path.empty:
        return None

    exit_price = float(path.iloc[-1]["close"])

    if signal == 1:
        mae      = (path["low"].min()  - entry_price) / entry_price
        mfe      = (path["high"].max() - entry_price) / entry_price
        stop_hit = mae < -stop
        raw_ret  = -stop if stop_hit else (exit_price / entry_price - 1)
    else:
        mae      = -((path["high"].max() - entry_price) / entry_price)
        mfe      = (entry_price - path["low"].min()) / entry_price
        stop_hit = (path["high"].max() - entry_price) / entry_price > stop
        raw_ret  = -stop if stop_hit else -(exit_price / entry_price - 1)

    return {
        "entry_time" : entry_time,
        "exit_time"  : path.iloc[-1]["datetime"],
        "signal"     : signal,
        "entry_price": entry_price,
        "exit_price" : exit_price,
        "stop_hit"   : stop_hit,
        "mae"        : mae,
        "mfe"        : mfe,
        "return"     : raw_ret - spread_cost,
    }


# ── Method A: Fibonacci pullback (current baseline) ──────────────────────────
def run_fibonacci_baseline(signals: pd.DataFrame, m15: pd.DataFrame) -> pd.DataFrame:
    """
    Current validated method: 0.786 Fibonacci pullback on 15M within 6h of signal.
    This reproduces the exact logic from combined_candidate_matrix_v1.py.
    """
    trades         = []
    last_exit_time = None

    for _, sig in signals.iterrows():
        if last_exit_time is not None and sig["datetime"] < last_exit_time:
            continue

        entry = find_entry_pullback(
            m15=m15, signal_row=sig, fib=FIB, wait_hours=6
        )
        if entry is None:
            continue

        trade = simulate_trade_full(
            price_df    =m15,
            entry_time  =entry["entry_time"],
            entry_price =entry["entry_price"],
            signal      =int(sig["signal"]),
            is_1h       =False,
        )
        if trade is None:
            continue

        trade["zscore_abs"] = abs(float(sig["lag_zscore_24h_v3"]))
        trades.append(trade)
        last_exit_time = trade["exit_time"]

    return pd.DataFrame(trades) if trades else pd.DataFrame()


# ── Method B: London open next day ───────────────────────────────────────────
def find_next_london_open(
    prices_1h  : pd.DataFrame,
    signal_time: pd.Timestamp,
) -> dict | None:
    """
    Finds the next 08:00 EET bar after the signal fires.

    Logic:
      - Signal fires at any hour during the day
      - We want the NEXT morning's 08:00 bar (never same day to avoid look-ahead)
      - If no 08:00 bar found within 36h, skip the signal

    Returns dict with entry_time and entry_price (open of the 08:00 bar).
    """
    # Look for the next 08:00 bar strictly after the signal time
    search_start = signal_time + pd.Timedelta(hours=1)
    search_end   = signal_time + pd.Timedelta(hours=36)

    window = prices_1h[
        (prices_1h["datetime"] >= search_start) &
        (prices_1h["datetime"] <= search_end) &
        (prices_1h["datetime"].dt.hour == LONDON_OPEN)
    ]

    if window.empty:
        return None

    bar = window.iloc[0]
    return {
        "entry_time" : bar["datetime"],
        "entry_price": float(bar["open"]),   # enter at open of the London bar
    }


def run_london_open(
    signals   : pd.DataFrame,
    prices_1h : pd.DataFrame,
) -> pd.DataFrame:
    """
    Method B: Enter at the opening price of the next 08:00 EET bar after signal.
    Simulates on 1H bars for the hold period.
    """
    trades         = []
    last_exit_time = None

    for _, sig in signals.iterrows():
        if last_exit_time is not None and sig["datetime"] < last_exit_time:
            continue

        entry = find_next_london_open(prices_1h, sig["datetime"])
        if entry is None:
            continue

        trade = simulate_trade_full(
            price_df    =prices_1h,
            entry_time  =entry["entry_time"],
            entry_price =entry["entry_price"],
            signal      =int(sig["signal"]),
            is_1h       =True,
        )
        if trade is None:
            continue

        trade["zscore_abs"] = abs(float(sig["lag_zscore_24h_v3"]))
        trades.append(trade)
        last_exit_time = trade["exit_time"]

    return pd.DataFrame(trades) if trades else pd.DataFrame()


# ── Build equity curve ────────────────────────────────────────────────────────
def build_equity(df: pd.DataFrame) -> pd.DataFrame:
    df = df.copy().sort_values("entry_time").reset_index(drop=True)
    df["equity"]   = (1 + df["return"]).cumprod()
    df["peak"]     = df["equity"].cummax()
    df["drawdown"] = df["equity"] / df["peak"] - 1
    df["win"]      = (df["return"] > 0).astype(int)

    streak = max_streak = 0
    streaks = []
    for r in df["return"]:
        if r <= 0:
            streak    += 1
            max_streak = max(max_streak, streak)
        else:
            streak = 0
        streaks.append(streak)
    df["lose_streak"] = streaks
    df["max_streak"]  = max_streak
    return df


# ── FTMO Monte Carlo ──────────────────────────────────────────────────────────
def run_mc(df: pd.DataFrame, n_sims: int = N_SIMS) -> dict:
    rng      = np.random.default_rng(RANDOM_SEED)
    returns  = df["return"].values
    zscores  = df["zscore_abs"].values
    exits    = df["exit_time"].values

    results = []
    for _ in range(n_sims):
        idx      = rng.permutation(len(returns))
        sh_ret   = returns[idx]
        sh_mult  = np.array([get_multiplier(z) for z in zscores[idx]])

        balance   = ACCOUNT_START
        peak      = ACCOUNT_START
        max_dd    = 0.0
        daily_pnl = {}
        outcome   = "INCOMPLETE"
        tdays     = set()
        ttrades   = 0

        for ret, mult, ex_dt in zip(sh_ret, sh_mult, exits):
            dk   = str(pd.Timestamp(ex_dt).date())
            if dk not in daily_pnl:
                daily_pnl[dk] = 0.0
            notional       = min(BASE_NOTIONAL * mult, BASE_NOTIONAL * 3)
            pnl            = ret * notional
            balance       += pnl
            ttrades       += 1
            tdays.add(dk)
            daily_pnl[dk] += pnl

            if balance > peak:
                peak = balance
            dd = (peak - balance) / ACCOUNT_START
            if dd > max_dd:
                max_dd = dd

            if daily_pnl[dk] < -MAX_DAILY:
                outcome = "BREACH_DAILY"; break
            if balance <= ACCOUNT_START - MAX_OVERALL:
                outcome = "BREACH_OVERALL"; break
            if balance >= ACCOUNT_START + PROFIT_TARGET:
                outcome = "PASS"; break

        if outcome == "INCOMPLETE":
            outcome = ("PASS" if balance >= ACCOUNT_START + PROFIT_TARGET
                       else "BREACH_OVERALL"
                       if (peak - balance) / ACCOUNT_START >= 0.10
                       else "INCOMPLETE")

        results.append({
            "outcome": outcome,
            "max_dd" : max_dd * 100,
            "trades" : ttrades,
        })

    mc = pd.DataFrame(results)
    passing = mc[mc["outcome"] == "PASS"]
    per_yr  = len(df) / YEARS

    return {
        "pass_rate"      : (mc["outcome"] == "PASS").mean() * 100,
        "breach_rate"    : mc["outcome"].str.startswith("BREACH").mean() * 100,
        "dd_95"          : mc["max_dd"].quantile(0.95),
        "dd_50"          : mc["max_dd"].quantile(0.50),
        "avg_trades_pass": passing["trades"].mean() if len(passing) > 0 else np.nan,
        "med_trades_pass": passing["trades"].median() if len(passing) > 0 else np.nan,
        "avg_months"     : (passing["trades"].mean()   / per_yr * 12
                            if len(passing) > 0 else np.nan),
        "med_months"     : (passing["trades"].median() / per_yr * 12
                            if len(passing) > 0 else np.nan),
    }


# ── Print method results ──────────────────────────────────────────────────────
def print_results(name: str, df: pd.DataFrame, mc: dict):
    n       = len(df)
    per_yr  = n / YEARS
    wr      = df["win"].mean()
    avg_ret = df["return"].mean()
    fin_eq  = df["equity"].iloc[-1]
    max_dd  = df["drawdown"].min()
    streak  = int(df["max_streak"].iloc[-1])
    sh_rate = df["stop_hit"].mean()
    mae     = df["mae"].mean() * 100
    mfe     = df["mfe"].mean() * 100

    print(f"\n  {name}")
    print(f"  {'─'*55}")
    print(f"    Trades          : {n}  ({per_yr:.1f}/year)")
    print(f"    Win rate        : {wr:.4%}")
    print(f"    Avg return      : {avg_ret:.6f}")
    print(f"    Final equity    : {fin_eq:.6f}")
    print(f"    Max DD          : {max_dd:.4%}")
    print(f"    Max lose streak : {streak}")
    print(f"    Stop hit rate   : {sh_rate:.4%}")
    print(f"    Avg MAE         : {mae:.3f}%")
    print(f"    Avg MFE         : {mfe:.3f}%")
    print(f"    MFE > |MAE|     : {'YES - edge exists' if mfe > abs(mae) else 'NO  - adverse'}")
    print(f"\n    MC pass rate    : {mc['pass_rate']:.2f}%")
    print(f"    MC 95pct DD     : {mc['dd_95']:.3f}%  (limit 10%)")
    print(f"    MC median DD    : {mc['dd_50']:.3f}%")
    print(f"    Avg months/pass : {mc['avg_months']:.1f}")
    print(f"    Med months/pass : {mc['med_months']:.1f}")


# ── Main ──────────────────────────────────────────────────────────────────────
def main():
    print("=" * 65)
    print("LONDON OPEN ENTRY V1  —  Entry Method Comparison")
    print(f"  Threshold: {THRESHOLD}  |  Hold: {HOLD_HOURS}h  |  Stop: {STOP:.2%}")
    print(f"  Comparing: Fibonacci 0.786 pullback vs London open next day")
    print("=" * 65)

    # Load data
    print("\nLoading price data...")
    prices_1h = load_eurusd().sort_values("datetime").reset_index(drop=True)
    m15       = load_eurusd_15m().sort_values("datetime").reset_index(drop=True)
    print(f"  1H bars : {len(prices_1h):,}")
    print(f"  15M bars: {len(m15):,}")

    # Build signals
    print("\nBuilding signals...")
    signals = build_frozen_signals(
        threshold=THRESHOLD,
        allowed_hours=ALLOWED_HOURS,
    )
    print(f"  Signals: {len(signals)}")

    # ── Method A: Fibonacci baseline ─────────────────────────────────────────
    print("\nRunning Method A: Fibonacci pullback (current baseline)...")
    df_fib = run_fibonacci_baseline(signals, m15)
    if df_fib.empty:
        print("  [ERROR] No trades generated")
        return
    df_fib = build_equity(df_fib)
    mc_fib = run_mc(df_fib)
    df_fib.to_csv(TRADES_DIR / "trades_fib_method.csv", index=False)

    # ── Method B: London open ─────────────────────────────────────────────────
    print("Running Method B: London open next day...")
    df_lon = run_london_open(signals, prices_1h)
    if df_lon.empty:
        print("  [ERROR] No trades generated")
        return
    df_lon = build_equity(df_lon)
    mc_lon = run_mc(df_lon)
    df_lon.to_csv(TRADES_DIR / "trades_london_method.csv", index=False)

    # ── Results ───────────────────────────────────────────────────────────────
    print(f"\n{'='*65}")
    print("RESULTS")
    print(f"{'='*65}")

    print_results("Method A: Fibonacci 0.786 Pullback (Current)", df_fib, mc_fib)
    print_results("Method B: London Open Next Day (New)",          df_lon, mc_lon)

    # ── Direct comparison ─────────────────────────────────────────────────────
    print(f"\n{'='*65}")
    print("DIRECT COMPARISON")
    print(f"{'='*65}")

    metrics = [
        ("Trades",           len(df_fib),                     len(df_lon)),
        ("Trades/year",      f"{len(df_fib)/YEARS:.1f}",      f"{len(df_lon)/YEARS:.1f}"),
        ("Win rate",         f"{df_fib['win'].mean():.4%}",   f"{df_lon['win'].mean():.4%}"),
        ("Avg return",       f"{df_fib['return'].mean():.6f}",f"{df_lon['return'].mean():.6f}"),
        ("Final equity",     f"{df_fib['equity'].iloc[-1]:.4f}",
                             f"{df_lon['equity'].iloc[-1]:.4f}"),
        ("Max DD",           f"{df_fib['drawdown'].min():.4%}",
                             f"{df_lon['drawdown'].min():.4%}"),
        ("Stop hit rate",    f"{df_fib['stop_hit'].mean():.4%}",
                             f"{df_lon['stop_hit'].mean():.4%}"),
        ("Avg MAE",          f"{df_fib['mae'].mean()*100:.3f}%",
                             f"{df_lon['mae'].mean()*100:.3f}%"),
        ("Avg MFE",          f"{df_lon['mfe'].mean()*100:.3f}%",
                             f"{df_lon['mfe'].mean()*100:.3f}%"),
        ("MC pass rate",     f"{mc_fib['pass_rate']:.2f}%",   f"{mc_lon['pass_rate']:.2f}%"),
        ("MC 95pct DD",      f"{mc_fib['dd_95']:.3f}%",       f"{mc_lon['dd_95']:.3f}%"),
        ("Avg months/pass",  f"{mc_fib['avg_months']:.1f}",   f"{mc_lon['avg_months']:.1f}"),
        ("Med months/pass",  f"{mc_fib['med_months']:.1f}",   f"{mc_lon['med_months']:.1f}"),
    ]

    print(f"\n  {'Metric':<26}{'Fibonacci':>16}{'London Open':>16}{'Delta':>10}")
    print(f"  {'─'*68}")
    for label, v_fib, v_lon in metrics:
        print(f"  {label:<26}{str(v_fib):>16}{str(v_lon):>16}")

    # Winner
    print(f"\n{'='*65}")
    print("VERDICT")
    print(f"{'='*65}")

    fib_score = (df_fib["return"].mean() > 0) + (mc_fib["pass_rate"] > 95) + \
                (mc_fib["dd_95"] < 10) + (df_fib["stop_hit"].mean() < 0.5)
    lon_score = (df_lon["return"].mean() > 0) + (mc_lon["pass_rate"] > 95) + \
                (mc_lon["dd_95"] < 10) + (df_lon["stop_hit"].mean() < 0.5)

    print(f"\n  Fibonacci score : {fib_score}/4")
    print(f"  London Open score: {lon_score}/4")

    if lon_score > fib_score:
        print(f"""
  WINNER: London Open entry method

  The London open entry reduces execution noise by entering at a clean,
  liquid institutional level rather than a precise intraday pullback.
  Proceed to Script 2: dynamic exit testing with this entry method.
""")
    elif fib_score > lon_score:
        print(f"""
  WINNER: Fibonacci pullback (current method)

  The Fibonacci entry is not the root cause of the stop hit rate problem.
  The issue likely lies elsewhere — proceed to exit framework testing
  with the current entry method.
""")
    else:
        print(f"""
  DRAW — both methods have similar characteristics.
  Review the stop hit rate and MAE/MFE ratios in detail above.
  The method with lower stop hit rate and positive avg return is preferred.
""")

    print(f"\n  Trade logs saved to: {TRADES_DIR}")
    print(f"  Next step: run dynamic_exit_v1.py with the winning entry method")


if __name__ == "__main__":
    main()
