Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Rotating ETF Algo [Broken]

This algo is basically a Frankenstein of code that I've cobbled together, but is currently broken and will not run. Would one of you guys with more experience then I have take a look at it and see if it can be fixed?

# Import the libraries that will be used in the algorithm  
import statsmodels.api as sm  
import talib

# "initialize(context)" is a required setup method for initializing state or  
# other bookkeeping. This method is called only once at the beginning of your  
# algorithm. "context" is an augmented Python dictionary used for maintaining  
# state during your backtest or live trading session. "context" should be used  
# instead of global variables in the algorithm. Properties can be accessed  
# using dot notation (ex. "context.some_property").  
def initialize(context):

# Ensure that only long positions are held in the portfolio  
    set_long_only()

# Define the securities that will be used within each portfolio variant.  
    context.aggressive =    [  
                            sid(25902), # Vanguard Consumer Discretionary ETF  
                            sid(25903), # Vanguard Consumer Staples ETF  
                            sid(26667), # Vanguard Energy ETF  
                            sid(25904), # Vanguard Financials ETF  
                            sid(25906), # Vanguard Health Care ETF  
                            sid(26668), # Vanguard Industrials ETF  
                            sid(25905), # Vanguard Information Technology ETF  
                            sid(25898), # Vanguard Materials ETF  
                            sid(26669), # Vanguard REIT ETF  
                            sid(26670), # Vanguard Telecommunication Services ETF  
                            sid(25908) # Vanguard Utilities ETF  
                            ]  
    context.defensive =     [  
                            sid(22887), # Vanguard Extended Duration Treasury ETF  
                            sid(33650), # Vanguard Intermediate-Term Bond ETF  
                            sid(38984), # Vanguard Intermediate-Term Government Bond ETF  
                            sid(33649), # Vanguard Long-Term Bond ETF  
                            sid(38988), # Vanguard Long-Term Government Bond ETF  
                            sid(38983), # Vanguard Mortgage-Backed Securities ETF  
                            sid(33651), # Vanguard Short-Term Bond ETF  
                            sid(38986), # Vanguard Short-Term Government Bond ETF  
                            sid(43529), # Vanguard Short-Term Inflation-Protected Securities ETF  
                            sid(33652), # Vanguard Total Bond Market ETF  
                            sid(38987), # Vanguard Intermediate-Term Corporate Bond ETF  
                            sid(38982), # Vanguard Long-Term Corporate Bond ETF  
                            sid(38985), # Vanguard Short-Term Corporate Bond ETF  
                            sid(49366), # Vanguard Tax-Exempt Bond ETF  
                            ]

# Define the benchmark security the algorithm will use to guage the overall  
# direction of the market.  
    context.benchmark = sid(8554) # SPDR S&P 500 ETF Trust

# Define the leverage that the algorithm will use. The inverse of the defined  
# amount will be held as cash.  
    context.leverage = 0.9 # Hold 10% of the total portfolio value as cash

# Define the lookback period (in days) that the algorithm will use to calculate  
# RSI.  
    context.fast_lookback = 100  
    context.slow_lookback = 200

# Define the rebalance schedule that the algorithm will use.  
    schedule_function(rebalance, date_rule = date_rules.week_start(days_offset = 2), time_rule = time_rules.market_open(minutes = 90)) # Rebalance every Wednesday 90 minutes after market open

# Define the regression calculation that the algorithm will use.  
def regression(context, data, price):  
    X = range(len(price))  
    A = sm.add_constant(X)  
    Y = price.values  
    result = sm.OLS(Y, A).fit()  
    (b, a) = result.params  
    slope = a / b * 252.0  
    return slope

# Define the parameters used to calculate RSI that the algorithm will use.  
def find_rsi(context, price):  
    fast_rsi = []  
    slow_rsi = []  
    rsi = talib.RSI(price, timeperiod = 20)  
    for i in range(1, context.fast_lookback + 1):  
        fast_rsi.append(rsi[-i])  
    for i in range(1, context.slow_lookback + 1):  
        slow_rsi.append(rsi[-i])  
    fast = sum(fast_rsi) / len(fast_rsi)  
    slow = sum(slow_rsi) / len(slow_rsi)  
    return {'fast':fast, 'slow':slow}

# Calculate the RSI of the benchmark security in order to guage the overall  
# direction of the market.  
def check_benchmark_rsi(context, data):  
    price = data.history(context.benchmark, 'price', 250, '1d')  
    rsi = find_rsi(context, price)  
    fast_rsi = rsi['fast']  
    slow_rsi = rsi['slow']  
    if fast_rsi > 50:  
        return True  
    elif slow_rsi < 50:  
        return False  
    else:  
        return True

# Define the rules and weights for each portfolio variant during rebalancing  
# that the algorithm will use.  
def rebalance(context, data):  
    if check_benchmark_rsi(context, data) == True:  
        price = data.history(context.aggressive, 'price', 250, '1d')  
        momentum_list = []  
        momentum_dict = {}  
    for s in context.aggressive:  
        momentum = regression(context, data, price[s])  
        momentum_list.append(momentum)  
        momentum_dict[s] = momentum  
    momentum_list.sort()  
    for s in context.aggressive:  
        if momentum_dict[s] == momentum_list[-1]:  
            order_target_percent(s, context.leverage * 0.25)  
        elif momentum_dict[s] == momentum_list[-2]:  
            order_target_percent(s, context.leverage * 0.25)  
        elif momentum_dict[s] == momentum_list[-3]:  
            order_target_percent(s, context.leverage * 0.25)  
        elif momentum_dict[s] == momentum_list[-4]:  
            order_target_percent(s, context.leverage * 0.25)

        if check_benchmark_rsi(context, data) == False:  
            price = data.history(context.defensive, 'price', 250, '1d')  
            momentum_list = []  
            momentum_dict = {}  
        for s in context.defensive:  
            momentum = regression(context, data, price[s])  
            momentum_list.append(momentum)  
            momentum_dict[s] = momentum  
        momentum_list.sort()  
        for s in context.defensive:  
            if momentum_dict[s] == momentum_list[-1]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-2]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-3]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-4]:  
                order_target_percent(s, context.leverage * 0.25)
21 responses

I managed to somehow fix some of it, but I still feel like something about my algo isn't 'quite right'. If anyone else would be so kind as to take a look, it would be very much appreciated. Backtest attached this time!

Try fixing indents in rebalance(), something like:

def rebalance(context, data):  
    if check_benchmark_rsi(context, data) == True:  
        price = data.history(context.aggressive, 'price', 250, '1d')  
        momentum_list = []  
        momentum_dict = {}  
        for s in context.aggressive:  
            momentum = regression(context, data, price[s])  
            momentum_list.append(momentum)  
            momentum_dict[s] = momentum  
        momentum_list.sort()  
        for s in context.aggressive:  
            if momentum_dict[s] == momentum_list[-1]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-2]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-3]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-4]:  
                order_target_percent(s, context.leverage * 0.25)

    if check_benchmark_rsi(context, data) == False:  
        price = data.history(context.defensive, 'price', 250, '1d')  
        momentum_list = []  
        momentum_dict = {}  
        for s in context.defensive:  
            momentum = regression(context, data, price[s])  
            momentum_list.append(momentum)  
            momentum_dict[s] = momentum  
        momentum_list.sort()  
        for s in context.defensive:  
            if momentum_dict[s] == momentum_list[-1]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-2]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-3]:  
                order_target_percent(s, context.leverage * 0.25)  
            elif momentum_dict[s] == momentum_list[-4]:  
                order_target_percent(s, context.leverage * 0.25)  

If you check out the backtest I posted, that was one quick thing I noticed. I write my code in Atom, and then copy it to the Quantopian IDE, which might be why I missed it during the first pasteeover. If you notice anything else, let me know. This code is kind of a hot mess right, and desperately needs a rework once most of the bugs are out.

Delman,

I see two "bugs" in your algorithm. One, during re-balancing there isn't a way to sell securities that aren't still in the top 4. Notice on your backtest "daily positions" you sometime have 5 securities. Two, the regression function is returning NANs in some cases (maybe there isn't data for some day?). When this happens those securities don't get ordered. Notice in your backtest "daily positions" you sometime have 3 securities. Maybe put a check in the regression function to eliminate NANs?

The second point highlights another issue which I'm not sure you really meant in the code. I believe you want to buy the top 4 securities with the highest momentum. The straightforward way to do this would be to take the securities, sort them by momentum, and take the top 4. However, the code does this in a roundabout fashion by just sorting the momentum values and then comparing the momentum of each security to those sorted values. If the momentums match, then the security is bought. This approach causes several issues. First, in the case where the momentum is NAN, the statement NAN == NAN will always return false (just the way Python works). Second, there is the remote remote chance that two securities may have the same momentum. In this case your algorithm would buy more than 4 securities. Thirdly, the momentums are real numbers. It's generally not good practice to compare real numbers for equality (though it does usually work). It works here because the momentum stored for the security is a reference to the same value returned from the regression function.

Also, as a side note. You may want to re-write this using pipelines and the built in RSI and RollingLinearRegressionOfReturns factors instead of using statsmodel and talib.

Good luck,

Dan

Dan,
Thank you for all of your insight. I knew my algo was a little wonky, but had no idea that it was that bad! I think everything you talked about is a little outside my skill level right now, since I'm basically learning python and how everything works as I go along. I might revisit this in a few months after I've learned a little bit more.

I think this does what you want, and it should be more dependable/faster since it uses Pipeline. I wasn't completely sure what parameters you intended to use for the rolling linear regression, so you should double-check the inputs under make_pipeline().
As far as I'm aware, there's no way to feed context data into the make_pipeline function, so any securities you choose to add to your aggressive/defensive lists must also be hard coded into the pipeline function.

----made some small edits to the code

Tom, that looks so much cleaner then what I originally had! I'm to clone it tonight as see if I can figure out what each piece does and how it relates to what I was originally going for. Thank you so much! The dream is still alive!

No problem, glad you found it helpful.

I found that you can pass the security lists as global variables to pipeline, and you could consider using RSI to further filter the securities you choose to trade beyond taking the 4 with greatest momentum (which I assume refers to the beta coefficient). Both updates and annotations were added in the backtest below

The pipeline framework also makes it easier to consider a dynamic set of securities, based on weekly market conditions, if you wanted to move beyond the fixed set of predetermined ETFs. I think that would be necessary if you wanted to achieve more aggressive returns using your strategy.

edit: actually you can pass context to Pipeline... below should work

from quantopian.pipeline import Pipeline, CustomFilter  
from quantopian.algorithm import attach_pipeline, pipeline_output  
from quantopian.pipeline.data.builtin import USEquityPricing  
from quantopian.pipeline.factors import RSI, RollingLinearRegressionOfReturns as RLR  
import numpy as np


# "initialize(context)" is a required setup method for initializing state or  
# other bookkeeping. This method is called only once at the beginning of your  
# algorithm. "context" is an augmented Python dictionary used for maintaining  
# state during your backtest or live trading session. "context" should be used  
# instead of global variables in the algorithm. Properties can be accessed  
# using dot notation (ex. "context.some_property").  
def initialize(context):

# Ensure that only long positions are held in the portfolio  
    set_long_only()

# Assign global variables to context object for efficient access in further functions  
# Define the securities that will be used within each portfolio variant.  
    context.aggressive = [  
        sid(25902), # Vanguard Consumer Discretionary ETF  
        sid(25903), # Vanguard Consumer Staples ETF  
        sid(26667), # Vanguard Energy ETF  
        sid(25904), # Vanguard Financials ETF  
        sid(25906), # Vanguard Health Care ETF  
        sid(26668), # Vanguard Industrials ETF  
        sid(25905), # Vanguard Information Technology ETF  
        sid(25898), # Vanguard Materials ETF  
        sid(26669), # Vanguard REIT ETF  
        sid(26670), # Vanguard Telecommunication Services ETF  
        sid(25908) # Vanguard Utilities ETF  
    ]  
    context.defensive = [  
        sid(22887), # Vanguard Extended Duration Treasury ETF  
        sid(33650), # Vanguard Intermediate-Term Bond ETF  
        sid(38984), # Vanguard Intermediate-Term Government Bond ETF  
        sid(33649), # Vanguard Long-Term Bond ETF  
        sid(38988), # Vanguard Long-Term Government Bond ETF  
        sid(38983), # Vanguard Mortgage-Backed Securities ETF  
        sid(33651), # Vanguard Short-Term Bond ETF  
        sid(38986), # Vanguard Short-Term Government Bond ETF  
        sid(43529), # Vanguard Short-Term Inflation-Protected Securities ETF  
        sid(33652), # Vanguard Total Bond Market ETF  
        sid(38987), # Vanguard Intermediate-Term Corporate Bond ETF  
        sid(38982), # Vanguard Long-Term Corporate Bond ETF  
        sid(38985), # Vanguard Short-Term Corporate Bond ETF  
        sid(49366), # Vanguard Tax-Exempt Bond ETF  
    ]  
# Define the benchmark security the algorithm will use to guage the overall market  
    context.benchmark = sid(8554)  

    attach_pipeline(make_pipeline(context), 'pipe')  

# Define the leverage that the algorithm will use. The inverse of the defined  
# amount will be held as cash.  
    context.leverage = 1.0 


# Define the rebalance schedule that the algorithm will use.  
    schedule_function(rebalance, date_rule = date_rules.week_start(days_offset = 2), time_rule = time_rules.market_open(minutes = 90)) # Rebalance every Wednesday 90 minutes after market open

def before_trading_start(context, data):  
    data = pipeline_output('pipe')  
    # Read in RSI fields from Pipeline into context objects  
    context.fast_rsi = data['fast_rsi']  
    context.slow_rsi = data['slow_rsi']  
    rlr = data['rlr']  
    # Select beta at index=1 from rlr tuple given from Pipeline  
    context.beta = rlr.map(lambda rlr: rlr[1])

    # Record gross leverage from account positions  
    # Note that gross_leverage = net_leverage for long only portfolio  
    record(gross_leverage = context.account.leverage)


# Define the rules and weights for each portfolio variant during rebalancing  
# that the algorithm will use.  
# Variable "s" refers to securities passed by iterations of the for loops  
def rebalance(context, data):  
    # Define maximum number of unique positions in portfolio  
    size = 4  
    # Assign portfolio position objects to variable  
    port = context.portfolio.positions  
    # Declare empty list to be populated with securities we intend to trade  
    longs = []  

    # Determine securities to long based on market conditions (represente by RSI)  
    if context.fast_rsi[context.benchmark] > 50:  
        # Sort betas for aggressive securities from highest to lowest  
        betas = context.beta[context.aggressive].sort(ascending=False, inplace=False)  
        # Filter for relatively underpriced securities by RSI  
        for s in betas.index:  
            if context.fast_rsi[s] < 50.0 and len(longs) <= size and data.can_trade(s):  
                longs.append(s)

    elif context.slow_rsi[context.benchmark] < 50:  
        # Sort betas for defensive securities from lowest to highest  
        betas = context.beta[context.defensive].sort(ascending=True, inplace=False)  
        # Filter for relatively underpriced securities by RSI  
        for s in betas.index:  
            if context.slow_rsi[s] < 50.0 and len(longs) <= size and data.can_trade(s):  
                longs.append(s)  
    else:  
        # Sort betas for aggressive securities from highest to lowest  
        betas = context.beta[context.aggressive].sort(ascending=False, inplace=False)  
        # Filter for relatively underpriced securities by RSI  
        for s in betas.index:  
            if context.fast_rsi[s] < 50.0 and len(longs) <= size and data.can_trade(s):  
                longs.append(s)  
    # Close previous positions from last week that aren't being longed for this week  
    for s in port:  
        if s not in longs:  
            order_target(s,0)  
    # Open new positions for current week  
    for s in longs:  
        order_target_percent(s, context.leverage/len(longs))  

# Function to convert list of assets into Pipeline filter for such assets  
def spec_assets(assets):  
    sids = set(map(int, assets))  
    is_my_sid = np.vectorize(lambda sid: sid in sids)

    class SpecificAssets(CustomFilter):  
        inputs = ()  
        window_length = 1  
        def compute(self, today, assets, out):  
            out[:] = is_my_sid(assets)

    return SpecificAssets()  

# Create Pipeline  
def make_pipeline(context):  
    # Security list from union of aggressive, defensive, and benchmark security lists  
    all_secs = list(set().union(context.aggressive,context.defensive,[context.benchmark]))  
    # Convert security list to Pipeline filter object  
    asset_filter = spec_assets(all_secs)  


    # Declare Pipeline fields for use in algorithm  
    # Define Lookback periods for fast/slow RSI  
    fast_rsi = RSI(inputs=[USEquityPricing.close], window_length=100, mask=asset_filter)  
    slow_rsi = RSI(inputs=[USEquityPricing.close], window_length=200, mask=asset_filter)  
    # Define Rolling Linear Regression of returns field  
    rlr = RLR(target=context.benchmark, returns_length=100, regression_length=10, mask=asset_filter)  

    # Return our pipeline  
    return Pipeline(  
        columns={  
            'fast_rsi':fast_rsi,  
            'slow_rsi':slow_rsi,  
            'rlr':rlr  
        },  
        screen = asset_filter  
    )  

When I try and run this backtest, every once in a while depending on the dates and securities chosen I get an out of bounds error. What could be causing this?

EDIT: It also appears that from about June 1, 2016 onwards it no longer trades.

The first thing to check is if all the securities existed during the date range - it's possible some securities traded in the algo were created after the simulation start date or discontinued before the end date. You could use
If data.can_trade(sec): order_target_percent(sec, leverage)
in the algo to avoid such out of bounds errors

I believe that logic is already implemented in the logic. Is it possible that if data.can_trade(s) is false, the algo fails because there's no follow-up logic?

# Determine securities to long based on market conditions (represente by RSI)
if context.fast_rsi[context.benchmark] > 50:
# Sort betas for aggressive securities from highest to lowest
betas = context.beta[context.aggressive].sort(ascending=False, inplace=False)
# Filter for relatively underpriced securities by RSI
for s in betas.index:
if context.fast_rsi[s] < 50.0 and len(longs) <= size and data.can_trade(s):
longs.append(s)

elif context.slow_rsi[context.benchmark] < 50:  
    # Sort betas for defensive securities from lowest to highest  
    betas = context.beta[context.defensive].sort(ascending=True, inplace=False)  
    # Filter for relatively underpriced securities by RSI  
    for s in betas.index:  
        if context.slow_rsi[s] < 50.0 and len(longs) <= size and data.can_trade(s):  
            longs.append(s)  
else:  
    # Sort betas for aggressive securities from highest to lowest  
    betas = context.beta[context.aggressive].sort(ascending=False, inplace=False)  
    # Filter for relatively underpriced securities by RSI  
    for s in betas.index:  
        if context.fast_rsi[s] < 50.0 and len(longs) <= size and data.can_trade(s):  
            longs.append(s)  
# Close previous positions from last week that aren't being longed for this week  
for s in port:  
    if s not in longs:  
        order_target(s,0)  
# Open new positions for current week  
for s in longs:  
    order_target_percent(s, context.leverage/len(longs))  

Yes you're right, sorry it's been some time since I last looked at the code. If data.can_trade returns false, there still shouldn't be an out of bounds error. I don't know what the issue is.
Edit: on second thought the out of bounds error might be due to the Pipeline trying to access data for securities outside their respective date ranges

I'm going to go through the code tonight and try and clean it up a little bit with an updated securities list from iShares. Once its up, would you mind taking a second look with me? I'm hoping that it will essentially become my permanent portfolio.

I'm trying to code a rotation algorithm that ranks the strongest ETFs momentum over the past month using Quantopians ETF/ETN/ETC universe with pipeline but haven't been able to determine if the security is an ETF in order to filter out the other types - I.e. non etfs.

Is there a flag or something in the instrument database which has a reliable "ETF identifier" as I don't want to pre-load SIDS like in this example.

Got it working in research using the load csv function which imports about 1500 etfs but would really like to run this in the back-tester.

The csv reader function in the algo doesn't seem to work.

Thanks, Stuart

Do you mind posting the research notebook?

the notebook is a bit messy but here's the code which filters on ETPs by joining to the csv
my_pipe = make_pipeline()
result = run_pipeline(my_pipe, '2016-11-07','2016-11-07')
result['asset']=result.index.levels[1].unique()
result['symbol'] = result.asset.apply(lambda x: x.symbol)
result.set_index('symbol', inplace=True)
etf_list = local_csv('final_etf_list.csv')
etf_list.set_index('symbol', inplace=True)
results=result.join(etf_list, how="inner")

I apologize for not getting back sooner. I think there is a way, using Pipeline, to filter for only securities with 'ETF' in the name. I'm going to look through the API documentation a little bit tonight and see if I can find it.

I used your algo as a template and have added ~ 1500 ETFs. Not pretty but seems to do the trick

Sorry I didn't get sooner, this past week was super busy. Is there a reason that your securities lists are split between 5 groups, or is it a limitation of Quantopian?

A limitation of Quantopian - it throws the error "SyntaxError: more than 255 arguments"

Good post!