"""
usdjpy_validation_v1.py
========================
Full validation of the best USDJPY config from the parameter sweep.

Best config: z30 t2.0 tp0.50% sl0.50% h52h
  - Full Sharpe: 1.83  WR: 72.2%  DD: -2.87%
  - OOS Sharpe:  2.40  WR: 73.7%  N: 152

Tests:
  1. Equity curve with buy & hold comparison
  2. Yearly P&L heatmap
  3. Monthly returns distribution
  4. Drawdown chart
  5. Walk-forward OOS validation
  6. Win rate by z-score band
  7. Direction analysis (LONG vs SHORT)
  8. YCC regime split (pre/post 2016)

Run from project root:
  python src/research/usdjpy_validation_v1.py
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from pathlib import Path
import sys
import warnings
warnings.filterwarnings('ignore')

BASE_PATH  = Path(__file__).resolve().parents[2]
PROC_PATH  = BASE_PATH / "data" / "processed"
RATES_PATH = BASE_PATH / "data" / "raw" / "rates"
OUT_PATH   = BASE_PATH / "data" / "output" / "usdjpy"
OUT_PATH.mkdir(parents=True, exist_ok=True)

if str(BASE_PATH / "src") not in sys.path:
    sys.path.insert(0, str(BASE_PATH / "src"))

# ── Locked best config ────────────────────────────────────────────────────────
Z_WINDOW   = 30
THRESHOLD  = 2.0
TP_PCT     = 0.0050
SL_PCT     = 0.0050
HOLD_HOURS = 52
FIB        = 0.786
SPREAD_COST= 0.0001
NOTIONAL   = 300_000
SESSION_S  = 7
SESSION_E  = 17
ZSCORE_EXIT= 1.5
WF_SPLIT   = pd.Timestamp('2020-01-01')
YCC_START  = pd.Timestamp('2016-01-01')
YCC_END    = pd.Timestamp('2024-01-01')


def load_rates():
    us2y = pd.read_csv(RATES_PATH / "us2y.csv")
    dc = [c for c in us2y.columns if 'date' in c.lower()][0]
    vc = [c for c in us2y.columns if c != dc][0]
    us2y[dc] = pd.to_datetime(us2y[dc])
    us2y = us2y.rename(columns={dc:'date', vc:'us2y'})[['date','us2y']].dropna()

    jp2y = pd.read_csv(RATES_PATH / "jp2y.csv")
    jp2y['date'] = pd.to_datetime(jp2y['date'])
    jp2y = jp2y[['date','jp2y']].dropna()

    rates = pd.merge(us2y, jp2y, on='date', how='outer').sort_values('date')
    rates = rates.set_index('date')
    full_idx = pd.date_range(rates.index.min(), rates.index.max(), freq='D')
    rates = rates.reindex(full_idx).ffill().dropna()
    rates['spread'] = rates['us2y'] - rates['jp2y']
    s = rates['spread']
    rates['zscore'] = (s - s.rolling(Z_WINDOW).mean()) / s.rolling(Z_WINDOW).std()
    return rates


def load_prices():
    fpath = next(BASE_PATH.rglob("USDJPY_15M*.csv"), None)
    df = pd.read_csv(fpath)
    df.columns = df.columns.str.strip().str.lower()
    dc = next(c for c in df.columns if 'date' in c or 'time' in c)
    df[dc] = pd.to_datetime(df[dc])
    df = df.rename(columns={dc:'datetime'}).set_index('datetime').sort_index()
    return df


def run_backtest(rates, m15):
    z_daily  = rates['zscore']
    full_idx_h = pd.date_range(z_daily.index.min(), z_daily.index.max(), freq='h')
    z_hourly = z_daily.reindex(full_idx_h).ffill()

    # Also load USDJPY 1H for buy & hold
    fpath_1h = next(BASE_PATH.rglob("USDJPY_1H*.csv"), None)
    bh_df = None
    if fpath_1h:
        bh_raw = pd.read_csv(fpath_1h)
        bh_raw.columns = bh_raw.columns.str.strip().str.lower()
        dc = next(c for c in bh_raw.columns if 'date' in c or 'time' in c)
        bh_raw[dc] = pd.to_datetime(bh_raw[dc])
        bh_df = bh_raw.rename(columns={dc:'datetime'}).set_index('datetime').sort_index()

    trades    = []
    last_exit = None

    for dt in pd.date_range('2003-01-01','2026-04-01',freq='h'):
        if dt not in z_hourly.index: continue
        z = float(z_hourly[dt])
        if abs(z) < THRESHOLD: continue
        h = (dt.hour + 2) % 24
        if not (SESSION_S <= h <= SESSION_E): continue
        if last_exit is not None and dt <= last_exit: continue

        direction = 1 if z >= THRESHOLD else -1

        bar_end   = dt + pd.Timedelta(hours=1) - pd.Timedelta(minutes=1)
        bar_slice = m15.loc[dt:bar_end] if dt in m15.index else pd.DataFrame()
        if bar_slice.empty: continue

        bh = float(bar_slice['high'].max())
        bl = float(bar_slice['low'].min())
        bc = float(bar_slice['close'].iloc[-1])

        if direction == 1:
            pull = bc - bl
            if pull <= 0.05: continue
            target = bc - FIB * pull
        else:
            pull = bh - bc
            if pull <= 0.05: continue
            target = bc + FIB * pull

        entry_window = m15.loc[
            dt + pd.Timedelta(minutes=1):dt + pd.Timedelta(hours=6)]
        entry_price = entry_time = None
        for edt, ebar in entry_window.iterrows():
            if direction==1 and float(ebar['low']) <= target:
                entry_price = target; entry_time = edt; break
            elif direction==-1 and float(ebar['high']) >= target:
                entry_price = target; entry_time = edt; break
        if entry_price is None: continue

        tp = entry_price*(1+TP_PCT) if direction==1 else entry_price*(1-TP_PCT)
        sl = entry_price*(1-SL_PCT) if direction==1 else entry_price*(1+SL_PCT)

        hold = m15.loc[
            entry_time + pd.Timedelta(minutes=1):
            entry_time + pd.Timedelta(hours=HOLD_HOURS)]

        exit_price  = None
        exit_reason = "hold_expiry"

        for hdt, hbar in hold.iterrows():
            hr = hdt.replace(minute=0,second=0,microsecond=0)
            if hr in z_hourly.index:
                hz = float(z_hourly[hr])
                if ((direction==1 and hz<=-ZSCORE_EXIT) or
                        (direction==-1 and hz>=ZSCORE_EXIT)):
                    exit_price = float(hbar['close']); exit_reason="z_exit"; break
            if direction==1:
                if float(hbar['high']) >= tp: exit_price=tp; exit_reason="tp"; break
                if float(hbar['low'])  <= sl: exit_price=sl; exit_reason="stop"; break
            else:
                if float(hbar['low'])  <= tp: exit_price=tp; exit_reason="tp"; break
                if float(hbar['high']) >= sl: exit_price=sl; exit_reason="stop"; break

        if exit_price is None:
            exit_price = float(hold['close'].iloc[-1]) if not hold.empty else entry_price

        pnl = round(((exit_price-entry_price)/entry_price*direction
                     - SPREAD_COST)*NOTIONAL, 2)

        zscore_abs = abs(z)
        band = "2.0-2.5" if zscore_abs<2.5 else ("2.5-3.0" if zscore_abs<3.0 else "3.0+")

        trades.append({
            'entry_time':   entry_time,
            'direction':    direction,
            'zscore':       round(z,4),
            'zscore_abs':   round(zscore_abs,4),
            'zscore_band':  band,
            'exit_reason':  exit_reason,
            'entry_price':  entry_price,
            'exit_price':   exit_price,
            'pnl':          pnl,
            'year':         entry_time.year,
            'month':        entry_time.month,
            'regime':       ('ycc' if YCC_START<=entry_time<YCC_END else
                            ('post_ycc' if entry_time>=YCC_END else 'pre_ycc')),
        })
        last_exit = entry_time + pd.Timedelta(hours=HOLD_HOURS)

    return pd.DataFrame(trades), bh_df


def make_charts(trades, rates, bh_df):
    print("Generating charts...")
    df = trades.copy().sort_values('entry_time').reset_index(drop=True)
    df['equity']   = 100_000 + df['pnl'].cumsum()
    df['peak']     = df['equity'].cummax()
    df['drawdown'] = (df['equity'] - df['peak']) / df['peak'] * 100
    dates = pd.to_datetime(df['entry_time'])

    # 1. Equity curve
    fig, ax = plt.subplots(figsize=(14,5))
    if bh_df is not None:
        try:
            bh = bh_df.loc[bh_df.index >= df['entry_time'].min()]['close']
            ax.plot(bh.index, bh/bh.iloc[0]*100_000,
                    color='grey', linewidth=1, alpha=0.5, label='Buy & Hold')
        except Exception:
            pass
    ax.plot(dates, df['equity'], color='#00ff88', linewidth=1.5, label='Strategy')
    ax.axvline(WF_SPLIT, color='gold', linewidth=1, linestyle='--', label='OOS split')
    ax.axhline(100_000, color='#666', linewidth=0.5)
    ax.set_title(f'USDJPY Equity Curve — z{Z_WINDOW} ±{THRESHOLD} TP{TP_PCT*100:.2f}% SL{SL_PCT*100:.2f}% H{HOLD_HOURS}h')
    ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x,_: f'${x:,.0f}'))
    ax.legend(); ax.grid(alpha=0.2)
    fig.tight_layout()
    fig.savefig(OUT_PATH / 'usdjpy_equity.png', dpi=150)
    print('  Saved: usdjpy_equity.png')
    plt.close(fig)

    # 2. Drawdown
    fig, ax = plt.subplots(figsize=(14,4))
    ax.fill_between(dates, df['drawdown'], 0, color='#ff4444', alpha=0.6)
    ax.set_title(f'Drawdown  |  Max: {df["drawdown"].min():.2f}%')
    ax.set_ylabel('%'); ax.grid(alpha=0.2)
    fig.tight_layout()
    fig.savefig(OUT_PATH / 'usdjpy_drawdown.png', dpi=150)
    print('  Saved: usdjpy_drawdown.png')
    plt.close(fig)

    # 3. Yearly P&L
    fig, ax = plt.subplots(figsize=(14,5))
    yearly = df.groupby('year')['pnl'].sum()
    colors = ['#00ff88' if v >= 0 else '#ff4444' for v in yearly.values]
    ax.bar(yearly.index, yearly.values/1000, color=colors, alpha=0.8)
    ax.axhline(0, color='#666', linewidth=0.5)
    ax.set_title('Yearly P&L ($k)')
    ax.set_ylabel('$k'); ax.grid(alpha=0.2, axis='y')
    fig.tight_layout()
    fig.savefig(OUT_PATH / 'usdjpy_yearly.png', dpi=150)
    print('  Saved: usdjpy_yearly.png')
    plt.close(fig)

    # 4. Monthly heatmap (pure matplotlib — no seaborn needed)
    hmap = df.pivot_table(values='pnl', index='year',
                          columns='month', aggfunc='sum') / 1000
    month_names = ['Jan','Feb','Mar','Apr','May','Jun',
                   'Jul','Aug','Sep','Oct','Nov','Dec']
    hmap.columns = [month_names[m-1] for m in hmap.columns]
    fig, ax = plt.subplots(figsize=(16, max(6, len(hmap)*0.4+2)))
    vmax = max(abs(hmap.values[~np.isnan(hmap.values)]).max(), 1)
    im = ax.imshow(hmap.values, cmap='RdYlGn', aspect='auto',
                   vmin=-vmax, vmax=vmax)
    ax.set_xticks(range(len(hmap.columns)))
    ax.set_xticklabels(hmap.columns, fontsize=9)
    ax.set_yticks(range(len(hmap.index)))
    ax.set_yticklabels(hmap.index, fontsize=9)
    for i in range(len(hmap.index)):
        for j in range(len(hmap.columns)):
            val = hmap.values[i, j]
            if not np.isnan(val):
                ax.text(j, i, f'{val:.1f}', ha='center', va='center',
                        fontsize=8, color='black' if abs(val) < vmax*0.6 else 'white')
    plt.colorbar(im, ax=ax, label='P&L $k')
    ax.set_title('Monthly P&L Heatmap ($k)')
    fig.tight_layout()
    fig.savefig(OUT_PATH / 'usdjpy_heatmap.png', dpi=150)
    print('  Saved: usdjpy_heatmap.png')
    plt.close(fig)

    plt.close('all')


def print_summary(trades, rates):
    df = trades.copy()
    years = (df['entry_time'].max() - df['entry_time'].min()).days/365.25
    is_df  = df[df['entry_time'] <  WF_SPLIT]
    oos_df = df[df['entry_time'] >= WF_SPLIT]
    yrs_oos = (df['entry_time'].max() - WF_SPLIT).days/365.25
    yrs_is  = (WF_SPLIT - df['entry_time'].min()).days/365.25

    def sh(d, yrs):
        if len(d)==0: return 0,0,0,0,0
        pnl=d['pnl'].values; wr=(pnl>0).mean()*100
        eq=100_000+np.cumsum(pnl); pk=np.maximum.accumulate(eq)
        dd=((eq-pk)/pk*100).min(); pyr=len(pnl)/yrs
        exc=pnl/100_000-0.04/pyr
        sharpe=exc.mean()/exc.std()*np.sqrt(pyr) if exc.std()>0 else 0
        return round(sharpe,2),round(wr,1),round(dd,2),int(pnl.sum()),len(pnl)

    sf,wf,df2,tf,nf = sh(df, years)
    si,wi,di,ti,ni  = sh(is_df, yrs_is)
    so,wo,do,to,no  = sh(oos_df, yrs_oos)

    print(f"\n{'='*72}")
    print("VALIDATION SUMMARY — USDJPY z30 t2.0 TP0.50% SL0.50% H52h")
    print(f"{'='*72}")
    print(f"\n  {'Period':<22}{'N':>6}{'N/yr':>6}{'WR%':>6}{'Sharpe':>8}"
          f"{'MaxDD%':>8}{'Total$':>12}")
    print(f"  {'─'*65}")
    pyr_f = nf/years; pyr_i=ni/yrs_is; pyr_o=no/yrs_oos
    print(f"  {'Full (2003-2026)':<22}{nf:>6}{pyr_f:>6.0f}{wf:>6.1f}"
          f"{sf:>8.2f}{df2:>8.2f}{tf:>12,.0f}")
    print(f"  {'IS (2003-2020)':<22}{ni:>6}{pyr_i:>6.0f}{wi:>6.1f}"
          f"{si:>8.2f}{di:>8.2f}{ti:>12,.0f}")
    print(f"  {'OOS (2020-2026)':<22}{no:>6}{pyr_o:>6.0f}{wo:>6.1f}"
          f"{so:>8.2f}{do:>8.2f}{to:>12,.0f}")

    # Compare with EURUSD
    print(f"\n  COMPARISON WITH EURUSD MODEL:")
    print(f"  {'Metric':<25}{'EURUSD':>12}{'USDJPY':>12}")
    print(f"  {'─'*50}")
    print(f"  {'Full Sharpe':<25}{'3.72':>12}{sf:>12.2f}")
    print(f"  {'OOS Sharpe':<25}{'2.73':>12}{so:>12.2f}")
    print(f"  {'Win Rate':<25}{'75.2%':>12}{wf:>11.1f}%")
    print(f"  {'Max Drawdown':<25}{'-4.28%':>12}{df2:>11.2f}%")
    print(f"  {'Trades/year':<25}{'182':>12}{pyr_f:>12.0f}")
    print(f"  {'Threshold':<25}{'±2.75':>12}{f'±{THRESHOLD}':>12}")
    print(f"  {'TP':<25}{'0.20%':>12}{f'{TP_PCT*100:.2f}%':>12}")

    # Direction split
    print(f"\n  DIRECTION ANALYSIS:")
    for d, dlabel in [(1,'LONG  (Short JPY)'),(-1,'SHORT (Long JPY)')]:
        sub = df[df['direction']==d]
        wr  = (sub['pnl']>0).mean()*100
        tot = sub['pnl'].sum()
        print(f"  {dlabel:<22}: n={len(sub):>4}  WR={wr:.1f}%  Total=${tot:>10,.0f}")

    # Regime split
    print(f"\n  BoJ REGIME ANALYSIS:")
    for reg, rlabel in [('pre_ycc','Pre-YCC (<2016)'),
                        ('ycc','YCC Era (2016-2024)'),
                        ('post_ycc','Post-YCC (2024+)')]:
        sub = df[df['regime']==reg]
        if len(sub)==0: continue
        wr  = (sub['pnl']>0).mean()*100
        tot = sub['pnl'].sum()
        print(f"  {rlabel:<22}: n={len(sub):>4}  WR={wr:.1f}%  Total=${tot:>10,.0f}")

    # Exit reason breakdown
    print(f"\n  EXIT BREAKDOWN:")
    for reason, n in df['exit_reason'].value_counts().items():
        pct = n/len(df)*100
        sub_wr = (df[df['exit_reason']==reason]['pnl']>0).mean()*100
        print(f"  {reason:<15}: {n:>4} ({pct:.1f}%)  WR={sub_wr:.1f}%")

    # Z-score band breakdown
    print(f"\n  Z-SCORE BAND BREAKDOWN:")
    for band in ['2.0-2.5','2.5-3.0','3.0+']:
        sub = df[df['zscore_band']==band]
        if len(sub)==0: continue
        wr  = (sub['pnl']>0).mean()*100
        avg = sub['pnl'].mean()
        print(f"  z {band:<10}: n={len(sub):>4}  WR={wr:.1f}%  Avg=${avg:.0f}")

    print(f"\n  Charts saved to: {OUT_PATH}")


def main():
    print("="*72)
    print("USDJPY VALIDATION v1")
    print(f"Config: z{Z_WINDOW} threshold=±{THRESHOLD} "
          f"TP={TP_PCT*100:.2f}% SL={SL_PCT*100:.2f}% hold={HOLD_HOURS}h")
    print("="*72)

    rates  = load_rates()
    m15    = load_prices()

    print("\nRunning backtest...")
    trades, bh_df = run_backtest(rates, m15)
    print(f"  {len(trades):,} trades generated")

    print_summary(trades, rates)
    make_charts(trades, rates, bh_df)

    # Save trades
    out = PROC_PATH / "usdjpy_trades_validated.csv"
    trades.to_csv(out, index=False)
    print(f"\n  Trades saved: {out.name}")


if __name__ == "__main__":
    main()
