I think there should be another parameter for that in param_grid. After all, too low values will result in lower winrate while too high values will result in missing some of the profitable trades.import numpy as np
import pandas as pd
import vectorbt as vbt
import ccxt
from datetime import datetime, timedelta
from numba import njit, prange
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
from itertools import product

warnings.filterwarnings('ignore')


@njit
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


@njit
def identify_volume_nodes(volume_profile, price_buckets, threshold, min_distance):
    """Identify high volume nodes using Numba optimization with absolute distance."""
    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 is_peak:
            if (normalized_volume[i] >= normalized_volume[i - 1] * 0.95 and
                normalized_volume[i] >= normalized_volume[i + 1] * 0.95):
                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 absolute minimum distance
    filtered_nodes = np.zeros((node_count, 2), dtype=np.float64)
    filtered_count = 0

    filtered_nodes[filtered_count] = nodes[0]
    filtered_count += 1

    for i in range(1, node_count):
        absolute_distance = np.abs(nodes[i, 0] - filtered_nodes[filtered_count - 1, 0])
        if absolute_distance > min_distance:
            filtered_nodes[filtered_count] = nodes[i]
            filtered_count += 1

    return filtered_nodes[:filtered_count]


@njit(parallel=True)
def process_volume_nodes_numba(close_prices, volumes, window_size, price_bucket_count, 
                               volume_threshold, node_distance, volume_nodes_prices, 
                               volume_nodes_volumes, num_volume_nodes):
    n = len(close_prices)

    for i in prange(window_size, n):
        # Define the window for volume profile
        window_close = close_prices[i - window_size:i]
        window_volume = volumes[i - window_size:i]

        # Calculate volume profile
        profile, buckets = calculate_volume_profile(window_close, window_volume, price_bucket_count)

        # Identify volume nodes
        nodes = identify_volume_nodes(profile, buckets, volume_threshold, node_distance)

        num_nodes = nodes.shape[0]
        if num_nodes > price_bucket_count:
            num_nodes = price_bucket_count  # Cap to maximum nodes

        num_volume_nodes[i] = num_nodes

        for j in range(num_nodes):
            volume_nodes_prices[i, j] = nodes[j, 0]
            volume_nodes_volumes[i, j] = nodes[j, 1]


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.0001,  # Threshold for identifying significant volume nodes
                 node_distance=1.0,         # Absolute minimum distance between volume nodes
                 entry_confirmation_bars=3, # Bars needed for entry confirmation
                 exit_confirmation_bars=3,  # Bars needed for exit confirmation
                 risk_per_trade=0.02,
                 interval='5m',
                 rsi_period=14,             # RSI period
                 rsi_overbought=70,         # RSI threshold for exiting long positions
                 rsi_oversold=30,           # RSI threshold for exiting short positions
                 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  # Now using absolute 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.rsi_period = rsi_period
        self.rsi_overbought = rsi_overbought
        self.rsi_oversold = rsi_oversold
        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 by running them with dummy data
        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

    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 = calculate_volume_profile(dummy_prices, dummy_volumes, 5)

        # Test volume nodes identification
        _ = identify_volume_nodes(profile, buckets, 0.2, self.node_distance)

    def fetch_raw_data(self, period):
        """Fetch raw OHLCV data and cache it."""
        cache_filename = f"raw_ohlcv_{self.symbol}_{self.interval}_{period}.pkl"

        if os.path.exists(cache_filename):
            print(f"Loading raw data from cache: {cache_filename}")
            df = pd.read_pickle(cache_filename)
            print(f"Total fetched OHLCV records: {len(df)}")
            return df

        exchange = self._get_exchange()
        end_time = int(time.time() * 1000)
        # Assuming period is in days for 'd' suffix, adjust as needed
        if period.endswith('d'):
            days = int(period[:-1])
            start_time = end_time - (days * 24 * 60 * 60 * 1000)
        elif period.endswith('h'):
            hours = int(period[:-1])
            start_time = end_time - (hours * 60 * 60 * 1000)
        else:
            raise NotImplementedError("Only daily ('d') and hourly ('h') periods are implemented.")

        # 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)

        print(f"Saving raw data to cache: {cache_filename}")
        df.to_pickle(cache_filename)

        return df

    def process_volume_nodes(self, raw_data):
        """Calculate volume nodes based on current strategy parameters."""
        df = raw_data.copy()

        # Convert relevant columns to NumPy arrays
        close_prices = df['close'].values.astype(np.float64)
        volumes = df['volume'].values.astype(np.float64)

        # Calculate window size based on profile_period and interval
        if self.profile_period.endswith('d') and self.interval.endswith('m'):
            days = int(self.profile_period[:-1])
            minutes = int(self.interval[:-1])
            window_size = days * 24 * 60 // minutes
        else:
            raise NotImplementedError("Only daily profile_period and minute intervals are implemented.")

        n = len(close_prices)

        # Initialize fixed-size arrays for volume nodes
        max_nodes = self.price_bucket_count  # Assuming maximum nodes equal to bucket count
        volume_nodes_prices = np.zeros((n, max_nodes), dtype=np.float64)
        volume_nodes_volumes = np.zeros((n, max_nodes), dtype=np.float64)
        num_volume_nodes = np.zeros(n, dtype=np.int32)

        # Call the Numba-jitted function
        process_volume_nodes_numba(
            close_prices,
            volumes,
            window_size,
            self.price_bucket_count,
            self.volume_threshold,
            self.node_distance,  # Now absolute distance
            volume_nodes_prices,
            volume_nodes_volumes,
            num_volume_nodes
        )

        # Assign results back to the DataFrame
        df['volume_nodes_prices'] = list(volume_nodes_prices)
        df['volume_nodes_volumes'] = list(volume_nodes_volumes)
        df['num_volume_nodes'] = num_volume_nodes

        # Calculate RSI
        rsi = vbt.RSI.run(df['close'], window=self.rsi_period).rsi
        df['RSI'] = rsi

        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_nodes_prices' in df.columns and 'volume_nodes_volumes' in df.columns:
            latest_prices = df['volume_nodes_prices'].iloc[-1]
            latest_volumes = df['volume_nodes_volumes'].iloc[-1]
            valid = latest_volumes > 0
            ax2.barh(latest_prices[valid], latest_volumes[valid], alpha=0.3)
            ax2.set_title("Volume Profile")
            ax2.set_xlabel("Volume")
            ax2.set_ylabel("Price")

        plt.tight_layout()
        plt.show()

    @staticmethod
    @njit(parallel=True)
    def generate_signals_numba(
        close_prices,
        volume_nodes_prices,
        num_volume_nodes,
        entry_conf_bars,
        exit_conf_bars,
        node_distance
    ):
        n = len(close_prices)
        long_entries = np.zeros(n, dtype=np.bool_)
        short_entries = np.zeros(n, dtype=np.bool_)

        for i in prange(n):
            if num_volume_nodes[i] == 0:
                continue  # No volume nodes to process

            current_price = close_prices[i]
            nodes = volume_nodes_prices[i, :num_volume_nodes[i]]

            # Initialize nearest nodes
            nearest_above = np.inf
            nearest_below = -np.inf

            # Find nearest above and below nodes
            for j in range(num_volume_nodes[i]):
                price = nodes[j]
                if price > current_price and price < nearest_above:
                    nearest_above = price
                elif price < current_price and price > nearest_below:
                    nearest_below = price

            # Flags to determine if entries are triggered
            long_signal = False
            short_signal = False

            # Long Entry Condition
            if nearest_below != -np.inf:
                price_diff = np.abs(current_price - nearest_below)  # Absolute distance
                if price_diff < node_distance:
                    # Check confirmation bars
                    if i >= entry_conf_bars:
                        positive_bars = 0
                        for k in range(i - entry_conf_bars, i):
                            if close_prices[k] > close_prices[k - 1]:
                                positive_bars += 1
                        if positive_bars >= entry_conf_bars * 0.6:  # At least 60% positive bars
                            long_signal = True

            # Short Entry Condition
            if nearest_above != np.inf:
                price_diff = np.abs(current_price - nearest_above)  # Absolute distance
                if price_diff < node_distance:
                    # Check confirmation bars
                    if i >= entry_conf_bars:
                        negative_bars = 0
                        for k in range(i - entry_conf_bars, i):
                            if close_prices[k] < close_prices[k - 1]:
                                negative_bars += 1
                        if negative_bars >= entry_conf_bars * 0.6:  # At least 60% negative bars
                            short_signal = True

            # Assign signals with priority to long entries
            if long_signal and not short_signal:
                long_entries[i] = True
            elif short_signal and not long_signal:
                short_entries[i] = True
            elif long_signal and short_signal:
                # If both signals are true, decide based on priority
                # Here, priority is given to long entries
                long_entries[i] = True

        # Placeholder for exits (handled separately)
        exits = np.zeros(n, dtype=np.bool_)

        return long_entries, short_entries, exits

    def generate_signals(self, data):
        close_prices = data['close'].values.astype(np.float64)
        volume_nodes_prices = np.vstack(data['volume_nodes_prices'].values).astype(np.float64)
        num_volume_nodes = data['num_volume_nodes'].values.astype(np.int32)
        rsi = data['RSI'].values.astype(np.float64)

        long_entries, short_entries, _ = self.generate_signals_numba(
            close_prices,
            volume_nodes_prices,
            num_volume_nodes,
            self.entry_confirmation_bars,
            self.exit_confirmation_bars,
            self.node_distance
        )

        # Convert to Pandas Series for easier manipulation
        long_entries = pd.Series(long_entries, index=data.index)
        short_entries = pd.Series(short_entries, index=data.index)

        # Initialize exit signals based on RSI
        rsi_series = pd.Series(rsi, index=data.index)
        exits_rsi_long = rsi_series > self.rsi_overbought
        exits_rsi_short = rsi_series < self.rsi_oversold

        # Initialize exit signals based on Volume Profiles
        exits_volume_long = pd.Series(False, index=data.index)
        exits_volume_short = pd.Series(False, index=data.index)

        # Iterate through the DataFrame to set volume-based exit signals with confirmation
        for i in range(1, len(data)):
            # For Long Positions: Exit if approaching an upper volume node
            if long_entries.iloc[i - 1]:
                # Identify upper volume nodes
                current_price = close_prices[i]
                nodes = volume_nodes_prices[i, :num_volume_nodes[i]]
                nearest_above = nodes[nodes > current_price].min() if np.any(nodes > current_price) else np.inf
                if nearest_above != np.inf and (nearest_above - current_price) < self.node_distance:
                    # Check exit confirmation bars
                    if i >= self.exit_confirmation_bars:
                        bearish_bars = 0
                        for k in range(i - self.exit_confirmation_bars, i):
                            if close_prices[k] < close_prices[k - 1]:
                                bearish_bars += 1
                        if bearish_bars >= self.exit_confirmation_bars * 0.6:  # At least 60% bearish bars
                            exits_volume_long.iloc[i] = True

            # For Short Positions: Exit if approaching a lower volume node
            if short_entries.iloc[i - 1]:
                # Identify lower volume nodes
                current_price = close_prices[i]
                nodes = volume_nodes_prices[i, :num_volume_nodes[i]]
                nearest_below = nodes[nodes < current_price].max() if np.any(nodes < current_price) else -np.inf
                if nearest_below != -np.inf and (current_price - nearest_below) < self.node_distance:
                    # Check exit confirmation bars
                    if i >= self.exit_confirmation_bars:
                        bullish_bars = 0
                        for k in range(i - self.exit_confirmation_bars, i):
                            if close_prices[k] > close_prices[k - 1]:
                                bullish_bars += 1
                        if bullish_bars >= self.exit_confirmation_bars * 0.6:  # At least 60% bullish bars
                            exits_volume_short.iloc[i] = True

        # Combined Exit Conditions: Either RSI-based or Volume Profile-Based exits
        exits_long = exits_rsi_long | exits_volume_long
        exits_short = exits_rsi_short | exits_volume_short
        exits = exits_long | exits_short

        # Initialize final signals
        final_long_entries = pd.Series(False, index=data.index)
        final_short_entries = pd.Series(False, index=data.index)

        # Iterate through the signals to ensure exclusivity
        position = None  # None, 'long', or 'short'

        for idx in data.index:
            if exits.loc[idx]:
                position = None

            if position is None:
                if long_entries.loc[idx]:
                    final_long_entries.loc[idx] = True
                    position = 'long'
                elif short_entries.loc[idx]:
                    final_short_entries.loc[idx] = True
                    position = 'short'
            elif position == 'long':
                if short_entries.loc[idx]:
                    final_short_entries.loc[idx] = True
                    final_long_entries.loc[idx] = False
                    position = 'short'
            elif position == 'short':
                if long_entries.loc[idx]:
                    final_long_entries.loc[idx] = True
                    final_short_entries.loc[idx] = False
                    position = 'long'

        # Assign the final signals
        signals = pd.DataFrame({
            'long_entries': final_long_entries,
            'short_entries': final_short_entries,
            'exits': exits
        }, index=data.index)

        return signals

    def backtest(self, data, initial_capital, fee_rate):
        """Run backtest using vectorbt with exclusive long/short positions."""
        # Define the numeric columns used in backtesting
        numeric_cols = ['close', 'volume']  # Add other numeric columns if necessary

        # Check for NaN values in numeric columns only
        if data[numeric_cols].isna().any().any():
            print("Data contains NaN values in numeric columns. Please clean the data before backtesting.")
            raise ValueError("Data contains NaN values.")

        # Check for infinite values in numeric columns only
        if np.isinf(data[numeric_cols].values).any():
            print("Data contains infinite values in numeric columns. Please clean the data before backtesting.")
            raise ValueError("Data contains infinite values.")

        signals = self.generate_signals(data)

        # Calculate position sizes based on initial capital and risk per trade
        size = initial_capital / data['close'].iloc[0]

        try:
            portfolio = vbt.Portfolio.from_signals(
                close=data['close'],
                entries=signals['long_entries'],
                exits=signals['exits'],
                short_entries=signals['short_entries'],
                short_exits=signals['exits'],  # Ensure that short positions are also exited
                sl_stop=None,  # Stop-loss is handled via RSI and volume profile exits
                tp_stop=None,  # Take-profit is handled via RSI and volume profile exits
                init_cash=initial_capital,
                fees=fee_rate,
                size=size,
                freq=self.interval
            )

            trades = portfolio.trades.records

            # Calculate Sharpe Ratio with risk-free rate 0
            sharpe = portfolio.sharpe_ratio()

            metrics = {
                'Total Return (%)': portfolio.total_return() * 100,
                'Sharpe Ratio': sharpe if not np.isnan(sharpe) else 0,
                '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."""
        # Optional implementation for verification
        pass


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_combinations, initial_capital, fee_rate):
    """Optimize strategy parameters over given combinations."""
    results = []
    print("Fetching raw data...")
    raw_data = strategy.fetch_raw_data(period='365d')  # Fetch raw OHLCV data here
    print("Raw data fetched successfully.")

    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)

        # Update strategy parameters
        for param, value in params.items():
            setattr(strategy, param, value)
            print(f"Set {param} to {value}")  # Logging parameter updates

        # Recalculate volume nodes with updated parameters
        data = strategy.process_volume_nodes(raw_data)
        strategy.verify_volume_nodes(data)  # Optional: for debugging

        # Run backtest
        try:
            metrics, _, _ = strategy.backtest(data, initial_capital, fee_rate)
            results.append({**params, **metrics})
        except Exception as e:
            results.append({**params, 'Error': str(e)})

    results_df = pd.DataFrame(results)
    print("\nAvailable columns in results:")
    print(results_df.columns.tolist())  # Print list of columns

    if results_df.empty:
        print("No successful backtests. `results` DataFrame is empty.")
    else:
        print("Backtests completed successfully.")

    return results_df


def backtest_with_single_params(strategy, params, initial_capital, fee_rate, period='365d'):
    """Run backtest with a single set of parameters."""
    print("Running backtest with a single parameter set...")

    # Update strategy parameters
    for param, value in params.items():
        setattr(strategy, param, value)
        print(f"Set {param} to {value}")  # Logging parameter updates

    # Fetch raw data
    raw_data = strategy.fetch_raw_data(period=period)

    # Process volume nodes
    data = strategy.process_volume_nodes(raw_data)

    # Run backtest
    metrics, portfolio, processed_data = strategy.backtest(data, initial_capital, fee_rate)

    # Display metrics
    print("\nBacktest Metrics:")
    for key, value in metrics.items():
        print(f"{key}: {value}")

    # Plot portfolio equity curve
    portfolio.plot().show()

    # Plot signals on price
    signals = strategy.generate_signals(processed_data)
    fig, ax = plt.subplots(figsize=(15, 6))
    ax.plot(processed_data.index, processed_data['close'], label='Close Price')
    ax.plot(processed_data.index[signals['long_entries']], processed_data['close'][signals['long_entries']], '^', markersize=10, color='g', label='Long Entry')
    ax.plot(processed_data.index[signals['short_entries']], processed_data['close'][signals['short_entries']], 'v', markersize=10, color='r', label='Short Entry')
    ax.plot(processed_data.index[signals['exits']], processed_data['close'][signals['exits']], 'x', markersize=10, color='k', label='Exit')
    ax.set_title(f"{strategy.symbol} Price with Signals")
    ax.set_xlabel("Time")
    ax.set_ylabel("Price")
    ax.legend()
    plt.show()

    # Plot volume profile for the last timeframe
    strategy.plot_data(processed_data)


if __name__ == "__main__":
    # Initialize strategy
    strategy = VolumeProfileStrategy(
        symbol='SOL/USDT',
        exchange_id='binance'
    )

    param_grid = {
        'price_bucket_count': np.linspace(45, 230, 10).astype(int).tolist(),
        'volume_threshold': [0.001],
        'node_distance': np.logspace(-3, 1, 10).tolist(),
        'entry_confirmation_bars': [3],
        'exit_confirmation_bars': [1],
        'rsi_period': [14],
        'rsi_overbought': [70],
        'rsi_oversold': [30],
        'risk_per_trade': [0.02]
    }
        
    # Generate all parameter combinations
    param_combinations = [dict(zip(param_grid.keys(), values)) 
                         for values in product(*param_grid.values())]
    
    # Determine if optimizing multiple parameter sets or running a single backtest
    if len(param_combinations) > 1:
        # Optimize parameters
        results = optimize_parameters(
            strategy=strategy,
            param_combinations=param_combinations,
            initial_capital=10000,
            fee_rate=0.001
        )
    
        optimize_for = 'Sharpe Ratio'
        
        print(f"\nTop 50 parameter combinations by {optimize_for}:")
        if optimize_for in results.columns:
            pd.set_option('display.max_columns', None)
            sorted_results = results.sort_values(optimize_for, ascending=False).head(50)
            sorted_results.to_csv('sorted_results.csv', index=False)  # Save to CSV without index
            print(sorted_results) 
        else:
            print(f"{optimize_for} not found in results.")
    
        
        optimizing_a = 'price_bucket_count'
        optimizing_b = 'node_distance'
        
        # Create the heatmap with corrected scaling
        if optimize_for in results.columns:
            heatmap_data = results.pivot_table(values=optimize_for, index=optimizing_a, columns=optimizing_b)
            heatmap_data_clean = heatmap_data.replace([np.inf, -np.inf], np.nan)
            finite_max = heatmap_data_clean.max().max()
            finite_min = heatmap_data_clean.min().min()
            plt.figure(figsize=(10, 8))
            sns.heatmap(heatmap_data, annot=False, cmap='viridis', fmt=".2f", 
                        vmin=finite_min, vmax=finite_max)
            plt.title(f'{optimize_for} Heatmap')
            plt.ylabel(optimizing_a)
            plt.xlabel(optimizing_b)
            plt.show()
        else:
            print(f"No {optimize_for} data available for heatmap.")
    else:
        # Extract the single parameter set
        single_params = param_combinations[0]
        
        # Run backtest with the single parameter set
        backtest_with_single_params(
            strategy=strategy,
            params=single_params,
            initial_capital=10000,
            fee_rate=0.001,
            period='365d'
        )