#!/usr/bin/env python3
"""
Optimisation Stratégie Hybride pour BNB/USD
Grid search sur les 3 sous-stratégies (BULL/BEAR/RANGE) sur 10 jours de données BNB
"""

import pandas as pd
import numpy as np
import requests
import json
import itertools
from typing import Dict, List
from datetime import datetime

from hybrid_strategy import HybridStrategy


class BNBHybridOptimizer:
    """Optimiseur pour stratégie hybride sur BNB"""

    def __init__(self, df: pd.DataFrame, initial_capital: float = 500.0):
        self.df = df
        self.initial_capital = initial_capital

    def _calculate_pnl(self, position: Dict, current_price: float) -> float:
        """Calcule PnL% avec leverage"""
        entry = position['entry_price']
        leverage = position['leverage']

        if position['side'] == 'LONG':
            price_change_pct = ((current_price - entry) / entry) * 100
        else:  # SHORT
            price_change_pct = ((entry - current_price) / entry) * 100

        return price_change_pct * leverage

    def backtest(self, strategy: HybridStrategy) -> Dict:
        """Execute backtest sur la stratégie"""
        capital = self.initial_capital
        trades = []
        open_position = None

        # Parcourir données
        for i in range(100, len(self.df)):
            window = self.df.iloc[:i+1].copy()

            # 1. Gérer position ouverte
            if open_position:
                current_price = window['close'].iloc[-1]
                pnl_pct = self._calculate_pnl(open_position, current_price)

                # Check SL/TP
                hit_sl = (
                    (open_position['side'] == 'LONG' and current_price <= open_position['stop_loss']) or
                    (open_position['side'] == 'SHORT' and current_price >= open_position['stop_loss'])
                )

                hit_tp = (
                    (open_position['side'] == 'LONG' and current_price >= open_position['take_profit']) or
                    (open_position['side'] == 'SHORT' and current_price <= open_position['take_profit'])
                )

                if hit_sl or hit_tp:
                    # Fermer position
                    exit_reason = 'TP' if hit_tp else 'SL'
                    exit_price = open_position['take_profit'] if hit_tp else open_position['stop_loss']

                    # Position size: 20% du capital
                    position_size = capital * 0.20
                    pnl_usd = position_size * (pnl_pct / 100)
                    capital += pnl_usd

                    trades.append({
                        'pnl_pct': pnl_pct,
                        'pnl_usd': pnl_usd,
                        'exit_reason': exit_reason,
                        'strategy_used': open_position.get('strategy_used', 'UNKNOWN')
                    })

                    open_position = None

            # 2. Chercher nouveau signal
            if not open_position:
                signal = strategy.analyze('BNB', window)

                if signal:
                    open_position = {
                        'side': signal.side,
                        'entry_price': signal.entry_price,
                        'stop_loss': signal.stop_loss,
                        'take_profit': signal.take_profit,
                        'leverage': signal.leverage,
                        'strategy_used': signal.strategy_used
                    }

        # Résultats
        if len(trades) == 0:
            return {
                'total_trades': 0,
                'total_pnl_pct': 0,
                'profit_factor': 0,
                'win_rate': 0,
                'final_capital': self.initial_capital
            }

        winning_trades = [t for t in trades if t['pnl_pct'] > 0]
        losing_trades = [t for t in trades if t['pnl_pct'] <= 0]

        total_win = sum(t['pnl_usd'] for t in winning_trades) if winning_trades else 0
        total_loss = abs(sum(t['pnl_usd'] for t in losing_trades)) if losing_trades else 0

        profit_factor = (total_win / total_loss) if total_loss > 0 else (10.0 if total_win > 0 else 0)

        total_pnl = capital - self.initial_capital
        total_pnl_pct = (total_pnl / self.initial_capital) * 100

        # Count par stratégie
        bull_trades = len([t for t in trades if t['strategy_used'] == 'BULL'])
        bear_trades = len([t for t in trades if t['strategy_used'] == 'BEAR'])
        range_trades = len([t for t in trades if t['strategy_used'] == 'RANGE'])

        return {
            'total_trades': len(trades),
            'total_pnl_pct': total_pnl_pct,
            'profit_factor': profit_factor,
            'win_rate': (len(winning_trades) / len(trades)) * 100,
            'final_capital': capital,
            'bull_trades': bull_trades,
            'bear_trades': bear_trades,
            'range_trades': range_trades
        }

    def optimize(self, param_grid: Dict) -> List[Dict]:
        """
        Grid search sur les paramètres

        Args:
            param_grid: Dict avec listes de valeurs à tester

        Returns:
            Liste des résultats triés par PnL
        """
        results = []

        # Générer toutes les combinaisons
        param_names = list(param_grid.keys())
        param_values = list(param_grid.values())

        total_tests = 1
        for values in param_values:
            total_tests *= len(values)

        print(f"🔬 Grid search: {total_tests} combinaisons à tester")
        print(f"📊 Paramètres: {param_names}")
        print()

        test_count = 0

        for combination in itertools.product(*param_values):
            test_count += 1

            # Créer dict de params
            params = dict(zip(param_names, combination))

            # Créer stratégie avec ces params
            strategy = HybridStrategy(leverage=200)

            # Appliquer params RANGE
            if 'range.rsi_oversold' in params:
                strategy.range_strategy.params['rsi_oversold'] = params['range.rsi_oversold']
            if 'range.rsi_overbought' in params:
                strategy.range_strategy.params['rsi_overbought'] = params['range.rsi_overbought']
            if 'range.range_atr_threshold' in params:
                strategy.range_strategy.params['range_atr_threshold'] = params['range.range_atr_threshold']
            if 'range.leverage' in params:
                strategy.range_strategy.leverage = params['range.leverage']

            # Appliquer params BULL
            if 'bull.min_trend_strength' in params:
                strategy.bull_strategy.min_trend_strength = params['bull.min_trend_strength']
            if 'bull.sl_atr_multiplier' in params:
                strategy.bull_strategy.sl_atr_multiplier = params['bull.sl_atr_multiplier']
            if 'bull.tp_atr_multiplier' in params:
                strategy.bull_strategy.tp_atr_multiplier = params['bull.tp_atr_multiplier']
            if 'bull.trailing_pct' in params:
                strategy.bull_strategy.trailing_pct = params['bull.trailing_pct']
            if 'bull.leverage' in params:
                strategy.bull_strategy.leverage = params['bull.leverage']

            # Appliquer params BEAR
            if 'bear.min_trend_strength' in params:
                strategy.bear_strategy.min_trend_strength = params['bear.min_trend_strength']
            if 'bear.sl_atr_multiplier' in params:
                strategy.bear_strategy.sl_atr_multiplier = params['bear.sl_atr_multiplier']
            if 'bear.tp_atr_multiplier' in params:
                strategy.bear_strategy.tp_atr_multiplier = params['bear.tp_atr_multiplier']
            if 'bear.trailing_pct' in params:
                strategy.bear_strategy.trailing_pct = params['bear.trailing_pct']
            if 'bear.leverage' in params:
                strategy.bear_strategy.leverage = params['bear.leverage']

            # Backtest
            result = self.backtest(strategy)

            # Ajouter params au résultat
            result['params'] = params
            results.append(result)

            # Log progress
            if test_count % 10 == 0 or test_count == total_tests:
                profitable = [r for r in results if r['total_pnl_pct'] > 0 and r['profit_factor'] > 1.0]
                print(f"  [{test_count}/{total_tests}] Tests: {len(results)} | Profitables: {len(profitable)} ({len(profitable)/len(results)*100:.1f}%)")

                if len(profitable) > 0:
                    best = sorted(profitable, key=lambda x: x['total_pnl_pct'], reverse=True)[0]
                    print(f"    💰 Meilleur: PnL {best['total_pnl_pct']:+.1f}% | PF {best['profit_factor']:.2f} | WR {best['win_rate']:.0f}% | Trades {best['total_trades']} (BULL:{best['bull_trades']}, BEAR:{best['bear_trades']}, RANGE:{best['range_trades']})")

        # Trier par PnL décroissant
        results.sort(key=lambda x: x['total_pnl_pct'], reverse=True)

        return results


def get_bnb_historical_data(days: int = 10, interval: str = '1m') -> pd.DataFrame:
    """Récupère données historiques BNB/USDT - Multi-requêtes pour 1m"""
    print(f"📊 Récupération données BNB ({days} jours, {interval})...")

    # Pour 1m: Faire plusieurs requêtes car limite 1000 bougies
    if interval == '1m':
        total_candles_needed = days * 1440  # 1440 bougies par jour en 1m
        num_requests = (total_candles_needed // 1000) + 1

        print(f"   🔄 {total_candles_needed} bougies nécessaires → {num_requests} requêtes")

        all_data = []
        end_time = None

        for i in range(num_requests):
            url = "https://api.binance.com/api/v3/klines"
            params = {
                'symbol': 'BNBUSDT',
                'interval': interval,
                'limit': 1000
            }

            if end_time:
                params['endTime'] = end_time

            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()

            if not data:
                break

            all_data = data + all_data  # Ajouter au début (ordre chronologique)
            end_time = data[0][0] - 1  # Timestamp de la 1ère bougie - 1ms

            print(f"   → Requête {i+1}/{num_requests}: {len(data)} bougies")

            if len(all_data) >= total_candles_needed:
                break

        # Limiter au nombre exact demandé
        all_data = all_data[-total_candles_needed:]

        df = pd.DataFrame(all_data, columns=[
            'timestamp', 'open', 'high', 'low', 'close', 'volume',
            'close_time', 'quote_volume', 'trades', 'taker_buy_base',
            'taker_buy_quote', 'ignore'
        ])

    else:
        # Pour 5m et autres: une seule requête suffit
        candles_per_day = 288 if interval == '5m' else 96
        limit = min(1000, days * candles_per_day)

        url = "https://api.binance.com/api/v3/klines"
        params = {
            'symbol': 'BNBUSDT',
            'interval': interval,
            'limit': limit
        }

        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()

        df = pd.DataFrame(response.json(), columns=[
            'timestamp', 'open', 'high', 'low', 'close', 'volume',
            'close_time', 'quote_volume', 'trades', 'taker_buy_base',
            'taker_buy_quote', 'ignore'
        ])

    # Convertir types
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    for col in ['open', 'high', 'low', 'close', 'volume']:
        df[col] = df[col].astype(float)

    print(f"   ✅ {len(df)} bougies chargées au total")
    print(f"   📅 {df['timestamp'].iloc[0]} → {df['timestamp'].iloc[-1]}")
    print(f"   💰 Prix: ${df['close'].iloc[0]:.2f} → ${df['close'].iloc[-1]:.2f}")

    # Stats de mouvement
    price_change = ((df['close'].iloc[-1] - df['close'].iloc[0]) / df['close'].iloc[0]) * 100
    print(f"   📈 Variation: {price_change:+.2f}%")

    return df


def main():
    print("=" * 80)
    print("🎯 OPTIMISATION BNB HYBRIDE - CANDLES 1MIN")
    print("=" * 80)
    print()

    # Charger données BNB 10 jours (5min = ~2880 bougies, limité à 1000 par Binance)
    df = get_bnb_historical_data(days=10, interval='1m')

    print()
    print("=" * 80)
    print("🔬 CONFIGURATION GRID SEARCH")
    print("=" * 80)

    # Définir grid de paramètres à optimiser
    # Grid réduit mais efficace (~2000 combinaisons, ~15min)
    param_grid = {
        # RANGE Strategy (V10) - 2x2x2x2 = 16 combinaisons
        'range.rsi_oversold': [30, 40],
        'range.rsi_overbought': [70, 80],
        'range.range_atr_threshold': [0.30, 0.40],
        'range.leverage': [200, 300],

        # BULL Strategy - 2x2x2x2x2 = 32 combinaisons
        'bull.min_trend_strength': [55, 65],
        'bull.sl_atr_multiplier': [1.5, 2.0],
        'bull.tp_atr_multiplier': [2.0, 3.0],
        'bull.trailing_pct': [0.10, 0.20],
        'bull.leverage': [150, 200],

        # BEAR Strategy - 2x2x2x2x2 = 32 combinaisons
        'bear.min_trend_strength': [55, 65],
        'bear.sl_atr_multiplier': [1.5, 2.0],
        'bear.tp_atr_multiplier': [2.5, 3.5],
        'bear.trailing_pct': [0.08, 0.12],
        'bear.leverage': [150, 200],
    }
    # Total: 16 * 32 * 32 = 16,384 combinaisons (~15-20 minutes)

    # Calculer nombre total de tests
    total_combinations = 1
    for param, values in param_grid.items():
        total_combinations *= len(values)
        print(f"  {param}: {len(values)} valeurs")

    print(f"\n🔢 Total combinaisons: {total_combinations:,}")
    print(f"⏱️  Temps estimé: ~{total_combinations * 0.5 / 60:.0f} minutes")
    print()

    # Optimiser
    optimizer = BNBHybridOptimizer(df, initial_capital=500.0)

    print("=" * 80)
    print("🚀 DÉBUT OPTIMISATION")
    print("=" * 80)
    print()

    results = optimizer.optimize(param_grid)

    # Filtrer résultats profitables
    profitable = [r for r in results if r['total_pnl_pct'] > 0 and r['profit_factor'] > 1.0 and r['total_trades'] >= 3]

    print()
    print("=" * 80)
    print("📊 RÉSULTATS OPTIMISATION")
    print("=" * 80)
    print(f"\nTotal tests: {len(results)}")
    print(f"Profitables (PnL>0, PF>1, Trades≥3): {len(profitable)} ({len(profitable)/len(results)*100:.1f}%)")

    if len(profitable) > 0:
        print("\n🏆 TOP 5 CONFIGURATIONS:")
        print("-" * 80)

        for i, result in enumerate(profitable[:5], 1):
            print(f"\n#{i} - PnL: {result['total_pnl_pct']:+.2f}% | PF: {result['profit_factor']:.2f} | WR: {result['win_rate']:.0f}%")
            print(f"    Trades: {result['total_trades']} (BULL:{result['bull_trades']}, BEAR:{result['bear_trades']}, RANGE:{result['range_trades']})")
            print(f"    Capital: ${result['initial_capital']:.0f} → ${result['final_capital']:.0f}")

            # Afficher params clés
            params = result['params']
            print(f"    RANGE: RSI {params['range.rsi_oversold']}/{params['range.rsi_overbought']}, ATR {params['range.range_atr_threshold']}, Lev {params['range.leverage']}x")
            print(f"    BULL: Trend {params['bull.min_trend_strength']}, SL {params['bull.sl_atr_multiplier']}x, TP {params['bull.tp_atr_multiplier']}x, Trail {params['bull.trailing_pct']:.0%}, Lev {params['bull.leverage']}x")
            print(f"    BEAR: Trend {params['bear.min_trend_strength']}, SL {params['bear.sl_atr_multiplier']}x, TP {params['bear.tp_atr_multiplier']}x, Trail {params['bear.trailing_pct']:.0%}, Lev {params['bear.leverage']}x")
    else:
        print("\n⚠️  Aucune configuration profitable trouvée avec critères (PnL>0, PF>1, Trades≥3)")
        print("    Affichage des meilleures configurations disponibles:")

        for i, result in enumerate(results[:5], 1):
            print(f"\n#{i} - PnL: {result['total_pnl_pct']:+.2f}% | Trades: {result['total_trades']}")

    # Sauvegarder résultats
    output_file = 'bnb_1m_hybrid_optimization_results.json'
    with open(output_file, 'w') as f:
        json.dump({
            'optimization_date': datetime.now().isoformat(),
            'asset': 'BNBUSDT',
            'period': f"{df['timestamp'].iloc[0]} to {df['timestamp'].iloc[-1]}",
            'total_tests': len(results),
            'profitable_configs': len(profitable),
            'top_results': profitable[:10] if profitable else results[:10],
            'all_results': results
        }, f, indent=2, default=str)

    print(f"\n💾 Résultats complets sauvegardés: {output_file}")

    # Créer fichier params optimaux
    if len(profitable) > 0:
        best = profitable[0]
        optimal_params = {
            'version': '1.0_BNB',
            'optimization_date': datetime.now().isoformat(),
            'optimization_period': f"{df['timestamp'].iloc[0]} to {df['timestamp'].iloc[-1]}",
            'performance': {
                'total_pnl_pct': best['total_pnl_pct'],
                'profit_factor': best['profit_factor'],
                'win_rate': best['win_rate'],
                'total_trades': best['total_trades']
            },
            'range_strategy': {
                'params': {
                    'rsi_oversold': best['params']['range.rsi_oversold'],
                    'rsi_overbought': best['params']['range.rsi_overbought'],
                    'range_atr_threshold': best['params']['range.range_atr_threshold'],
                    'leverage': best['params']['range.leverage']
                }
            },
            'bull_strategy': {
                'params': {
                    'min_trend_strength': best['params']['bull.min_trend_strength'],
                    'sl_atr_multiplier': best['params']['bull.sl_atr_multiplier'],
                    'tp_atr_multiplier': best['params']['bull.tp_atr_multiplier'],
                    'trailing_pct': best['params']['bull.trailing_pct'],
                    'leverage': best['params']['bull.leverage']
                }
            },
            'bear_strategy': {
                'params': {
                    'min_trend_strength': best['params']['bear.min_trend_strength'],
                    'sl_atr_multiplier': best['params']['bear.sl_atr_multiplier'],
                    'tp_atr_multiplier': best['params']['bear.tp_atr_multiplier'],
                    'trailing_pct': best['params']['bear.trailing_pct'],
                    'leverage': best['params']['bear.leverage']
                }
            }
        }

        optimal_file = 'optimal_bnb_1m_hybrid_params.json'
        with open(optimal_file, 'w') as f:
            json.dump(optimal_params, f, indent=2)

        print(f"💾 Paramètres optimaux BNB sauvegardés: {optimal_file}")

    print()
    print("=" * 80)
    print("✅ OPTIMISATION TERMINÉE")
    print("=" * 80)


if __name__ == '__main__':
    main()
