avatarMichael Hsia

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

6127

Abstract

yourself before you adopt this as an executable trading strategy. Other than those, <code>the number of trades</code> and <code>the backtest time span</code> 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.</p><h1 id="d1e4">Appendix</h1><h2 id="a6e6">Code of alpha model for QuantConnect platform</h2><p id="f69f"><i>(including signal generation)</i></p><div id="e007"><pre><span class="hljs-keyword">from</span> AlgorithmImports <span class="hljs-keyword">import</span> *

<span class="hljs-keyword">class</span> <span class="hljs-title class_">SymbolData</span>:

<span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, algorithm, symbol</span>):
    self.symbol = symbol
    self.algorithm = algorithm
    self.rolling_close = RollingWindow[<span class="hljs-built_in">float</span>](<span class="hljs-number">2</span>)
    hist = algorithm.History(
        [self.symbol],
        <span class="hljs-number">4</span>,
        Resolution.Daily
    )
    <span class="hljs-keyword">if</span> <span class="hljs-string">'close'</span> <span class="hljs-keyword">in</span> hist.columns:
        self.rolling_close.Add(hist[<span class="hljs-string">'close'</span>][-<span class="hljs-number">2</span>])
        self.rolling_close.Add(hist[<span class="hljs-string">'close'</span>][-<span class="hljs-number">1</span>])
        <span class="hljs-comment"># algorithm.Debug(f'[{self.symbol}] - {self.rolling_close[0]} , {self.rolling_close[1]} init complete')</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">Update</span>(<span class="hljs-params">self, close</span>):
    self.rolling_close.Add(close)
    <span class="hljs-comment"># self.algorithm.Debug(f'[{self.symbol}] - {self.rolling_close[0]} , {self.rolling_close[1]} update complete')</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">Remove</span>(<span class="hljs-params">self</span>):
    <span class="hljs-keyword">pass</span>

<span class="hljs-meta"> @property</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">monthly_return</span>(<span class="hljs-params">self</span>): <span class="hljs-keyword">return</span> (self.rolling_close[<span class="hljs-number">0</span>] - self.rolling_close[<span class="hljs-number">1</span>])/self.rolling_close[<span class="hljs-number">1</span>]

<span class="hljs-keyword">class</span> <span class="hljs-title class_">TopNReturnAlphaModel</span>(<span class="hljs-title class_ inherited__">AlphaModel</span>): <span class="hljs-keyword">def</span> <span class="hljs-title function_">init</span>(<span class="hljs-params"> self, capacity = <span class="hljs-number">10</span>, replacement = <span class="hljs-number">5</span>, resolution = Resolution.Daily </span>): self.resolution = resolution self._changes = <span class="hljs-literal">None</span> self.capacity = capacity self.replacement = replacement self.lastMonth = -<span class="hljs-number">1</span> self.symbol_data = <span class="hljs-built_in">dict</span>() self.trade_flag = <span class="hljs-literal">True</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">Update</span>(<span class="hljs-params">self, algorithm, data</span>):
    insights = []

    <span class="hljs-keyword">if</span> algorithm.Time.month == self.lastMonth <span class="hljs-keyword">and</span> self.traded_flag == <span class="hljs-literal">True</span>:
        <span class="hljs-comment"># algorithm.Debug(f'Time is {algorithm.Time}: Data is empty')</span>
        <span class="hljs-keyword">return</span> insights
    <span class="hljs-keyword">else</span>:
        self.lastMonth = algorithm.Time.month
        self.traded_flag = <span class="hljs-literal">False</span>

    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(data.Bars) &lt;= <span class="hljs-number">0</span>:
        <span class="hljs-keyword">return</span> insights
    <span class="hljs-keyword">else</span>:
        algorithm.Debug(<span class="hljs-string">f'Time from Update: <span class="hljs-subst">{algorithm.Time}</span>'</span>)
        self.traded_flag = <span class="hljs-literal">True</span>

    addedSecuritiesSymbols = [x.Symbol <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> self._changes.AddedSecurities] <span class="hljs-keyword">if</span> self._changes <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>
    removedSecuritiesSymbols = [x.Symbol <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> self._changes.RemovedSecurities] <span class="hljs-keyword">if</span> self._changes <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>

    <span class="hljs-keyword">for</span> security <span class="hljs-keyword">in</span> algorithm.ActiveSecurities.Keys:
        <span class="hljs-keyword">if</span> addedSecuritiesSymbols:
            <span class="hljs-keyword">if</span> security <span class="hljs-keyword">in</span> addedSecuritiesSymbols:
                <span class="hljs-comment"># Add rolling window</span>
                <span class="hljs-keyword">if</span> security <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self.symbol_data.keys():
                    self.symbol_data[security] = SymbolData(algorithm, security)
                    <spa

Options

n class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> self.symbol_data[security].rolling_close.IsReady: <span class="hljs-comment"># Not ready so remove again</span> symbolData = self.symbol_data.pop(security, <span class="hljs-literal">None</span>) <span class="hljs-keyword">else</span>: <span class="hljs-comment"># Update rolling window</span> self.symbol_data[security].Update(data.Bars[security].Close)

    <span class="hljs-keyword">if</span> removedSecuritiesSymbols:
        <span class="hljs-keyword">for</span> security <span class="hljs-keyword">in</span> removedSecuritiesSymbols:
            <span class="hljs-comment"># Remove rolling window</span>
            <span class="hljs-keyword">if</span> security <span class="hljs-keyword">in</span> self.symbol_data.keys():
                symbolData = self.symbol_data.pop(security, <span class="hljs-literal">None</span>)

    sorted_symbol_data = {k:v <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> <span class="hljs-built_in">sorted</span>(
        self.symbol_data.items(),
        key = <span class="hljs-keyword">lambda</span> x: x[<span class="hljs-number">1</span>].monthly_return,
        reverse = <span class="hljs-literal">True</span>
    )[:self.capacity]}

    invested_stocks = <span class="hljs-built_in">dict</span>()
    <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> algorithm.Portfolio:
        <span class="hljs-keyword">if</span> s.Value.Invested:
            invested_stocks[s.Key] = algorithm.Portfolio[s.Key].UnrealizedProfitPercent

    invested_stocks = {k:v <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> <span class="hljs-built_in">sorted</span>(
        invested_stocks.items(),
        key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-number">1</span>],
        reverse=<span class="hljs-literal">True</span>
    )}

    algorithm.Debug(<span class="hljs-string">f'Invested number: <span class="hljs-subst">{<span class="hljs-built_in">len</span>(invested_stocks)}</span>'</span>)

    insightExpiry = Expiry.EndOfDay(algorithm.Time)
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(invested_stocks) &lt;= <span class="hljs-number">0</span>:
        <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> sorted_symbol_data.keys():
            insights.append(
                Insight.Price(
                    <span class="hljs-built_in">str</span>(s),
                    insightExpiry,
                    InsightDirection.Up,
                    <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, <span class="hljs-number">0.05</span>
                ),
            )
            <span class="hljs-comment"># algorithm.Debug(f'Buying {str(s)}')</span>
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">0</span>, self.replacement):
            <span class="hljs-comment"># Close the positions that have the worse performances</span>
            worse_stock = invested_stocks.popitem()
            insights.append(
                Insight.Price(
                    <span class="hljs-built_in">str</span>(worse_stock[<span class="hljs-number">0</span>]),
                    insightExpiry,
                    InsightDirection.Flat,
                    <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, <span class="hljs-number">0.05</span>    <span class="hljs-comment"># Weight</span>
                ),
            )
            <span class="hljs-comment"># algorithm.Debug(f'Selling {str(worse_stock[0])}')</span>

        open_positions = self.replacement
        <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> sorted_symbol_data.keys():
            <span class="hljs-keyword">if</span> open_positions &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> s <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> invested_stocks.keys():
                insights.append(
                    Insight.Price(
                        <span class="hljs-built_in">str</span>(s),
                        insightExpiry,
                        InsightDirection.Up,
                        <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, <span class="hljs-literal">None</span>, <span class="hljs-number">0.05</span>    <span class="hljs-comment"># Weight</span>
                    ),
                )
                open_positions -= <span class="hljs-number">1</span>
                <span class="hljs-comment"># algorithm.Debug(f'Buying {str(s)}')</span>

    <span class="hljs-comment"># Reset the changes</span>
    self._changes = <span class="hljs-literal">None</span>

    <span class="hljs-comment"># algorithm.Debug(f'Sent out {len(insights)} on {algorithm.Time}')</span>
    <span class="hljs-keyword">return</span> insights

<span class="hljs-keyword">def</span> <span class="hljs-title function_">OnSecuritiesChanged</span>(<span class="hljs-params">self, algorithm, changes</span>):
    self._changes = changes</pre></div><p id="8cd5"><i>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.</i></p></article></body>

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 a maximum of 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 these 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

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.

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)

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.

Quantitative Trading
Momentum Trading
Backtesting
Recommended from ReadMedium