(Auto_Trade) C:\Python\RSI - Trade - Algo>python app.py

Fetching 5-Minute BTC/USDT data from Binance...
Fetched data from 2021-01-01 00:00:00 to 2021-02-01 00:00:00 (Total klines fetched: 8929)
Fetched data from 2021-02-01 00:00:00 to 2021-03-01 00:00:00 (Total klines fetched: 8050)
Fetched data from 2021-03-01 00:00:00 to 2021-04-01 00:00:00 (Total klines fetched: 8911)
Fetched data from 2021-04-01 00:00:00 to 2021-05-01 00:00:00 (Total klines fetched: 8555)
Fetched data from 2021-05-01 00:00:00 to 2021-06-01 00:00:00 (Total klines fetched: 8929)
Fetched data from 2021-06-01 00:00:00 to 2021-07-01 00:00:00 (Total klines fetched: 8641)
Fetched data from 2021-07-01 00:00:00 to 2021-08-01 00:00:00 (Total klines fetched: 8929)
Fetched data from 2021-08-01 00:00:00 to 2021-09-01 00:00:00 (Total klines fetched: 8875)
Fetched data from 2021-09-01 00:00:00 to 2021-10-01 00:00:00 (Total klines fetched: 8617)
Fetched data from 2021-10-01 00:00:00 to 2021-11-01 00:00:00 (Total klines fetched: 8929)
Fetched data from 2021-11-01 00:00:00 to 2021-12-01 00:00:00 (Total klines fetched: 8641)
Fetched data from 2021-12-01 00:00:00 to 2022-01-01 00:00:00 (Total klines fetched: 8929)

Fetching 1-Hour BTC/USDT data from Binance...
Fetched data from 2021-01-01 00:00:00 to 2021-02-01 00:00:00 (Total klines fetched: 745)
Fetched data from 2021-02-01 00:00:00 to 2021-03-01 00:00:00 (Total klines fetched: 672)
Fetched data from 2021-03-01 00:00:00 to 2021-04-01 00:00:00 (Total klines fetched: 744)
Fetched data from 2021-04-01 00:00:00 to 2021-05-01 00:00:00 (Total klines fetched: 716)
Fetched data from 2021-05-01 00:00:00 to 2021-06-01 00:00:00 (Total klines fetched: 745)
Fetched data from 2021-06-01 00:00:00 to 2021-07-01 00:00:00 (Total klines fetched: 721)
Fetched data from 2021-07-01 00:00:00 to 2021-08-01 00:00:00 (Total klines fetched: 745)
Fetched data from 2021-08-01 00:00:00 to 2021-09-01 00:00:00 (Total klines fetched: 741)
Fetched data from 2021-09-01 00:00:00 to 2021-10-01 00:00:00 (Total klines fetched: 719)
Fetched data from 2021-10-01 00:00:00 to 2021-11-01 00:00:00 (Total klines fetched: 745)
Fetched data from 2021-11-01 00:00:00 to 2021-12-01 00:00:00 (Total klines fetched: 721)
Fetched data from 2021-12-01 00:00:00 to 2022-01-01 00:00:00 (Total klines fetched: 745)

Fetching 6-Hour BTC/USDT data from Binance...
Fetched data from 2021-01-01 00:00:00 to 2021-02-01 00:00:00 (Total klines fetched: 125)
Fetched data from 2021-02-01 00:00:00 to 2021-03-01 00:00:00 (Total klines fetched: 113)
Fetched data from 2021-03-01 00:00:00 to 2021-04-01 00:00:00 (Total klines fetched: 125)
Fetched data from 2021-04-01 00:00:00 to 2021-05-01 00:00:00 (Total klines fetched: 121)
Fetched data from 2021-05-01 00:00:00 to 2021-06-01 00:00:00 (Total klines fetched: 125)
Fetched data from 2021-06-01 00:00:00 to 2021-07-01 00:00:00 (Total klines fetched: 121)
Fetched data from 2021-07-01 00:00:00 to 2021-08-01 00:00:00 (Total klines fetched: 125)
Fetched data from 2021-08-01 00:00:00 to 2021-09-01 00:00:00 (Total klines fetched: 125)
Fetched data from 2021-09-01 00:00:00 to 2021-10-01 00:00:00 (Total klines fetched: 121)
Fetched data from 2021-10-01 00:00:00 to 2021-11-01 00:00:00 (Total klines fetched: 125)
Fetched data from 2021-11-01 00:00:00 to 2021-12-01 00:00:00 (Total klines fetched: 121)
Fetched data from 2021-12-01 00:00:00 to 2022-01-01 00:00:00 (Total klines fetched: 125)

Renaming columns for differentiation...

Merging 5-Minute and 1-Hour data...

Merging merged data with 6-Hour data...
Traceback (most recent call last):
  File "C:\Python\RSI - Trade - Algo\app.py", line 598, in <module>
    bt_data = prepare_data()
  File "C:\Python\RSI - Trade - Algo\app.py", line 172, in prepare_data
    df_combined = pd.merge(
  File "C:\Users\Maur\anaconda3\envs\Auto_Trade\lib\site-packages\pandas\core\reshape\merge.py", line 184, in merge
    return op.get_result(copy=copy)
  File "C:\Users\Maur\anaconda3\envs\Auto_Trade\lib\site-packages\pandas\core\reshape\merge.py", line 896, in get_result
    self._maybe_add_join_keys(result, left_indexer, right_indexer)
  File "C:\Users\Maur\anaconda3\envs\Auto_Trade\lib\site-packages\pandas\core\reshape\merge.py", line 1117, in _maybe_add_join_keys
    result.insert(i, name or f"key_{i}", key_col)
  File "C:\Users\Maur\anaconda3\envs\Auto_Trade\lib\site-packages\pandas\core\frame.py", line 5158, in insert
    raise ValueError(f"cannot insert {column}, already exists")
ValueError: cannot insert key_0, already exists:   import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import backtrader as bt
from binance.client import Client
import ta  # Technical Analysis library
import datetime
import time
from dateutil.relativedelta import relativedelta

# Suppress warnings for cleaner output
import warnings
warnings.filterwarnings('ignore')


# ===============================
# 1. Data Collection Functions
# ===============================

def get_historical_klines_chunked(symbol, interval, start_date, end_date, max_retries=5, backoff_factor=1):
    """
    Fetch historical klines from Binance in chunks to avoid timeouts.
    
    :param symbol: Trading pair symbol (e.g., 'BTCUSDT')
    :param interval: Kline interval (e.g., '5m' for 5 minutes)
    :param start_date: Start date as a datetime object
    :param end_date: End date as a datetime object
    :param max_retries: Maximum number of retries for each chunk
    :param backoff_factor: Factor for exponential backoff
    :return: Pandas DataFrame with historical data
    """
    client = Client(api_key='', api_secret='')  # No authentication for public data

    all_klines = []
    current_start = start_date

    while current_start < end_date:
        # Define the end of the current chunk (e.g., next month)
        current_end = current_start + relativedelta(months=1)
        if current_end > end_date:
            current_end = end_date

        for attempt in range(max_retries):
            try:
                klines = client.get_historical_klines(
                    symbol=symbol,
                    interval=interval,
                    start_str=current_start.strftime("%d %b, %Y"),
                    end_str=current_end.strftime("%d %b, %Y")
                )
                if not klines:
                    print(f"No data returned for period {current_start} to {current_end}. Skipping...")
                    break
                all_klines.extend(klines)
                print(f"Fetched data from {current_start} to {current_end} (Total klines fetched: {len(klines)})")
                break  # Exit retry loop on success
            except Exception as e:
                wait_time = backoff_factor * (2 ** attempt)
                print(f"Error fetching data from {current_start} to {current_end}: {e}")
                print(f"Retrying in {wait_time} seconds...")
                time.sleep(wait_time)
        else:
            print(f"Failed to fetch data from {current_start} to {current_end} after {max_retries} attempts.")
            # Optionally, you can choose to exit or continue
            continue

        # Move to the next chunk
        current_start = current_end

    # Convert to DataFrame
    df = pd.DataFrame(all_klines, columns=[
        'Open Time', 'Open', 'High', 'Low', 'Close', 'Volume',
        'Close Time', 'Quote Asset Volume', 'Number of Trades',
        'Taker Buy Base Asset Volume', 'Taker Buy Quote Asset Volume', 'Ignore'
    ])

    # Drop any duplicate entries
    df.drop_duplicates(subset=['Open Time'], inplace=True)

    # Convert time columns to datetime
    df['Open Time'] = pd.to_datetime(df['Open Time'], unit='ms')
    df['Close Time'] = pd.to_datetime(df['Close Time'], unit='ms')

    # Set index to Open Time
    df.set_index('Open Time', inplace=True)

    # Convert numerical columns from strings to floats
    for col in ['Open', 'High', 'Low', 'Close', 'Volume']:
        df[col] = df[col].astype(float)

    # Keep only necessary columns
    df = df[['Open', 'High', 'Low', 'Close', 'Volume']]

    return df


# =================================
# 2. Data Preparation Functions
# =================================

def prepare_data():
    """
    Fetches and prepares BTC/USDT historical data for Backtrader.
    
    :return: Prepared Pandas DataFrame suitable for Backtrader
    """
    symbol = 'BTCUSDT'
    interval_5m = '5m'  # 5-Minute interval
    interval_1h = '1h'  # 1-Hour interval
    interval_6h = '6h'  # 6-Hour interval

    start_date_str = '1 Jan, 2021'
    end_date_str = '1 Jan, 2022'

    # Convert string dates to datetime objects
    start_date = datetime.datetime.strptime(start_date_str, '%d %b, %Y')
    end_date = datetime.datetime.strptime(end_date_str, '%d %b, %Y')

    print("\nFetching 5-Minute BTC/USDT data from Binance...")
    df_5m = get_historical_klines_chunked(symbol, interval_5m, start_date, end_date)

    print("\nFetching 1-Hour BTC/USDT data from Binance...")
    df_1h = get_historical_klines_chunked(symbol, interval_1h, start_date, end_date)

    print("\nFetching 6-Hour BTC/USDT data from Binance...")
    df_6h = get_historical_klines_chunked(symbol, interval_6h, start_date, end_date)

    # =============================
    # 2.1 Rename Columns for Clarity
    # =============================
    print("\nRenaming columns for differentiation...")
    df_5m.rename(columns={
        'Open': 'open_5m',
        'High': 'high_5m',
        'Low': 'low_5m',
        'Close': 'close_5m',
        'Volume': 'volume_5m'
    }, inplace=True)

    df_1h.rename(columns={
        'Open': 'open_1h',
        'High': 'high_1h',
        'Low': 'low_1h',
        'Close': 'close_1h',
        'Volume': 'volume_1h'
    }, inplace=True)

    df_6h.rename(columns={
        'Open': 'open_6h',
        'High': 'high_6h',
        'Low': 'low_6h',
        'Close': 'close_6h',
        'Volume': 'volume_6h'
    }, inplace=True)

    # ====================================
    # 2.2 Merge 5-Minute and 1-Hour Data
    # ====================================
    print("\nMerging 5-Minute and 1-Hour data...")
    df_combined = pd.merge(
        df_5m,
        df_1h,
        left_on=df_5m.index.floor('H'),
        right_index=True,
        how='left'
    )

    # ====================================
    # 2.3 Merge with 6-Hour Data
    # ====================================
    print("\nMerging merged data with 6-Hour data...")
    df_combined = pd.merge(
        df_combined,
        df_6h,
        left_on=df_combined.index.floor('6H'),
        right_index=True,
        how='left'
    )

    # ============================
    # 2.4 Forward fill longer timeframes
    # ============================
    print("\nForward filling 1-Hour and 6-Hour data for each 5-Minute interval...")
    df_combined.ffill(inplace=True)

    # ============================
    # 2.5 Calculate RSI indicators
    # ============================
    print("\nCalculating RSI indicators...")
    # RSI for 5-Minute
    df_combined['rsi_5m'] = ta.momentum.RSIIndicator(close=df_combined['close_5m'], window=14).rsi()

    # RSI for 1-Hour
    df_combined['rsi_1h'] = ta.momentum.RSIIndicator(close=df_combined['close_1h'], window=14).rsi()

    # RSI for 6-Hour
    df_combined['rsi_6h'] = ta.momentum.RSIIndicator(close=df_combined['close_6h'], window=14).rsi()

    # ===============================
    # 2.6 Drop rows with NaN RSI
    # ===============================
    print("\nDropping rows with NaN RSI values...")
    df_combined.dropna(inplace=True)

    # ===============================
    # 2.7 Select and rename columns for BT
    # ===============================
    print("\nSelecting and renaming necessary columns for Backtrader...")
    bt_data = df_combined[['open_5m', 'high_5m', 'low_5m', 'close_5m', 'volume_5m',
                           'rsi_5m', 'rsi_1h', 'rsi_6h']].copy()
    bt_data.rename(columns={
        'open_5m': 'open',
        'high_5m': 'high',
        'low_5m': 'low',
        'close_5m': 'close',
        'volume_5m': 'volume'
    }, inplace=True)

    # ===============================
    # 2.8 Reset Index and Set Datetime
    # ===============================
    print("\nResetting index and setting datetime for Backtrader...")
    bt_data.reset_index(inplace=True)

    # Backtrader expects 'datetime' as the index; ensure it's set
    bt_data['datetime'] = pd.to_datetime(bt_data['Open Time'])
    bt_data.set_index('datetime', inplace=True)

    # Drop 'Open Time' as it's now redundant
    bt_data.drop(['Open Time'], axis=1, inplace=True)

    print("\nData preparation complete.")
    return bt_data


# ===================================
# 3. Custom Backtrader Data Feed Class
# ===================================

class CustomPandasData(bt.feeds.PandasData):
    """
    Custom Pandas Data Feed to include 'rsi_1h' and 'rsi_6h'.
    """
    lines = ('rsi_1h', 'rsi_6h',)
    params = (
        ('rsi_1h', 'rsi_1h'),  # Map the 'rsi_1h' column to the custom line 'rsi_1h'
        ('rsi_6h', 'rsi_6h'),  # Map the 'rsi_6h' column to the custom line 'rsi_6h'
    )


# ===================================
# 4. Strategy Implementation Class
# ===================================

class MultiTimeframeRSIStrategy(bt.Strategy):
    """
    Multi-Timeframe RSI Strategy:
    - Sell (Enter Short) when RSI 5m > 70 and RSI 1h > 50
    - Exit Short when RSI 5m < 30
    - Buy (Enter Long) when RSI 5m < 30 and RSI 1h < 50
    - Exit Long when RSI 5m > 70
    - Additionally:
      - Buy when 6h RSI > 50
      - Sell when 6h RSI < 50
    Includes comprehensive analytics for each signal.
    """

    params = (
        ('rsi_period_short', 14),       # RSI period for 5-Minute
        ('rsi_period_long', 14),        # RSI period for 1-Hour
        ('rsi_period_extra', 14),       # RSI period for 6-Hour
        ('rsi_overbought', 70),         # Overbought threshold for 5-Minute RSI
        ('rsi_oversold', 30),           # Oversold threshold for 5-Minute RSI
        ('rsi_long_threshold', 50),     # RSI threshold for 1-Hour RSI
        ('rsi_extra_threshold', 50),    # RSI threshold for 6-Hour RSI
        ('order_size', 0.001),          # Position size in BTC
    )

    def __init__(self):
        # Initialize RSI indicators
        self.rsi_5m = bt.indicators.RSI_Safe(self.data.close, period=self.params.rsi_period_short)
        self.rsi_1h = self.data.rsi_1h  # Accessing the precomputed RSI_1h
        self.rsi_6h = self.data.rsi_6h  # Accessing the precomputed RSI_6h

        # Track pending orders
        self.order = None

        # To keep track of current position type: 'long', 'short', or None
        self.position_type = None

        # Initialize counters for analysis
        self.buy_signals = 0
        self.sell_signals = 0
        self.executed_buys = 0
        self.executed_sells = 0
        self.failed_buys = 0
        self.failed_sells = 0
        self.buy_reasons = []
        self.sell_reasons = []
        self.extra_buy_signals = 0
        self.extra_sell_signals = 0
        self.executed_extra_buys = 0
        self.executed_extra_sells = 0
        self.failed_extra_buys = 0
        self.failed_extra_sells = 0
        self.extra_buy_reasons = []
        self.extra_sell_reasons = []

    def log(self, txt, dt=None):
        """
        Logging function for this strategy.
        Logs to both console and an external file for detailed analysis.
        """
        dt = dt or self.datas[0].datetime.datetime(0)
        log_entry = f"{dt} - {txt}"
        print(log_entry)
        with open('trade_log.txt', 'a') as f:
            f.write(log_entry + '\n')

    def notify_order(self, order):
        """
        Notification handler for order events.
        Logs executed orders and handles failures.
        """
        if order.status in [order.Submitted, order.Accepted]:
            # Order is submitted/accepted but not yet completed
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f"BUY EXECUTED, Price: {order.executed.price:.2f}, "
                         f"Cost: {order.executed.value:.4f}, Comm {order.executed.comm:.4f}")
                self.executed_buys += 1
                self.position_type = 'long'
            elif order.issell():
                self.log(f"SELL EXECUTED, Price: {order.executed.price:.2f}, "
                         f"Cost: {order.executed.value:.4f}, Comm {order.executed.comm:.4f}")
                self.executed_sells += 1
                self.position_type = 'short'

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order Canceled/Margin/Rejected")
            # Depending on the reason, increment failed counters
            if order.isbuy():
                self.failed_buys += 1
                self.buy_reasons.append('Order Canceled/Margin/Rejected')
            elif order.issell():
                self.failed_sells += 1
                self.sell_reasons.append('Order Canceled/Margin/Rejected')

        # Reset orders
        self.order = None

    def next(self):
        """
        Called for each new bar (5-Minute in this case).
        Evaluates trade signals and places orders accordingly.
        """
        # If an order is pending, do nothing
        if self.order:
            return

        # Current RSI values
        rsi_5 = self.rsi_5m[0]
        rsi_1h = self.rsi_1h[0]
        rsi_6h = self.rsi_6h[0]

        # Log current RSI values
        self.log(f"Current RSI 5m: {rsi_5:.2f}, RSI 1h: {rsi_1h:.2f}, RSI 6h: {rsi_6h:.2f}")

        # Current position size
        position_size = self.position.size

        # ======================================
        # Existing Buy/Sell Signal Logic
        # ======================================

        # SELL SIGNAL: Enter Short Position
        if (rsi_5 > self.params.rsi_overbought) and (rsi_1h > self.params.rsi_long_threshold):
            self.sell_signals += 1
            if self.position_type != 'short':
                self.log(f"SELL SIGNAL: RSI 5m={rsi_5:.2f}, RSI 1h={rsi_1h:.2f}")
                # If currently long, close the long position first
                if self.position:
                    self.log("Closing existing long position before entering short.")
                    self.order = self.close()
                # Enter short
                try:
                    self.order = self.sell(size=self.params.order_size)
                except Exception as e:
                    self.failed_sells += 1
                    self.sell_reasons.append(str(e))

        # BUY SIGNAL: Enter Long Position
        elif (rsi_5 < self.params.rsi_oversold) and (rsi_1h < self.params.rsi_long_threshold):
            self.buy_signals += 1
            if self.position_type != 'long':
                self.log(f"BUY SIGNAL: RSI 5m={rsi_5:.2f}, RSI 1h={rsi_1h:.2f}")
                # If currently short, close the short position first
                if self.position:
                    self.log("Closing existing short position before entering long.")
                    self.order = self.close()
                # Enter long
                try:
                    self.order = self.buy(size=self.params.order_size)
                except Exception as e:
                    self.failed_buys += 1
                    self.buy_reasons.append(str(e))

        # EXIT SHORT POSITION: When RSI 5m < 30
        elif (self.position_type == 'short') and (rsi_5 < self.params.rsi_oversold):
            self.log(f"EXIT SHORT SIGNAL: RSI 5m={rsi_5:.2f}")
            try:
                self.order = self.close()
            except Exception as e:
                self.failed_sells += 1
                self.sell_reasons.append(str(e))

        # EXIT LONG POSITION: When RSI 5m > 70
        elif (self.position_type == 'long') and (rsi_5 > self.params.rsi_overbought):
            self.log(f"EXIT LONG SIGNAL: RSI 5m={rsi_5:.2f}")
            try:
                self.order = self.close()
            except Exception as e:
                self.failed_buys += 1
                self.buy_reasons.append(str(e))

        # ======================================
        # New Buy/Sell Logic Based on 6-Hour RSI
        # ======================================

        # BUY CONDITION: 6-Hour RSI > 50
        if rsi_6h > self.params.rsi_extra_threshold:
            self.extra_buy_signals += 1
            if not self.position or self.position_type != 'long':
                self.log(f"EXTRA BUY SIGNAL: RSI 6h={rsi_6h:.2f}")
                # Enter long
                try:
                    # Check if not already in long position
                    if self.position_type != 'long':
                        # If short, close the short position first
                        if self.position and self.position_type == 'short':
                            self.log("Closing existing short position before entering long as per 6h RSI > 50.")
                            self.order = self.close()
                        # Place buy order
                        self.order = self.buy(size=self.params.order_size)
                except Exception as e:
                    self.failed_extra_buys += 1
                    self.extra_buy_reasons.append(str(e))

        # SELL CONDITION: 6-Hour RSI < 50
        if rsi_6h < self.params.rsi_extra_threshold:
            self.extra_sell_signals += 1
            if not self.position or self.position_type != 'short':
                self.log(f"EXTRA SELL SIGNAL: RSI 6h={rsi_6h:.2f}")
                # Enter short
                try:
                    # Check if not already in short position
                    if self.position_type != 'short':
                        # If long, close the long position first
                        if self.position and self.position_type == 'long':
                            self.log("Closing existing long position before entering short as per 6h RSI < 50.")
                            self.order = self.close()
                        # Place sell order
                        self.order = self.sell(size=self.params.order_size)
                except Exception as e:
                    self.failed_extra_sells += 1
                    self.extra_sell_reasons.append(str(e))


        # ==========================================================================
        # EXIT STRATEGY FOR EXTRA BUY/Sell LOGIC (Optional)
        # ==========================================================================

        # Example: Exit the extra buy when 6h RSI drops below 50
        # Similarly, exit the extra sell when 6h RSI rises above 50
        # Currently, the strategy enters a long when 6h RSI >50 unless already long, and enters short when 6h RSI <50 unless already short
        # Exiting positions is already handled by the main logic based on 5m RSI

        # You can customize exit conditions for the extra signals if needed

    # ===================================
    # 5. Backtesting Setup
    # ===================================

    def run_backtest(bt_data):
        """
        Sets up and runs the Backtrader backtest.
        
        :param bt_data: Prepared Pandas DataFrame suitable for Backtrader
        """
        cerebro = bt.Cerebro()

        # Add strategy
        cerebro.addstrategy(MultiTimeframeRSIStrategy)

        # Convert pandas DataFrame to Backtrader DataFeed
        data_feed = CustomPandasData(
            dataname=bt_data,
            open='open',
            high='high',
            low='low',
            close='close',
            volume='volume',
            rsi_1h='rsi_1h',
            rsi_6h='rsi_6h',
            openinterest=-1  # -1 indicates no 'openinterest' column
        )

        # Add data to Cerebro
        cerebro.adddata(data_feed)

        # Set initial cash
        cerebro.broker.setcash(10000.0)  # Starting with $10,000

        # Set commission (e.g., 0.1% per trade)
        cerebro.broker.setcommission(commission=0.001)

        # Add analyzers for performance metrics
        cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio')
        cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')

        # Print starting portfolio value
        print('\nStarting Portfolio Value: %.2f' % cerebro.broker.getvalue())

        # Run the backtest
        results = cerebro.run()
        strat = results[0]

        # Print ending portfolio value
        print('Ending Portfolio Value: %.2f' % cerebro.broker.getvalue())

        # Extract analyzers
        sharpe = strat.analyzers.sharpe_ratio.get_analysis()
        drawdown = strat.analyzers.drawdown.get_analysis()

        # Print Analyzer Results
        sharpe_ratio = sharpe.get('sharperatio', 'N/A')
        max_drawdown = drawdown.max.drawdown if 'max' in drawdown else 'N/A'
        print(f"Sharpe Ratio: {sharpe_ratio}")
        print(f"Max Drawdown: {max_drawdown}%")

        # Print Strategy Summary Analytics
        print("\nStrategy Analytics:")
        print("-" * 30)
        print(f"Total Buy Signals Checked: {strat.buy_signals}")
        print(f"Total Sell Signals Checked: {strat.sell_signals}")
        print(f"Executed Buys: {strat.executed_buys}")
        print(f"Executed Sells: {strat.executed_sells}")
        print(f"Failed Buy Signals: {strat.failed_buys}")
        print(f"Failed Sell Signals: {strat.failed_sells}")
        print(f"Buy Signal Success Rate: {(strat.executed_buys / strat.buy_signals) * 100:.2f}%")
        print(f"Buy Signal Failure Rate: {(strat.failed_buys / strat.buy_signals) * 100:.2f}%")
        if strat.buy_reasons:
            fail_reasons_buys = pd.Series(strat.buy_reasons).value_counts()
            print("Buy Signal Failure Reasons:")
            for reason, count in fail_reasons_buys.items():
                print(f"  {reason}: {count} times")
        
        print(f"Sell Signal Success Rate: {(strat.executed_sells / strat.sell_signals) * 100:.2f}%")
        print(f"Sell Signal Failure Rate: {(strat.failed_sells / strat.sell_signals) * 100:.2f}%")
        if strat.sell_reasons:
            fail_reasons_sells = pd.Series(strat.sell_reasons).value_counts()
            print("Sell Signal Failure Reasons:")
            for reason, count in fail_reasons_sells.items():
                print(f"  {reason}: {count} times")
        
        # Extra Buy/Sell Signals
        print(f"Total Extra Buy Signals Checked (6h RSI >50): {strat.extra_buy_signals}")
        print(f"Executed Extra Buys: {strat.executed_extra_buys}")
        print(f"Failed Extra Buy Signals: {strat.failed_extra_buys}")
        if strat.extra_buy_reasons:
            fail_reasons_extra_buys = pd.Series(strat.extra_buy_reasons).value_counts()
            print("Extra Buy Signal Failure Reasons:")
            for reason, count in fail_reasons_extra_buys.items():
                print(f"  {reason}: {count} times")
        
        print(f"Total Extra Sell Signals Checked (6h RSI <50): {strat.extra_sell_signals}")
        print(f"Executed Extra Sells: {strat.executed_extra_sells}")
        print(f"Failed Extra Sell Signals: {strat.failed_extra_sells}")
        if strat.extra_sell_reasons:
            fail_reasons_extra_sells = pd.Series(strat.extra_sell_reasons).value_counts()
            print("Extra Sell Signal Failure Reasons:")
            for reason, count in fail_reasons_extra_sells.items():
                print(f"  {reason}: {count} times")
        
        print("\nMax Drawdown: {:.2f}%".format(max_drawdown))

        # Plot the results
        cerebro.plot(style='candlestick')


# ===================================
# 6. Execute Backtest
# ===================================

if __name__ == "__main__":
    bt_data = prepare_data()
    run_backtest(bt_data)