#!/usr/bin/env python3
"""
Backtest SPY Strategy - 3 mois de données 1-minute
Simule la détection de surges et les exits du market_spy sur données historiques.
"""
import json
import os
import time
import requests
from datetime import datetime, timedelta
from collections import defaultdict

# ═══════════════════════════════════════════════════════════════
# PARAMÈTRES DU SPY (extraits de market_spy.py)
# ═══════════════════════════════════════════════════════════════
POSITION_SIZE = 500          # USDT par trade
MAX_POSITIONS = 3            # Positions simultanées max
MAX_TRADES_PER_HOUR = 6

# ENTRY
SURGE_MIN_PCT = 1.0          # +1% en 1 scan (~1 bougie 1m)
SURGE_BREAKOUT_PCT = 1.5     # +1.5% en 2 bougies
SURGE_VOL_RATIO = 3.0        # Volume 3x la moyenne
MIN_GREEN_CANDLES = 2        # Min 2/3 bougies vertes (sur 3 dernières)
MIN_MOMENTUM_PCT = 0.2       # Momentum 3-min positif
MAX_PUMPED_24H = 50.0        # Bloquer si >50% en 24h
MAX_DECLINE_24H = -15.0      # Bloquer si <-15% en 24h

# EXIT
HARD_SL_PCT = -1.2
TRAILING_ACTIVATION = 1.0    # Activer trailing à +1%
MAX_HOLD_MINUTES = 15
STAGNATION_MINUTES = 10
STAGNATION_THRESHOLD = 0.5   # Sortir si <0.5% après stagnation_minutes
MOMENTUM_EXIT_DROPS = 3
VOLUME_EXIT_SELL_RATIO = 0.38
EARLY_SL_DELAY_SEC = 60
EARLY_SL_MAX_PEAK = 0.2
EARLY_SL_PCT = -0.5
INSTANT_REVERSAL_SEC = 45
INSTANT_REVERSAL_PCT = -0.6

# TRAILING TIERS
TRAIL_TIERS = [
    (0.5, 0.3), (1.0, 0.7), (2.0, 1.0), (3.0, 1.8),
    (5.0, 2.0), (10.0, 2.5), (15.0, 4.0), (999, 5.0)
]

# ═══════════════════════════════════════════════════════════════
# TÉLÉCHARGEMENT DES DONNÉES
# ═══════════════════════════════════════════════════════════════
CACHE_DIR = "backtest_cache"
os.makedirs(CACHE_DIR, exist_ok=True)

def fetch_klines_1m(symbol, days=90):
    """Télécharge les klines 1m depuis Binance (réel, pas testnet)"""
    cache_file = os.path.join(CACHE_DIR, f"{symbol}_1m_{days}d.json")
    
    if os.path.exists(cache_file):
        age_hours = (time.time() - os.path.getmtime(cache_file)) / 3600
        if age_hours < 24:
            with open(cache_file) as f:
                data = json.load(f)
            print(f"  ✅ {symbol}: {len(data)} klines (cache)")
            return data
    
    all_klines = []
    end_time = int(time.time() * 1000)
    start_time = end_time - (days * 24 * 3600 * 1000)
    
    current = start_time
    while current < end_time:
        url = "https://api.binance.com/api/v3/klines"
        params = {
            "symbol": symbol,
            "interval": "1m",
            "startTime": current,
            "limit": 1000
        }
        try:
            resp = requests.get(url, params=params, timeout=10)
            resp.raise_for_status()
            data = resp.json()
            if not data:
                break
            all_klines.extend(data)
            current = data[-1][0] + 60000  # Next minute
            time.sleep(0.05)  # Rate limit
        except Exception as e:
            print(f"  ❌ {symbol}: erreur API: {e}")
            time.sleep(1)
            continue
    
    # Convertir format: [timestamp, open, high, low, close, volume, ...]
    result = []
    for k in all_klines:
        result.append({
            'ts': k[0],
            'open': float(k[1]),
            'high': float(k[2]),
            'low': float(k[3]),
            'close': float(k[4]),
            'volume': float(k[5]),
            'quote_volume': float(k[7]),
            'trades': int(k[8]),
            'taker_buy_volume': float(k[9]),
            'taker_buy_quote': float(k[10])
        })
    
    with open(cache_file, 'w') as f:
        json.dump(result, f)
    
    print(f"  ✅ {symbol}: {len(result)} klines téléchargées ({days}j)")
    return result

# ═══════════════════════════════════════════════════════════════
# SIMULATION DU SPY
# ═══════════════════════════════════════════════════════════════
class SpyBacktest:
    def __init__(self):
        self.trades = []
        self.open_positions = []
        self.trades_this_hour = defaultdict(int)
        self.total_pnl = 0
        
    def get_trail_pct(self, pnl_pct):
        for threshold, trail in TRAIL_TIERS:
            if pnl_pct < threshold:
                return trail
        return 5.0
    
    def detect_surge(self, klines, idx):
        """Détecte un surge sur les klines[idx]"""
        if idx < 20:
            return None
        
        curr = klines[idx]
        prev = klines[idx - 1]
        
        # Changement 1-bougie
        change_1 = ((curr['close'] - prev['close']) / prev['close']) * 100
        
        # Changement 2-bougies
        prev2 = klines[idx - 2]
        change_2 = ((curr['close'] - prev2['close']) / prev2['close']) * 100
        
        # Volume moyen sur 20 bougies
        avg_vol = sum(k['quote_volume'] for k in klines[idx-20:idx]) / 20
        vol_ratio = curr['quote_volume'] / avg_vol if avg_vol > 0 else 0
        
        surge = None
        surge_strength = 0
        
        # FLASH SURGE: +1% en 1 bougie + volume 3x
        if change_1 >= SURGE_MIN_PCT and vol_ratio >= SURGE_VOL_RATIO:
            surge = 'FLASH_SURGE'
            surge_strength = change_1
        # BREAKOUT: +1.5% en 2 bougies
        elif change_2 >= SURGE_BREAKOUT_PCT and change_1 >= 0.5 and vol_ratio >= 2.0:
            surge = 'BREAKOUT_SURGE'
            surge_strength = change_2
        
        if not surge:
            return None
        
        # --- Confirmations ---
        # Bougies vertes (3 dernières)
        last3 = klines[idx-2:idx+1]
        green = sum(1 for k in last3 if k['close'] > k['open'])
        if green < MIN_GREEN_CANDLES and surge_strength < 3.0:
            return None
        
        # Momentum 3 min
        if idx >= 3:
            mom3 = ((curr['close'] - klines[idx-3]['close']) / klines[idx-3]['close']) * 100
            min_mom = -1.0 if surge_strength >= 2.0 else MIN_MOMENTUM_PCT
            if mom3 < min_mom:
                return None
        
        # Taker buy ratio (acheteurs vs vendeurs)
        if curr['quote_volume'] > 0:
            buy_ratio = curr['taker_buy_quote'] / curr['quote_volume']
            if buy_ratio < 0.35:  # Distribution vendeurs
                return None
        
        # Changement 24h (1440 bougies)
        if idx >= 1440:
            price_24h_ago = klines[idx - 1440]['close']
            change_24h = ((curr['close'] - price_24h_ago) / price_24h_ago) * 100
            if change_24h > MAX_PUMPED_24H or change_24h < MAX_DECLINE_24H:
                return None
        
        # Rejet wick: grande ombre supérieure
        body = abs(curr['close'] - curr['open'])
        upper_wick = curr['high'] - max(curr['close'], curr['open'])
        if body > 0 and upper_wick > 2 * body:
            return None
        
        return {
            'type': surge,
            'strength': surge_strength,
            'vol_ratio': vol_ratio,
            'price': curr['close'],
            'ts': curr['ts']
        }
    
    def simulate_exit(self, klines, entry_idx, entry_price, surge_strength):
        """Simule les exits du spy minute par minute"""
        max_price = entry_price
        max_pnl_pct = 0
        trailing_active = False
        trailing_stop_price = 0
        
        for i in range(entry_idx + 1, min(entry_idx + MAX_HOLD_MINUTES + 2, len(klines))):
            candle = klines[i]
            hold_minutes = i - entry_idx
            
            # Simuler tick par tick dans la bougie (open, high, low, close)
            for tick_price in [candle['open'], candle['high'], candle['low'], candle['close']]:
                pnl_pct = ((tick_price - entry_price) / entry_price) * 100
                
                if tick_price > max_price:
                    max_price = tick_price
                    max_pnl_pct = ((max_price - entry_price) / entry_price) * 100
                
                # Rule 0: INSTANT REVERSAL
                if hold_minutes <= 1 and max_pnl_pct < 0.05 and pnl_pct <= INSTANT_REVERSAL_PCT:
                    return tick_price, i, 'INSTANT_REVERSAL', pnl_pct
                
                # Rule 0b: EARLY SL  
                if hold_minutes >= 1 and max_pnl_pct < EARLY_SL_MAX_PEAK:
                    early_sl = max(EARLY_SL_PCT, -surge_strength * 0.8)
                    early_sl = max(early_sl, HARD_SL_PCT * 0.9)
                    if pnl_pct <= early_sl:
                        return tick_price, i, 'EARLY_SL', pnl_pct
                
                # Rule 1: HARD SL
                if pnl_pct <= HARD_SL_PCT:
                    return tick_price, i, 'HARD_SL', pnl_pct
                
                # Rule 2: TRAILING STOP
                if max_pnl_pct >= TRAILING_ACTIVATION:
                    trailing_active = True
                    trail_pct = self.get_trail_pct(max_pnl_pct)
                    new_ts = max_price * (1 - trail_pct / 100)
                    trailing_stop_price = max(trailing_stop_price, new_ts)  # Ratchet
                    
                    if tick_price <= trailing_stop_price:
                        return tick_price, i, 'TRAILING', pnl_pct
            
            # Fin de bougie checks
            current_price = candle['close']
            pnl_pct = ((current_price - entry_price) / entry_price) * 100
            
            # Rule 3b: STAGNATION
            if hold_minutes >= STAGNATION_MINUTES and abs(pnl_pct) < STAGNATION_THRESHOLD:
                return current_price, i, 'STAGNATION', pnl_pct
            
            # Rule 4: MOMENTUM EXIT (3 bougies rouges consécutives)
            if i >= entry_idx + 3 and pnl_pct < 5.0:
                last_3 = klines[i-2:i+1]
                reds = sum(1 for k in last_3 if k['close'] < k['open'])
                if reds >= MOMENTUM_EXIT_DROPS:
                    return current_price, i, 'MOMENTUM_EXIT', pnl_pct
            
            # Rule 5: REVERSAL (peak retrace)
            if max_pnl_pct >= 3.0 and pnl_pct < 1.0 and max_pnl_pct < 8.0:
                return current_price, i, 'REVERSAL', pnl_pct
            
            # Rule 6: VOLUME ROUGE
            if hold_minutes >= 2 and candle['quote_volume'] > 0:
                buy_ratio = candle['taker_buy_quote'] / candle['quote_volume']
                if buy_ratio < VOLUME_EXIT_SELL_RATIO and pnl_pct < 1.0:
                    return current_price, i, 'VOLUME_ROUGE', pnl_pct
            
            # Rule 3: MAX HOLD  
            if hold_minutes >= MAX_HOLD_MINUTES and pnl_pct < 3.0:
                return current_price, i, 'MAX_HOLD', pnl_pct
        
        # Fallback: fin des données
        last_price = klines[min(entry_idx + MAX_HOLD_MINUTES, len(klines)-1)]['close']
        pnl_pct = ((last_price - entry_price) / entry_price) * 100
        return last_price, entry_idx + MAX_HOLD_MINUTES, 'TIMEOUT', pnl_pct
    
    def run(self, symbol, klines):
        """Lance le backtest sur un symbole"""
        symbol_trades = []
        last_trade_idx = 0
        cooldown = 3  # Min 3 bougies entre trades
        
        for idx in range(20, len(klines) - MAX_HOLD_MINUTES - 5):
            if idx < last_trade_idx + cooldown:
                continue
            
            surge = self.detect_surge(klines, idx)
            if not surge:
                continue
            
            entry_price = surge['price']
            exit_price, exit_idx, reason, pnl_pct = self.simulate_exit(
                klines, idx, entry_price, surge['strength']
            )
            
            pnl_usdt = (pnl_pct / 100) * POSITION_SIZE
            
            entry_time = datetime.utcfromtimestamp(klines[idx]['ts'] / 1000)
            exit_time = datetime.utcfromtimestamp(klines[exit_idx]['ts'] / 1000)
            
            trade = {
                'symbol': symbol,
                'entry_time': entry_time.isoformat(),
                'exit_time': exit_time.isoformat(),
                'entry_price': entry_price,
                'exit_price': exit_price,
                'pnl_pct': round(pnl_pct, 2),
                'pnl_usdt': round(pnl_usdt, 2),
                'reason': reason,
                'surge_type': surge['type'],
                'surge_strength': round(surge['strength'], 2),
                'hold_minutes': exit_idx - idx
            }
            symbol_trades.append(trade)
            last_trade_idx = exit_idx
        
        return symbol_trades

# ═══════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════
def main():
    # Symboles les plus tradés par le spy + quelques majeurs
    # Symboles déjà en cache + quelques supplémentaires importants
    SYMBOLS = [
        'ONTUSDT', 'CHRUSDT', 'NOMUSDT', 'FIDAUSDT', 'STOUSDT',
        'HEMIUSDT', 'ZBTUSDT', 'KERNELUSDT', 'CTSIUSDT', 'XPLUSDT',
        'DUSDT',  # Les 11 ci-dessus sont déjà en cache
        'BLURUSDT', 'SEIUSDT', 'ENJUSDT', 'ANKRUSDT',
        'THETAUSDT', 'COSUSDT', 'GASUSDT', 'WIFUSDT'
    ]
    
    DAYS = 90
    
    print(f"╔══════════════════════════════════════════════════════════════╗")
    print(f"║  🔍 BACKTEST SPY STRATEGY — {DAYS} JOURS — {len(SYMBOLS)} SYMBOLES          ║")
    print(f"╚══════════════════════════════════════════════════════════════╝")
    print()
    
    # Phase 1: Téléchargement
    print("📥 Phase 1: Téléchargement des données 1-minute...")
    all_data = {}
    for sym in SYMBOLS:
        try:
            data = fetch_klines_1m(sym, DAYS)
            if len(data) > 1000:
                all_data[sym] = data
            else:
                print(f"  ⚠️ {sym}: pas assez de données ({len(data)})")
        except Exception as e:
            print(f"  ❌ {sym}: {e}")
    
    print(f"\n✅ {len(all_data)}/{len(SYMBOLS)} symboles chargés")
    print()
    
    # Phase 2: Backtest
    print("🧪 Phase 2: Simulation sur chaque symbole...")
    bt = SpyBacktest()
    all_trades = []
    
    for sym, klines in all_data.items():
        trades = bt.run(sym, klines)
        all_trades.extend(trades)
        
        if trades:
            sym_pnl = sum(t['pnl_usdt'] for t in trades)
            wins = sum(1 for t in trades if t['pnl_usdt'] > 0)
            losses = sum(1 for t in trades if t['pnl_usdt'] <= 0)
            wr = (wins/(wins+losses)*100) if (wins+losses) > 0 else 0
            print(f"  {sym:16} | {len(trades):4} trades | WR={wr:5.1f}% | P&L={sym_pnl:+8.1f} USDT")
    
    # Phase 3: Résultats
    print()
    print(f"╔══════════════════════════════════════════════════════════════╗")
    print(f"║  📊 RÉSULTATS BACKTEST SPY — {DAYS} JOURS                         ║")
    print(f"╚══════════════════════════════════════════════════════════════╝")
    
    if not all_trades:
        print("  ❌ Aucun trade détecté")
        return
    
    total_pnl = sum(t['pnl_usdt'] for t in all_trades)
    wins = [t for t in all_trades if t['pnl_usdt'] > 0]
    losses = [t for t in all_trades if t['pnl_usdt'] <= 0]
    win_rate = (len(wins) / len(all_trades) * 100) if all_trades else 0
    
    avg_win = sum(t['pnl_usdt'] for t in wins) / len(wins) if wins else 0
    avg_loss = sum(t['pnl_usdt'] for t in losses) / len(losses) if losses else 0
    profit_factor = abs(sum(t['pnl_usdt'] for t in wins) / sum(t['pnl_usdt'] for t in losses)) if losses and sum(t['pnl_usdt'] for t in losses) != 0 else 999
    
    # Max drawdown
    running = 0
    peak = 0
    max_dd = 0
    for t in sorted(all_trades, key=lambda x: x['exit_time']):
        running += t['pnl_usdt']
        peak = max(peak, running)
        dd = running - peak
        max_dd = min(max_dd, dd)
    
    # Stats par mois
    by_month = defaultdict(list)
    for t in all_trades:
        month = t['exit_time'][:7]
        by_month[month].append(t['pnl_usdt'])
    
    # Stats par raison de sortie
    by_reason = defaultdict(list)
    for t in all_trades:
        by_reason[t['reason']].append(t['pnl_usdt'])
    
    # Stats par type de surge
    by_surge = defaultdict(list)
    for t in all_trades:
        by_surge[t['surge_type']].append(t['pnl_usdt'])
    
    # Affichage
    print()
    print(f"  📈 Total trades:     {len(all_trades)}")
    print(f"  ✅ Gagnants:         {len(wins)} ({win_rate:.1f}%)")
    print(f"  ❌ Perdants:         {len(losses)}")
    print(f"  💰 P&L TOTAL:        {total_pnl:+.2f} USDT")
    print(f"  📊 P&L moyen/trade:  {total_pnl/len(all_trades):+.2f} USDT")
    print(f"  🟢 Gain moyen:       {avg_win:+.2f} USDT")
    print(f"  🔴 Perte moyenne:    {avg_loss:+.2f} USDT")
    print(f"  📐 Profit Factor:    {profit_factor:.2f}")
    print(f"  📉 Max Drawdown:     {max_dd:+.2f} USDT")
    pnl_per_day = total_pnl / DAYS
    print(f"  📅 P&L moyen/jour:   {pnl_per_day:+.2f} USDT")
    print(f"  📅 P&L moyen/mois:   {pnl_per_day * 30:+.2f} USDT")
    
    # Hold time moyen
    avg_hold = sum(t['hold_minutes'] for t in all_trades) / len(all_trades)
    print(f"  ⏱️  Hold moyen:       {avg_hold:.1f} min")
    
    print()
    print("  ═══ PAR MOIS ═══")
    for month in sorted(by_month.keys()):
        trades_m = by_month[month]
        pnl_m = sum(trades_m)
        w = sum(1 for p in trades_m if p > 0)
        wr_m = (w / len(trades_m) * 100) if trades_m else 0
        print(f"  {month}: {len(trades_m):4} trades | WR={wr_m:5.1f}% | P&L={pnl_m:+8.1f} USDT")
    
    print()
    print("  ═══ PAR RAISON DE SORTIE ═══")
    for reason in sorted(by_reason.keys(), key=lambda r: sum(by_reason[r]), reverse=True):
        trades_r = by_reason[reason]
        pnl_r = sum(trades_r)
        print(f"  {reason:20} | {len(trades_r):4} trades | P&L={pnl_r:+8.1f} USDT | avg={pnl_r/len(trades_r):+.2f}")
    
    print()
    print("  ═══ PAR TYPE DE SURGE ═══")
    for stype in sorted(by_surge.keys(), key=lambda s: sum(by_surge[s]), reverse=True):
        trades_s = by_surge[stype]
        pnl_s = sum(trades_s)
        w = sum(1 for p in trades_s if p > 0)
        wr_s = (w / len(trades_s) * 100) if trades_s else 0
        print(f"  {stype:18} | {len(trades_s):4} trades | WR={wr_s:5.1f}% | P&L={pnl_s:+8.1f} USDT")
    
    # Sauvegarder
    results = {
        'period_days': DAYS,
        'symbols': len(all_data),
        'total_trades': len(all_trades),
        'total_pnl': round(total_pnl, 2),
        'win_rate': round(win_rate, 1),
        'profit_factor': round(profit_factor, 2),
        'max_drawdown': round(max_dd, 2),
        'avg_pnl_per_day': round(pnl_per_day, 2),
        'avg_pnl_per_month': round(pnl_per_day * 30, 2),
        'trades': all_trades
    }
    with open('backtest_spy_results.json', 'w') as f:
        json.dump(results, f, indent=2)
    print(f"\n  💾 Résultats sauvegardés dans backtest_spy_results.json")
    
    # Comparaison avec testnet
    print()
    print("  ═══ COMPARAISON TESTNET vs BACKTEST ═══")
    testnet_pnl_day = 943 / 3.67  # ~257 USDT/jour (200 trades en 3.67j)
    bt_pnl_day = pnl_per_day
    
    print(f"  Testnet (3.5j):  ~{testnet_pnl_day:.0f} USDT/jour | ~200 trades")
    trades_per_day = len(all_trades) / DAYS
    print(f"  Backtest ({DAYS}j):  {bt_pnl_day:+.0f} USDT/jour | ~{trades_per_day:.0f} trades/jour")
    
    if bt_pnl_day > 0:
        ratio = bt_pnl_day / testnet_pnl_day
        print(f"  Ratio BT/Testnet: {ratio:.1%}")
        if ratio >= 0.7:
            print(f"  ✅ CONFIRMÉ: La stratégie spy tient sur {DAYS} jours")
        elif ratio >= 0.3:
            print(f"  ⚠️ PARTIEL: Performance réduite mais positive")
        else:
            print(f"  🔴 DÉGRADÉ: Performance très diminuée sur la durée")
    else:
        print(f"  🔴 NÉGATIF: La stratégie ne tient pas sur {DAYS} jours")


if __name__ == '__main__':
    main()
