/Users/raylai/Library/Caches/org.R-project.R/R/reticulate/uv/cache/archive-v0/n27kI_pu2kgd7cyXmF2Fh/lib/python3.11/site-packages/backtesting/_plotting.py:55: UserWarning: Jupyter Notebook detected. Setting Bokeh output to notebook. This may not work in Jupyter clients without JavaScript support, such as old IDEs. Reset with `backtesting.set_bokeh_output(notebook=False)`.
warnings.warn('Jupyter Notebook detected. '
Loading BokehJS ...
3.11.11 (main, Mar 17 2025, 21:33:08) [Clang 20.1.0 ]
/Users/raylai/Desktop/github/raylai-co-quant-blog/posts/breakout-strategy
Breakout Strategy and its variations
Breakout Strategy is a simple but effective trend-following / swing trade strategy. The rule is simple, entry when the price moves above the high or low of the past N days.
Although the strategy looks simple, there can be many variations:
Using Close price vs using High/Low price
The length of the window
Volatility measure
The most important thing to understand the breakout strategy is same as understanding other trend strategies.
Breakout strategy is a trend-following strategy, which means the underlying trend of an asset is the most important. The variations are only refinement to create different risk profile. For example, adding volatility filter may improve successful rate, but the risk per trade could be higher. There are always trade-offs between different variations.
Breakout strategy can also considered as “Event-driven” strategy, as if the price breaks out from the top or from the bottom, there must be something happened.
The simpliest Breakout strategy
We will first test the simpliest Breakout rule:
Buy when today’s high moves above the high of the past N days.
Sell when today’s low moves below the low of the past N days. Enter into short immediately.
No Stop Loss. No Profit Taking.
Position Sizing: 1 unit or 1 contract
Code
from importlib importreloadimport strategy.BreakoutStrategyreload(strategy.BreakoutStrategy)from strategy.BreakoutStrategy import BreakoutStrategy
fig, ax = plt.subplots()sns.barplot(returns_df, x="index", y="Returns", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Returns from 2000 to 2025 by using 20-day Breakout")ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Loading ITables v2.2.5 from the internet...
(need help?)
We can see that the strategy works well for low-lose assets, however, as expected, doesn’t work well for noisy market. There are 2 assets that stand out, which is Corn (ZC=F) and Gold (GC=F) - Not sure why is that but just keep it for future exploration.
Comparison with Moving averages
There are a lot of questions about to what extent the trend tracking method matters. We can have a quick comparison between different lengths of breakout and moving average trends towards our testable assets.
Testing Plan
Breakout
Moving Average
Entry Rule
Buy when today’s high moves above the high of the past N days.
Buy when Moving Averages turns up \(\text{MA}_t > \text{MA}_{t-1} > \text{MA}_{t-2}\)
Exit / Selling Rule
Sell when today’s low moves below the low of the past N days. Enter into short immediately.
Sell when Moving Averages turns down \(MA_t < MA_{t-1} < MA_{t-2}\)
Position Sizing
1 unit or 1 contract
1 unit or 1 contract
Stop Loss
No Stop Loss
No Stop Loss
Profit Taking
No Profit Taking
No Profit Taking
Code
from importlib importreloadimport strategy.MovingAverageStrategyreload(strategy.MovingAverageStrategy)from strategy.MovingAverageStrategy import MovingAverageStrategy
# Plot Chartn =5fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =10fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =20fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =40fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =80fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =120fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =120fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs 20-day MA"% n)ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs 20-day MA"% n)ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Returns
Code
# Plot Chartn =200fig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Return %", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Return Percent from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Number of Trades
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Number of Trades", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Avg. Holding Period
Code
# Plot Chartfig, ax = plt.subplots()sns.barplot(all_performances.query('Length == %d'% n), x="Universe", y="Avg. Holding Period", hue="Strategy", ax=ax)ax.tick_params(axis='x', rotation=45, labelsize=8)ax.set_title("Number of trades from 2015 to 2025 by using %s-day Breakout vs %s-day MA"% (n, n))ax.set_xlabel("Assets (Ranked by efficiency ratio from high to low)")plt.show()
Learnings
In terms of the return %, both Breakout and Moving Averages strategy they don’t differ a lot. However, they have a very distinctive risk profile.
Similarities
Differences
The returns of both systems go with a similar way. For assets that are not profitable for one strategy, it is likely to be not profitable for another strategy as well
Breakout strategy has a lot less number of trades than Moving Average strategy
For both strategies, in general (ignore ZS and GC), longer the trend, the better the performance
Breakout strategy holds a lot more longer on the assets, especially on the longer moving average, than MA trend strategy.
They both work well for trending assets, in general, and do poorly for noisy assets