Hey there. I am debugging my backtesting method but cant find the issue yet.
Here is the code:

import numpy as np
import pandas as pd
import vectorbt as vbt
import ccxt
from datetime import datetime, timedelta
from numba import jit
import warnings
from concurrent.futures import ThreadPoolExecutor
from functools import partial
import time
from threading import Lock
import matplotlib.pyplot as plt
import seaborn as sns
import os
warnings.filterwarnings('ignore')

class VolumeProfileStrategy:
    def __init__(self, 
                 symbol,
                 exchange_id='binance',
                 price_bucket_count=100,  # Number of price buckets for volume profile
                 profile_period='1d',     # Period for calculating volume profile
                 volume_threshold=0.7,     # Threshold for identifying significant volume nodes
                 node_distance=0.02,       # Minimum distance between volume nodes
                 entry_confirmation_bars=3, # Bars needed for entry confirmation
                 exit_confirmation_bars=2,  # Bars needed for exit confirmation
                 risk_per_trade=0.02,
                 interval='1m',
                 max_workers=5):
        
        self.symbol = symbol
        self.price_bucket_count = price_bucket_count
        self.profile_period = profile_period
        self.volume_threshold = volume_threshold
        self.node_distance = node_distance
        self.entry_confirmation_bars = entry_confirmation_bars
        self.exit_confirmation_bars = exit_confirmation_bars
        self.risk_per_trade = risk_per_trade
        self.interval = interval
        self.max_workers = max_workers
        
        # Create exchange connection pool
        self.exchange_pool = [
            getattr(ccxt, exchange_id)({
                'enableRateLimit': True,
                'options': {'defaultType': 'future'}
            }) for _ in range(max_workers)
        ]
        self.pool_lock = Lock()
        self.pool_index = 0
        
        # Pre-compile Numba functions
        self._compile_numba_functions()

    def _get_exchange(self):
        """Get next available exchange instance from the pool"""
        with self.pool_lock:
            exchange = self.exchange_pool[self.pool_index]
            self.pool_index = (self.pool_index + 1) % len(self.exchange_pool)
            return exchange

    @staticmethod
    @jit(nopython=True)
    def _calculate_volume_profile(prices, volumes, bucket_count):
        """Calculate volume profile metrics using Numba optimization"""
        if len(prices) == 0 or len(volumes) == 0:
            return np.zeros(bucket_count), np.zeros(bucket_count)
            
        min_price = np.min(prices)
        max_price = np.max(prices)
        
        # Prevent division by zero
        if min_price == max_price:
            price_range = 1.0
        else:
            price_range = max_price - min_price
            
        bucket_size = price_range / bucket_count
        
        volume_profile = np.zeros(bucket_count)
        price_buckets = np.zeros(bucket_count)
        
        for i in range(len(prices)):
            if bucket_size > 0:
                bucket_idx = int((prices[i] - min_price) / bucket_size)
                if 0 <= bucket_idx < bucket_count:
                    volume_profile[bucket_idx] += volumes[i]
                    price_buckets[bucket_idx] = min_price + (bucket_idx + 0.5) * bucket_size
            
        return volume_profile, price_buckets
    
    @staticmethod
    @jit(nopython=True)
    def _identify_volume_nodes(volume_profile, price_buckets, threshold, min_distance):
        """Identify high volume nodes using Numba optimization with more lenient conditions"""
        volume_profile = np.asarray(volume_profile)
        price_buckets = np.asarray(price_buckets)
        
        if len(volume_profile) == 0 or len(price_buckets) == 0:
            return np.zeros((0, 2), dtype=np.float64)
            
        total_volume = np.sum(volume_profile)
        if total_volume == 0:
            return np.zeros((0, 2), dtype=np.float64)
            
        normalized_volume = volume_profile / total_volume
        
        # Pre-allocate maximum possible nodes
        max_nodes = len(volume_profile)
        temp_nodes = np.zeros((max_nodes, 2), dtype=np.float64)
        node_count = 0
        
        # Find peaks more leniently
        for i in range(1, len(volume_profile) - 1):
            is_peak = normalized_volume[i] > threshold
            
            # Check if it's a local maximum or close to being one
            if i > 0:
                is_peak = is_peak and (normalized_volume[i] >= normalized_volume[i-1] * 0.95)  # Allow 5% tolerance
            if i < len(volume_profile) - 1:
                is_peak = is_peak and (normalized_volume[i] >= normalized_volume[i+1] * 0.95)  # Allow 5% tolerance
                
            if is_peak:
                temp_nodes[node_count, 0] = price_buckets[i]
                temp_nodes[node_count, 1] = normalized_volume[i]
                node_count += 1
        
        if node_count == 0:
            return np.zeros((0, 2), dtype=np.float64)
        
        # Create array with actual nodes
        nodes = temp_nodes[:node_count]
        
        # Sort nodes by price
        sorted_indices = np.argsort(nodes[:, 0])
        nodes = nodes[sorted_indices]
        
        # Filter nodes by minimum distance more leniently
        filtered_nodes = np.zeros((node_count, 2), dtype=np.float64)
        filtered_count = 0
        
        filtered_nodes[0] = nodes[0]
        filtered_count += 1
        
        for i in range(1, node_count):
            if abs(nodes[i, 0] - filtered_nodes[filtered_count-1, 0]) / filtered_nodes[filtered_count-1, 0] > min_distance:
                filtered_nodes[filtered_count] = nodes[i]
                filtered_count += 1
                
        return filtered_nodes[:filtered_count]

    def _compile_numba_functions(self):
        """Pre-compile Numba functions with dummy data"""
        # Use larger dummy dataset for more robust testing
        dummy_prices = np.array([100.0, 101.0, 102.0, 103.0, 104.0], dtype=np.float64)
        dummy_volumes = np.array([1.0, 2.0, 3.0, 2.0, 1.0], dtype=np.float64)
        
        # Test volume profile calculation
        profile, buckets = self._calculate_volume_profile(dummy_prices, dummy_volumes, 5)
        
        # Test volume nodes identification
        _ = self._identify_volume_nodes(profile, buckets, 0.2, 0.01)

    def fetch_data(self, period):
        """Fetch OHLCV data and calculate volume profile metrics with fixed storage"""
        cache_filename = f"volume_profile_cache_{self.symbol}_{self.interval}_{period}.pkl"
        
        if os.path.exists(cache_filename):
            print(f"Loading data from cache: {cache_filename}")
            print(f"Total fetched OHLCV records: {len(pd.read_pickle(cache_filename))}")
            return pd.read_pickle(cache_filename)
        
        exchange = self._get_exchange()
        end_time = int(time.time() * 1000)
        start_time = end_time - (int(period[:-1]) * 24 * 60 * 60 * 1000)
        
        # Fetch historical data
        all_ohlcv = []
        while start_time < end_time:
            print(f"Fetching data from {pd.to_datetime(start_time, unit='ms')}...")
            try:
                ohlcv = exchange.fetch_ohlcv(
                    symbol=self.symbol,
                    timeframe=self.interval,
                    since=start_time,
                    limit=1500
                )
                
                if not ohlcv:
                    break
                    
                all_ohlcv.extend(ohlcv)
                start_time = ohlcv[-1][0] + 1
                
            except Exception as e:
                print(f"Error fetching data: {str(e)}")
                time.sleep(1)
                continue
        
        # Create DataFrame
        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)
        
        # Calculate rolling volume profile
        window_size = int(pd.Timedelta(self.profile_period) / pd.Timedelta(self.interval))
        
        # Store nodes as lists instead of numpy arrays
        df['volume_nodes'] = None  # Initialize with None
        
        print(f"Calculating volume profiles for {len(df)} rows...")
        for i in range(window_size, len(df)):
            if i % 1000 == 0:  # Progress update
                print(f"Processing row {i}/{len(df)}")
                
            try:
                window = df.iloc[i-window_size:i]
                
                if len(window) == 0 or window['close'].isna().any() or window['volume'].isna().any():
                    continue
                
                profile, buckets = self._calculate_volume_profile(
                    window['close'].values.astype(np.float64),
                    window['volume'].values.astype(np.float64),
                    self.price_bucket_count
                )
                
                nodes = self._identify_volume_nodes(
                    profile,
                    buckets,
                    self.volume_threshold,
                    self.node_distance
                )
                
                # Store nodes as a list of tuples for better serialization
                if len(nodes) > 0:
                    df.at[df.index[i], 'volume_nodes'] = [(float(price), float(vol)) for price, vol in nodes]
                
            except Exception as e:
                print(f"Error calculating volume profile for window ending at {df.index[i]}: {str(e)}")
                continue
        
        print(f"Saving data to cache: {cache_filename}")
        df.to_pickle(cache_filename)
        
        return df
    
    def plot_data(self, df):
        """Plots the OHLCV and volume profile data."""
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
        
        # Plot OHLCV
        ax1.plot(df.index, df['close'], label='Close Price')
        ax1.set_title(f"{self.symbol} Price")
        ax1.set_ylabel("Price")
        ax1.legend()
        
        # Plot Volume Profile
        if 'volume_profile' in df.columns and 'price_buckets' in df.columns:
            latest_profile = df['volume_profile'].iloc[-1]
            latest_buckets = df['price_buckets'].iloc[-1]
            ax2.barh(latest_buckets, latest_profile, alpha=0.3)
            ax2.set_title("Volume Profile")
            ax2.set_xlabel("Volume")
            ax2.set_ylabel("Price")
        
        plt.tight_layout()
        plt.show()

    
    def generate_signals(self, data):
        """Generate trading signals with fixed node access"""
        signals = pd.DataFrame(index=data.index)
        
        # Initialize signal columns
        signals['long_entries'] = False
        signals['short_entries'] = False
        signals['exits'] = False
        
        debug_stats = {
            'total_rows': len(data),
            'rows_with_nodes': 0,
            'potential_long_entries': 0,
            'potential_short_entries': 0,
            'confirmed_long_entries': 0,
            'confirmed_short_entries': 0,
            'exits': 0
        }
        
        for i in range(len(data)):
            if i < self.entry_confirmation_bars:
                continue
            
            current_price = data['close'].iloc[i]
            nodes = data['volume_nodes'].iloc[i]
            
            # Skip if no nodes
            if nodes is None:
                continue
                
            # Convert stored nodes back to numpy array
            nodes = np.array(nodes, dtype=np.float64)
            
            if len(nodes) > 0:
                debug_stats['rows_with_nodes'] += 1
                
                # Sort nodes by price
                sorted_nodes = nodes[nodes[:, 0].argsort()]
                node_prices = sorted_nodes[:, 0]
                
                # Find nearest nodes
                price_diffs = node_prices - current_price
                nearest_above = node_prices[price_diffs > 0].min() if len(price_diffs[price_diffs > 0]) > 0 else np.inf
                nearest_below = node_prices[price_diffs < 0].max() if len(price_diffs[price_diffs < 0]) > 0 else -np.inf
                
                # Long entry conditions
                if (abs(current_price - nearest_below) / current_price < self.node_distance):
                    price_history = data['close'].iloc[i-self.entry_confirmation_bars:i]
                    price_changes = price_history.diff().iloc[1:]
                    if sum(price_changes > 0) >= len(price_changes) * 0.5:
                        signals.iloc[i, signals.columns.get_loc('long_entries')] = True
                        debug_stats['confirmed_long_entries'] += 1
                
                # Short entry conditions
                elif (abs(current_price - nearest_above) / current_price < self.node_distance):
                    price_history = data['close'].iloc[i-self.entry_confirmation_bars:i]
                    price_changes = price_history.diff().iloc[1:]
                    if sum(price_changes < 0) >= len(price_changes) * 0.5:
                        signals.iloc[i, signals.columns.get_loc('short_entries')] = True
                        debug_stats['confirmed_short_entries'] += 1
                
                # Exit conditions
                if i >= self.exit_confirmation_bars:
                    if signals['long_entries'].iloc[i-1:i].any() or signals['short_entries'].iloc[i-1:i].any():
                        price_history = data['close'].iloc[i-self.exit_confirmation_bars:i]
                        price_changes = price_history.diff().iloc[1:]
                        if ((signals['long_entries'].iloc[i-1:i].any() and sum(price_changes < 0) >= len(price_changes) * 0.4) or
                            (signals['short_entries'].iloc[i-1:i].any() and sum(price_changes > 0) >= len(price_changes) * 0.4)):
                            signals.iloc[i, signals.columns.get_loc('exits')] = True
                            debug_stats['exits'] += 1
        
        print("\nDebug Statistics:")
        for key, value in debug_stats.items():
            print(f"{key}: {value}")
        
        return signals

    def backtest(self, data, initial_capital, fee_rate):
        """Run backtest using vectorbt with additional debugging"""
        print("\nStarting backtest...")
        print(f"Data shape: {data.shape}")
        print(f"Data columns: {data.columns}")
        print(f"First few rows of close prices:\n{data['close'].head()}")
        
        signals = self.generate_signals(data)
        
        # Debug: Check for any NaN or infinite values
        print("\nChecking for NaN/Inf values:")
        print(f"NaN in close prices: {data['close'].isna().sum()}")
        print(f"Inf in close prices: {np.isinf(data['close']).sum()}")
        
        # Calculate position sizes and stop-losses
        entry_prices = pd.Series(index=data.index, dtype=float)
        entry_prices[signals['long_entries']] = data.loc[signals['long_entries'], 'close']
        entry_prices[signals['short_entries']] = data.loc[signals['short_entries'], 'close']
        
        stop_prices = pd.Series(index=data.index, dtype=float)
        stop_prices[signals['long_entries']] = entry_prices[signals['long_entries']] * (1 - self.risk_per_trade)
        stop_prices[signals['short_entries']] = entry_prices[signals['short_entries']] * (1 + self.risk_per_trade)
        
        size = initial_capital / data['close'].iloc[0]
        
        print("\nPre-vectorbt checks:")
        print(f"Size calculated: {size}")
        print(f"Initial close price: {data['close'].iloc[0]}")
        
        try:
            portfolio = vbt.Portfolio.from_signals(
                close=data['close'],
                entries=signals['long_entries'] | signals['short_entries'],
                exits=signals['exits'],
                short_entries=signals['short_entries'],
                sl_stop=stop_prices,
                init_cash=initial_capital,
                fees=fee_rate,
                size=size,
                freq=self.interval
            )
            
            trades = portfolio.trades.records
            
            print("\nTrade Summary:")
            if trades is not None:
                print(f"Number of trades: {len(trades)}")
                if len(trades) > 0:
                    print("First trade details:")
                    print(trades[0])
            else:
                print("No trades generated")
            
            metrics = {
                'Total Return (%)': portfolio.total_return() * 100,
                'Sharpe Ratio': portfolio.sharpe_ratio(),
                'Max Drawdown (%)': portfolio.max_drawdown() * 100,
                'Number of Trades': len(trades) if trades is not None else 0,
                'Win Rate (%)': (np.sum(trades['pnl'] > 0) / len(trades)) * 100 if trades is not None and len(trades) > 0 else 0,
                'Profit Factor': np.sum(trades[trades['pnl'] > 0]['pnl']) / abs(np.sum(trades[trades['pnl'] < 0]['pnl'])) if trades is not None and len(trades) > 0 and abs(np.sum(trades[trades['pnl'] < 0]['pnl'])) > 0 else 0
            }
            
            return metrics, portfolio, data
            
        except Exception as e:
            print(f"\nError in vectorbt processing: {str(e)}")
            raise
        
    def verify_volume_nodes(self, data):
        """Verify that volume nodes are being calculated correctly"""
        print("\nVerifying volume nodes calculation...")
        
        # Check a specific time window
        sample_window = data.iloc[-100:]  # Last 100 rows
        
        # Calculate volume profile for this window
        prices = sample_window['close'].values.astype(np.float64)
        volumes = sample_window['volume'].values.astype(np.float64)
        
        profile, buckets = self._calculate_volume_profile(
            prices,
            volumes,
            self.price_bucket_count
        )
        
        nodes = self._identify_volume_nodes(
            profile,
            buckets,
            self.volume_threshold,
            self.node_distance
        )
        
        print(f"\nSample window statistics:")
        print(f"Price range: {prices.min():.2f} - {prices.max():.2f}")
        print(f"Volume range: {volumes.min():.2f} - {volumes.max():.2f}")
        print(f"Number of volume nodes found: {len(nodes)}")
        
        if len(nodes) > 0:
            print("\nVolume nodes found:")
            for node in nodes:
                print(f"Price level: {node[0]:.2f}, Relative volume: {node[1]:.4f}")
        else:
            print("No volume nodes found - this may indicate an issue with the parameters")
    
def progress_bar(current, total):
    """Prints a simple progress bar."""
    percent_complete = int(100 * current / total)
    if percent_complete % 1 == 0: # Only update for whole percentage changes
      bar_length = 20
      filled_length = int(bar_length * current / total)
      bar = '=' * filled_length + '-' * (bar_length - filled_length)
      print(f'\r[{bar}] {percent_complete}%', end='', flush=True)

def optimize_parameters(strategy, param_grid, initial_capital, fee_rate, period):
    results = []
    print("Fetching data...")
    data = strategy.fetch_data(period)  # Fetch data here
    print("Data fetched successfully.")

    from itertools import product
    param_combinations = [dict(zip(param_grid.keys(), values)) 
                         for values in product(*param_grid.values())]
    
    total_combinations = len(param_combinations)
    print(f"Testing {total_combinations} parameter combinations...")
    
    for i, params in enumerate(param_combinations, 1):
        progress_bar(i, total_combinations)
        print(f"\nTesting combination {i}/{total_combinations}: {params}")
        
        # Update strategy parameters
        for param, value in params.items():
            setattr(strategy, param, value)
        
        # Run backtest
        try:
            metrics, _, _ = strategy.backtest(data, initial_capital, fee_rate)  # Pass data to backtest
            print(f"Metrics for combination {i}: {metrics}")
            results.append({**params, **metrics})
        except Exception as e:
            print(f"Error testing combination {params}: {str(e)}")
            continue
    
    return pd.DataFrame(results)

if __name__ == "__main__":
    # Test with a single parameter set
    strategy = VolumeProfileStrategy(
        symbol='SOL/USDT',
        exchange_id='binance',
        price_bucket_count=10,
        volume_threshold=0.001,
        node_distance=0.0005,
        entry_confirmation_bars=1,
        exit_confirmation_bars=2,
        risk_per_trade=0.02
    )

        # Fetch data
    data = strategy.fetch_data('90d')

    # Run backtest
    metrics, portfolio, processed_data = strategy.backtest(data, initial_capital=10000, fee_rate=0.001)
    print("Backtest Metrics:", metrics)

    param_grid = {
        'price_bucket_count': [50],
        'volume_threshold': np.linspace(0.01, 0.1, 2).tolist(),  # Much lower thresholds
        'node_distance': np.linspace(0.003, 0.015, 2).tolist(),  # More granular distances
        'entry_confirmation_bars': [3],
        'exit_confirmation_bars': [2],
        'risk_per_trade': [0.02]
    }
        
    results = optimize_parameters(
        strategy=strategy,
        param_grid=param_grid,
        initial_capital=10000,
        fee_rate=0.001,
        period='90d'
    )
    
    print("\nTop 5 parameter combinations by Sharpe ratio:")
    print(results.sort_values('Sharpe Ratio', ascending=False).head())
        # Create the heatmap with corrected scaling
    heatmap_data = results.pivot_table(values='Sharpe Ratio', index='volume_threshold', columns='node_distance')
    plt.figure(figsize=(10, 8))
    sns.heatmap(heatmap_data, annot=False, cmap='viridis', fmt=".2f", vmin=heatmap_data.min().min(), vmax=heatmap_data.max().max()) # Set vmin and vmax
    plt.title('Sharpe Ratio Heatmap')
    plt.ylabel('volume_threshold')
    plt.xlabel('node_distance')
    plt.show()

    

    print("\nTop 5 parameter combinations by Sharpe ratio:")
    print(results.sort_values('Sharpe Ratio', ascending=False).head())


And here is the result I am getting:

Starting backtest...
Data shape: (129601, 6)
Data columns: Index(['open', 'high', 'low', 'close', 'volume', 'volume_nodes'], dtype='object')
First few rows of close prices:
timestamp
2024-08-16 03:13:00    142.537
2024-08-16 03:14:00    142.673
2024-08-16 03:15:00    142.745
2024-08-16 03:16:00    142.602
2024-08-16 03:17:00    142.740
Name: close, dtype: float64

Debug Statistics:
total_rows: 129601
rows_with_nodes: 128161
potential_long_entries: 0
potential_short_entries: 0
confirmed_long_entries: 4922
confirmed_short_entries: 4925
exits: 4747

Checking for NaN/Inf values:
NaN in close prices: 0
Inf in close prices: 0

Pre-vectorbt checks:
Size calculated: 70.1572223352533
Initial close price: 142.537

Trade Summary:
Number of trades: 943
First trade details:

Error in vectorbt processing: 0
Traceback (most recent call last):
  File "C:\Users\Fedol\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pandas\core\indexes\base.py", line 3653, in get_loc
    return self._engine.get_loc(casted_key)
  File "pandas\_libs\index.pyx", line 147, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\index.pyx", line 176, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\hashtable_class_helper.pxi", line 7080, in pandas._libs.hashtable.PyObjectHashTable.get_item
  File "pandas\_libs\hashtable_class_helper.pxi", line 7088, in pandas._libs.hashtable.PyObjectHashTable.get_item
KeyError: 0

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\Fedol\OneDrive\Рабочий стол\KaboomiArbitrage\GigaBoomi\GigaBoomi_VP_CUDA.py", line 510, in <module>
    metrics, portfolio, processed_data = strategy.backtest(data, initial_capital=10000, fee_rate=0.001)
  File "C:\Users\Fedol\OneDrive\Рабочий стол\KaboomiArbitrage\GigaBoomi\GigaBoomi_VP_CUDA.py", line 397, in backtest
    print(trades[0])
  File "C:\Users\Fedol\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pandas\core\frame.py", line 3761, in __getitem__
    indexer = self.columns.get_loc(key)
  File "C:\Users\Fedol\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pandas\core\indexes\base.py", line 3655, in get_loc
    raise KeyError(key) from err
KeyError: 0