avatarDiego Degese

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

13162

Abstract

of the trading strategy using the provided parameters. It applies Bollinger Bands to the data, generates buy and sell signals, and computes various performance metrics, including reward, wins, losses, and P&L.</p><div id="02d3"><pre><span class="hljs-keyword">def</span> <span class="hljs-title function_">get_result</span>(<span class="hljs-params">df, buy_length, buy_std, sell_length, sell_std, is_test=<span class="hljs-literal">False</span></span>):

<span class="hljs-comment"># Round to 2 digit to avoid the Bollinger bands function to generate weird field names</span>
buy_std = <span class="hljs-built_in">round</span>(buy_std, <span class="hljs-number">2</span>)
sell_std = <span class="hljs-built_in">round</span>(sell_std, <span class="hljs-number">2</span>)

<span class="hljs-comment"># Generate suffixes for Bollinger bands fields</span>
buy_suffix = <span class="hljs-string">f'<span class="hljs-subst">{<span class="hljs-built_in">int</span>(buy_length)}</span>_<span class="hljs-subst">{buy_std}</span>'</span>
sell_suffix = <span class="hljs-string">f'<span class="hljs-subst">{<span class="hljs-built_in">int</span>(sell_length)}</span>_<span class="hljs-subst">{sell_std}</span>'</span>

<span class="hljs-comment"># Generate a copy to avoid changing the original data</span>
df = df.copy().reset_index(drop=<span class="hljs-literal">True</span>)

<span class="hljs-comment"># Calculate Bollinger bands based on parameters</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-string">f'BBU_<span class="hljs-subst">{buy_suffix}</span>'</span> <span class="hljs-keyword">in</span> df.columns:
    df.ta.bbands(close=df[<span class="hljs-string">'close'</span>], length=buy_length, std=buy_std, append=<span class="hljs-literal">True</span>)
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-string">f'BBU_<span class="hljs-subst">{sell_suffix}</span>'</span> <span class="hljs-keyword">in</span> df.columns:
    df.ta.bbands(close=df[<span class="hljs-string">'close'</span>], length=sell_length, std=sell_std, append=<span class="hljs-literal">True</span>)
df = df.dropna()

<span class="hljs-comment"># Buy Signal</span>
df[<span class="hljs-string">'signal'</span>] = np.where(df[<span class="hljs-string">'close'</span>] &lt; df[<span class="hljs-string">f'BBL_<span class="hljs-subst">{buy_suffix}</span>'</span>], <span class="hljs-number">1</span>, <span class="hljs-number">0</span>)

<span class="hljs-comment"># Sell Signal</span>
df[<span class="hljs-string">'signal'</span>] = np.where(df[<span class="hljs-string">'close'</span>] &gt; df[<span class="hljs-string">f'BBU_<span class="hljs-subst">{sell_suffix}</span>'</span>], -<span class="hljs-number">1</span>, df[<span class="hljs-string">'signal'</span>])

<span class="hljs-comment"># Remove all rows without operations, rows with the same consecutive operation, first row selling, and last row buying</span>
result = df[df[<span class="hljs-string">'signal'</span>] != <span class="hljs-number">0</span>]
result = result[result[<span class="hljs-string">'signal'</span>] != result[<span class="hljs-string">'signal'</span>].shift()]
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">len</span>(result) &gt; <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span> (result.iat[<span class="hljs-number">0</span>, -<span class="hljs-number">1</span>] == -<span class="hljs-number">1</span>): result = result.iloc[<span class="hljs-number">1</span>:]
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">len</span>(result) &gt; <span class="hljs-number">0</span>) <span class="hljs-keyword">and</span> (result.iat[-<span class="hljs-number">1</span>, -<span class="hljs-number">1</span>] == <span class="hljs-number">1</span>): result = result.iloc[:-<span class="hljs-number">1</span>]

<span class="hljs-comment"># Calculate the reward &amp; result / operation</span>
result[<span class="hljs-string">'reward'</span>] = np.where(result[<span class="hljs-string">'signal'</span>] == -<span class="hljs-number">1</span>, (result[<span class="hljs-string">'close'</span>] - result[<span class="hljs-string">'close'</span>].shift()) * (CASH // result[<span class="hljs-string">'close'</span>].shift()), <span class="hljs-number">0</span>)
result[<span class="hljs-string">'wins'</span>] = np.where(result[<span class="hljs-string">'reward'</span>] &gt; <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>)
result[<span class="hljs-string">'losses'</span>] = np.where(result[<span class="hljs-string">'reward'</span>] &lt; <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">0</span>)

<span class="hljs-comment"># Generate window and filter windows without operations</span>
result_window = result.set_index(<span class="hljs-string">'date'</span>).resample(<span class="hljs-string">'3M'</span>).agg(
    {<span class="hljs-string">'close'</span>:<span class="hljs-string">'last'</span>,<span class="hljs-string">'reward'</span>:<span class="hljs-string">'sum'</span>,<span class="hljs-string">'wins'</span>:<span class="hljs-string">'sum'</span>,<span class="hljs-string">'losses'</span>:<span class="hljs-string">'sum'</span>}).reset_index()

min_operations = <span class="hljs-number">252</span> <span class="hljs-comment"># 1 Year</span>
result_window = result_window[(result_window[<span class="hljs-string">'wins'</span>] + result_window[<span class="hljs-string">'losses'</span>]) != <span class="hljs-number">0</span>]

<span class="hljs-comment"># Generate the result</span>
wins = result_window[<span class="hljs-string">'wins'</span>].mean() <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result_window) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>
losses = result_window[<span class="hljs-string">'losses'</span>].mean() <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(result_window) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>
reward = result_window[<span class="hljs-string">'reward'</span>].mean() <span class="hljs-keyword">if</span> (min_operations &lt; (wins + losses)) <span class="hljs-keyword">or</span> is_test <span class="hljs-keyword">else</span> -min_operations + (wins + losses)
pnl = result_window[<span class="hljs-string">'reward'</span>].<span class="hljs-built_in">sum</span>()

<span class="hljs-keyword">return</span> reward, wins, losses, pnl</pre></div><h2 id="41d5">Loading Data and Process Data</h2><p id="80af">After all the required methods are defined, we load the training and testing data using the <code>get_data()</code> function and prints a message to indicate that data processing is beginning.</p><div id="9c8c"><pre>train, test = get_data()

<span class="hljs-built_in">print</span>(<span class="hljs-string">""</span>.center(<span class="hljs-number">60</span>, <span class="hljs-string">""</span>)) <span class="hljs-built_in">print</span>(<span class="hljs-string">f' PROCESSING DATA '</span>.center(<span class="hljs-number">60</span>, <span class="hljs-string">''</span>)) <span class="hljs-built_in">print</span>(<span class="hljs-string">""</span>.center(<span class="hljs-number">60</span>, <span class="hljs-string">"*"</span>))</pre></div><h2 id="97c9">Genetic Algorithm Configuration and Execution</h2><p id="52be">A Genetic Algorithm is configured using the <code>pygad.GA</code> class. It specifies parameters like the number of generations, the number of parents mating, the fitness function, the solution space (ranges for parameters), and various other GA settings. The <code>on_generation</code> callback is used to update a progress bar as the GA progresses.</p><div id="e127"><pre><span class="hljs-keyword">with</span> tqdm(total=GENERATIONS) <span class="hljs-keyword">as</span> pbar:

ga_instance = pygad.GA(num_generations=GENERATIONS,
                       num_parents_mating=<span class="hljs-number">5</span>,
                       fitness_func=fitness_func,
                       sol_per_pop=SOLUTIONS,
                       num_genes=<span class="hljs-number">4</span>,
                       gene_space=[
                        {<span class="hljs-string">'low'</span>: <span class="hljs-number">1</span>, <span class="hljs-string">'high'</span>: <span class="hljs-number">200</span>, <span class="hljs-string">'step'</span>: <span class="hljs-number">1</span>},
                        {<span class="hljs-string">'low'</span>: <span class="hljs-number">0.1</span>, <span class="hljs-string">'high'</span>: <span class="hljs-number">3</span>, <span class="hljs-string">'step'</span>: <span class="hljs-number">0.01</span>},
                        {<span class="hljs-string">'low'</span>: <span class="hljs-number">1</span>, <span class="hljs-string">'high'</span>: <span class="hljs-number">200</span>, <span class="hljs-string">'step'</span>: <span class="hljs-number">1</span>},
                        {<span class="hljs-string">'low'</span>: <span class="hljs-number">0.1</span>, <span class="hljs-string">'high'</span>: <span class="hljs-number">3</span>, <span class="hljs-string">'step'</span>: <span class="hljs-number">0.01</span>}],
                       parent_selection_type=<span class="hljs-string">"sss"</span>,
                       crossover_type=<span class="hljs-string">"single_point"</span>,
                       mutation_type=<span class="hljs-string">"random"</span>,
                       mutation_num_genes=<span class="hljs-number">1</span>,
                       keep_parents=-<span class="hljs-number">1</span>,
                       random_seed=<span class="hljs-number">42</span>,
                       on_generation=<span class="hljs-keyword">lambda</span> _: pbar.update(<span class="hljs-number">1</span>),
                       )

ga_instance.run()</pre></div><h2 id="65ca">Display the Best Solution and Results</h2><p id="d442">Next, we retrieve the best solution found by the Genetic Algorithm and display the parameters. It then calculates and displays the strategy’s results for both the training and testing data.</p><div id="a17e"><pre><span class="hljs-comment"># Show details of the best solution.</span>

solution, solution_fitness, _ = ga_instance.best_solution()

<span class="hljs-built_in">print</span>(<span class="hljs-string">f' Best Solution Parameters '</span>.center(<span class="hljs-number">60</span>, <span class="hljs-string">'*'</span>)) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'Buy Length : <span class="hljs-subst">{solution[<span class="hljs-number">0</span>]:<span class="hljs-number">.0</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'Buy Std : <span class="hljs-subst">{solution[<span class="hljs-number">1</span>]:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'Sell Length : <span class="hljs-subst">{solution[<span class="hljs-number">2</span>]:<span class="hljs-number">.0</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'Sell Std : <span class="hljs-subst">{solution[<span class="hljs-number">3</span>]:<span class="hljs-number">.2</span>f}</span>'</span>)

<span class="hljs-comment"># Get result from train data</span> reward, wins, losses, pnl = get_result(train, solution[<span class="hljs-number">0</span>], solution[<span class="hljs-number">1</span>], solution[<span class="hljs-number">2</span>], solution[<span class="hljs-number">3</span>])

<span class="hljs-comment"># Show the train result</span> <span class="hljs-built_in">print</span>(<span class="hljs-string">f' Result (TRAIN) '</span>.center(<span class="hljs-number">60</span>, <span class="hljs-string">''</span>)) <span class="hljs-built_in">print</span>(<span class="hljs-string">f' Reward : <span class="hljs-subst">{reward:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Profit / Loss (B&H) : <span class="hljs-subst">{(train[<span class="hljs-string">"close"</span>].iloc[-<span class="hljs-number">1</span>] - train[<span class="hljs-string">"close"</span>].iloc[<span class="hljs-number">0</span>]) * (CASH // train[<span class="hljs-string">"close"</span>].iloc[<span class="hljs-number">0</span>]):<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Profit / Loss (Strategy) : <span class="hljs-subst">{pnl:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs

Options

-built_in">print</span>(<span class="hljs-string">f'* Wins / Losses : <span class="hljs-subst">{wins:<span class="hljs-number">.2</span>f}</span> / <span class="hljs-subst">{losses:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Win Rate : <span class="hljs-subst">{(<span class="hljs-number">100</span> * (wins/(wins + losses)) <span class="hljs-keyword">if</span> wins + losses > <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>):<span class="hljs-number">.2</span>f}</span>%'</span>)

<span class="hljs-comment"># Get result from test data</span> reward, wins, losses, pnl = get_result(test, solution[<span class="hljs-number">0</span>], solution[<span class="hljs-number">1</span>], solution[<span class="hljs-number">2</span>], solution[<span class="hljs-number">3</span>], <span class="hljs-literal">True</span>)

<span class="hljs-comment"># Show the test result</span> <span class="hljs-built_in">print</span>(<span class="hljs-string">f' Result (TEST) '</span>.center(<span class="hljs-number">60</span>, <span class="hljs-string">''</span>)) <span class="hljs-built_in">print</span>(<span class="hljs-string">f' Reward : <span class="hljs-subst">{reward:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Profit / Loss (B&H) : <span class="hljs-subst">{(test[<span class="hljs-string">"close"</span>].iloc[-<span class="hljs-number">1</span>] - test[<span class="hljs-string">"close"</span>].iloc[<span class="hljs-number">0</span>]) * (CASH // test[<span class="hljs-string">"close"</span>].iloc[<span class="hljs-number">0</span>]):<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Profit / Loss (Strategy) : <span class="hljs-subst">{pnl:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Wins / Losses : <span class="hljs-subst">{wins:<span class="hljs-number">.2</span>f}</span> / <span class="hljs-subst">{losses:<span class="hljs-number">.2</span>f}</span>'</span>) <span class="hljs-built_in">print</span>(<span class="hljs-string">f'* Win Rate : <span class="hljs-subst">{(<span class="hljs-number">100</span> * (wins/(wins + losses)) <span class="hljs-keyword">if</span> wins + losses > <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-number">0</span>):<span class="hljs-number">.2</span>f}</span>%'</span>)</pre></div><p id="d3b5">With this, we have all the required code to optimize the data. Now we can continue to the next step that are the results of this algorithm.</p><h1 id="7be6">Strategy Results</h1><p id="139e">The result indicates the performance of the trading strategy based on the Bollinger Bands with the best solution parameters found through the Genetic Algorithm optimization.</p><div id="4cf5"><pre>***************** Best Solution Parameters ***************** Buy Length : 3 Buy Std : 0.17 Sell Length : 3 Sell Std : 1.11 ********************** Result (TRAIN) **********************

  • Reward : 68.91
  • Profit / Loss (B&H) : 2040.01
  • Profit / Loss (Strategy) : 3790.27
  • Wins / Losses : 483.13 / 236.27
  • Win Rate : 67.16% ********************** Result (TEST) ***********************
  • Reward : 21.65
  • Profit / Loss (B&H) : 56.70
  • Profit / Loss (Strategy) : 216.48
  • Wins / Losses : 379.80 / 184.60
  • Win Rate : 67.29%</pre></div><p id="7386">Let’s break down each part of the displayed results:</p><p id="84f4"><b>Best Solution Parameters</b></p><ul><li><code>Buy Length</code>: The length of the simple moving average for the "Buy" signal is set to <b>3</b>.</li><li><code>Buy Std</code>: The number of standard deviations above the SMA for the "Buy" signal is approximately <b>0.17</b>.</li><li><code>Sell Length</code>: The length of the simple moving average for the "Sell" signal is set to <b>3</b>.</li><li><code>Sell Std</code>: The number of standard deviations below the SMA for the "Sell" signal is approximately <b>1.11</b>.</li></ul><p id="ae8a">These parameters represent the optimized configuration of the Bollinger Bands strategy.</p><p id="d961"><b>Result (TRAIN)</b></p><ul><li><code>Reward</code>: The average reward generated by the strategy on the training data is approximately <b>68.91</b>. This is the strategy's performance measure based on the training dataset.</li><li><code>Profit / Loss (B&H)</code>: This represents the profit or loss that could have been achieved through a simple "Buy and Hold" strategy over the training period. It shows a profit of approximately <b>2040.01</b>.</li><li><code>Profit / Loss (Strategy)</code>: This is the profit or loss achieved by the strategy over the training period. It shows a profit of approximately <b>3790.27</b>, indicating that the strategy outperformed a basic buy-and-hold approach.</li><li><code>Wins / Losses</code>: The strategy recorded approximately <b>483.13</b> winning trades and <b>236.27</b> losing trades during the training period.</li><li><code>Win Rate</code>: The win rate, calculated as the percentage of winning trades out of the total number of trades (wins / wins + losses), is approximately <b>67.16%</b>.</li></ul><p id="4f9c">The results suggest that the strategy performed significantly better than a simple “Buy and Hold” approach during the training period.</p><p id="825c"><b>Result (TEST)</b></p><ul><li><code>Reward</code>: The average reward generated by the strategy on the test data is approximately 21.65. This is the strategy's performance measure based on the testing dataset, unseen during the optimization process.</li><li><code>Profit / Loss (B&H)</code>: This represents the profit or loss that could have been achieved through a basic "Buy and Hold" strategy over the test period. It shows a profit of approximately<b> 56.70</b>.</li><li><code>Profit / Loss (Strategy)</code>: This is the profit or loss achieved by the strategy over the test period. It shows a profit of approximately <b>216.48</b>, indicating that the strategy outperformed the buy-and-hold approach in the unseen data.</li><li><code>Wins / Losses</code>: The strategy recorded approximately <b>379.80</b> winning trades and 184.60 losing trades during the test period.</li><li><code>Win Rate</code>: The win rate for the test data is approximately <b>67.29%</b>, showing a similar win rate to the training data.</li></ul><blockquote id="8927"><p>NOTE: The strategy works by buying up to $1000 per trade of SPY and it uses the close price as the ‘Buy’ and ‘Sell’ prices.</p></blockquote><h1 id="07bd">Comparison with the Strategy that Optimizes the Bandwith and the Distance to the Bands</h1><p id="bc18">The previously presented strategy used also Bollinger Bands and Genetic Algorithm for optimization. The differences are the parameters to optimize and how we calculate the reward of the fitness function.</p><p id="e98d">For details, please check the following article.</p><div id="8008" class="link-block"> <a href="https://wire.insiderfinance.io/how-i-found-the-spys-potential-with-a-bollinger-bands-strategy-optimized-by-genetic-algorithms-b87907dd62a7"> <div> <div> <h2>How I Found the SPY’s Potential With a Bollinger Bands Strategy Optimized by Genetic Algorithms</h2> <div><h3>Blending the Power of Genetic Algorithms in Machine Learning with the Bollinger Bands Trading Strategy</h3></div> <div><p>wire.insiderfinance.io</p></div> </div> <div> <div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/0*zmeP52qziQE7CT4G.jpeg)"></div> </div> </div> </a> </div><p id="ab06">These are the results obtained in the previous strategy (The reward used in the optimization is the Profit and Loss).</p><div id="4c44"><pre>***************** Best Solution Parameters ***************** Min Bandwidth : 0.0010 Max Perc to Buy : 0.94 Min Perc to Sell : 0.66 ********************** Result (TRAIN) **********************
  • Profit / Loss : 2366.46
  • Wins / Losses : 17138 / 5755
  • Win Rate : 74.86% ********************** Result (TEST) ***********************
  • Profit / Loss : 150.60
  • Wins / Losses : 2665 / 825
  • Win Rate : 76.36%</pre></div><p id="0443">Let’s compare the results of this strategy against the results of the other strategy.</p><p id="1dce"><b>Training Set Performance</b></p><ul><li>In terms of “Profit / Loss”, the previous strategy generated a profit of <b>2366.46</b>, which was outperformed by this strategy with a profit of <b>3790.27</b>.</li><li>In terms of “Win Rate”, this strategy recorded a <b>67.16%</b> win rate, while the previous strategy achieved a slightly higher win rate of <b>74.86%</b>.</li></ul><p id="58b0"><b>Testing Set Performance:</b></p><ul><li>In terms of “Profit / Loss”, the previous strategy generated a profit of <b>150.60</b>, which was also outperformed by this strategy with a profit of <b>216.48</b>.</li><li>In terms of “Win Rate”, both sets of parameters maintained similar win rates, with this strategy at <b>67.29%</b> and the previous strategy at <b>76.36%</b>.</li></ul><p id="31a8">In summary, although the win rate of the previous strategy is higher than the win rate of this strategy, the profit obtained is greater in this strategy, also surpassing the Buy and Hold strategy, which is very promising.</p><h1 id="b4c6">Conclusion</h1><p id="b394">The combination of genetic algorithms and Bollinger bands offers a strong route forward in the quest for trading success. We showed that our data-driven, adaptively optimized method could consistently outperform the B&H strategy. With its robustness and adaptability, the Bollinger Bands Reversal Strategy optimized by Genetic Algorithm provides traders with an effective tool to handle the constantly shifting financial landscape. Although achieving trading success is a dynamic and difficult process, this methodology offers a methodical and data-driven way to improve trading results.</p><p id="b658">Is this the best strategy or the holy grail? No, but it is a strategy that may be worth investigating and doing some additional testing.</p><p id="ad7f">If you enjoy my work, please support me on Medium by becoming a member through my <a href="https://medium.com/@diegodegese/membership">referral link</a>, and consider giving it a clap as a small gesture of motivation. Thank you!</p><p id="e5c4">Download the <a href="https://github.com/crapher/medium/blob/main/08.GABBStrategy/code/spy_gabb_reversal.py">full source code</a> and the <a href="https://github.com/crapher/medium/blob/main/08.GABBStrategy/colab/spy_gabb_reversal.ipynb">colab notebook</a> of this article from <a href="https://github.com/crapher/medium/blob/main/08.GABBStrategy">here</a></p><p id="78da">Twitter / X: <a href="https://twitter.com/diegodegese">https://twitter.com/diegodegese</a> LinkedIn: <a href="https://www.linkedin.com/in/ddegese/">https://www.linkedin.com/in/ddegese</a> Github: <a href="https://github.com/crapher">https://github.com/crapher</a></p><p id="0a31"><i>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.</i></p><p id="1504">Subscribe to <i>DDIntel <a href="https://www.ddintel.com"></a></i><a href="https://www.ddintel.com">here</a>.</p><p id="6226">Submit your work to <i>DDIntel</i> <a href="https://datadriveninvestor.com/ddintelsubmission">here</a>.</p><p id="5601">Join our creator ecosystem <a href="https://join.datadriveninvestor.com/">here</a>.</p><p id="3bcf"><a href="https://www.ddintel.com"><i>DDIntel</i> </a>captures the more notable pieces from our <a href="https://www.datadriveninvestor.com/"><i>main site</i></a> and our popular <a href="https://medium.datadriveninvestor.com/"><i>DDI Medium publication</i></a>. Check us out for more insightful work from our community.</p><p id="8438">DDI Official Telegram Channel: <a href="https://t.me/+tafUp6ecEys4YjQ1">https://t.me/+tafUp6ecEys4YjQ1</a></p><p id="93f5">Follow us on <a href="https://www.linkedin.com/company/data-driven-investor"><i>LinkedIn</i></a>, <a href="https://twitter.com/@DDInvestorHQ"><i>Twitter</i></a>, <a href="https://www.youtube.com/c/datadriveninvestor"><i>YouTube</i></a>, and <a href="https://www.facebook.com/datadriveninvestor"><i>Facebook</i></a>.</p></article></body>

How I Beat the Buy & Hold Strategy Using the Bollinger Bands Reversal Strategy 🤯

Optimizing the Bollinger Bands Reversal Strategy using Genetic Algorithms

In an effort to beat the market, I’m constantly searching for fresh ideas and strategies that might work most of the time. Because the markets are so complex, there is a never-ending search for profitable trading strategies. An approach that I found interesting is the Bollinger Bands Reversal Strategy, which I am going to use with the SPY ETF to see how it behaves and if it is worth it.

Image from schwab.com

The Bollinger Bands Reversal Strategy

Before we jump into how to optimize the Bollinger Bands Reversal Strategy, let’s briefly revisit what Bollinger Bands are and how they form the basis of our trading strategy. Named after its creator, John Bollinger, Bollinger Bands consists of three bands: the middle band, which is a simple moving average (SMA), and two outer bands that are standard deviations away from the middle band.

Here’s a quick breakdown:

  1. The Middle Band (SMA): The middle band represents the average price over a specific period. Commonly, a 20-day SMA is used, but in our case will be determined by the optimization.
  2. Upper Bollinger Band: This band is typically set at 2 standard deviations above the SMA. The standard deviation is the second parameter we will optimize.
  3. Lower Bollinger Band: This band is usually set at 2 standard deviations below the SMA. As I mentioned in the previous point, the standard deviation is the second parameter we will optimize.

The idea behind the Bollinger Bands Reversal Strategy is that asset prices typically go back to their mean, or the middle band. A signal is generated when the price of an asset crosses or touches one of the outer bands. When the price approaches the upper band, it may be a sign that the asset is overbought and could soon reverse course. On the other hand, a price that crosses the lower band may indicate that the asset is oversold and could move in the opposite direction.

Now, you might be wondering, “How can we turn these signals into a profitable trading strategy?” This is where Genetic Algorithms come into play.

Genetic Algorithms in Trading

Genetic Algorithms are a subset of evolutionary algorithms that mimic the process of natural selection to find optimal solutions to complex problems. In the context of trading, a Genetic Algorithm can be a powerful tool for optimizing trading strategies and parameters.

The Genetic Algorithm process involves several key steps:

  1. Initialization: It begins with generating an initial population of candidate solutions. In our case, these candidate solutions would be different sets of parameters for the Bollinger Bands Strategy (Buy SMA, Buy Standard Deviation, Sell SMA, Sell Standard Deviation).
  2. Evaluation: Each candidate solution (set of parameters) is evaluated using a fitness function. The fitness function assesses the performance of the Bollinger Bands Reversal Strategy with the given parameters. In our case, we will evaluate the strategy’s performance across multiple 3-month periods within our training data. (This multiple 3-month period addresses a possible overfitting mentioned in previous articles)
  3. Selection: The best-performing solutions are selected to form the next generation. This emulates the concept of ‘survival of the fittest.’
  4. Crossover: Solutions from the selected individuals are combined to create new candidate solutions, mimicking the genetic process of recombination.
  5. Mutation: A certain proportion of the solutions undergo random changes, simulating genetic mutations.
  6. Replacement: The new generation of solutions replaces the previous one.
  7. Termination: The process continues for a number of generations. The best-performing solution is then selected as the optimal parameter value for our trading strategy.

You are not a Medium member yet? Why not take the next step and become a member for less than a Starbucks coffee per month? Don’t miss out — become a member now!

Also, if you enjoy reading my articles, please don’t forget to hit the follow button — Diego Degese

Applying Genetic Algorithms to Bollinger Bands

Now that we have a fundamental understanding of both the Bollinger Bands Reversal Strategy and Genetic Algorithms, let’s explore how we can combine these two elements to create an optimized trading strategy for the SPY ETF.

Data and Training

For our analysis, we’ll use historical SPY price data from January 2008 to May 2021 as our training dataset. This dataset allows us to train our Genetic Algorithm to find the optimal Bollinger Bands parameters.

Testing on Unseen Data

To ensure that our strategy is robust and not overfitted to the training data, we’ll evaluate its performance on unseen data. We’ll use SPY price data from June 2021 to July 2023 for this purpose. By using unseen data, we can assess the strategy’s real-world performance.

Evaluating Strategy Performance

In most trading strategy optimization scenarios, the performance is evaluated based on the entire dataset. However, to address the concern of potential overfitting, I’ll take a different approach. Instead of evaluating the strategy on the entire dataset, we’ll break it into multiple 3-month periods. For each of these periods, we’ll calculate the Reward, Wins, Losses, and Profit & Loss.

By using this approach, we aim to answer important questions about overfitting. Does the strategy perform consistently across different market conditions? Does it adapt to changing trends? This approach helps us understand the strategy’s robustness, which is crucial for successful trading.

Optimizing Bollinger Bands Parameters

The key parameters we want to optimize using the Genetic Algorithm are:

  1. Buy SMA: The length of the simple moving average for the “Buy” signal.
  2. Buy Standard Deviation: The number of standard deviations below the SMA for the “Buy” signal.
  3. Sell SMA: The length of the simple moving average for the “Sell” signal.
  4. Sell Standard Deviation: The number of standard deviations above the SMA for the “Sell” signal.

The Genetic Algorithm will search through various combinations of these parameters to find the optimal set that maximizes our defined fitness function.

Fitness Function

To evaluate the performance of our Bollinger Bands Reversal Strategy, we need a fitness function. In this case, our fitness function will use an average of the profit of all 3-month periods.

Genetic Algorithm Parameters

Genetic Algorithms themselves have parameters that need careful consideration:

  1. Population Size: The number of candidate solutions in each generation. We will use 30 in this example.
  2. Generations: The number of iterations the algorithm will go through. We will use 50 in this example.
  3. Gene Space: The limits of the values the algorithm will use for each parameter

The choice of these parameters can significantly impact the optimization process.

Results and Analysis

After running the Genetic Algorithm on the training data, we will obtain the optimal Bollinger Bands parameters for our strategy. However, the true test lies in how these parameters perform on unseen data.

The strategy’s performance on the unseen data will be our ultimate benchmark of success. If the strategy has indeed been effectively optimized, it should perform consistently well across various 3-month periods, adapting to the market changes.

Before we proceed with the source code, I want to mention that I’ve personally created all the source code featured in my articles. I’ve done this to share my own experiences and help you avoid the need to spend your valuable time redeveloping the same strategies. If you enjoy reading my articles, please share them with your friends and please don’t forget to hit the follow button — Diego Degese

Implementation of the Bollinger Bands Reversal Strategy Optimization

The following code is the implementation of a Genetic Algorithm that optimizes the Bollinger Bands Reversal Strategy. The goal is to find the optimal set of parameters that maximizes the strategy’s performance.

Import Libraries

The code begins by importing several libraries, including numpy for numerical operations, pandas for data manipulation, pandas_ta for technical analysis functions, pygad for the Genetic Algorithm, and tqdmto show the progress bar while the system is training.

import numpy as np
import pandas as pd
import pandas_ta as ta
import pygad

from tqdm import tqdm

Constants and Configuration

Then, it sets some constants and configurations, including initial cash (CASH), the number of solutions to evaluate (SOLUTIONS), the number of generations for the Genetic Algorithm (GENERATIONS), and file paths of the training and testing datasets.

CASH = 1000
SOLUTIONS = 30
GENERATIONS = 50
TRAIN_FILE = '../data/spy.train.csv.gz'
TEST_FILE = '../data/spy.test.csv.gz'

Data Loading

The get_data() function loads the training and testing data from the CSV files, converts the 'date' column to a datetime format (required to group the information later), and removes any rows with missing data.

def get_data():

    train = pd.read_csv(TRAIN_FILE, compression='gzip')
    train['date'] = pd.to_datetime(train['date'])
    train = train.dropna()

    test = pd.read_csv(TEST_FILE, compression='gzip')
    test['date'] = pd.to_datetime(test['date'])
    test = test.dropna()

    return train, test

Fitness Function Definition

The fitness_func() function is defined as the fitness function to be used by the Genetic Algorithm. It calculates the reward based on the training data and the given solution parameters.

def fitness_func(self, solution, sol_idx):

    # Get Reward from train data
    reward, wins, losses, pnl = get_result(train, solution[0], solution[1], solution[2], solution[3])

    # Return the solution reward
    return reward

Result Calculation Function

The get_result() function calculates the results of the trading strategy using the provided parameters. It applies Bollinger Bands to the data, generates buy and sell signals, and computes various performance metrics, including reward, wins, losses, and P&L.

def get_result(df, buy_length, buy_std, sell_length, sell_std, is_test=False):

    # Round to 2 digit to avoid the Bollinger bands function to generate weird field names
    buy_std = round(buy_std, 2)
    sell_std = round(sell_std, 2)

    # Generate suffixes for Bollinger bands fields
    buy_suffix = f'{int(buy_length)}_{buy_std}'
    sell_suffix = f'{int(sell_length)}_{sell_std}'

    # Generate a copy to avoid changing the original data
    df = df.copy().reset_index(drop=True)

    # Calculate Bollinger bands based on parameters
    if not f'BBU_{buy_suffix}' in df.columns:
        df.ta.bbands(close=df['close'], length=buy_length, std=buy_std, append=True)
    if not f'BBU_{sell_suffix}' in df.columns:
        df.ta.bbands(close=df['close'], length=sell_length, std=sell_std, append=True)
    df = df.dropna()

    # Buy Signal
    df['signal'] = np.where(df['close'] < df[f'BBL_{buy_suffix}'], 1, 0)

    # Sell Signal
    df['signal'] = np.where(df['close'] > df[f'BBU_{sell_suffix}'], -1, df['signal'])

    # Remove all rows without operations, rows with the same consecutive operation, first row selling, and last row buying
    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]

    # Calculate the reward & result / operation
    result['reward'] = np.where(result['signal'] == -1, (result['close'] - result['close'].shift()) * (CASH // result['close'].shift()), 0)
    result['wins'] = np.where(result['reward'] > 0, 1, 0)
    result['losses'] = np.where(result['reward'] < 0, 1, 0)

    # Generate window and filter windows without operations
    result_window = result.set_index('date').resample('3M').agg(
        {'close':'last','reward':'sum','wins':'sum','losses':'sum'}).reset_index()

    min_operations = 252 # 1 Year
    result_window = result_window[(result_window['wins'] + result_window['losses']) != 0]

    # Generate the result
    wins = result_window['wins'].mean() if len(result_window) > 0 else 0
    losses = result_window['losses'].mean() if len(result_window) > 0 else 0
    reward = result_window['reward'].mean() if (min_operations < (wins + losses)) or is_test else -min_operations + (wins + losses)
    pnl = result_window['reward'].sum()

    return reward, wins, losses, pnl

Loading Data and Process Data

After all the required methods are defined, we load the training and testing data using the get_data() function and prints a message to indicate that data processing is beginning.

train, test = get_data()

print("".center(60, "*"))
print(f' PROCESSING DATA '.center(60, '*'))
print("".center(60, "*"))

Genetic Algorithm Configuration and Execution

A Genetic Algorithm is configured using the pygad.GA class. It specifies parameters like the number of generations, the number of parents mating, the fitness function, the solution space (ranges for parameters), and various other GA settings. The on_generation callback is used to update a progress bar as the GA progresses.

with tqdm(total=GENERATIONS) as pbar:

    ga_instance = pygad.GA(num_generations=GENERATIONS,
                           num_parents_mating=5,
                           fitness_func=fitness_func,
                           sol_per_pop=SOLUTIONS,
                           num_genes=4,
                           gene_space=[
                            {'low': 1, 'high': 200, 'step': 1},
                            {'low': 0.1, 'high': 3, 'step': 0.01},
                            {'low': 1, 'high': 200, 'step': 1},
                            {'low': 0.1, 'high': 3, 'step': 0.01}],
                           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),
                           )

    ga_instance.run()

Display the Best Solution and Results

Next, we retrieve the best solution found by the Genetic Algorithm and display the parameters. It then calculates and displays the strategy’s results for both the training and testing data.

# Show details of the best solution.
solution, solution_fitness, _ = ga_instance.best_solution()

print(f' Best Solution Parameters '.center(60, '*'))
print(f'Buy Length    : {solution[0]:.0f}')
print(f'Buy Std       : {solution[1]:.2f}')
print(f'Sell Length   : {solution[2]:.0f}')
print(f'Sell Std      : {solution[3]:.2f}')

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

# Show the train result
print(f' Result (TRAIN) '.center(60, '*'))
print(f'* Reward                   : {reward:.2f}')
print(f'* Profit / Loss (B&H)      : {(train["close"].iloc[-1] - train["close"].iloc[0]) * (CASH // train["close"].iloc[0]):.2f}')
print(f'* Profit / Loss (Strategy) : {pnl:.2f}')
print(f'* Wins / Losses            : {wins:.2f} / {losses:.2f}')
print(f'* Win Rate                 : {(100 * (wins/(wins + losses)) if wins + losses > 0 else 0):.2f}%')

# Get result from test data
reward, wins, losses, pnl = get_result(test, solution[0], solution[1], solution[2], solution[3], True)

# Show the test result
print(f' Result (TEST) '.center(60, '*'))
print(f'* Reward                   : {reward:.2f}')
print(f'* Profit / Loss (B&H)      : {(test["close"].iloc[-1] - test["close"].iloc[0]) * (CASH // test["close"].iloc[0]):.2f}')
print(f'* Profit / Loss (Strategy) : {pnl:.2f}')
print(f'* Wins / Losses            : {wins:.2f} / {losses:.2f}')
print(f'* Win Rate                 : {(100 * (wins/(wins + losses)) if wins + losses > 0 else 0):.2f}%')

With this, we have all the required code to optimize the data. Now we can continue to the next step that are the results of this algorithm.

Strategy Results

The result indicates the performance of the trading strategy based on the Bollinger Bands with the best solution parameters found through the Genetic Algorithm optimization.

***************** Best Solution Parameters *****************
Buy Length    : 3
Buy Std       : 0.17
Sell Length   : 3
Sell Std      : 1.11
********************** Result (TRAIN) **********************
* Reward                   : 68.91
* Profit / Loss (B&H)      : 2040.01
* Profit / Loss (Strategy) : 3790.27
* Wins / Losses            : 483.13 / 236.27
* Win Rate                 : 67.16%
********************** Result (TEST) ***********************
* Reward                   : 21.65
* Profit / Loss (B&H)      : 56.70
* Profit / Loss (Strategy) : 216.48
* Wins / Losses            : 379.80 / 184.60
* Win Rate                 : 67.29%

Let’s break down each part of the displayed results:

Best Solution Parameters

  • Buy Length: The length of the simple moving average for the "Buy" signal is set to 3.
  • Buy Std: The number of standard deviations above the SMA for the "Buy" signal is approximately 0.17.
  • Sell Length: The length of the simple moving average for the "Sell" signal is set to 3.
  • Sell Std: The number of standard deviations below the SMA for the "Sell" signal is approximately 1.11.

These parameters represent the optimized configuration of the Bollinger Bands strategy.

Result (TRAIN)

  • Reward: The average reward generated by the strategy on the training data is approximately 68.91. This is the strategy's performance measure based on the training dataset.
  • Profit / Loss (B&H): This represents the profit or loss that could have been achieved through a simple "Buy and Hold" strategy over the training period. It shows a profit of approximately $2040.01.
  • Profit / Loss (Strategy): This is the profit or loss achieved by the strategy over the training period. It shows a profit of approximately $3790.27, indicating that the strategy outperformed a basic buy-and-hold approach.
  • Wins / Losses: The strategy recorded approximately 483.13 winning trades and 236.27 losing trades during the training period.
  • Win Rate: The win rate, calculated as the percentage of winning trades out of the total number of trades (wins / wins + losses), is approximately 67.16%.

The results suggest that the strategy performed significantly better than a simple “Buy and Hold” approach during the training period.

Result (TEST)

  • Reward: The average reward generated by the strategy on the test data is approximately 21.65. This is the strategy's performance measure based on the testing dataset, unseen during the optimization process.
  • Profit / Loss (B&H): This represents the profit or loss that could have been achieved through a basic "Buy and Hold" strategy over the test period. It shows a profit of approximately $56.70.
  • Profit / Loss (Strategy): This is the profit or loss achieved by the strategy over the test period. It shows a profit of approximately $216.48, indicating that the strategy outperformed the buy-and-hold approach in the unseen data.
  • Wins / Losses: The strategy recorded approximately 379.80 winning trades and 184.60 losing trades during the test period.
  • Win Rate: The win rate for the test data is approximately 67.29%, showing a similar win rate to the training data.

NOTE: The strategy works by buying up to $1000 per trade of SPY and it uses the close price as the ‘Buy’ and ‘Sell’ prices.

Comparison with the Strategy that Optimizes the Bandwith and the Distance to the Bands

The previously presented strategy used also Bollinger Bands and Genetic Algorithm for optimization. The differences are the parameters to optimize and how we calculate the reward of the fitness function.

For details, please check the following article.

These are the results obtained in the previous strategy (The reward used in the optimization is the Profit and Loss).

***************** Best Solution Parameters *****************
Min Bandwidth    : 0.0010
Max Perc to Buy  :   0.94
Min Perc to Sell :   0.66
********************** Result (TRAIN) **********************
* Profit / Loss  : 2366.46
* Wins / Losses  : 17138 / 5755
* Win Rate       : 74.86%
********************** Result (TEST) ***********************
* Profit / Loss  : 150.60
* Wins / Losses  : 2665 / 825
* Win Rate       : 76.36%

Let’s compare the results of this strategy against the results of the other strategy.

Training Set Performance

  • In terms of “Profit / Loss”, the previous strategy generated a profit of $2366.46, which was outperformed by this strategy with a profit of $3790.27.
  • In terms of “Win Rate”, this strategy recorded a 67.16% win rate, while the previous strategy achieved a slightly higher win rate of 74.86%.

Testing Set Performance:

  • In terms of “Profit / Loss”, the previous strategy generated a profit of $150.60, which was also outperformed by this strategy with a profit of $216.48.
  • In terms of “Win Rate”, both sets of parameters maintained similar win rates, with this strategy at 67.29% and the previous strategy at 76.36%.

In summary, although the win rate of the previous strategy is higher than the win rate of this strategy, the profit obtained is greater in this strategy, also surpassing the Buy and Hold strategy, which is very promising.

Conclusion

The combination of genetic algorithms and Bollinger bands offers a strong route forward in the quest for trading success. We showed that our data-driven, adaptively optimized method could consistently outperform the B&H strategy. With its robustness and adaptability, the Bollinger Bands Reversal Strategy optimized by Genetic Algorithm provides traders with an effective tool to handle the constantly shifting financial landscape. Although achieving trading success is a dynamic and difficult process, this methodology offers a methodical and data-driven way to improve trading results.

Is this the best strategy or the holy grail? No, but it is a strategy that may be worth investigating and doing some additional testing.

If you enjoy my work, please support me on Medium by becoming a member through my referral link, and consider giving it a clap as a small gesture of motivation. Thank you!

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

Twitter / X: https://twitter.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.

Subscribe to DDIntel here.

Submit your work to DDIntel here.

Join our creator ecosystem here.

DDIntel captures the more notable pieces from our main site and our popular DDI Medium publication. Check us out for more insightful work from our community.

DDI Official Telegram Channel: https://t.me/+tafUp6ecEys4YjQ1

Follow us on LinkedIn, Twitter, YouTube, and Facebook.

Stock Market Tips
Backtesting Strategy
Data Science
Python Programming
Etf
Recommended from ReadMedium