import backtrader as bt
import ccxt
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import pytz
import itertools
import os
from dotenv import load_dotenv

# Load environment variables
load_dotenv()

# Configuration section
CONFIG = {
    'symbol': 'BTC/USDT',
    'timeframes': ['1h'],  # Reduced to only one timeframe for quicker testing
    'since': '2023-01-01T00:00:00Z',
    'until': '2023-12-31T23:59:59Z',
    'timezone': 'UTC',
    'initial_cash': 10000,
    'commission': 0.001,  # 0.1% commission
    'indicators': {
        'SMA': {
            'enabled': True,
            'fast_periods': [10],  # Reduced to only one period for quicker testing
            'slow_periods': [50]   # Reduced to only one period for quicker testing
        },
        'EMA': {
            'enabled': True,
            'fast_periods': [10],  # Reduced to only one period for quicker testing
            'slow_periods': [50]   # Reduced to only one period for quicker testing
        },
        'RSI': {
            'enabled': True,
            'periods': [14],
            'overbought': [70],
            'oversold': [30]
        },
        'MACD': {
            'enabled': True,
            'fast_period': [12],
            'slow_period': [26],
            'signal_period': [9]
        }
    }
}

class FlexibleStrategy(bt.Strategy):
    params = (
        ('sma_fast', 20),
        ('sma_slow', 50),
        ('ema_fast', 20),
        ('ema_slow', 50),
        ('rsi_period', 14),
        ('rsi_overbought', 70),
        ('rsi_oversold', 30),
        ('macd_fast', 12),
        ('macd_slow', 26),
        ('macd_signal', 9),
        ('use_sma', True),
        ('use_ema', True),
        ('use_rsi', True),
        ('use_macd', True),
    )

    def __init__(self):
        self.order = None
        self.sma_fast = bt.indicators.SMA(period=self.params.sma_fast) if self.params.use_sma else None
        self.sma_slow = bt.indicators.SMA(period=self.params.sma_slow) if self.params.use_sma else None
        self.ema_fast = bt.indicators.EMA(period=self.params.ema_fast) if self.params.use_ema else None
        self.ema_slow = bt.indicators.EMA(period=self.params.ema_slow) if self.params.use_ema else None
        self.rsi = bt.indicators.RSI(period=self.params.rsi_period) if self.params.use_rsi else None
        self.macd = bt.indicators.MACD(
            period_me1=self.params.macd_fast,
            period_me2=self.params.macd_slow,
            period_signal=self.params.macd_signal
        ) if self.params.use_macd else None

    def next(self):
        if self.order:
            return

        if not self.position:
            buy_signal = True
            if self.params.use_sma:
                buy_signal = buy_signal and self.sma_fast > self.sma_slow
            if self.params.use_ema:
                buy_signal = buy_signal and self.ema_fast > self.ema_slow
            if self.params.use_rsi:
                buy_signal = buy_signal and self.rsi < self.params.rsi_oversold
            if self.params.use_macd:
                buy_signal = buy_signal and self.macd.macd > self.macd.signal

            if buy_signal:
                self.order = self.buy()

        else:
            sell_signal = True
            if self.params.use_sma:
                sell_signal = sell_signal and self.sma_fast < self.sma_slow
            if self.params.use_ema:
                sell_signal = sell_signal and self.ema_fast < self.ema_slow
            if self.params.use_rsi:
                sell_signal = sell_signal and self.rsi > self.params.rsi_overbought
            if self.params.use_macd:
                sell_signal = sell_signal and self.macd.macd < self.macd.signal

            if sell_signal:
                self.order = self.sell()

def download_binance_data(symbol, timeframe, since, until):
    api_key = os.getenv('BINANCE_API_KEY')
    secret_key = os.getenv('BINANCE_SECRET_KEY')

    if not api_key or not secret_key:
        raise ValueError("BINANCE_API_KEY or BINANCE_SECRET_KEY not found in environment variables")

    exchange = ccxt.binance({
        'apiKey': api_key,
        'secret': secret_key,
        'enableRateLimit': True,
        'options': {
            'defaultType': 'spot'
        }
    })

    try:
        print(f"Fetching {symbol} data for {timeframe} timeframe from {since} to {until}...")
        since_ms = exchange.parse8601(since)
        until_ms = exchange.parse8601(until)
        all_ohlcv = []

        while since_ms < until_ms:
            ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since_ms)
            if not ohlcv:
                break
            all_ohlcv.extend(ohlcv)
            since_ms = ohlcv[-1][0] + 1
            
        if not all_ohlcv:
            raise ValueError(f"No data retrieved for {symbol} with {timeframe} timeframe")
        
        df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
        df.set_index('timestamp', inplace=True)
        df = df.tz_localize(pytz.UTC).tz_convert(CONFIG['timezone'])
        print(f"Downloaded {len(df)} candles for {symbol} ({timeframe})")
        return df
    except Exception as e:
        print(f"An error occurred: {str(e)}")
        return None

def run_backtest(data, strategy, params):
    cerebro = bt.Cerebro()
    cerebro.addstrategy(strategy, **params)

    datafeed = bt.feeds.PandasData(dataname=data)
    cerebro.adddata(datafeed)

    cerebro.broker.setcash(CONFIG['initial_cash'])
    cerebro.broker.setcommission(commission=CONFIG['commission'])

    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')

    results = cerebro.run()
    
    sharpe_ratio = results[0].analyzers.sharpe.get_analysis().get('sharperatio', 0)
    max_drawdown = results[0].analyzers.drawdown.get_analysis().get('max', {}).get('drawdown', 0)
    total_return = results[0].analyzers.returns.get_analysis().get('rtot', 0)
    trade_analysis = results[0].analyzers.trades.get_analysis()
    
    return {
        'sharpe_ratio': sharpe_ratio,
        'max_drawdown': max_drawdown,
        'total_return': total_return,
        'final_value': cerebro.broker.getvalue(),
        'trade_analysis': trade_analysis
    }

def generate_parameter_combinations():
    indicator_params = CONFIG['indicators']
    combinations = []

    for sma_fast in indicator_params['SMA']['fast_periods']:
        for sma_slow in indicator_params['SMA']['slow_periods']:
            if sma_fast >= sma_slow:
                continue
            for ema_fast in indicator_params['EMA']['fast_periods']:
                for ema_slow in indicator_params['EMA']['slow_periods']:
                    if ema_fast >= ema_slow:
                        continue
                    for rsi_period in indicator_params['RSI']['periods']:
                        for rsi_overbought in indicator_params['RSI']['overbought']:
                            for rsi_oversold in indicator_params['RSI']['oversold']:
                                for macd_fast in indicator_params['MACD']['fast_period']:
                                    for macd_slow in indicator_params['MACD']['slow_period']:
                                        for macd_signal in indicator_params['MACD']['signal_period']:
                                            combinations.append({
                                                'sma_fast': sma_fast,
                                                'sma_slow': sma_slow,
                                                'ema_fast': ema_fast,
                                                'ema_slow': ema_slow,
                                                'rsi_period': rsi_period,
                                                'rsi_overbought': rsi_overbought,
                                                'rsi_oversold': rsi_oversold,
                                                'macd_fast': macd_fast,
                                                'macd_slow': macd_slow,
                                                'macd_signal': macd_signal,
                                                'use_sma': indicator_params['SMA']['enabled'],
                                                'use_ema': indicator_params['EMA']['enabled'],
                                                'use_rsi': indicator_params['RSI']['enabled'],
                                                'use_macd': indicator_params['MACD']['enabled'],
                                            })
    return combinations

def test_strategies():
    results = []
    parameter_combinations = generate_parameter_combinations()

    for tf in CONFIG['timeframes']:
        print(f"Testing timeframe: {tf}")
        data = download_binance_data(CONFIG['symbol'], tf, CONFIG['since'], CONFIG['until'])
        if data is None or data.empty:
            print(f"No data for timeframe: {tf}")
            continue

        for params in parameter_combinations:
            print(f"Testing strategy with params: {params}")
            result = run_backtest(data, FlexibleStrategy, params)
            results.append({
                'timeframe': tf,
                **params,
                **result,
            })

    return results

def print_results(results):
    results.sort(key=lambda x: x['total_return'], reverse=True)
    print("\nTop 10 Strategies:")
    for i, result in enumerate(results[:10], 1):
        print(f"{i}. Timeframe: {result['timeframe']}")
        print(f"   SMA: {result['sma_fast']}/{result['sma_slow']}, EMA: {result['ema_fast']}/{result['ema_slow']}")
        print(f"   RSI: {result['rsi_period']}/{result['rsi_overbought']}/{result['rsi_oversold']}")
        print(f"   MACD: {result['macd_fast']}/{result['macd_slow']}/{result['macd_signal']}")
        print(f"   Return: {(result['total_return']*100):.2f}%")
        print(f"   Sharpe Ratio: {result['sharpe_ratio']:.2f if result['sharpe_ratio'] is not None else 'N/A'}")
        print(f"   Max Drawdown: {(result['max_drawdown']*100):.2f}%")
        print(f"   Final Value: ${result['final_value']:.2f}")
        print(f"   Total Trades: {result['trade_analysis'].get('total', {}).get('total', 0)}")
        total_trades = result['trade_analysis'].get('total', {}).get('total', 0)
        won_trades = result['trade_analysis'].get('won', {}).get('total', 0)
        win_rate = (won_trades / total_trades * 100) if total_trades > 0 else 0
        print(f"   Win Rate: {win_rate:.2f}%")
        print()

def plot_results(results):
    plt.figure(figsize=(15, 10))

    for result in results[:10]:  # Plot only top 10 strategies
        label = f"TF: {result['timeframe']}, Return: {(result['total_return']*100):.2f}%"
        plt.plot(np.linspace(0, 1, 100), np.linspace(CONFIG['initial_cash'], result['final_value'], 100), label=label)

    plt.title('Equity Curves for Top 10 Strategies')
    plt.xlabel('Time')
    plt.ylabel('Equity')
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    plt.tight_layout()
    plt.grid(True)
    plt.savefig('equity_curves.png')  # Save the plot as a file
    print("Equity curves plot saved as 'equity_curves.png'")
    # plt.show()  # Commented out to avoid blocking the script

if __name__ == "__main__":
    try:
        print("Checking environment variables...")
        api_key = os.getenv('BINANCE_API_KEY')
        secret_key = os.getenv('BINANCE_SECRET_KEY')
        if not api_key or not secret_key:
            raise ValueError("BINANCE_API_KEY or BINANCE_SECRET_KEY not found in environment variables")
        
        print("Running strategy tests...")
        strategy_results = test_strategies()

        if not strategy_results:
            print("No valid results were obtained. Please check your strategy parameters.")
        else:
            print_results(strategy_results)
            plot_results(strategy_results)
    
    except Exception as e:
        print(f"An error occurred: {str(e)}")
        import traceback
        print(traceback.format_exc())

def plot_results(results):
    plt.figure(figsize=(15, 10))

    for result in results[:10]:  # Plot only top 10 strategies
        label = f"TF: {result['timeframe']}, Return: {(result['total_return']*100):.2f}%"
        plt.plot(np.linspace(0, 1, 100), np.linspace(CONFIG['initial_cash'], result['final_value'], 100), label=label)

    plt.title('Equity Curves for Top 10 Strategies')
    plt.xlabel('Time')
    plt.ylabel('Equity')
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    plt.tight_layout()
    plt.grid(True)
    plt.show()

if __name__ == "__main__":
    try:
        print("Checking environment variables...")
        api_key = os.getenv('BINANCE_API_KEY')
        secret_key = os.getenv('BINANCE_SECRET_KEY')
        if not api_key or not secret_key:
            raise ValueError("BINANCE_API_KEY or BINANCE_SECRET_KEY not found in environment variables")
        
        print("Running strategy tests...")
        strategy_results = test_strategies()

        if not strategy_results:
            print("No valid results were obtained. Please check your strategy parameters.")
        else:
            print_results(strategy_results)
            plot_results(strategy_results)
    
    except Exception as e:
        print(f"An error occurred: {str(e)}")
        import traceback
        print(traceback.format_exc())