Back to Community
ETF Region Rotation strategy

Hi together,

I am referring to a research paper by Gary Antonacci, available for free here: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1833722

Gary suggests to perform a rotation between regional ETFs, Gold and Treasuries:
IWB, IEV, EWJ, EPP, IEF, SHY, GLD

He suggests to determine the 6-Months-Momentum (e.g. about 120 days) and rebalance the portfolio every month by buying 2 or 3 ETFs with the highest Momentum(120d). He writes about annual returns of 15-30%.

My search in the forums and the help function yielded no results for this specific problem.

Could someone provide me with some help how to code this rotation strategy for backtesting? Unfortunately I am an absolute beginner to algorithmic programming and Python, but willing to learn. Some help at the start nonetheless would be great!

Thanks so much in advance, and sorry for my bad English - I am from Germany.
Best wishes,
Stephan

16 responses

Dear Klaus -
thanks a lot for your immediate response.
This was very helpful and I already had success in modifying this strategy.

  • I took out the volatility component as Gary Antonacci describes his strategy purely based on momentum
  • I changed the time window to 120 days
  • I modified the list of instruments

The following might sound a little ridiculous to an experienced programmer like you, but:

  • how can I modify this strategy so that the 2 ETFs with the highest momentum are bought? At the moment it's just the one ETF with the very highest momentum
  • where can I specify that the portfolio will be rebalanced every month?

Thanks a lot in advance. I really appreciate your help!

Stephan

# Region Rotation Strategy based on Gary Antonacci - see http://papers.ssrn.com/sol3/papers.cfm?abstract_id=1833722  
import math  
import pandas

def initialize(context):  
    context.stocks = {21516: sid(21516), 21769: sid(21769),  
                      14520: sid(14520), 23118: sid(23118),  
                      23870: sid(23870), 23911: sid(23911), 26807: sid(26807)} 

    context.month = None  
    context.period = 120 # 6 months period

def _order(stock, amount, price):  
    if amount != 0:  
        order(stock, amount)  
        log.info("%s %d shares of %s = %.2f" % \  
                 # If less than 0, it'll print out selling  
                 (["buying", "selling"][amount<0], abs(amount),  
                 stock.symbol, abs(price*amount)))

@batch_transform(window_length=120)  
def get_metrics(dp, security, period):  
    '''Calculate performance and volatility for given period.'''  
    # Get's all the close prices of the security in the last 120 days (6 months)  
    prices = dp['close_price'][security.sid][-period-1:]  
    begin, end = prices[-period], prices[-1]  
    # volatility = (pandas.rolling_std(prices,20)*math.sqrt(period/20)).mean()  
    return (end - begin)/begin #, volatility/begin

def normalise(data, stocks, period):  
    # Need to return normalised return and volume  
    stocks_ret = {}  
    #stocks_vol = {}  
    for stock in stocks.values():  
        ret = get_metrics(data, stock, period)  
        stocks_ret[stock] = ret  
       # stocks_vol[stock] = vol  
    # Return max = highest performance, while volatility max is lowest volatility  
    ret_max, ret_min  = max(stocks_ret.values()), min(stocks_ret.values()),  
    return ret_max, ret_min  


def get_best(data, stocks, period):  
    best = None  
    ret_max, ret_min = normalise(data, stocks, period)  
    for stock in stocks.values():  
        ret = get_metrics(data, stock, period)  
        #log.debug('%s: return: %.2f, vol: %.2f' % (stock.symbol, ret, vol))  
        ret = (ret-ret_min)/(ret_max-ret_min)  
        #vol = (vol-vol_min)/(vol_max-vol_min)  
        rank = ret * 1.0 #+ vol * 0.3  
        log.debug('%s: return: %.2f, rank: %.2f' % \  
                  (stock.symbol, ret, rank))  
        if best is None or best[1] < rank:  
            best = stock, rank  
    log.debug("The BEST rank is: " + best[0].symbol)  
    return best[0]


def handle_data(context, data):  
    stocks = context.stocks  
    month = data[stocks.values()[0]].datetime.month  
    if not context.month:  
        context.month = month

    ret = get_metrics(data, stocks.values()[0], context.period)  
    # check if next month began  
    if context.month == month:  
        return  
    context.month = month

    if ret:  
        stock = get_best(data, stocks, context.period)  
        positions = context.portfolio.positions  
        if positions[stock.sid]['amount']:  
            return  
        sold = 0.0  
        for position in positions.values():  
            if position.amount:  
                pstock = context.stocks[position.sid]  
                price = data[pstock].price  
                _order(pstock, -position.amount, price)  
                sold += position.amount * price  
        amount = int((context.portfolio.cash+sold)/data[stock].price)  
        _order(stock, amount, data[stock].price)  
Clone Algorithm
24
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a8009a71efea0913df12d6
There was a runtime error.

Hi Stephan
I have also just started using quantopian, but it should of course be doable :)

Dear Klaus,
dear all,

I tried for several hours now. I cannot get it wo work.
Could someone please help me and show me how to implement a function that the 2 ETFs with the highest momentum are bought?
Thanks a lot!

Hey Stephan,
It sounds like you would like to buy the 2 securities with the largest returns over the last N days. I simplified this algorithm for you, that last implementation is way overcomplicated. This version uses the schedule_function to rebalance every month, and uses history in favor of batch_transform, which has been depreciated. I hope this version is easier for you to follow, let me know if you have any questions.

David

Clone Algorithm
102
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54a850ada780880900fd3aac
There was a runtime error.

OMG David,

thanks so much. That seems to be exactly what I was looking for.
I really appreciate your help and efforts.
I just have programming skills in BASIC and HTML and am an absolute beginner to Python. Nonetheless eager to learn.
Do you have any suggestions where programming in Python with a special focus to Quantopian can be learned (steep learning curve preferred).

Thanks again so much!

Hmm, computational investing is a coursera class that uses Python, I have not taken it, but it might be on my to-do list now. I would search on coursera.org and edx.org for relevant classes. I'm a fan of doing puzzles and problems when learning a new language, you could check out codewars.com, checkio.org, and projecteuler.net if that suits you. As far as data structures go, you will benefit a lot from learning to work with the pandas and numpy libs. Python for econometrics looks like a good reference.

This thread has some more suggestions too. https://www.quantopian.com/posts/suggestions-on-quant-finance-books-slash-texts

Welcome to Quantopian and have fun learning.

David

Wow David, lots of suggestions, thanks! I will try to find one or two that fit for my purposes.
One more questions to the algorithm you posted:
The term "context.period" which we set to 120 - does it refer to calendar days or trading days??
If it is trading days then 120 might equate 6 months, otherwise in the case of calendar days the period would have to be set to about 150....Thx!

I can vouch for the CompInvest class on coursera. T. Balch has some considerable wisdom to impart. And his LucenaResearch and Stumpgrinder sites are also helpful. You'll end up climbing a steep python learning curve (at least I did) but there were many helpful TAs and students to assist. My particular take, an objected oriented one, was generally derided, so you might want to prepare your mind for matrix algebra. A topic I still can't quite grok.

Dave, couldn't you just use the number of periods to fetch for the return calculation?

prices = history(context.period, '1d', 'price')  

And, like I've been personally struggling with, the returns for only 2014 are sub-par.

@Market Tech, you're correct about passing context.period into history, that enhancement must have snuck by me, it used to be a C-like macro that parsed for history functions at the beginning of the backtest. Thanks for pointing that out, you have just made my life slightly easier.

@Stephan, the 'bar_count' argument in history is the number of trading days (or minutes if '1m' is the frequency you use).

Dear all,

may I once again ask for your help? I studied all night and tried to learn python basics and do some coding.
Let's say I want to modify the above algorithm so that stocks are taken out of the list (context.stocks) that are trading below their 200-SMA.
From the remaining stocks within the list (which are then trading above their 200 SMA) the 2 with the highest momentum shall be bought.
I tried coding the following but I am told "28 Error SyntaxError: invalid syntax" - this is the line "def handle_data(context, data):"

Actually I added only a few lines to the original code. The one in the beginning to define SMA and the ones beginning from line 28.
Can anybody offer some help what I am doing wrong? Thanks a lot on advance!!

# Global Market Rotation Strategy  
sma = ta.SMA(timeperiod=200)

def initialize(context):  
    context.stocks = {  
        21516: sid(21516), # IWB Russell 2000  
        21769: sid(21769), # IEV (ISHARES EUROPE ETF)  
        14520: sid(14520), # EWJ (Japan)  
        23118: sid(23118), # EPP (ISHARES MSCI PACIFIC EX JAPAN)  
        23870: sid(23870), # IEF (VANGUARD 7-10 years bond)  
        23911: sid(23911), # SHY (1-3 years Treasury)  
        26807: sid(26807), # GLD Gold  
        #23921: sid(23921), #20y T Bonds  
        #41715: sid(41715), # Aggregate Bonds  
        #36201: sid(36201), # RWO (Dow Jones Global Real Estate REIT)  
        #24700: sid(24700), # DJP Bloomberg Commodity Index  
        #40513: sid(40513), # ZIV Midterm Inverse VIX  
        #38055: sid(38055), # VXZ VIX Midterm  
    }  

    context.period = 120 # 6 months period  
    schedule_function(rebalance, date_rule=date_rules.month_start())  
    context.buy_count = 2  
    context.pct_per_stock = 0.5

def handle_data(context, data)  
    for stock in context.stocks:  
        sma_data = sma(data)  
        stock_sma = sma_data[stock]  
        price = data[stock].price  
i=0  
while i<len(context.stocks):  
    if data[stock].close_price < stock_sma:  
        del context.stocks[i]  
    else:  
        i+=1        



def rebalance(context, data):  
    prices = history(context.period, '1d', 'price').iloc[-context.period:]  
    # Drop stocks with NaN values  
    prices = prices.dropna(axis=1)  
    # Calculate returns  
    returns = (prices.iloc[-1] - prices.iloc[0]) / prices.iloc[0]  
    # Sort by returns in ascending order  
    returns.sort()  
    # The last two securities have the highest returns  
    winners = returns.iloc[-context.buy_count:]  
    for stock in data:  
        if stock in winners.index:  
            order_target_percent(stock, context.pct_per_stock)  
        else:  
            order(stock, -context.portfolio.positions[stock].amount)  
def handle_data(context, data):  
    pass  

Figured it out myself, this seems to be the solution:

# Global Market Rotation Strategy  
sma = ta.SMA(timeperiod=200)

def initialize(context):  
    context.stocks = {  
        21516: sid(21516), # IWB Russell 2000  
        21769: sid(21769), # IEV (ISHARES EUROPE ETF)  
        14520: sid(14520), # EWJ (Japan)  
        23118: sid(23118), # EPP (ISHARES MSCI PACIFIC EX JAPAN)  
        23870: sid(23870), # IEF (VANGUARD 7-10 years bond)  
        23911: sid(23911), # SHY (1-3 years Treasury)  
        26807: sid(26807), # GLD Gold  
        #23921: sid(23921), #20y T Bonds  
        #41715: sid(41715), # Aggregate Bonds  
        #36201: sid(36201), # RWO (Dow Jones Global Real Estate REIT)  
        #24700: sid(24700), # DJP Bloomberg Commodity Index  
        #40513: sid(40513), # ZIV Midterm Inverse VIX  
        #38055: sid(38055), # VXZ VIX Midterm  
    }  
    context.period = 120 # 6 months period  
    schedule_function(rebalance, date_rule=date_rules.month_start())  
    context.buy_count = 2  
    context.pct_per_stock = 0.5

def handle_data(context, data):  
    for stock in data:  
        sma_data = sma(data)  
        stock_sma = sma_data[stock]  
        i=0  
        while i<len(data):  
          if data[stock].close_price < stock_sma:  
           del data[i]  
          else:  
           i+=1        



def rebalance(context, data):  
    prices = history(context.period, '1d', 'price').iloc[-context.period:]  
    # Drop stocks with NaN values  
    prices = prices.dropna(axis=1)  
    # Calculate returns  
    returns = (prices.iloc[-1] - prices.iloc[0]) / prices.iloc[0]  
    # Sort by returns in ascending order  
    returns.sort()  
    # The last two securities have the highest returns  
    winners = returns.iloc[-context.buy_count:]  
    for stock in data:  
        if stock in winners.index:  
            order_target_percent(stock, context.pct_per_stock)  
        else:  
            order(stock, -context.portfolio.positions[stock].amount)  
def handle_data(context, data):  
    pass  

Sorry...thought I got it but obviously didn't.
It doesn't make any difference so I think the list is not modified and the stocks trading below their SMA are not deleted.
Any suggestions? Thx

Stephan,
You shouldn't have to delete the stocks from the data object, I would just add another condition to the buy criteria, (throw an 'and' in there). It looks like there is not much difference anyway when adding the moving average screen so maybe what you were doing did work. You would have to look at the transactions and positions values sections on the full backtest page to see if there are any days where it has less than 2 stocks. I added a condition to make sure the stock is above its 200 day moving average in this version.
David

Clone Algorithm
102
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 54ad7cad503b8879bab29b72
There was a runtime error.

Dear David,

thanks so much for all your help, time and efforts. It is great having such highly qualified members here willing to help.
All the best!
Stephan

@david: i love your simple and powerfull code!
Here is with some different inputs.

Clone Algorithm
225
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Backtest ID: 5514e20662a40b30a7369660
There was a runtime error.