0%

Is my trading strategy one step away from making a fortune? - From research to backtest

After reading the post How to Improve Investment Portfolio with Rebalancing Strategy in Python by Bee Guan Teo, I was thrilled to know that this trading strategy can be that powerful and the portfolio return is greater than any of my existing trading strategies. Therefore I decided to give it a try and backtest this strategy to verify the profitability it claimed.

Extracting the essences of the strategy

First of all, we extracted several things from the post in order to formulate the skeleton of our backtest strategy.

Perform the autopsy on the trading strategy

1. Platform

I’m using QuantConnect to backtest this seemingly lucrative strategy.

2. Universe

Instead of using a fixed set of stocks as in the original article, I’m using the following rules to filter the stocks that are similar in nature:

  1. Rank all the stocks by DollarVolume
  2. Choose stocks that are in NASDAQ and NYSE
  3. Filter out the stocks that are listed less than 180 days (3 months)
  4. Filter out the companies whose market capitalization are less than 500 million dollars
  5. Lastly, we sorted all the remaining stocks by Dollar Volume, and limit them to top either 100 or 200 stocks

3. Rebalancing strategy

  1. As instructed in the article, we keep maximum five stocks that are most likely to rise.
  2. We assign weight to each position evenly.
  3. We don’t adjust the weight of each stock until we close this positions.

4. Signal generation

The strategy described in the article essentially is a kind of momentum strategy. It assumes that the stocks will continue to have great performances when they have good performances in the previous month. By holding this assumption, the author suggested:

  1. Every month we long five stocks that have the best monthly return in the previous month.
  2. In the next month, we abandon two stocks that have the worst performance and
  3. Replace them with the other two stocks that have good monthly returns in the previous month.

5. Other parameters

Other than the size of our universe, we pick backtest time frame as another parameter for us to test against with. In the end, we’re going to conduct four backtests:

  1. Limit the number of stocks in the universe to 100 and backtest it for 2 years
  2. Limit the number of stocks in the universe to 100 and backtest it for 5 years
  3. Limit the number of stocks in the universe to 200 and backtest it for 2 years
  4. Limit the number of stocks in the universe to 200 and backtest it for 5 years

Backtest results

Momentum-100-2y Momentum-100-5y Momentum-200-2y Momentum-100-5y
No. of trade 93 233 93 235
Return 577.34% 162.471% 230.94 % 738.94 %
Annual Return 178.398% 21.922% 89.750% 54.789%
Annual Standard Deviation 0.546 0.469 0.476 0.373
Max D.D. 46.5% 66.9% 37.800% 43.000%
Beta 0.947 1.127 0.846 1.107
Alpha 1.186 0.132 0.594 0.306
Sharpe Ratio 2.471 0.594 1.555 1.205

Backtest results summary

Wow! Even though the backtest time span has across 5 years, the annual return rates look promising, ranging from 21% to 178%. Sharpe ratios are also telling us that we’re making a good amount of money under reasonable risk. Even though the max drawdown is a bit intimidatingly high, the huge amount of compensation looks lucrative enough to take that degree of risk. In the end, by looking at the stats of these backtests, this strategy has the potential to make some hard coin!

Hold on. Take a step back and don’t jump the gun. Let’s do a double check by looking at the return diagram of the described scenario respectively. See whether we can discover some patterns that are not hidden behind these numbers.

Duration \
# Universe
100 200
2 years

Limit universe to contain 100 stocks
from 2019/12/02 to 2021/10/13

Limit universe to contain 200 stocks
from 2019/12/02 to 2021/10/13

5 years

Limit universe to contain 100 stocks
from 2016/12/02 to 2021/10/13

Limit universe to contain 200 stocks
from 2016/12/02 to 2021/10/13

Did you notice the pattern in the above four diagrams? Apparently, the majority of the portfolio return is generated starting the late 2020s. Any time before the late 2020s actually very little return over years. This fact indicates that the portfolio return over years is contributed by luck so that we can capture this uprising wave. From the asset sales volume distribution diagram in the backtest below, you can tell that most of the return is contributed by Tesla (TSLA), ROKU (ROKU), Advanced Micro Devices (AMD), and GameStop (GME), which are highly volatile and unpredictable MEME stocks.

Asset sales volume over time

It’s a fair question to ask that whether this type of opportunity will continue to be captured by this strategy. Also, what if your capital won’t sustain until the day you capture this opportunity? These are the objective questions that you need to ask yourself before you adopt this as an executable trading strategy. Other than those, the number of trades and the backtest time span are also the important KPIs that I keep my eyes on. I didn’t mean to judge whether this is a good or bad strategy, but knowing the answers to the above questions can help you adjust the position of this strategy in your investment portfolio.

Appendix

Code of alpha model for QuantConnect platform

(including signal generation)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
from AlgorithmImports import *

class SymbolData:

def __init__(self, algorithm, symbol):
self.symbol = symbol
self.algorithm = algorithm
self.rolling_close = RollingWindow[float](2)
hist = algorithm.History(
[self.symbol],
4,
Resolution.Daily
)
if 'close' in hist.columns:
self.rolling_close.Add(hist['close'][-2])
self.rolling_close.Add(hist['close'][-1])
# algorithm.Debug(f'[{self.symbol}] - {self.rolling_close[0]} , {self.rolling_close[1]} init complete')

def Update(self, close):
self.rolling_close.Add(close)
# self.algorithm.Debug(f'[{self.symbol}] - {self.rolling_close[0]} , {self.rolling_close[1]} update complete')

def Remove(self):
pass

@property
def monthly_return(self):
return (self.rolling_close[0] - self.rolling_close[1])/self.rolling_close[1]

class TopNReturnAlphaModel(AlphaModel):
def __init__(
self,
capacity = 10,
replacement = 5,
resolution = Resolution.Daily
):
self.resolution = resolution
self._changes = None
self.capacity = capacity
self.replacement = replacement
self.lastMonth = -1
self.symbol_data = dict()
self.trade_flag = True


def Update(self, algorithm, data):
insights = []

if algorithm.Time.month == self.lastMonth and self.traded_flag == True:
# algorithm.Debug(f'Time is {algorithm.Time}: Data is empty')
return insights
else:
self.lastMonth = algorithm.Time.month
self.traded_flag = False

if len(data.Bars) <= 0:
return insights
else:
algorithm.Debug(f'Time from Update: {algorithm.Time}')
self.traded_flag = True

addedSecuritiesSymbols = [x.Symbol for x in self._changes.AddedSecurities] if self._changes is not None else None
removedSecuritiesSymbols = [x.Symbol for x in self._changes.RemovedSecurities] if self._changes is not None else None

for security in algorithm.ActiveSecurities.Keys:
if addedSecuritiesSymbols:
if security in addedSecuritiesSymbols:
# Add rolling window
if security not in self.symbol_data.keys():
self.symbol_data[security] = SymbolData(algorithm, security)
if not self.symbol_data[security].rolling_close.IsReady:
# Not ready so remove again
symbolData = self.symbol_data.pop(security, None)
else:
# Update rolling window
self.symbol_data[security].Update(data.Bars[security].Close)

if removedSecuritiesSymbols:
for security in removedSecuritiesSymbols:
# Remove rolling window
if security in self.symbol_data.keys():
symbolData = self.symbol_data.pop(security, None)

sorted_symbol_data = {k:v for k, v in sorted(
self.symbol_data.items(),
key = lambda x: x[1].monthly_return,
reverse = True
)[:self.capacity]}

invested_stocks = dict()
for s in algorithm.Portfolio:
if s.Value.Invested:
invested_stocks[s.Key] = algorithm.Portfolio[s.Key].UnrealizedProfitPercent

invested_stocks = {k:v for k, v in sorted(
invested_stocks.items(),
key=lambda x: x[1],
reverse=True
)}

algorithm.Debug(f'Invested number: {len(invested_stocks)}')

insightExpiry = Expiry.EndOfDay(algorithm.Time)
if len(invested_stocks) <= 0:
for s in sorted_symbol_data.keys():
insights.append(
Insight.Price(
str(s),
insightExpiry,
InsightDirection.Up,
None, None, None, 0.05
),
)
# algorithm.Debug(f'Buying {str(s)}')
else:
for _ in range(0, self.replacement):
# Close the positions that have the worse performances
worse_stock = invested_stocks.popitem()
insights.append(
Insight.Price(
str(worse_stock[0]),
insightExpiry,
InsightDirection.Flat,
None, None, None, 0.05 # Weight
),
)
# algorithm.Debug(f'Selling {str(worse_stock[0])}')

open_positions = self.replacement
for s in sorted_symbol_data.keys():
if open_positions > 0 and s not in invested_stocks.keys():
insights.append(
Insight.Price(
str(s),
insightExpiry,
InsightDirection.Up,
None, None, None, 0.05 # Weight
),
)
open_positions -= 1
# algorithm.Debug(f'Buying {str(s)}')

# Reset the changes
self._changes = None

# algorithm.Debug(f'Sent out {len(insights)} on {algorithm.Time}')
return insights

def OnSecuritiesChanged(self, algorithm, changes):
self._changes = changes

Disclaimer: Nothing herein is financial advice or even a recommendation to trade real money. Many platforms exist for simulated trading (paper trading) which can be used for building and developing the strategies discussed. Please use common sense and consult a professional before trading or investing your hard-earned money.

Enjoy reading? Some donations would motivate me to produce more quality content