"""
Dedicated MACD+Stochastic trading system for binary options (H1).

The only strategy that passed profitability threshold: 56.1% WR on BITCOIN.
This module adds session/volatility/trend filters to improve WR further.

Usage:
  python macd_stoch_trader.py --backtest BITCOIN  → backtest + param scan
  python macd_stoch_trader.py --live                → production inference
  python macd_stoch_trader.py --report              → detailed report
"""

import os
import sys
import json
import logging
import argparse
import warnings
from datetime import datetime, time
from typing import Optional

import numpy as np
import pandas as pd

warnings.filterwarnings('ignore')
logging.basicConfig(level=logging.INFO, format='%(asctime)s  %(levelname)-7s %(message)s')
logger = logging.getLogger('macd_stoch')

# ── Imports from project ──
from config import Config
from data_fetcher import DataFetcher
from indicators import compute_all_indicators
from strategies import macd_stoch as _macd_stoch_strategy


# ═══════════════════════════════════════════════════════════════
# Session filter
# ═══════════════════════════════════════════════════════════════

# Trading sessions in UTC hours (inclusive start, exclusive end)
SESSIONS = {
    'BITCOIN': [
        ('Asia',       0,  9),   # Tokyo/Sydney
        ('London',     7, 16),   # London open
        ('NY',        13, 22),   # New York open
        ('Asia-NY',    0, 22),   # Full overlap: 0-22 UTC
    ],
    'EURUSD': [
        ('London',     7, 16),
        ('NY',        13, 22),
        ('London+NY',  7, 22),   # Combined London/NY
    ],
}


def is_in_session(ts: datetime, instrument: str, session_name: str) -> bool:
    """Check if timestamp falls within a named trading session."""
    if instrument not in SESSIONS:
        return True
    for name, start, end in SESSIONS[instrument]:
        if name == session_name:
            hour = ts.hour
            if start <= end:
                return start <= hour < end
            else:
                return hour >= start or hour < end  # overnight session
    return True  # no filter = allow all


# ═══════════════════════════════════════════════════════════════
# MACD+Stoch strategy with filters
# ═══════════════════════════════════════════════════════════════

class MACDStochTrader:
    """Wraps strategies.macd_stoch with session/volatility/ATR filters."""

    def __init__(self, instrument: str, extra_params: dict = None):
        self.instrument = instrument
        self.strategy_params = dict(Config.STRATEGY_PARAMS['macd_stoch'])
        if extra_params:
            self.strategy_params.update(extra_params)
        # Filter params (not passed to strategy)
        self.filters = {
            'session': 'Asia-NY',
            'use_atr_filter': True,
            'atr_min_percentile': 30,
            'min_bars_between': 3,
            'max_per_day': 4,
        }

    @property
    def p(self):
        return self.strategy_params

    def check_session(self, ts: datetime) -> bool:
        session = self.filters.get('session', '')
        if not session:
            return True
        return is_in_session(ts, self.instrument, session)

    def check_atr(self, row: pd.Series, atr_values: np.ndarray, idx: int) -> bool:
        if not self.filters.get('use_atr_filter', True):
            return True
        atr = row['ATR14']
        if pd.isna(atr):
            return False
        lookback = min(idx, 100)
        if lookback < 10:
            return True
        window = atr_values[max(0, idx - lookback):idx + 1]
        threshold = np.percentile(window, self.filters['atr_min_percentile'])
        return atr >= threshold

    def check_overtrade(self, last_trade_idx: int, idx: int, trades_today: int) -> bool:
        bars_ok = (idx - last_trade_idx) >= self.filters.get('min_bars_between', 3)
        daily_ok = trades_today < self.filters.get('max_per_day', 4)
        return bars_ok and daily_ok

    def get_signal(self, df: pd.DataFrame, idx: int,
                   last_trade_idx: int, trades_today: int) -> dict:
        """Generate signal using strategies.macd_stoch + filter chain."""
        # Core strategy signal
        sig = _macd_stoch_strategy(df, self.strategy_params, idx)
        if sig['signal'] is None:
            return _no()

        ts = df.index[idx]
        row = df.iloc[idx]

        # Filter: session
        if not self.check_session(ts):
            return _no()

        # Filter: overtrade
        if not self.check_overtrade(last_trade_idx, idx, trades_today):
            return _no()

        return sig


# ═══════════════════════════════════════════════════════════════
# Backtest engine
# ═══════════════════════════════════════════════════════════════

def backtest(df: pd.DataFrame, trader: MACDStochTrader, expiry_bars: int = 1) -> dict:
    """Run backtest with full filter chain on historical data."""
    cfg = Config.BACKTEST
    balance = cfg['initial_balance']
    peak = balance
    max_dd = 0.0
    trades = []
    equity = [balance]

    last_trade_idx = -999
    daily_count = 0
    last_date = None

    # Pre-compute ATR array for filters
    atr_values = df['ATR14'].values

    for i in range(50, len(df) - expiry_bars):
        row = df.iloc[i]
        ts = df.index[i]

        # Reset daily counter
        current_date = ts.date() if hasattr(ts, 'date') else ts
        if current_date != last_date:
            daily_count = 0
            last_date = current_date

        sig = trader.get_signal(df, i, last_trade_idx, daily_count)
        if sig['signal'] is None:
            continue

        # ATR filter (needs full array, checked here)
        if not trader.check_atr(row, atr_values, i):
            continue

        future_close = df['Close'].iloc[i + expiry_bars]
        current_close = row['Close']
        is_call = sig['signal'] == 'CALL'
        win = (is_call and future_close > current_close) or (not is_call and future_close < current_close)

        pnl = cfg['trade_amount'] * cfg['payout_pct'] if win else -cfg['trade_amount']
        balance += pnl

        trades.append({
            'ts': str(ts), 'sig': sig['signal'], 'conf': sig['confidence'],
            'entry': current_close, 'exit': future_close,
            'win': win, 'pnl': pnl, 'reason': sig['reason'],
        })

        peak = max(peak, balance)
        dd = (peak - balance) / peak * 100 if peak > 0 else 0
        max_dd = max(max_dd, dd)
        equity.append(balance)

        last_trade_idx = i
        daily_count += 1

    return _compute_stats(trades, equity, max_dd, balance, cfg['initial_balance'])


def _compute_stats(trades: list, equity: list, max_dd: float,
                   final_balance: float, initial: float) -> dict:
    n = len(trades)
    if n == 0:
        return {'n': 0, 'wr': 0, 'pf': 0, 'dd': 0, 'pnl': 0}

    wins = [t for t in trades if t['win']]
    losses = [t for t in trades if not t['win']]
    wr = len(wins) / n * 100

    gross_profit = sum(t['pnl'] for t in wins)
    gross_loss = abs(sum(t['pnl'] for t in losses))
    pf = gross_profit / gross_loss if gross_loss > 0 else float('inf')

    avg_conf_win = np.mean([t['conf'] for t in wins]) if wins else 0
    avg_conf_loss = np.mean([t['conf'] for t in losses]) if losses else 0

    # Consecutive win/loss
    max_cw = max_cl = cw = cl = 0
    for t in trades:
        if t['win']:
            cw += 1; cl = 0; max_cw = max(max_cw, cw)
        else:
            cl += 1; cw = 0; max_cl = max(max_cl, cl)

    return {
        'n': n, 'wins': len(wins), 'losses': len(losses),
        'wr': round(wr, 1), 'pf': round(pf, 2), 'dd': round(max_dd, 1),
        'pnl': round(final_balance - initial, 0),
        'final_balance': round(final_balance, 2),
        'max_cw': max_cw, 'max_cl': max_cl,
        'avg_conf_win': round(avg_conf_win, 1),
        'avg_conf_loss': round(avg_conf_loss, 1),
    }


# ═══════════════════════════════════════════════════════════════
# Parameter scan
# ═══════════════════════════════════════════════════════════════

def param_scan(df: pd.DataFrame, instrument: str) -> list:
    """Grid search over session + ATR + oversold/overbought."""
    results = []

    sessions = ['Asia-NY', '']
    atr_pcts = [0, 25, 35]
    oversolds = [35, 38, 40]
    overboughts = [58, 62, 65]

    total = len(sessions) * len(atr_pcts) * len(oversolds) * len(overboughts)
    count = 0

    for session in sessions:
        for atr_pct in atr_pcts:
            for os_val in oversolds:
                for ob_val in overboughts:
                    if ob_val <= os_val:
                        continue
                    count += 1

    logger.info("Scanning %d combinations (session × ATR% × OS × OB)...", count)
    count = 0

    for session in sessions:
        for atr_pct in atr_pcts:
            for os_val in oversolds:
                for ob_val in overboughts:
                    if ob_val <= os_val:
                        continue
                    count += 1
                    if count % 50 == 0:
                        logger.info("  %d/%d...", count, total)

                    strat_params = dict(Config.STRATEGY_PARAMS['macd_stoch'])
                    strat_params.update({'stoch_oversold': os_val, 'stoch_overbought': ob_val})
                    trader = MACDStochTrader(instrument, strat_params)
                    trader.filters.update({
                        'session': session,
                        'use_atr_filter': atr_pct > 0,
                        'atr_min_percentile': atr_pct,
                    })
                    stats = backtest(df, trader, expiry_bars=1)

                    if stats['n'] >= 30:
                        results.append({
                            'session': session, 'os': os_val, 'ob': ob_val,
                            'atr_pct': atr_pct,
                            **stats,
                        })

    results.sort(key=lambda x: (x['wr'] > 55, x['pf']), reverse=True)
    logger.info("Done: %d results with N≥30", len(results))
    return results


# ═══════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════

def cmd_backtest(instrument: str):
    """Run parameter scan + best backtest."""
    Config.setup_dirs()
    path = os.path.join(Config.DATA_DIR, f'{instrument}_H1_indicators.csv')
    if not os.path.exists(path):
        logger.error("No data. Run: python3 main.py --fetch")
        sys.exit(1)

    df = pd.read_csv(path, index_col=0, parse_dates=True)
    logger.info("Loaded %s: %d candles", instrument, len(df))

    # Ensure indicators are computed (in case CSV doesn't have them)
    if 'STOCH_K_8' not in df.columns:
        logger.info("Computing indicators...")
        df = compute_all_indicators(df)

    # Quick baseline
    base_trader = MACDStochTrader(instrument)
    base_stats = backtest(df, base_trader)
    logger.info("Baseline: N=%d  WR=%.1f%%  PF=%.2f  DD=%.1f%%  PnL=%+.0f",
                base_stats['n'], base_stats['wr'], base_stats['pf'],
                base_stats['dd'], base_stats['pnl'])

    # Parameter scan
    top = param_scan(df, instrument)

    # Show top 10
    print(f"\n{'='*90}")
    print(f"  TOP 10 {instrument}")
    print(f"{'='*90}")
    print(f"  {'Session':12s} {'OS':>4s} {'OB':>4s} {'ATR%':>5s} {'N':>4s} {'WR':>6s} {'PF':>6s} {'DD':>6s} {'PnL':>7s}")
    print(f"  {'-'*73}")

    for r in top[:15]:
        print(f"  {r['session']:12s} {r['os']:4d} {r['ob']:4d} "
              f"{r['atr_pct']:4d}% {r['n']:4d} {r['wr']:5.1f}% {r['pf']:5.2f} {r['dd']:5.1f}% {r['pnl']:+6.0f}")

    # Save best config
    if top:
        best = top[0]
        best_config = {
            'instrument': instrument,
            'config': {k: best[k] for k in ['session', 'os', 'ob', 'atr_pct']},
            'stats': {k: v for k, v in best.items() if k not in ['session', 'os', 'ob', 'atr_pct']},
        }
        path = os.path.join(Config.REPORT_DIR, f'macd_stoch_best_{instrument}.json')
        with open(path, 'w') as f:
            json.dump(best_config, f, indent=2, default=str)
        logger.info("Best config saved to %s", path)

    # Test best with multi-expiry
    if top:
        best_config_vals = {k: top[0][k] for k in ['os', 'ob']}
        strat_params = dict(Config.STRATEGY_PARAMS['macd_stoch'])
        strat_params.update(best_config_vals)
        trader = MACDStochTrader(instrument, strat_params)
        trader.filters.update({
            'session': top[0]['session'],
            'use_atr_filter': top[0]['atr_pct'] > 0,
            'atr_min_percentile': top[0]['atr_pct'],
        })
        for exp in [1, 2, 3]:
            stats = backtest(df, trader, expiry_bars=exp)
            logger.info("Best config exp%dh: N=%d  WR=%.1f%%  PF=%.2f  PnL=%+.0f",
                        exp, stats['n'], stats['wr'], stats['pf'], stats['pnl'])


def cmd_live(instrument: str = 'BITCOIN'):
    """Production inference: fetch latest data, output signal."""
    fetcher = DataFetcher()
    df = fetcher.fetch_ohlcv(instrument)
    if len(df) < 100:
        logger.error("Need ≥100 candles, got %d", len(df))
        sys.exit(1)

    df = compute_all_indicators(df)
    idx = len(df) - 1

    trader = MACDStochTrader(instrument)
    # Load best params if available
    best_path = os.path.join(Config.REPORT_DIR, f'macd_stoch_best_{instrument}.json')
    if os.path.exists(best_path):
        with open(best_path) as f:
            best = json.load(f)
            trader.strategy_params.update({k: best['config'][k] for k in ['os', 'ob'] if k in best.get('config', {})})
            trader.filters.update({
                'session': best.get('config', {}).get('session', ''),
                'use_atr_filter': best.get('config', {}).get('atr_pct', 0) > 0,
                'atr_min_percentile': best.get('config', {}).get('atr_pct', 30),
                'min_bars_between': best.get('config', {}).get('bars', 3),
                'max_per_day': best.get('config', {}).get('max_day', 4),
            })
            logger.info("Loaded best params from %s", best_path)

    sig = trader.get_signal(df, idx, -999, 0)
    row = df.iloc[idx]
    atr_vals = df['ATR14'].values
    atr_ok = trader.check_atr(row, atr_vals, idx)

    print(f"\n{'='*50}")
    print(f"  MACD+Stoch LIVE — {instrument}")
    print(f"  Time: {df.index[-1]}")
    print(f"  Close: {row['Close']:.5f}")
    print(f"{'='*50}")
    print(f"  Signal:      {sig['signal'] or 'HOLD'}")
    print(f"  Confidence:  {sig['confidence']:.0f}")
    print(f"  Reason:      {sig['reason']}")
    print(f"  MACD_hist:   {row['MACD_hist']:.6f}")
    print(f"  Stoch_K(8):  {row['STOCH_K_8']:.1f}")
    print(f"  ATR14:       {row['ATR14']:.5f}")
    print(f"  ATR filter:  {'PASS' if atr_ok else 'FAIL'}")
    print(f"  Session:     {'PASS' if trader.check_session(df.index[-1]) else 'FAIL'}")

    if sig['signal']:
        print(f"\n  >>> TRADE: {sig['signal']} ({sig['confidence']:.0f}%) <<<")
    else:
        print(f"\n  >>> NO TRADE <<<")
    print()


def _no() -> dict:
    return {'signal': None, 'confidence': 0, 'reason': '', '_direction': 0}


def main():
    parser = argparse.ArgumentParser(description='MACD+Stochastic Binary Options Trader')
    parser.add_argument('--backtest', metavar='INSTR', help='Backtest + param scan (BITCOIN or EURUSD)')
    parser.add_argument('--live', nargs='?', const='BITCOIN', metavar='INSTR',
                        help='Production inference (default: BITCOIN)')
    parser.add_argument('--report', action='store_true', help='Show best config and stats')
    args = parser.parse_args()

    Config.setup_dirs()

    if args.backtest:
        cmd_backtest(args.backtest)
    elif args.live:
        cmd_live(args.live)
    elif args.report:
        for instr in ['BITCOIN', 'EURUSD']:
            path = os.path.join(Config.REPORT_DIR, f'macd_stoch_best_{instr}.json')
            if os.path.exists(path):
                with open(path) as f:
                    data = json.load(f)
                print(f"\n{instr}: WR={data['stats']['wr']}% PF={data['stats']['pf']} "
                      f"N={data['stats']['n']} PnL={data['stats']['pnl']:+}")
                print(f"  Config: {data['config']}")
    else:
        parser.print_help()


if __name__ == '__main__':
    main()
