In the first article of this series, we explored the theoretical underpinnings of these concepts, setting a robust foundation for practical application.

If you didn't read it, I recommend you stop reading this article and read the article mentioned below first, to understand the basic concepts behind this backtest.

Now, it's time to shift from theory to practice!!

In this article, we turn our attention to the QQQ (Invesco QQQ Trust Series I). This practical article aims to apply the principles and strategies previously shown to real-world data, offering a hands-on perspective that will enrich your understanding and skills. We'll employ our unique combination of Bollinger Bands and genetic algorithms to analyze and trade the QQQ.

Whether you're a seasoned trader or a curious newcomer, this practical exploration of the QQQ will offer valuable insights and experience. So, grab your Python toolkit, and let's explore where theory meets practice in the quest for financial mastery.

Let's get started!

TL;DR: The code and explanation in this article remain pretty much the same as the rest in the series, just with a twist: we're using a different ticker. So, it's like meeting an old friend in a new city! Below, you'll find the results of our backtest with this new ticker, as well as the link to the source code. Think of it as a remix of a favorite song — same rhythm, new beat! 🤪

Marrying Genetic Algorithm with Bollinger Bands

Now, let's see how the genetic algorithm improves our Bollinger Bands strategy. We will use Python to bring this idea to life, and here's a step-by-step breakdown of the code.

Setting Up

Importing Libraries

  • NumPy (numpy): This is a fundamental package for scientific computing in Python. It provides support for arrays (like mathematical arrays), along with a collection of mathematical functions to operate on these arrays.
  • Pandas (pandas): A powerful data manipulation and analysis tool. In our case, it's essential for handling financial data, like stock prices, in a structured and efficient way (through DataFrame objects).
  • Pandas Technical Analysis (pandas_ta): An easy-to-use library that extends Pandas with technical analysis capabilities. It helps us calculate Bollinger Bands directly from stock price data.
  • PyGAD (pygad): This is a library for building the genetic algorithm. It simplifies creating and running the genetic algorithm, which we use to optimize our trading strategy.
  • TQDM (tqdm): A library to show a progress bar while we train our model.
import numpy as np
import pandas as pd
import pandas_ta as ta
import pygad

from tqdm import tqdm

User Configuration

Here, we're defining several key variables that set the parameters for our trading strategy and genetic algorithm.

  • Ticker (TICKER): The symbol we're analyzing.
  • Cash (CASH): The cash amount available for each operation.
  • Bollinger Bands Parameters (BB_SMA, BB_STD): BB_SMA sets the Simple Moving Average period for the middle band, and BB_STD defines the standard deviation, which determines the spacing of the upper and lower bands.
  • Maximum Bandwidth (BB_MAX_BANDWIDTH): This is the maximum volatility (measured by the Bollinger Bandwidth) we're allowing in our strategy. All the values over this maximum will be clipped toBB_MAX_BANDWIDTH.
  • Testing and Reward Parameters: These include DAYS_FOR_TESTING, WINDOW_REWARD, and WINDOW_MIN_OPERATIONS, which helps segment our data for testing and define how we measure the success of our trades.
  • Generations and Solutions: GENERATIONS are how many iterations the genetic algorithm is going to do. SOLUTIONS are how many solutions our genetic algorithm is going to generate for each generation.
TICKER = 'QQQ'
CASH = 10_000

BB_SMA = 20
BB_STD = 2.0
BB_MAX_BANDWIDTH = 5

DAYS_FOR_TESTING = 365 * 1.5
WINDOW_REWARD = '3M'
WINDOW_MIN_OPERATIONS = 21 * 3

GENERATIONS = 50
SOLUTIONS = 20

Constants and Data Preparation

We're setting up some constants and preparing our data formats. This includes defining the file name for data retrieval, adjusting our Bollinger Bands settings for use in the calculations, and setting options for NumPy and Pandas to ensure smooth data handling.

### Constants ###
FILENAME = f'../data/{TICKER.lower()}.csv.gz'
TIMEFRAMES = ['5T','15T','1H']

### Data format & preparation ###
BB_SMA = int(BB_SMA)
BB_STD = round(BB_STD, 2)
BB_UPPER = f'BBU_{int(BB_SMA)}_{BB_STD}'
BB_LOWER = f'BBL_{int(BB_SMA)}_{BB_STD}'
BB_VOLATILITY = f'BBB_{int(BB_SMA)}_{BB_STD}'

DAYS_FOR_TESTING = int(DAYS_FOR_TESTING)
WINDOW_MIN_OPERATIONS = int(WINDOW_MIN_OPERATIONS)

### Output preparation ###
np.set_printoptions(suppress=True)
pd.options.mode.chained_assignment = None

This step is crucial as it lays the foundation for the entire trading strategy. It ensures we have the right tools (libraries) and settings (variables) to effectively implement and test our Bollinger Bands strategy optimized by genetic algorithm.

Fetching and Preparing Data

The get_data function is key. It fetches historical data, calculates the Bollinger Bands, and sets the high and low limits for our strategy. It also split the data into training and testing sets for our model.

How it works

Read Data:

  • We start by reading the historical data from a file using pandas.read_csv(). The data includes only dates and closing prices.
  • The date column is converted to datetime format for easier manipulation.
df = pd.read_csv(FILENAME) 
df['date'] = pd.to_datetime(df['date'])

Resample Data:

  • The data is resampled according to the specified timeframe (like ƋT' for 5 minutes). This means we aggregate the data to represent the chosen intervals.
  • We use .resample() and .agg() to create a new DataFrame with these timeframes, focusing on the closing price.
df = df.set_index('date').resample(timeframe).agg({'close':'last'}).dropna().reset_index()

Calculate Bollinger Bands:

  • We use pandas_ta to calculate the Bollinger Bands.
  • The Bollinger Bands are added to our DataFrame, giving us the upper and lower bands based on our predefined SMA and standard deviation.
  • We remove the fields without data
df.ta.bbands(close=df['close'], length=BB_SMA, std=BB_STD, append=True)
df = df.dropna()

Define Trading Limits:

  • High and low limits are set for our trading strategy, calculated as 25% and 75% of the distance between the upper and lower bands.
  • We also calculate the close percentage, which indicates where the closing price sits between our high (1) and low (0) limits.
df['high_limit'] = df[BB_UPPER] + (df[BB_UPPER] - df[BB_LOWER]) / 2
df['low_limit'] = df[BB_LOWER] - (df[BB_UPPER] - df[BB_LOWER]) / 2
df['close_percentage'] = np.clip((df['close'] - df['low_limit']) / (df['high_limit'] - df['low_limit']), 0, 1)

Handle Volatility:

  • Volatility is represented by the Bollinger Bandwidth. We normalize it to a scale of 0 to 1 to understand how volatile the market is, relative to our maximum bandwidth setting.
df['volatility'] = np.clip(df[BB_VOLATILITY] / (100 / BB_MAX_BANDWIDTH), 0, 1)

Data Cleaning and Splitting:

  • We remove all the Bollinger Bands fields to free resources.
  • The dataset is split into training and testing sets based on the date. We use the DAYS_FOR_TESTING variable to determine the split.
df = df.loc[:,~df.columns.str.startswith('BB')]

train = df[df['date'].dt.date <= (df['date'].dt.date.max() - pd.Timedelta(DAYS_FOR_TESTING, 'D'))]
test = df[df['date'] > train['date'].max()]

If you enjoy reading my articles, please don't forget to share this article with your friends and hit the follow button — Diego Degese

Defining the Trading Signals

The get_result function is where the magic of decision-making in our trading strategy occurs.

How it works

DataFrame's copy:

  • We start by creating a copy of the received DataFrame. This is a good practice to avoid modifying the original data unintentionally.
df = df.copy().reset_index(drop=True)

Generating Buy and Sell Signals:

  • We determine a buy signal based on two conditions: 1. The market volatility is greater than our minimum threshold (min_volatility). This means we're looking for times when the market is sufficiently active. 2. The ticker's closing price as a percentage of our Bollinger Band range (close_percentage) is less than the maximum percentage we're willing to buy at (max_buy_perc). This implies we're buying when the price is relatively low within our Bollinger Band framework.
  • Similarly, a sell signal is generated when the close_percentage exceeds our min_sell_perc, indicating that the price is now higher and possibly a good time to sell.
# Buy signal
df['signal'] = np.where((df['volatility'] > min_volatility) & (df['close_percentage'] < max_buy_perc), 1, 0)

# Sell signal
df['signal'] = np.where((df['close_percentage'] > min_sell_perc), -1, df['signal'])

Cleaning Up Signals:

  • Filtering: The function then filters out all rows where no buy or sell signal was generated.
  • Removing Consecutive Duplicate Signals: It also removes consecutive duplicate signals (like two buys in a row) because we can't buy again if we haven't sold yet, and vice versa.
  • Handling Edge Cases: It also ensures that the first action isn't a sell and the last isn't a buy, as these are incomplete transactions.
result = df[df['signal'] != 0]
result = result[result['signal'] != result['signal'].shift()]
if (len(result) > 0) and (result.iat[0, -1] == -1): result = result.iloc[1:]
if (len(result) > 0) and (result.iat[-1, -1] == 1): result = result.iloc[:-1]

Calculating Profit and Loss (PnL):

  • PnL Calculation: For each sell operation (where signal == -1), it calculates the profit or loss. This is done by comparing the selling price with the previous buying price and considering the amount of cash available for trading (CASH).
  • Wins and Losses: The function also counts the number of wins (profitable transactions) and losses (unprofitable transactions).
result['pnl'] = np.where(result['signal'] == -1, (result['close'] - result['close'].shift()) * (CASH // result['close'].shift()), 0)
result['wins'] = np.where(result['pnl'] > 0, 1, 0)
result['losses'] = np.where(result['pnl'] < 0, 1, 0)

Removing 'buy rows' and returning the Result:

  • We remove all the rows that are 'buy rows' because they don't have any important information at this point.
  • Finally, we return a cleaned DataFrame with details about each transaction, the profit or loss from each transaction, and the number of wins and losses.
result = result[result['signal'] == -1]

return result.drop('signal', axis=1)

Calculating Rewards

The calculate_reward function evaluates the performance of a given set of trading parameters. In simpler terms, it's like a scoring system that tells us how well our strategy worked.

How it works

Creating a Reward Window and Aggregating Data:

  • The function first sets up a time window based on WINDOW_REWARD (which is '3M', or three months, in our code). This window is used to analyze the performance of the trading strategy over different periods.
  • It then groups our trade data into these windows and calculates sums for certain columns like 'wins', 'losses', and 'pnl'. This aggregation helps in understanding the strategy's performance over each time period.
df_reward = df.set_index('date').resample(WINDOW_REWARD).agg(
        {'close':'last','wins':'sum','losses':'sum','pnl':'sum'}).reset_index()

Calculating Averages, Determining the Reward, and Handling Inactive Periods:

  • The function computes the average values of wins, losses, and pnl for these periods. This step is critical as it smoothens short-term fluctuations and provides a clearer picture of the strategy's effectiveness over time.
  • The reward is calculated as the average pnl (profit and loss). However, there's a catch: the reward is only considered valid if the number of operations (wins + losses) is greater than a minimum threshold set by WINDOW_MIN_OPERATIONS. This ensures that the reward is based on a sufficiently active trading strategy, rather than a few lucky trades.
  • If the strategy doesn't meet the minimum operation threshold, the reward is adjusted negatively. This is done to penalize strategies that are too conservative or inactive.
wins = df_reward['wins'].mean() if len(df_reward) > 0 else 0
losses = df_reward['losses'].mean() if len(df_reward) > 0 else 0
reward = df_reward['pnl'].mean() if (WINDOW_MIN_OPERATIONS < (wins + losses)) else -WINDOW_MIN_OPERATIONS + (wins + losses)

Are you still there? The best part is about to come, but if you enjoy reading my articles, please don't forget to share this article with your friends and hit the follow button — Diego Degese

Optimizing with the Genetic Algorithm

In the get_best_solution function, we're setting up and running our genetic algorithm using the pygad library. The genetic algorithm will evolve and optimize the parameters of our trading strategy. Here are the key components:

  1. num_generations: The number of iterations the algorithm runs. Each generation represents an evolution of the solutions.
  2. num_parents_mating: This specifies how many solutions from the current generation will be selected for mating (i.e., producing the next generation).
  3. fitness_func: This is the heart of our genetic algorithm. The fitness function evaluates how good a solution is. In our case, it's defined as fitness_func, which calculates the reward of the trading strategy based on the given parameters.
  4. sol_per_pop: The number of solutions in each generation.
  5. num_genes: The number of parameters we're optimizing. Here, it's three, corresponding to the minimum volatility, maximum buy percentage, and minimum sell percentage.
  6. gene_space: Defines the range and step size for each parameter. This ensures our algorithm searches within reasonable bounds.
  7. parent_selection_type, crossover_type, mutation_type: These settings determine how parents are selected, how their 'genes' are combined to produce offspring, and how mutation (random changes) is applied to introduce variability.
  8. mutation_num_genes: Determines how many genes (parameters) are subject to mutation.
  9. keep_parents: Indicates whether to keep any of the parent solutions in the new generation.
  10. random_seed: Sets a seed for the random number generator, ensuring reproducibility.
def fitness_func(self, solution, sol_idx):

    # Get reward from train data
    result = get_result(train, solution[0], solution[1], solution[2])

    # Return the solution reward
    return calculate_reward(result)

def get_best_solution():

    with tqdm(total=GENERATIONS) as pbar:

        # Create genetic algorithm
        ga_instance = pygad.GA(num_generations=GENERATIONS,
                               num_parents_mating=5,
                               fitness_func=fitness_func,
                               sol_per_pop=SOLUTIONS,
                               num_genes=3,
                               gene_space=[
                                {'low': 0, 'high': 1, 'step': 0.0001},
                                {'low': 0, 'high': 1, 'step': 0.0001},
                                {'low': 0, 'high': 1, 'step': 0.0001}],
                               parent_selection_type='sss',
                               crossover_type='single_point',
                               mutation_type='random',
                               mutation_num_genes=1,
                               keep_parents=-1,
                               random_seed=42,
                               on_generation=lambda _: pbar.update(1),
                               )

        # Run the genetic algorithm
        ga_instance.run()

    # Return the best solution
    return ga_instance.best_solution()[0]

How it works

With these settings, the genetic algorithm proceeds as follows:

  • Initialization: The algorithm starts with a random population of solutions.
  • Fitness Evaluation: Each solution's fitness is assessed using fitness_func, which, in our context, involves running the trading strategy and calculating its reward.
  • Selection: Based on fitness, the best-performing solutions are selected for mating.
  • Crossover and Mutation: These solutions produce offspring that combine and slightly alter their 'genes'. This simulates natural evolutionary processes.
  • New Generation: The offspring form the new generation of solutions.
  • Repeat: This process repeats over a specified number of generations. With each iteration, the solutions should, in theory, become more optimized towards our goal.

Extracting the Best Solution

After all generations have been processed, the algorithm identifies the solution with the highest fitness score. This solution represents the optimized parameters for our trading strategy under the given conditions.

Show Results Function

This function is like the grand finale of a show, where it presents the performance of our trading strategy.

How it works

Calculating Key Metrics:

  • The function first calculates various performance metrics using the calculate_reward function. These include the total profit or loss (pnl), number of wins, number of losses, win rate, maximum profit in a single trade, largest drawdown, and average profit or loss per trade.
reward = calculate_reward(df)
pnl = df['pnl'].sum()
wins = df['wins'].sum() if len(df) > 0 else 0
losses = df['losses'].sum() if len(df) > 0 else 0
win_rate = (100 * (wins / (wins + losses)) if wins + losses > 0 else 0)
max_profit = df['pnl'].max()
min_drawdown = df['pnl'].min()
avg_pnl = df['pnl'].mean()

Displaying Summary Results:

  • The function then prints a summary of these metrics. This gives a quick snapshot of the strategy's performance, including the overall reward, profit/loss, win/loss ratio, maximum profit, maximum drawdown, and average profit/loss per trade.
print(f' SUMMARIZED RESULT - {name} '.center(60, '*'))
print(f'* Reward              : {reward:.2f}')
print(f'* Profit / Loss       : {pnl:.2f}')
print(f'* Wins / Losses       : {wins:.0f} / {losses:.0f} ({win_rate:.2f}%)')
print(f'* Max Profit          : {max_profit:.2f}')
print(f'* Max Drawdown        : {min_drawdown:.2f}')
print(f'* Profit / Loss (Avg) : {avg_pnl:.2f}')

Monthly Detailed Results (Optional):

  • If show_monthly is True, the function further breaks down the performance month by month. This is useful for seeing how the strategy performs across different market conditions over time. It aggregates the data on a monthly basis and calculates the total profit or loss, number of wins, and number of losses for each month. It also calculates the monthly win rate.
if show_monthly:
    print(f' MONTHLY DETAIL RESULT '.center(60, '*'))
    df_monthly = df.set_index('date').resample('1M').agg(
          {'wins':'sum','losses':'sum','pnl':'sum'}).reset_index()
    df_monthly = df_monthly[['date','pnl','wins','losses']]
    df_monthly['year_month'] = df_monthly['date'].dt.strftime('%Y-%m')
    df_monthly = df_monthly.drop('date', axis=1)
    df_monthly = df_monthly.groupby(['year_month']).sum()
    df_monthly['win_rate'] = round(100 * df_monthly['wins'] / (df_monthly['wins'] + df_monthly['losses']), 2)

print(df_monthly)

Main Function

The main function ties everything together. It's the conductor of our financial orchestra, orchestrating different pieces of the code to play in harmony.

How it works

Loop Through Timeframes

  • The function starts by iterating through different timeframes (like ƋT', ཋT', ƇH'). These represent different granularities of the historical market data (5 minutes, 15 minutes, 1 hour).
for timeframe in TIMEFRAMES:
    # ...

Fetching Data

  • For each timeframe, it calls get_data(ticker, timeframe) to fetch and prepare the data specific to that timeframe. This includes calculating the Bollinger Bands and setting the high and low limits for trading.
train, test = get_data(ticker, timeframe)

Finding the Best Solution with Genetic Algorithm

  • It then calls get_best_solution(), which runs the genetic algorithm. This process iteratively evolves the parameters (like minimum volatility, maximum buy percentage, and minimum sell percentage) to find the best set that maximizes the reward in the training dataset.
solution = get_best_solution()

Displaying Solution Parameters

  • After finding the best solution, it prints out these optimized parameters, giving us insights into what the genetic algorithm has determined to be the most effective strategy for the given timeframe.
print(f'Min Volatility   : {solution[0]:6.4f}')
print(f'Max Perc to Buy  : {solution[1]:6.4f}')
print(f'Min Perc to Sell : {solution[2]:6.4f}')

Evaluating Performance on Training Data

  • Next, the function evaluates how well this solution performs on the training dataset. It uses get_result to apply the trading strategy based on the optimized parameters and show_result to display the summarized performance, like profit/loss, wins/losses, and other key metrics.
result = get_result(train, solution[0], solution[1], solution[2])
show_result(result, f'TRAIN ({train["date"].min().date()} - {train["date"].max().date()})', False)

Testing on Unseen Data

  • The real test of any strategy is how it performs on new, unseen data. So, the function repeats the performance evaluation on the test dataset. This gives us a sense of how robust and adaptable our strategy is in real-world scenarios.
result = get_result(test, solution[0], solution[1], solution[2])
show_result(result, f'TEST ({test["date"].min().date()} - {test["date"].max().date()})', True)

What do you think? I know that it appears to be a lot, but I wanted to be as specific as possible. Leave me a comment about your opinion, suggestion, or anything you want to share with me. Also, if you want me to apply this to any specific ticker, let me know. — Diego Degese

Let's continue to the results…

Results

In our practical application of the Bollinger bands and genetic algorithm strategy to the QQQ, we observed the following results across different timeframes.

5-Minute Timeframe Results

************************************************************
************** PROCESSING QQQ - TIMEFRAME 5T ***************
************************************************************
100%|████████████████████████████████████████████████████| 50/50 [00:12<00:00,  4.08it/s]
***************** Best Solution Parameters *****************
Min Volatility   : 0.0009
Max Perc to Buy  : 0.9235
Min Perc to Sell : 0.6530
*** SUMMARIZED RESULT - TRAIN (2008-05-05 - 2022-06-01) ****
* Reward              : 439.96
* Profit / Loss       : 25517.44
* Wins / Losses       : 13683 / 3083 (81.61%)
* Max Profit          : 592.76
* Max Drawdown        : -1100.88
* Profit / Loss (Avg) : 1.51
**** SUMMARIZED RESULT - TEST (2022-06-02 - 2023-11-30) ****
* Reward              : 316.84
* Profit / Loss       : 2217.85
* Wins / Losses       : 1438 / 348 (80.52%)
* Max Profit          : 294.22
* Max Drawdown        : -558.72
* Profit / Loss (Avg) : 1.24
****************** MONTHLY DETAIL RESULT *******************
                  pnl  wins  losses  win_rate
year_month                                   
2022-06     -842.6848    55      22     71.43
2022-07     1131.1534    95      17     84.82
2022-08     -442.1143    79      23     77.45
2022-09     -486.3656    69      21     76.67
2022-10      124.3698    77      19     80.21
2022-11      151.7041    60      21     74.07
2022-12     -925.3281    60      21     74.07
2023-01     1004.5355   107      14     88.43
2023-02      340.6154    64      19     77.11
2023-03      318.2845    89      23     79.46
2023-04       31.5115    84      17     83.17
2023-05      575.2424    82      18     82.00
2023-06      421.7660    94      16     85.45
2023-07      172.2845    69      18     79.31
2023-08       88.7790   102      22     82.26
2023-09     -249.7959    68      19     78.16
2023-10      -11.9631    96      22     81.36
2023-11      815.8541    88      16     84.62
  • Overall, we saw a significant profit of $25,517.44 in training and $2,217.85 in testing, indicating a strong performance in shorter intervals.
  • The strategy achieved a high win rate of over 80% in both periods, with 13,683 wins versus 3,083 losses in training, and 1,438 wins against 348 losses in testing.
  • The maximum profit per trade was $294.22, while the maximum drawdown, a measure of the largest drop, was limited to $-558.72, showcasing a balanced risk profile.
  • Monthly performance varied, with notable gains in July 2022 and January 2023, but challenges in June 2022 and December 2022, reflecting the strategy's responsiveness to market volatility.

15-Minute Timeframe Results

************************************************************
************** PROCESSING QQQ - TIMEFRAME 15T **************
************************************************************
100%|████████████████████████████████████████████████████| 50/50 [00:05<00:00,  8.69it/s]
***************** Best Solution Parameters *****************
Min Volatility   : 0.0094
Max Perc to Buy  : 0.9633
Min Perc to Sell : 0.7186
*** SUMMARIZED RESULT - TRAIN (2008-05-05 - 2022-06-01) ****
* Reward              : 253.35
* Profit / Loss       : 14694.03
* Wins / Losses       : 2902 / 801 (78.37%)
* Max Profit          : 575.70
* Max Drawdown        : -1264.00
* Profit / Loss (Avg) : 3.96
**** SUMMARIZED RESULT - TEST (2022-06-02 - 2023-11-30) ****
* Reward              : -8.00
* Profit / Loss       : 1850.01
* Wins / Losses       : 291 / 94 (75.58%)
* Max Profit          : 312.84
* Max Drawdown        : -713.92
* Profit / Loss (Avg) : 4.81
****************** MONTHLY DETAIL RESULT *******************
                  pnl  wins  losses  win_rate
year_month                                   
2022-06      -12.2201    17       5     77.27
2022-07      652.0510    17       3     85.00
2022-08     -372.0271    14       5     73.68
2022-09     -794.0131    16       7     69.57
2022-10      124.8657    12       4     75.00
2022-11      -99.7139    15       5     75.00
2022-12    -1112.6833     7       9     43.75
2023-01      922.1689    24       5     82.76
2023-02      149.1151    15       6     71.43
2023-03      515.0560    21       5     80.77
2023-04      103.1897    10       5     66.67
2023-05      714.0070    22       4     84.62
2023-06      609.4713    23       4     85.19
2023-07      326.9752    18       5     78.26
2023-08     -167.6179    13       6     68.42
2023-09     -349.2672     8       8     50.00
2023-10     -238.6385    16       6     72.73
2023-11      879.2884    23       2     92.00
  • In the 15-minute timeframe, the strategy yielded $14,694.03 in training profit and $1,850.01 in testing profit.
  • The win/loss ratio remained strong at 78.37% in training and 75.58% in testing, indicating consistency across timeframes.
  • The maximum profit here was $312.84, with a higher maximum drawdown of $-713.92, suggesting slightly increased volatility at this interval.
  • Monthly details reveal a mixed performance, with strong results in January 2023 and May 2023, but challenges in September 2022 and December 2022.

1-Hour Timeframe Results

************************************************************
************** PROCESSING QQQ - TIMEFRAME 1H ***************
************************************************************
100%|████████████████████████████████████████████████████| 50/50 [00:03<00:00, 13.91it/s]
***************** Best Solution Parameters *****************
Min Volatility   : 0.0009
Max Perc to Buy  : 0.9235
Min Perc to Sell : 0.6183
*** SUMMARIZED RESULT - TRAIN (2008-05-07 - 2022-06-01) ****
* Reward              : -35.84
* Profit / Loss       : 9408.34
* Wins / Losses       : 1300 / 275 (82.54%)
* Max Profit          : 407.64
* Max Drawdown        : -1910.40
* Profit / Loss (Avg) : 5.97
**** SUMMARIZED RESULT - TEST (2022-06-02 - 2023-11-30) ****
* Reward              : -40.71
* Profit / Loss       : 1957.38
* Wins / Losses       : 121 / 35 (77.56%)
* Max Profit          : 426.42
* Max Drawdown        : -861.12
* Profit / Loss (Avg) : 12.55
****************** MONTHLY DETAIL RESULT *******************
                 pnl  wins  losses  win_rate
year_month                                  
2022-06     -33.4003     7       1     87.50
2022-07     -16.7712     4       2     66.67
2022-08     219.3418     8       1     88.89
2022-09    -721.3740     5       3     62.50
2022-10     494.7148    10       1     90.91
2022-11     268.2900     4       3     57.14
2022-12    -484.8947     4       3     57.14
2023-01     400.8723     8       2     80.00
2023-02     355.6032     7       3     70.00
2023-03     504.5571     8       1     88.89
2023-04      69.2912     5       2     71.43
2023-05     357.9288    11       2     84.62
2023-06     403.2830     9       1     90.00
2023-07     320.5376     7       1     87.50
2023-08    -278.8173     3       3     50.00
2023-09    -323.3074     4       2     66.67
2023-10    -138.3106     6       3     66.67
2023-11     559.8405    11       1     91.67
  • The hourly timeframe showed a total profit of $9,408.34 in training and $1,957.38 in testing, which is notable for longer-term trades.
  • This interval had an impressive win rate of over 77%, with 1,300 wins to 275 losses in training, and 121 wins to 35 losses in testing.
  • The maximum profit reached $426.42, while experiencing the highest drawdown of $-861.12, reflecting the increased risk associated with longer timeframes.
  • The monthly analysis indicates a more stable performance with significant gains in October 2022 and June 2023, but notable dips in September 2022 and December 2022.

Conclusion

In conclusion, our strategy demonstrates robust performance across all timeframes, with a consistently high win rate and a balanced approach to maximizing profits while managing drawdowns.

Before you leave, please do me a favor guys, and clap and comment so much, so medium puss this article to more readers, and they can join this series and learn in detail how to find new opportunities to be successful in the market too…

Also don't forget to follow Diego Degese so you don't miss the "Bollinger Bands Strategy and Genetic Algorithm Optimization" series notifications of every episode.

Would you like to see the rest of the backtests in this series?

Download the full source code and the colab notebook of this article from here

X (Twitter): https://x.com/diegodegese LinkedIn: https://www.linkedin.com/in/ddegese Github: https://github.com/crapher

Disclaimer: Investing in the stock market involves risk and may not be suitable for all investors. The information provided in this article is for educational purposes only and should not be construed as investment advice or a recommendation to buy or sell any particular security. Always do your own research and consult with a licensed financial advisor before making any investment decisions. Past performance is not indicative of future results.

A Message from InsiderFinance

None

Thanks for being a part of our community! Before you go: