Back to Community
A modified momentum measure (different to percent change)

Fairly new and inexperienced to the Quantopian platform (well not in terms of when I first joined, but in terms of time committed due to procrastination). I thought the best way to learn would be to try and put something together and share it. I thought the process of writing the algorithm to test my theory would help me to learn the coding required for Quantopian and any discussion generated afterwards would be beneficial in helping me learn how to communicate with this community.

A lot of the momentum systems I've come across use a simple percentage change over the last x days to measure momentum. This is simply the price today less the price x days ago (for simplicity let's call x 200 days), as a percentage. I see 2 flaws to this:

  1. This makes the calculation quite dependent on only 2 prices (today and price 200 days ago) in the entire 200 day lookback period.
  2. While the price change is normalized for overall price magnitude (by taking the percent change), some stocks are much more volatile than others. Doesn't it feel like a very stable stock that has risen 50% in 200 days has more momentum behind it than a very volatile stock up 50% in 200 days?

To try to correct for this we can:

  1. Take a simple moving average at each of these 2 price points. As in, today's 200 day SMA, less the 200 day SMA 200 days ago.
  2. Instead of normalizing by price, normalize by the volatility, using the recent average true range.
  3. I did apply a filter as well that the stock had to have positive momentum.

Note - I've applied a simple market gate (to both examples) of exiting positions when the SPY drops below the 200 day MA (using 200 day again for consistency).

With these 3 changes applied, the returns stayed almost the same, and risk reduced significantly, making overall performance much better.

I've attached a notebook to this post and I'll attach both my backtests in following posts, so you can see the code as well.

This is not a complete system, it just aims to show that it may be possible to increase the performance of momentum systems by using a modified measure of momentum instead of "percent change over time" as a momentum measure. Like I said, I'm a beginner, so if you see errors in my code, errors in my reasoning, errors in my haircut in my profile photo, just let me know and I'm happy to learn.

Thanks all.

Loading notebook preview...
Notebook previews are currently unavailable.
4 responses

This is with the original percent change momentum calculation.

Clone Algorithm
46
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
"""
This is the base system for a Momentum Monthly Rotational Trading System
For simplicity and to avoid curve fitting, we will just use a 200 day lookback period for all moving averages
"""
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import Q500US
from quantopian.pipeline.factors import CustomFactor
import numpy as np

# OPTIONS
market_gate_on = 1 # set this option to 1 to turn on a market gate based on close above/below SPY 200 day MA
bond_portfolio_on = 0 # set this option to 1 to go to 20 year treasury bonds ETF when gate closes
use_new_momentum_calc = 0 
# set this option to 0 to use a standard percent change momentum calculation
# set this option to 1 to use a smoothed momentum calculation normalized 

# CUSTOM FACTORS
# We want to use certain custom factors in our ranking process with the Pipeline
# Can test these custom factors in a research notebook to check if they are performing as expected

# 0. Standard momentum - based on percent change
class Momentum(CustomFactor):
    inputs = [USEquityPricing.close]
    window_length = 200 # 200 day lookback for simplicity, try to standardize at this
    
    def compute(self, today, assets, out, close):
        pct_change = (close[-1] - close[0]) / close[0]
        out[:] = pct_change
        
# 1. Modified momentum        
class Modified_Momentum(CustomFactor):
    # Default inputs
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 400  # 200 day lookback period, preceded by 200 day MA, so need 400 days of data.

    def compute(self, today, assets, out, high, low, close):  
        # the inputs will be np arrays, not pandas dataframes
        # calculate the difference between the SMA200 now and 200 days ago
        diff = np.mean(close[-200:],axis=0) - np.mean(close[:-200], axis=0)
                
        # compute the ATR (credit to Burrito Dan's ATR code https://www.quantopian.com/posts/custom-factor-atr and Nathan Pawelczyk's use of 10 day ATR)
        hml = high - low
        hmpc = np.abs(high - np.roll(close, 1, axis=0))
        lmpc = np.abs(low - np.roll(close, 1, axis=0))
        tr = np.maximum(hml, np.maximum(hmpc, lmpc))
        atr = np.mean(tr[-10:], axis=0) # use 10 day ATR
        
        # return the velocity normalized by ATR    
        out[:] = diff/atr
        
def initialize(context):
    """
    Called once at the start of the algorithm.
    """
    # Rebalance every month, 1st trading day, 1 minute after open.
    schedule_function(
        rebalance,
        date_rules.month_start(days_offset=0),
        time_rules.market_open(minutes=1),
    )

    # Record tracking variables at the end of each day
    schedule_function(
        record_vars,
        date_rules.every_day(),
        time_rules.market_close(),
    )

    # Create our dynamic stock selector, will depend on which momentum measure we use
    if use_new_momentum_calc:
        attach_pipeline(make_new_momentum_pipeline(), 'pipeline')
    else:
        attach_pipeline(make_pipeline(), 'pipeline')

def make_new_momentum_pipeline():
    """
    Can also use a research notebook to follow the creation of the pipeline.
    The pipeline is created using the new momentum measure.
    This includes smoothing using a SMA and then normalizing using the volatilty (ATR). 
    Filters out anything with a negative momentum.
    """

    momentum = Modified_Momentum(inputs=[USEquityPricing.high, USEquityPricing.low, USEquityPricing.close], window_length=400) # based on 200 day SMA and 200 day lookback period
    
    momentum_positive_filter = ( momentum > 0.0 ) # don't want stocks with falling momentum
    momentum_top_30 = momentum.top(30, mask=Q500US()) # take the top 30 stocks by modified momentum, only choosing from Q500US stocks.
    tradeable = momentum_positive_filter & momentum_top_30 # combine both filters
    
    return Pipeline(
        columns={
            'Modified Momentum': momentum
        },
        screen=tradeable
    )

def make_pipeline():
    """
    This pipeline is the traditionla percentage change momentum.
    """

    momentum = Momentum(inputs=[USEquityPricing.close], window_length=200) # based on 200 day lookback
    momentum_top_30 = momentum.top(30, mask=Q500US()) # take the top 30 stocks by percentage change standard momentum, only choosing from Q500US stocks
    
    return Pipeline(
        columns={
            'Standard Momentum': momentum
        },
        screen=momentum_top_30
    )
    

def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    pass

def rebalance(context, data):
    """
    Called at start of month 1 minute after market open.
    """
    # MARKET GATE - a simple 1 year MA gate
    if market_gate_on:
        spy = symbol('SPY') # returns a security object
        if data.current(spy,'price') < data.history(spy,'price', 200, '1d').mean(): # use 200 day SMA for market gate as well
            print ("Market Gate is shut, exit all positions.")
            for stock in context.portfolio.positions: # for each stock in our portfolio of positions
                order_target(stock, 0) # exit trade
            if bond_portfolio_on: 
                order_target_percent(symbol('TLT'), 1) # fully stock up on TLT
                print ("Enter a full position of TLT, 20 year bonds ETF")
            return # this will break the whole rebalance loop, as we don't want to reblance once we know the market gate is shut

    context.output = pipeline_output('pipeline')

    # These are the securities that we are interested in trading each month.
    context.security_list = context.output.index
    # same as context.output.index.get_level_values(0), because only 1 level when called in algorithm
    # this gives a pandas index class, with each element being a zipline equity object
    
    weight = 0.99/len(context.security_list) # calculate the weight for each stock we want
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        # where each stock in the context.portfolio.positions is a zipline equity object
        # same as what we have in context.security_list
        if stock not in context.security_list: # if the stock is not in the index of the context.security_list (top 30 stocks)
            order_target(stock, 0) # set the order_target to 0
           
    # Rebalance all stocks to target weights
    for stock in context.security_list: # for each stock in our top 30 list
        if weight != 0 and data.can_trade(stock): # if we can trade the stock
            order_target_percent(stock, weight) # place an order

def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    long_count = 0
    
    for position in context.portfolio.positions.itervalues():
        if position.amount > 0:
            long_count += 1
            
    record(num_longs = long_count, leverage = context.account.leverage)


def handle_data(context, data):
    """
    Called every minute.
    """
    pass
There was a runtime error.

This is with the modified momentum calculation. Slightly less total returns (13.2% pa to 12.5% pa), but everything else looks much better.

Clone Algorithm
46
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
"""
This is the base system for a Momentum Monthly Rotational Trading System
For simplicity and to avoid curve fitting, we will just use a 200 day lookback period for all moving averages
"""
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import Q500US
from quantopian.pipeline.factors import CustomFactor
import numpy as np

# OPTIONS
market_gate_on = 1 # set this option to 1 to turn on a market gate based on close above/below SPY 200 day MA
bond_portfolio_on = 0 # set this option to 1 to go to 20 year treasury bonds ETF when gate closes
use_new_momentum_calc = 1 
# set this option to 0 to use a standard percent change momentum calculation
# set this option to 1 to use a smoothed momentum calculation normalized 

# CUSTOM FACTORS
# We want to use certain custom factors in our ranking process with the Pipeline
# Can test these custom factors in a research notebook to check if they are performing as expected

# 0. Standard momentum - based on percent change
class Momentum(CustomFactor):
    inputs = [USEquityPricing.close]
    window_length = 200 # 200 day lookback for simplicity, try to standardize at this
    
    def compute(self, today, assets, out, close):
        pct_change = (close[-1] - close[0]) / close[0]
        out[:] = pct_change
        
# 1. Modified momentum        
class Modified_Momentum(CustomFactor):
    # Default inputs
    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close]  
    window_length = 400  # 200 day lookback period, preceded by 200 day MA, so need 400 days of data.

    def compute(self, today, assets, out, high, low, close):  
        # the inputs will be np arrays, not pandas dataframes
        # calculate the difference between the SMA200 now and 200 days ago
        diff = np.mean(close[-200:],axis=0) - np.mean(close[:-200], axis=0)
                
        # compute the ATR (credit to Burrito Dan's ATR code https://www.quantopian.com/posts/custom-factor-atr and Nathan Pawelczyk's use of 10 day ATR)
        hml = high - low
        hmpc = np.abs(high - np.roll(close, 1, axis=0))
        lmpc = np.abs(low - np.roll(close, 1, axis=0))
        tr = np.maximum(hml, np.maximum(hmpc, lmpc))
        atr = np.mean(tr[-10:], axis=0) # use 10 day ATR
        
        # return the velocity normalized by ATR    
        out[:] = diff/atr
        
def initialize(context):
    """
    Called once at the start of the algorithm.
    """
    # Rebalance every month, 1st trading day, 1 minute after open.
    schedule_function(
        rebalance,
        date_rules.month_start(days_offset=0),
        time_rules.market_open(minutes=1),
    )

    # Record tracking variables at the end of each day
    schedule_function(
        record_vars,
        date_rules.every_day(),
        time_rules.market_close(),
    )

    # Create our dynamic stock selector, will depend on which momentum measure we use
    if use_new_momentum_calc:
        attach_pipeline(make_new_momentum_pipeline(), 'pipeline')
    else:
        attach_pipeline(make_pipeline(), 'pipeline')

def make_new_momentum_pipeline():
    """
    Can also use a research notebook to follow the creation of the pipeline.
    The pipeline is created using the new momentum measure.
    This includes smoothing using a SMA and then normalizing using the volatilty (ATR). 
    Filters out anything with a negative momentum.
    """

    momentum = Modified_Momentum(inputs=[USEquityPricing.high, USEquityPricing.low, USEquityPricing.close], window_length=400) # based on 200 day SMA and 200 day lookback period
    
    momentum_positive_filter = ( momentum > 0.0 ) # don't want stocks with falling momentum
    momentum_top_30 = momentum.top(30, mask=Q500US()) # take the top 30 stocks by modified momentum, only choosing from Q500US stocks.
    tradeable = momentum_positive_filter & momentum_top_30 # combine both filters
    
    return Pipeline(
        columns={
            'Modified Momentum': momentum
        },
        screen=tradeable
    )

def make_pipeline():
    """
    This pipeline is the traditionla percentage change momentum.
    """

    momentum = Momentum(inputs=[USEquityPricing.close], window_length=200) # based on 200 day lookback
    momentum_top_30 = momentum.top(30, mask=Q500US()) # take the top 30 stocks by percentage change standard momentum, only choosing from Q500US stocks
    
    return Pipeline(
        columns={
            'Standard Momentum': momentum
        },
        screen=momentum_top_30
    )
    

def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    pass

def rebalance(context, data):
    """
    Called at start of month 1 minute after market open.
    """
    # MARKET GATE - a simple 1 year MA gate
    if market_gate_on:
        spy = symbol('SPY') # returns a security object
        if data.current(spy,'price') < data.history(spy,'price', 200, '1d').mean(): # use 200 day SMA for market gate as well
            print ("Market Gate is shut, exit all positions.")
            for stock in context.portfolio.positions: # for each stock in our portfolio of positions
                order_target(stock, 0) # exit trade
            if bond_portfolio_on: 
                order_target_percent(symbol('TLT'), 1) # fully stock up on TLT
                print ("Enter a full position of TLT, 20 year bonds ETF")
            return # this will break the whole rebalance loop, as we don't want to reblance once we know the market gate is shut

    context.output = pipeline_output('pipeline')

    # These are the securities that we are interested in trading each month.
    context.security_list = context.output.index
    # same as context.output.index.get_level_values(0), because only 1 level when called in algorithm
    # this gives a pandas index class, with each element being a zipline equity object
    
    weight = 0.99/len(context.security_list) # calculate the weight for each stock we want
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        # where each stock in the context.portfolio.positions is a zipline equity object
        # same as what we have in context.security_list
        if stock not in context.security_list: # if the stock is not in the index of the context.security_list (top 30 stocks)
            order_target(stock, 0) # set the order_target to 0
           
    # Rebalance all stocks to target weights
    for stock in context.security_list: # for each stock in our top 30 list
        if weight != 0 and data.can_trade(stock): # if we can trade the stock
            order_target_percent(stock, weight) # place an order

def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    long_count = 0
    
    for position in context.portfolio.positions.itervalues():
        if position.amount > 0:
            long_count += 1
            
    record(num_longs = long_count, leverage = context.account.leverage)


def handle_data(context, data):
    """
    Called every minute.
    """
    pass
There was a runtime error.

How exaclty do you normalize by volatlity versus price? Im new here and learning, thanks for the help.

Essentially you divide the change in price by a volatility measure. This helps you see if the price has moved a lot in the last 200 days relative to how volatile the stock is. Assuming you have 2 stocks that have both increased by $100 over the last 200 days. Calculate the volatility of both using any measure (in this case we use ATR (see wikipedia link) which you can think about as the size of an average candle or daily bar for the stock. Assuming you have a stock that moves on average $1 per day and a stock that moves on average $10 per day.
Before dividing by ATR, both stocks show a $100 movement over the last 200 days. But once you divide by ATR, you see stock 1 has moved 100 times its daily movement, while stock 2 moved only 10 times its daily movement over the same time period. Hence it seems like stock 1 has had a more significant move relevant to stock 2, which you only see by dividing by the ATR.