Back to Community
Value Momentum Strategy

Hi, I am new to Quantopian. I was playing around with Johnny Wu's "EV/EBITDA Value, then momentum" strategy and got some good results with a few tweaks (although it seems like it hasn't performed well since 2014). Are there any obvious issues with my criteria? I allow stocks with market cap > $1m and share count < 200m which makes illiquid stocks an issue. How does the backtester handle low volume and wide bid/ask spreads? Does it determine how much of the stock's volume you can get filled on and does it just cross when buying/selling? And how would I filter against illiquid stocks if I wanted to? I did not see anything in the filter for share price, average volume, or anything like that. If I were to use this screen for live trading, are there any issues that I should look out for? Thanks.

Clone Algorithm
330
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.month_count != context.holding_months:
        return
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.ev_to_ebitda,
 fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.valuation.shares_outstanding < 2e8)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.valuation_ratios.pe_ratio > 1)
        .filter(fundamentals.valuation_ratios.pe_ratio < 30)
        .filter(fundamentals.valuation_ratios.fcf_ratio < 30)
        .filter(fundamentals.valuation_ratios.ev_to_ebitda < 30)
        .filter(fundamentals.valuation_ratios.ps_ratio < 5)
        .filter(fundamentals.operation_ratios.roe > 0.1)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        .order_by(fundamentals.valuation_ratios.pe_ratio.asc())
        .limit(context.num_screener)
    )

    # Filter out only stocks that fits in criteria
    context.stocks = [stock for stock in fundamental_df]
    # Update context.fundamental_df with the securities that we need
    context.fundamental_df = fundamental_df[context.stocks]
    
    update_universe(context.fundamental_df.columns.values)

def initialize(context):
  

    
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    #number of stocks to pass through the fundamental screener
    context.num_screener = 500
    #number of stocks in portfolio at any time
    context.num_stock = 50
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    # Rebalance monthly on the first day of the month at market open
    schedule_function(rebalance,
                      date_rule=date_rules.month_start(),
                      time_rule=time_rules.market_open())
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]
    
    # Create weights for each stock
    weight = 0.95/len(chosen_df.columns)
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df:
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            order_target_percent(stock, weight)
    
def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change#calculate percent change
        
    return temp
       
def create_weights(context, stocks):
    """
        Takes in a list of securities and weights them all equally 
    """
    if len(stocks) == 0:
        return 0 
    else:
        # Buy only 0.9 of portfolio value to avoid borrowing
        weight = .99/len(stocks)
        return weight
    
    #if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
    fmean = df.mean(axis=1)
    print fmean.loc['ev_to_ebitda']
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    pass
    
There was a runtime error.
25 responses

Hey Walter,
We use a slippage model that is customizable in the backtest. See our help page for information about that. We don't currently have bid/ask spreads, we use trade data for every security. Orders submitted in one bar are filled in the next bar. For transitioning from backtesting to live trading, I would read the relevant points on the help page — it's tough to say if something is going to run OK on live trading. It usually does, but yeah, read through and make sure. You can also use paper trading to test live trading before you transition to real money.

As you're doing, you can filter out a lot of stocks by using the fundamentals API. You could also filter more at other places in your code. For example, you could do:

if data[stock].volume > 200000:  
    ...  

To check if a stock has volume over 200,000 in handle_data. This uses the data object. Let me know if you need any more help with that.

Gus

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

I thought it would be fun to see if I could get the beta lower in this algo - at least low enough to be in the .3 to -.3 range needed for the Quantopian Open

As a quick solution, I threw in a short of SPY with half the portfolio value and reduced the weights of the long positions by half to keep leverage under 1.

The beta came down from .95 to .21, which would make the cut.

The returns and Sharpe Ratio were dampened significantly but the max drawdown also came down from a gut-wrenching 54% to an almost livable 14% (in other words, it handles 2008 a little better :)

There's probably better ways to hedge here by finding a specific bucket of stocks to rank and short. Also, the leverage bounces between .5 and 1 weirdly. Probably a better way to rebalance with the short in the algo . . . but it was fun to try out and play with.

Clone Algorithm
76
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.month_count != context.holding_months:
        return
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.ev_to_ebitda,
 fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.valuation.shares_outstanding < 2e8)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.valuation_ratios.pe_ratio > 1)
        .filter(fundamentals.valuation_ratios.pe_ratio < 30)
        .filter(fundamentals.valuation_ratios.fcf_ratio < 30)
        .filter(fundamentals.valuation_ratios.ev_to_ebitda < 30)
        .filter(fundamentals.valuation_ratios.ps_ratio < 5)
        .filter(fundamentals.operation_ratios.roe > 0.1)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        .order_by(fundamentals.valuation_ratios.pe_ratio.asc())
        .limit(context.num_screener)
    )

    # Filter out only stocks that fits in criteria
    context.stocks = [stock for stock in fundamental_df]
    # Update context.fundamental_df with the securities that we need
    context.fundamental_df = fundamental_df[context.stocks]
    
    update_universe(context.fundamental_df.columns.values)

def initialize(context):
  

    
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    #number of stocks to pass through the fundamental screener
    context.num_screener = 500
    #number of stocks in portfolio at any time
    context.num_stock = 50
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    context.spy = sid(8554)
    
    # Rebalance monthly on the first day of the month at market open
    schedule_function(rebalance,
                      date_rule=date_rules.month_start(),
                      time_rule=time_rules.market_open())
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]
    
    # Create weights for each stock
    weight = (0.95/len(chosen_df.columns))/2
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df:
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            order_target_percent(stock, weight)

            
    order_target_percent(context.spy, -.5)

def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change#calculate percent change
        
    return temp
       
def create_weights(context, stocks):
    """
        Takes in a list of securities and weights them all equally 
    """
    if len(stocks) == 0:
        return 0 
    else:
        # Buy only 0.9 of portfolio value to avoid borrowing
        weight = .99/len(stocks)
        return weight
    
    #if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
    fmean = df.mean(axis=1)
    print fmean.loc['ev_to_ebitda']
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    record(leverage=context.account.leverage)
There was a runtime error.
Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

I doesn't do very well in the last 2 years for some reason. I am going to tweak the parameters to try to take that bad 2013-2015 performance and make it better.

I haven't investigated, but I suspect that every other month, you are adjusting the hedge taking into account the previous month's new hedge?

All right, I figured it out.

I think the hedge stays in place now for the entirety of the backtest. Returns worsen, especially in recent years. The good news is that drawdown is further minimized and beta is brought close to 0 as well.

A smarter hedging strategy is probably needed and I'd have concerns about the performance over the last two years but a fun exercise.

Clone Algorithm
76
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.month_count != context.holding_months:
        return
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.ev_to_ebitda,
 fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.valuation.shares_outstanding < 2e8)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.valuation_ratios.pe_ratio > 1)
        .filter(fundamentals.valuation_ratios.pe_ratio < 30)
        .filter(fundamentals.valuation_ratios.fcf_ratio < 30)
        .filter(fundamentals.valuation_ratios.ev_to_ebitda < 30)
        .filter(fundamentals.valuation_ratios.ps_ratio < 5)
        .filter(fundamentals.operation_ratios.roe > 0.1)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        .order_by(fundamentals.valuation_ratios.pe_ratio.asc())
        .limit(context.num_screener)
    )

    # Filter out only stocks that fits in criteria
    context.stocks = [stock for stock in fundamental_df]
    # Update context.fundamental_df with the securities that we need
    context.fundamental_df = fundamental_df[context.stocks]
    
    update_universe(context.fundamental_df.columns.values)

def initialize(context):
  

    
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    #number of stocks to pass through the fundamental screener
    context.num_screener = 500
    #number of stocks in portfolio at any time
    context.num_stock = 50
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    context.spy = sid(8554)
    
    # Rebalance monthly on the first day of the month at market open
    schedule_function(rebalance,
                      date_rule=date_rules.month_start(),
                      time_rule=time_rules.market_open())
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]
    
    # Create weights for each stock
    weight = (0.95/len(chosen_df.columns))/2
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df and stock != context.spy:
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            order_target_percent(stock, weight)

            
    order_target_percent(context.spy, -.5)

def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change#calculate percent change
        
    return temp
       
def create_weights(context, stocks):
    """
        Takes in a list of securities and weights them all equally 
    """
    if len(stocks) == 0:
        return 0 
    else:
        # Buy only 0.9 of portfolio value to avoid borrowing
        weight = .99/len(stocks)
        return weight
    
    #if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
    fmean = df.mean(axis=1)
    print fmean.loc['ev_to_ebitda']
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    record(leverage=context.account.leverage)
There was a runtime error.

Great returns on your tweaks, Walter.

This is the same test as yours, except with picking the biggest losers instead of winners.

As you can see, the results are almost identical.

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
#backtest note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.month_count != context.holding_months:
        return
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.ev_to_ebitda,
 fundamentals.asset_classification.morningstar_sector_code, fundamentals.valuation.enterprise_value, fundamentals.income_statement.ebit, fundamentals.income_statement.ebitda
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.valuation.shares_outstanding < 2e8)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.valuation_ratios.pe_ratio > 1)
        .filter(fundamentals.valuation_ratios.pe_ratio < 30)
        .filter(fundamentals.valuation_ratios.fcf_ratio < 30)
        .filter(fundamentals.valuation_ratios.ev_to_ebitda < 30)
        .filter(fundamentals.valuation_ratios.ps_ratio < 5)
        .filter(fundamentals.operation_ratios.roe > 0.1)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        .order_by(fundamentals.valuation_ratios.pe_ratio.asc())
        .limit(context.num_screener)
    )

    # Filter out only stocks that fits in criteria
    context.stocks = [stock for stock in fundamental_df]
    # Update context.fundamental_df with the securities that we need
    context.fundamental_df = fundamental_df[context.stocks]
    
    update_universe(context.fundamental_df.columns.values)

def initialize(context):
  

    
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    #number of stocks to pass through the fundamental screener
    context.num_screener = 500
    #number of stocks in portfolio at any time
    context.num_stock = 50
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = True
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    # Rebalance monthly on the first day of the month at market open
    schedule_function(rebalance,
                      date_rule=date_rules.month_start(),
                      time_rule=time_rules.market_open())
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:(context.num_stock-1)]
    
    # Create weights for each stock
    weight = 0.95/len(chosen_df.columns)
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df:
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            order_target_percent(stock, weight)
    
def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change#calculate percent change
        
    return temp
       
def create_weights(context, stocks):
    """
        Takes in a list of securities and weights them all equally 
    """
    if len(stocks) == 0:
        return 0 
    else:
        # Buy only 0.9 of portfolio value to avoid borrowing
        weight = .99/len(stocks)
        return weight
    
    #if the code gets to here, it means that there has been an error. I don't want my code to continue if there is a bull market and my screener doesn't have enough stocks that pass this filter
#def mom_filter(context, data):

def print_ev_ebitda(df):
    fmean = df.mean(axis=1)
    print fmean.loc['ev_to_ebitda']
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    pass
    
There was a runtime error.

Thanks for the ideas, guys. I found out that my screen is too restrictive and ends up with only like <20 stocks most periods. So the momentum ranking doesn't even do anything and the returns have actually all been from value. And I think this is part of the reason the performance since 2014 were bad - too few stocks to choose from. The bull market has lifted valuations and the only ones passing the screen are the dregs. I'm working on some changes to fix these issues.

Here are the results after I fixed the issue I mentioned. I changed the screen to do multiple sorts and cull the worst stocks, instead of just having static limits. I used some standard valuation ratios: EV/EBITDA, P/CF, P/E, P/FCF (P/B and P/S did not test well). This way we'll get the cheapest stocks but still have enough stocks to buy if valuations are higher than the limits. The performance improved a lot (and are frankly unbelievable), and returns are actually positive since 2014, matching the market's performance.

Some surprising things I found:
- Removing the momentum component improved the returns, even risk adjusted (by setting num_stock = num_screener_end).
- Removing the profitability filters improved returns (ROE and ROIC). I also tried to sort/cull by them, didn't help.

I still need to get the logging of leverage to work. For some reason it complains when I try to use record_leverage: "OrderSecurityOutsideUniverse: 0036 You have placed an order with an unexpected security: Security(2114 [DCO]). Please declare all securities in the initialize function by calling sid(123) for each security that will be used later in your algorithm."

I'm not sure if the strategy is using leverage or not. It's not supposed to but I suspect it might be, if it tries to close out a position but is not able to, while still opening new positions.

Also need to figure out how to use limit orders. I'm thinking some of these stocks are pretty illiquid and market orders would be risky.

And I want to try adding some logic to reduce drawdowns, maybe using a stoploss or shorting the index like Josh showed.

Clone Algorithm
210
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):   
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    
    #number of stocks to pass through the fundamental screener
    context.num_screener_start = 400
    context.num_screener_end = 25
    #number of stocks in portfolio at any time
    context.num_stock = 25
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months

    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0.1, price_impact=0.2))
    #set_commission(commission.PerShare(cost=0.005, min_trade_cost=1))

    # Rebalance monthly
    schedule_function(rebalance, date_rules.month_end(7), time_rules.market_close(hours=2, minutes=30))

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.month_count != context.holding_months:
        return
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.pcf_ratio,
            fundamentals.valuation_ratios.pe_ratio,
            fundamentals.valuation_ratios.fcf_ratio,
            #fundamentals.operation_ratios.roic
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.valuation.shares_outstanding < 2e8)
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > 0)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
        .limit(context.num_screener_start)
    )
    
    metrics = len(fundamental_df.index)
    screen_df = fundamental_df.T
    if metrics > 0:
        num_to_cut = (context.num_screener_start - context.num_screener_end) / metrics
        for i in range(0, metrics):
            screen_df = sort_and_cut(screen_df, fundamental_df.index[i], num_to_cut)
    
    #log_df(screen_df)
    context.fundamental_df = screen_df.T
    update_universe(context.fundamental_df.columns.values)
    
def sort_and_cut(screen_df, metric, num_to_cut):
    if (metric.startswith('ro') and len(metric) < 5) or metric.endswith('_yield') or  metric.endswith('_growth'):
        is_ascending = False
    else:
        is_ascending = True
    
    return screen_df.sort(metric, ascending=is_ascending)[:-num_to_cut]

def log_df(df):
    print "--------------------------------------------------"
    #for i in range(0, len(screen_df.index)):
    for i in df.index[-10:]:
        line = i.symbol + " "
        for j in range(0, len(df.columns)):
            line += df.columns.values[j] + ": " + str(df.ix[i][j]) + " "
        print line
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener_end:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:context.num_stock]
    
    # Create weights for each stock
    weight = 0
    if len(chosen_df.columns) > 0:
        weight = 0.999/len(chosen_df.columns)

    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df:
            cancel_orders_for_stock(stock)
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            cancel_orders_for_stock(stock)
            order_target_percent(stock, weight)
    
def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change
        
    return temp
    
def cancel_orders_for_stock(stock):
    for order in get_open_orders(stock.sid):
        cancel_order(order)
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    #record_leverage(context, data)
    
def record_leverage(context, data):
    P = context.portfolio
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)    
    record(leverage=market_value / max(P.portfolio_value, 1))    
There was a runtime error.

Looks pretty sweet to me! What is the reason for the shares outstanding filter, just to target microcaps directly?

Thanks. That was just to screen out stocks that heavily dilute shareholders. I didn't know how to screen for growth in outstanding shares YOY or 3-year avg or something like that. It does exclude a lot of large-caps as an unfortunate side-effect, although I wasn't too worried since I'm guessing the best value stocks are usually small-caps.

Yeah, I'm just guessing that this universe is particularly microcap though. Point in time fundamentals data is coming soon I hear, then we'll be able to do these sorts of things. Still impressive!

The most recent code made $2,403,993 on max spent $385,345 (on 2011-11-22) for 624%.
Maybe see if you can find a common thread in some of the worst performers: CPA, CTCM, DYP, ENG, FSLR, MTEX, QCCO, SB, SKBI, SPP, TGIS among the 573 securiites traded.

Impressive results.

Throwing in half of spy is something generalizeable. Making the hedge too dynamic may not generalize too well and hence might fail in paper trade and in real life.

That's pretty amazing.

I'm a bit surprised that the turnover rate is necessarily set at every month even when you don't employ momentum. The fundamental filter doesn't work at all if you are to hold the portfolio for 1 year.

Since I'm a one trick pony, here's the same hedging strategy as above (half the portfolio value shorting SPY).

Clone Algorithm
30
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):   
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    
    #number of stocks to pass through the fundamental screener
    context.num_screener_start = 400
    context.num_screener_end = 25
    #number of stocks in portfolio at any time
    context.num_stock = 25
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    context.spy = sid(8554)

    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0.1, price_impact=0.2))
    #set_commission(commission.PerShare(cost=0.005, min_trade_cost=1))

    # Rebalance monthly
    schedule_function(rebalance, date_rules.month_end(7), time_rules.market_close(hours=2, minutes=30))

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.month_count != context.holding_months:
        return
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.pcf_ratio,
            fundamentals.valuation_ratios.pe_ratio,
            fundamentals.valuation_ratios.fcf_ratio,
            #fundamentals.operation_ratios.roic
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.valuation.shares_outstanding < 2e8)
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > 0)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
        .limit(context.num_screener_start)
    )
    
    metrics = len(fundamental_df.index)
    screen_df = fundamental_df.T
    if metrics > 0:
        num_to_cut = (context.num_screener_start - context.num_screener_end) / metrics
        for i in range(0, metrics):
            screen_df = sort_and_cut(screen_df, fundamental_df.index[i], num_to_cut)
    
    #log_df(screen_df)
    context.fundamental_df = screen_df.T
    update_universe(context.fundamental_df.columns.values)
    
def sort_and_cut(screen_df, metric, num_to_cut):
    if (metric.startswith('ro') and len(metric) < 5) or metric.endswith('_yield') or  metric.endswith('_growth'):
        is_ascending = False
    else:
        is_ascending = True
    
    return screen_df.sort(metric, ascending=is_ascending)[:-num_to_cut]

def log_df(df):
    print "--------------------------------------------------"
    #for i in range(0, len(screen_df.index)):
    for i in df.index[-10:]:
        line = i.symbol + " "
        for j in range(0, len(df.columns)):
            line += df.columns.values[j] + ": " + str(df.ix[i][j]) + " "
        print line
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener_end:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:context.num_stock]
    
    # Create weights for each stock
    weight = 0
    if len(chosen_df.columns) > 0:
        weight = 0.999/len(chosen_df.columns)

    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df and stock != context.spy:
            cancel_orders_for_stock(stock)
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            cancel_orders_for_stock(stock)
            order_target_percent(stock, weight/2)
            
    order_target_percent(context.spy, -0.5)
    
def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change
        
    return temp
    
def cancel_orders_for_stock(stock):
    for order in get_open_orders(stock.sid):
        cancel_order(order)
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    #record_leverage(context, data)
    record(leverage=context.account.leverage)
    
def record_leverage(context, data):
    P = context.portfolio
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)    
    record(leverage=market_value / max(P.portfolio_value, 1))    
There was a runtime error.

Here's a backtest with hedging and a tweak to the code to make it run a lot faster. Before, it was doing a query every day, now it's just once a month, so it runs like 30 times faster. For hedging, I hedged with IWM (small cap index) instead of SPY since it's more appropriate considering the stocks the screen is buying. Plus, IWM has underperformed SPY since 2014 so this hedge helps with that. One other idea: you can use a leveraged ETF like TNA as the hedge. This way you could keep your leverage at 1 but increase your exposure to the strategy.

Clone Algorithm
185
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):   
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    context.should_filter = True
    
    #number of stocks to pass through the fundamental screener
    context.num_screener_start = 400
    context.num_screener_end = 25
    #number of stocks in portfolio at any time
    context.num_stock = 25
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    context.target_leverage = 1
    #context.hedge_symbol = sid(8554) #SPY
    context.hedge_symbol = sid(21519) #IWM
    context.hedge_ratio = 0.40

    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0.1, price_impact=0.2))
    #set_commission(commission.PerShare(cost=0.005, min_trade_cost=1))

    # Rebalance monthly
    schedule_function(set_filter_true, date_rules.month_end(8), time_rules.market_close())
    schedule_function(rebalance, date_rules.month_end(7), time_rules.market_close(hours=2, minutes=30))
    
def set_filter_true(context, data):
    context.should_filter = True    

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.should_filter == False:
        return
    context.should_filter = False
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.pcf_ratio,
            fundamentals.valuation_ratios.pe_ratio,
            fundamentals.valuation_ratios.fcf_ratio,
            #fundamentals.operation_ratios.roic
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.valuation.shares_outstanding < 2.5e8)
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > 0)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
        .limit(context.num_screener_start)
    )
    
    metrics = len(fundamental_df.index)
    screen_df = fundamental_df.T
    if metrics > 0:
        num_to_cut = (context.num_screener_start - context.num_screener_end) / metrics
        for i in range(0, metrics):
            screen_df = sort_and_cut(screen_df, fundamental_df.index[i], num_to_cut)
    
    #log_df(screen_df)
    context.fundamental_df = screen_df.T
    update_universe(context.fundamental_df.columns.values)
    
def sort_and_cut(screen_df, metric, num_to_cut):
    if (metric.startswith('ro') and len(metric) < 5) or metric.endswith('_yield') or metric.endswith('_growth'):
        is_ascending = False
    else:
        is_ascending = True
    
    return screen_df.sort(metric, ascending=is_ascending)[:-num_to_cut]

def log_df(df):
    print "--------------------------------------------------"
    #for i in range(0, len(screen_df.index)):
    for i in df.index[-10:]:
        line = i.symbol + " "
        for j in range(0, len(df.columns)):
            line += df.columns.values[j] + ": " + str(df.ix[i][j]) + " "
        print line
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener_end:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:context.num_stock]
    
    # Create weights for each stock
    weight = 0
    if len(chosen_df.columns) > 0:
        weight = ((1 - context.hedge_ratio) * context.target_leverage) / len(chosen_df.columns)

    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df and stock != context.hedge_symbol:
            cancel_orders_for_stock(stock)
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            cancel_orders_for_stock(stock)
            order_target_percent(stock, weight)
            
    order_target_percent(context.hedge_symbol, - context.hedge_ratio * context.target_leverage)
    
def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change
        
    return temp
    
def cancel_orders_for_stock(stock):
    for order in get_open_orders(stock.sid):
        cancel_order(order)
    
def handle_data(context, data):
    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    record(leverage=context.account.leverage)   
There was a runtime error.

thanks for sharing, walter. very interesting.

Walter,

Nice algo. The beta looks fantastic here. Why did you end up at a hedge ratio of .40?

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Thanks. I just tried different numbers and that got the beta close to 0.

Will selling a couple thousand unowned IWM shares work in the real world to actually make money? Could you add shorting costs? Recently somewhere Simon says what some are.

At IB, IWM borrow cost is 1.0732% and SPY is 0.262%. Not too bad. But personally I'd just run the long-only version if I wanted to live trade this, since the returns are better and I'd want to run it in my Roth IRA to avoid taxes since the turnover is high, and IRAs can't short.

Great work!
I will be taking a deeper look into it soon.

The most recent algo shows 561%:
Profited 778048 on 377108 activated/transacted for PvR of only 206.3% barely beating the benchmark. Unfortunate.

Clone Algorithm
140
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 note: high momentum
import pandas as pd
import numpy as np
import datetime
import math

def initialize(context):   
#### Variables to change for your own liking #################
    #the constant for portfolio turnover rate
    context.holding_months = 1
    context.should_filter = True
    
    #number of stocks to pass through the fundamental screener
    context.num_screener_start = 400
    context.num_screener_end = 25
    #number of stocks in portfolio at any time
    context.num_stock = 25
    #number of days to "look back" if employing momentum. ie formation
    context.formation_days = 200
    #set False if you want the highest momentum, True if you want low
    context.lowmom = False
    #################################################################
    #month counter for holding period logic.
    context.month_count = context.holding_months
    
    context.target_leverage = 1
    #context.hedge_symbol = sid(8554) #SPY
    context.hedge_symbol = sid(21519) #IWM
    context.hedge_ratio = 0.40

    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0.1, price_impact=0.2))
    #set_commission(commission.PerShare(cost=0.005, min_trade_cost=1))

    # Rebalance monthly
    schedule_function(set_filter_true, date_rules.month_end(8), time_rules.market_close())
    schedule_function(rebalance, date_rules.month_end(7), time_rules.market_close(hours=2, minutes=30))
    
def set_filter_true(context, data):
    context.should_filter = True    

def before_trading_start(context, data): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    #this code prevents query every day
    if context.should_filter == False:
        return
    context.should_filter = False
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.pcf_ratio,
            fundamentals.valuation_ratios.pe_ratio,
            fundamentals.valuation_ratios.fcf_ratio,
            #fundamentals.operation_ratios.roic
        )
        .filter(fundamentals.valuation.market_cap > 1e6)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .filter(fundamentals.company_reference.country_id != "CHN")
        .filter(fundamentals.company_reference.business_country_id != "CHN")
        .filter(fundamentals.valuation.shares_outstanding < 2.5e8)
        .filter(fundamentals.income_statement.ebitda > 0)
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > 0)
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1)
        .filter(fundamentals.operation_ratios.current_ratio > 1)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        #.filter(fundamentals.asset_classification.morningstar_sector_code != 207)
        .order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
        .limit(context.num_screener_start)
    )
    
    metrics = len(fundamental_df.index)
    screen_df = fundamental_df.T
    if metrics > 0:
        num_to_cut = (context.num_screener_start - context.num_screener_end) / metrics
        for i in range(0, metrics):
            screen_df = sort_and_cut(screen_df, fundamental_df.index[i], num_to_cut)
    
    #log_df(screen_df)
    context.fundamental_df = screen_df.T
    update_universe(context.fundamental_df.columns.values)
    
def sort_and_cut(screen_df, metric, num_to_cut):
    if (metric.startswith('ro') and len(metric) < 5) or metric.endswith('_yield') or metric.endswith('_growth'):
        is_ascending = False
    else:
        is_ascending = True
    
    return screen_df.sort(metric, ascending=is_ascending)[:-num_to_cut]

def log_df(df):
    print "--------------------------------------------------"
    #for i in range(0, len(screen_df.index)):
    for i in df.index[-10:]:
        line = i.symbol + " "
        for j in range(0, len(df.columns)):
            line += df.columns.values[j] + ": " + str(df.ix[i][j]) + " "
        print line
    
def rebalance(context, data):
    #This condition block is to skip every "holding_months"
    if context.month_count >= context.holding_months:
        context.month_count = 1
    else:
        context.month_count += 1
        return
    
    chosen_df = calc_return(context)
    
    if context.num_stock < context.num_screener_end:
        chosen_df = sort_return(chosen_df, context.lowmom)
    
    chosen_df = chosen_df.iloc[:,:context.num_stock]
    
    # Create weights for each stock
    weight = 0
    if len(chosen_df.columns) > 0:
        weight = ((1 - context.hedge_ratio) * context.target_leverage) / len(chosen_df.columns)

    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in chosen_df and stock != context.hedge_symbol:
            cancel_orders_for_stock(stock)
            order_target(stock, 0)
           
    # Rebalance all stocks to target weights
    for stock in chosen_df:
        if weight != 0 and stock in data:
            cancel_orders_for_stock(stock)
            order_target_percent(stock, weight)
            
    order_target_percent(context.hedge_symbol, - context.hedge_ratio * context.target_leverage)
    
def sort_return(df, lowmom):
    '''a cheap and quick way to sort columns according to index value. Sorts by descending order. Ie higher returns are first'''
    df = df.T
    df = df.sort(columns='return', ascending = lowmom)
    df = df.T
    
    return df
    
def calc_return(context):
    price_history = history(bar_count=context.formation_days, frequency="1d", field='price')
    
    temp = context.fundamental_df.copy()
    
    for s in temp:
        now = price_history[s].ix[-20]
        old = price_history[s].ix[0]
        pct_change = (now - old) / old
        if np.isnan(pct_change):
            temp = temp.drop(s,1)
        else:
            temp.loc['return', s] = pct_change
        
    return temp
    
def cancel_orders_for_stock(stock):
    for order in get_open_orders(stock.sid):
        cancel_order(order)
    
def pvr(context, data):
    ''' Custom chart and/or log of profit_vs_risk returns and related information
    '''
    # # # # # # # # # #  Options  # # # # # # # # # #
    record_max_lvrg = 1         # Maximum leverage encountered
    record_leverage = 0         # Leverage (context.account.leverage)
    record_q_return = 0         # Quantopian returns (percentage)
    record_pvr      = 1         # Profit vs Risk returns (percentage)
    record_pnl      = 0         # Profit-n-Loss
    record_shorting = 1         # Total value of any shorts
    record_overshrt = 0         # Shorts beyond longs+cash
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash
    record_risk_hi  = 1         # Highest risk overall
    record_cash     = 0         # Cash available
    record_cash_low = 1         # Any new lowest cash level
    logging         = 1         # Also to logging window conditionally (1) or not (0)
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.
                                #   Move to top of algo for better efficiency.
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]
    if 'pvr' not in c:
        date_strt = get_environment('start').date()
        date_end  = get_environment('end').date()
        cash_low  = c.portfolio.starting_cash
        mode      = get_environment('data_frequency')
        c.pvr = {
            'max_lvrg': 0,
            'risk_hi' : 0,
            'date_prv': '',
            'cash_low': cash_low,
            'date_end': date_end,
            'mode'    : mode,
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)
        }
        log.info(c.pvr['run_str'])
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent
    profit_loss  = 0            # Profit-n-loss
    shorts       = 0            # Shorts value
    longs        = 0            # Longs  value
    overshorts   = 0            # Shorts value beyond longs plus cash
    new_risk_hi  = 0
    new_cash_low = 0                           # To trigger logging in cash_low case
    lvrg         = c.account.leverage          # Standard leverage, in-house
    date         = get_datetime().date()       # To trigger logging in daily case
    cash         = c.portfolio.cash
    start        = c.portfolio.starting_cash
    cash_dip     = int(max(0, start - cash))
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low
        new_cash_low = 1
        c.pvr['cash_low']   = int(cash)
        if record_cash_low:
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:
        if c.account.leverage > c.pvr['max_lvrg']:
            c.pvr['max_lvrg'] = c.account.leverage
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:
        profit_loss = c.portfolio.pnl
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:
        shrs = c.portfolio.positions[p].amount
        if shrs < 0:
            shorts += int(abs(shrs * data[p].price))
        if shrs > 0:
            longs  += int(shrs * data[p].price)

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts, overshorts))
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:
        c.pvr['risk_hi'] = risk
        new_risk_hi = 1

        if record_risk_hi:
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with the minute of the day.
        if get_environment('data_frequency') == 'minute':
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)
            return str(minute).rjust(3)
        return ''    # Daily mode, just leave it out.

    if logging:
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.date_prv != date \
          or new_cash_low:
            qret    = ' QRet '   + '%.1f' % q_rtrn
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))
    c.date_prv = date
    if c.pvr['date_end'] == date:
        # Summary on last minute of last day.
        # If using schedule_function(), backtest last day/time may need to match for this to execute.
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0
        log_summary = 0
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:
            log_summary = 1
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):
            log_summary = 1
        if log_summary and not c.pvr_summary_done:
            log.info('PvR {} ... {}'.format('%.1f' % pvr_rtrn, c.pvr['run_str']))
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'], '%.1f' % pvr_rtrn))
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {}'.format(
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi']))
            c.pvr_summary_done = 1

def handle_data(context, data):
    pvr(context, data)
    return

    """
      Code logic to run during the trading day.
      handle_data() gets called every bar.
    """
    record(leverage=context.account.leverage)   
There was a runtime error.

So have you gotten any any real trading results?

There was a runtime error.
ValueError:The get_fundamentals method has been removed. To use fundamental data in your notebooks and algorithms, use Pipeline.
See: https://www.quantopian.com/help#fundamental-data
Line: 72 inbefore_trading_start
.limit(context.num_screener_start)