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 tqdmUser 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_SMAsets the Simple Moving Average period for the middle band, andBB_STDdefines 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, andWINDOW_MIN_OPERATIONS, which helps segment our data for testing and define how we measure the success of our trades. - Generations and Solutions:
GENERATIONSare how many iterations the genetic algorithm is going to do.SOLUTIONSare 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 = 20Constants 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 = NoneThis 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
datecolumn is converted todatetimeformat 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_tato 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_TESTINGvariable 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_percentageexceeds ourmin_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:
num_generations: The number of iterations the algorithm runs. Each generation represents an evolution of the solutions.num_parents_mating: This specifies how many solutions from the current generation will be selected for mating (i.e., producing the next generation).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 asfitness_func, which calculates the reward of the trading strategy based on the given parameters.sol_per_pop: The number of solutions in each generation.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.gene_space: Defines the range and step size for each parameter. This ensures our algorithm searches within reasonable bounds.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.mutation_num_genes: Determines how many genes (parameters) are subject to mutation.keep_parents: Indicates whether to keep any of the parent solutions in the new generation.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_rewardfunction. 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_monthlyis 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_resultto apply the trading strategy based on the optimized parameters andshow_resultto 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

Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the InsiderFinance Wire
- 📚 Take our FREE Masterclass
- 📈 Discover Powerful Trading Tools