Skip to content
All library documents

Monthly ETF Rotation by Weighted One- and Three-Month Momentum

Article Strategy library · Author: QuantConnect

Summary

This algorithm ranks a universe of growth and safety ETFs using a weighted combination of one month and three month performance. About every month, it selects the symbol with the highest score. If that top-ranked fund has a positive score, the algorithm liquidates other holdings when needed and invests available margin in it; if the best score is not positive, it moves to cash. The listed growth funds cover US mid-cap, European, emerging-market, Latin American, and Pacific ex-Japan equities, while the safety choices are Treasury funds.

The document is a code example, not a performance study. It specifies a starting date in 2007 and initializes a cash balance, but gives no end date, benchmark, backtest results, or risk statistics. It also does not explain how to handle missing history or transaction costs. The strategy therefore illustrates a simple relative-momentum allocation and defensive cash decision, but supplies no evidence that its scoring weights, monthly interval, or fund universe are optimal or profitable.

Key ideas

  • The algorithm ranks growth and Treasury ETFs using a weighted score based on one and three month returns.
  • It rotates approximately monthly into the highest-scoring fund when that score is positive.
  • When the best score is not positive, it liquidates holdings and stays in cash.
  • The example provides no benchmark, performance results, or risk statistics to assess the approach.

Tags

Full text
# ETFGlobalRotationAlgorithm


# ETFGlobalRotationAlgorithm









Strategy example using a portfolio of ETF Global Rotation Strategy example using a portfolio of ETF Global Rotation

## Source (Apache-2.0)

```python
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from AlgorithmImports import *

### <summary>
### Strategy example using a portfolio of ETF Global Rotation
### </summary>
### <meta name="tag" content="strategy example" />
### <meta name="tag" content="momentum" />
### <meta name="tag" content="using data" />

### <summary>
### Strategy example using a portfolio of ETF Global Rotation
### </summary>
### <meta name="tag" content="strategy example" />
### <meta name="tag" content="momentum" />
### <meta name="tag" content="using data" />
class ETFGlobalRotationAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_cash(25000)
        self.set_start_date(2007,1,1)
        self.last_rotation_time = datetime.min
        self.rotation_interval = timedelta(days=30)
        self.first = True

        # these are the growth symbols we'll rotate through
        growth_symbols =["MDY", # US S&P mid cap 400
                        "IEV", # i_shares S&P europe 350
                        "EEM", # i_shared MSCI emerging markets
                        "ILF", # i_shares S&P latin america
                        "EPP" ] # i_shared MSCI Pacific ex-Japan

        # these are the safety symbols we go to when things are looking bad for growth
        safety_symbols = ["EDV", "SHY"] # "EDV" Vangaurd TSY 25yr, "SHY" Barclays Low Duration TSY
        # we'll hold some computed data in these guys
        self.symbol_data = []
        for symbol in list(set(growth_symbols) | set(safety_symbols)):
            self.add_security(SecurityType.EQUITY, symbol, Resolution.MINUTE)
            self.one_month_performance = self.mom(symbol, 30, Resolution.DAILY)
            self.three_month_performance = self.mom(symbol, 90, Resolution.DAILY)
            self.symbol_data.append((symbol, self.one_month_performance, self.three_month_performance))

    def on_data(self, data):
        # the first time we come through here we'll need to do some things such as allocation
        # and initializing our symbol data

        if self.first:
            self.first = False
            self.last_rotation_time = self.time
            return
        delta = self.time - self.last_rotation_time
        if delta > self.rotation_interval:
            self.last_rotation_time = self.time

            ordered_obj_scores = sorted(self.symbol_data, key=lambda x: Score(x[1].current.value,x[2].current.value).objective_score(), reverse=True)
            for x in ordered_obj_scores:
                self.log(">>SCORE>>" + x[0] + ">>" + str(Score(x[1].current.value,x[2].current.value).objective_score()))
            # pick which one is best from growth and safety symbols
            best_growth = ordered_obj_scores[0]
            if Score(best_growth[1].current.value,best_growth[2].current.value).objective_score() > 0:
                if (self.portfolio[best_growth[0]].quantity == 0):
                    self.log("PREBUY>>LIQUIDATE>>")
                    self.liquidate()
                self.log(">>BUY>>" + str(best_growth[0]) + "@" + str(100 * best_growth[1].current.value))
                qty = self.portfolio.margin_remaining / self.securities[best_growth[0]].close
                self.market_order(best_growth[0], int(qty))
            else:
            # if no one has a good objective score then let's hold cash this month to be safe
                self.log(">>LIQUIDATE>>CASH")
                self.liquidate()

class Score(object):

    def __init__(self,one_month_performance_value,three_month_performance_value):
        self.one_month_performance = one_month_performance_value
        self.three_month_performance = three_month_performance_value

    def objective_score(self):
        weight1 = 100
        weight2 = 75
        return (weight1 * self.one_month_performance + weight2 * self.three_month_performance) / (weight1 + weight2)

```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.