Back to Community
Value, Momentum & Trend 2.0

Updated to work with latest build

Clone Algorithm
23
Loading...
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
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.factors import CustomFactor, SimpleMovingAverage, Latest
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar, Fundamentals

import numpy as np
import pandas as pd
from scipy import stats
import talib
import datetime
import calendar

# https://www.quantopian.com/posts/value-momentum-and-trend

#ALGORITHM INITIAL STATES
START_HOLD             = 0
START_CASH             = 0
START_HEDGED           = 0
START_NEW_TOP_TEN      = 1
START_BALANCE          = 0

START_STATES = [START_HOLD, \
                START_CASH, \
                START_HEDGED, \
                START_NEW_TOP_TEN, \
                START_BALANCE]

# DASHBOARD CONFFESSIONAL
HISTORY_PERIOD         = 200
MAX_POSITIONS          = 100

HEDGE_CHECK_INTERVAL   = 22
BOND_SMA_DAYS          = 200
GOLD_SMA_DAYS          = 200

######### STRATEGY DASHBOARD #########
######################################

#TODO Set this to true to rebalance immediately
#REBALANCE_IMMEDIATE  = True
# Use the GLD and TLT MA strat 50% max from either, cash otherwise
S_GLD_TLT_MA         = False
# Use the GLD and TLT MA, if both below, go to SHY
S_GLD_TLT_SHY_MA     = True
# Suppress or enable end of day logging (disable for BT performance)
SUPPRESS_LOGGING     = False


def initialize(context):

    set_asset_restrictions(security_lists.restrict_leveraged_etfs)
    
    schedule_function(rebalance_new_top_ten,
        date_rule=date_rules.month_start(),
        time_rule=time_rules.market_open())
    
    #schedule_function(hedge_check,
        #date_rule=date_rules.every_day(),
        #time_rule=time_rules.market_open())
    
    schedule_function(record_things,
        date_rule=date_rules.every_day(),
        time_rule=time_rules.market_close())
    
    context.mkt         = symbol('SPY') #sid(8554) # SPY - Large cap Market cap weighted index
    context.spy         = symbol('SPY') #sid(8554)
    context.rsp         = symbol('RSP') #sid(8554)
    context.tlt         = symbol('TLT') #sid(23921) #10 year treasury
    context.gld         = symbol('GLD') #sid(23921) #10 year treasury
    context.uup         = symbol('UUP') #sid(23921) #10 year treasury
    context.shy         = symbol('SHY') #sid(23921) #10 year treasury
    
    context.last_month = -1
    context.days_running = 0
    context.hedge_triggered_count = 0
    context.value_stocks = []
    context.momentum_stocks = []

    context.start = True
    
    context.market = symbol('SPY') #sid(8554)
    context.market_window = 200 #200
    context.atr_window = 80 #80 
    context.talib_window = context.atr_window + 5 #5
    context.risk_factor = 0.003  #0.003                   # 0.01 = less position, more % but more risk
    
    context.momentum_window_length = 180 #180
    context.market_cap_limit = 700 # 700 original # MAKE DYNAMIC?
    context.rank_table_percentile = 0.30 #0.30
    context.significant_position_difference = 0.1 #0.1
    context.min_momentum = 30.0 #30
    context.leverage_factor = 1.0    #1.0                
    context.use_stock_trend_filter = 0 #0             # either 0 = Off, 1 = On #NOT SURE IF THIS IS EVEN WORKING
    context.sma_window_length = 200 #200                # Used for the stock trend filter
    context.use_market_trend_filter = 1 #1             # either 0 = Off, 1 = On. Filter on SPY 
    context.use_average_true_range = 1 #1            # either 0 = Off, 1 = On. Manage risk with individual stock volatility
    context.average_true_rage_multipl_factor = 2.0 #2.0   # Change the weight of the ATR. 1327%
    
    
    attach_pipeline(make_pipeline(context, context.sma_window_length,
                                  context.market_cap_limit), 'screen')
    attach_pipeline(make_value_stocks_pipeline(context), 'value_stocks')
     
    # Schedule my rebalance function
    schedule_function(rebalance,
                      date_rules.month_start(),  
                      time_rules.market_open(hours=0.1))
    
    # Cancel all open orders at the end of each day.
    schedule_function(cancel_open_orders, date_rules.every_day(), time_rules.market_close())
    
    #set_slippage(slippage.FixedSlippage(spread=0.00))
    

def before_trading_start(context, data):
    ''' Gather some data related to the current day and time '''
    context.selected_universe = pipeline_output('screen')
    context.assets = context.selected_universe.index
    
    context.now = datetime.datetime.now()
    context.days_running = context.days_running + 1
    context.current_month = get_datetime().month
    #set_market_benchmark(context, data)

    if context.last_month == context.current_month:
        return
    
    context.last_month = context.current_month
    set_scores(context, pipeline_output('value_stocks'))
    
def handle_data(context, data):
    
    if context.start:
        set_start_state(context, data)
        context.start = False
    else:
        pass

    
def set_start_state(context, data):
    
    # TODO Test this safeguard
    if sum(START_STATES) > 1:
        log.info("ERROR: Multiple start states have been set. Only one should be set, exiting.")
        cancel = cancel
        return
    
    if START_HOLD == 1:
        return
    #if START_CASH == 1:
    #    sell_all_positions(context, data)
    if START_HEDGED == 1:
        hedge(context, data)
    if START_NEW_TOP_TEN == 1:
        rebalance_new_top_ten(context, data)
        rebalance(context, data)
    if START_BALANCE == 1:
        buy_stuff(context, data)       

def set_scores(context, df):
    df.sort_values('ps_ratio', ascending=True, inplace=True)
    df = df.head(2000)
    
    context.score = pd.Series(0, index=df.index)
    
    # EV/EBITDA, in-order (lower is better), nan goes last
    context.score += df['ev_to_ebitda'].rank(ascending=True, na_option='bottom')
    
    # sales yield, inverse (higher is better), nan goes last
    context.score += df['sales_yield'].rank(ascending=False, na_option='top')
    
    # return on invested capital, inverse (higher is better), nan goes last
    context.score += df['roic'].rank(ascending=False, na_option='top')
    
    print('Number of securities that passed the filter: %d' % len(df))
    
def rebalance_new_top_ten(context, data):
    ''' Sell/buy logic '''
    P = data.history(context.score.index, 'price', HISTORY_PERIOD, '1d')
    V = data.history(context.score.index, 'volume', HISTORY_PERIOD, '1d')

    w = (P * V).median()
    w = w[w > 1.0E+06]

    context.score = context.score[w.index]  
    context.value_stocks = context.score.dropna().sort_values().head(MAX_POSITIONS).index

    P = data.history([context.mkt], 'price', HISTORY_PERIOD, '1d')
    u = P[context.mkt]

    if u.tail(MAX_POSITIONS).median() < u.median():
        log.info("Market below the median when rebalancing at month start, hedging.")
        hedge(context, data)
    else:        
        log.info("Generating new list of top companies and buying orders using market comparison %s." % context.mkt)
        buy_stuff(context, data)
        
def hedge_check(context, data):
    
    interval_triggered = (context.days_running % HEDGE_CHECK_INTERVAL == 0)
    okay_to_hedge = (calendar.monthrange(context.now.year, context.now.month)[1]) > 5
    
    if(interval_triggered and okay_to_hedge):

        P = data.history([context.mkt], 'price', HISTORY_PERIOD, '1d')
        u = P[context.mkt]

        if u.tail(MAX_POSITIONS).median() < u.median():
            log.info("Hedge Check - %s is below median, hedging." % context.mkt)
            hedge(context, data)
          

def hedge(context, data):            

    ''' Start S_LIBOR Strategy '''
    options = []

    ''' Start S_GLD_TLT_MA Strategy '''
    
    if(S_GLD_TLT_MA):
        # Add goldz to our buy list if above SMA
        if(symbol_above_sma(data, context.gld, GOLD_SMA_DAYS)):
            options.append(context.gld)
        if(symbol_above_sma(data, context.tlt, BOND_SMA_DAYS)):
            options.append(context.tlt)
            
    ''' Start S_GLD_TLT_SHY_MA Strategy '''
    
    if(S_GLD_TLT_SHY_MA):
        # Add goldz to our buy list if above SMA
        if(symbol_above_sma(data, context.gld, GOLD_SMA_DAYS)):
            options.append(context.gld)
        # Add bonds to our list of stocks to order if above SMA
        if(symbol_above_sma(data, context.tlt, BOND_SMA_DAYS)):
            options.append(context.tlt)
        if(len(options) == 0):
            options.append(context.shy)
    
    context.value_stocks = options
    buy_stuff(context, data)
    
    
def buy_stuff(context, data):
    ''' Buys the stocks within longs '''
    
    for s in context.portfolio.positions:
        if s in context.value_stocks:
            continue
        if not data.can_trade(s):
            continue 
        if s in context.momentum_stocks:
            continue
        order_target(s, 0)    
            
    for s in context.value_stocks:
        if s in security_lists.leveraged_etf_list.current_securities(get_datetime()):
            continue
        if not data.can_trade(s):
            continue
        order_target_percent(s, 0.5 / len(context.value_stocks))

        
def set_market_benchmark(context, data):
    if(context.now.time() < datetime.datetime(2003, 5, 1, 5, 0).time()):
        context.mkt = context.spy
    else:
        context.mkt = context.rsp
        
def benchmark_above_sma(context, data):
    ''' Calculate the market's SMA for the last SMA_THRESHOLD days '''
    market_sma = data.history(context.mkt, fields='price', bar_count=HISTORY_PERIOD+1, frequency='1d')[:-1].mean()
    current_price = data.current(context.mkt, 'price')
    #TODO: Replace 0 with SMA_BUFFER if you want
    return (current_price > ((1 + 0) * market_sma))

    
def symbol_above_sma(data, symbol, days):
    ''' Calculate the SMA for a single symbol '''
    current_symbol_price = data.current(symbol, 'price')
    symbol_sma = data.history(symbol, fields='price', bar_count=days+1, frequency='1d')[:-1].mean()
    return (current_symbol_price > symbol_sma)


def record_things(context, data):
    ''' Plot some custom signals ''' 
    market_sma = data.history(context.mkt, fields='price', bar_count=HISTORY_PERIOD+1, frequency='1d')[:-1].mean()
    current_price = data.current(sid(8554), 'price')
    #record(MKT=current_price)
    #record(ACC_LEV=context.account.leverage, TOTAL=context.portfolio.portfolio_value)
    
    
def cancel_open_orders(context, data):
    open_orders = get_open_orders()
    for security in open_orders:
        for order in open_orders[security]:
            cancel_order(order)
    
    #record(lever=context.account.leverage,
    record(Exposure=(context.account.leverage)*10)
    record(Fund_Value=context.portfolio.portfolio_value)
    pos_count = len([s for s in context.portfolio.positions if context.portfolio.positions[s].amount != 0])
    record(Stocks=(pos_count)) 

    
def rebalance(context, data):
    highs = data.history(context.assets, "high", context.talib_window, "1d")
    lows = data.history(context.assets, "low", context.talib_window, "1d")
    closes = data.history(context.assets, "price", context.market_window, "1d")
    
    estimated_cash_balance = context.portfolio.cash
    slopes = closes[context.selected_universe.index].tail(context.momentum_window_length).apply(slope)
    print(slopes.order(ascending=False).head(10))
    slopes = slopes[slopes > context.min_momentum]
    ranking_table = slopes[slopes > slopes.quantile(1 - context.rank_table_percentile)].order(ascending=False)
    log.info( len(ranking_table.index))
    # close positions that are no longer in the top of the ranking table
    positions = context.portfolio.positions
    for security in positions:
        if security in context.value_stocks:
            continue
        price = data.current(security, "price")
        position_size = positions[security].amount
        if data.can_trade(security) and security not in ranking_table.index:
            order_target(security, 0, style=LimitOrder(price)) #MARKET ORDER
            if security in context.momentum_stocks:
                context.momentum_stocks.remove(security)
            estimated_cash_balance += price * position_size
        elif data.can_trade(security):
            new_position_size = get_position_size(context, highs[security], lows[security], closes[security],security)
            if significant_change_in_position_size(context, new_position_size, position_size):
                estimated_cost = price * (new_position_size * context.leverage_factor - position_size)
                order_target(security, new_position_size * context.leverage_factor, style=LimitOrder(price)) #MARKET ORDER
                context.momentum_stocks.append(security)
                estimated_cash_balance -= estimated_cost
    
    
    # Market history is not used with the trend filter disabled
    # Removed for efficiency
    if context.use_market_trend_filter:
        market_history = data.history(context.market, "price", context.market_window, "1d")  ##SPY##
        current_market_price = market_history[-1]
        average_market_price = market_history.mean()
    else:
        average_market_price = 0
    
    if (current_market_price > average_market_price) :  #if average is 0 then jump in
        for security in ranking_table.index:
            if data.can_trade(security) and security not in context.portfolio.positions:
                new_position_size = get_position_size(context, highs[security], lows[security], closes[security],
                                                     security)
                estimated_cost = data.current(security, "price") * new_position_size * context.leverage_factor
                if estimated_cash_balance > estimated_cost:
                    order_target(security, new_position_size * context.leverage_factor, style=LimitOrder(data.current(security, "price"))) #MARKET ORDER
                    context.momentum_stocks.append(security)
                    estimated_cash_balance -= estimated_cost
    
     
def get_position_size(context, highs, lows, closes, security):
    try:
        average_true_range = talib.ATR(highs.ffill().dropna().tail(context.talib_window),
                                       lows.ffill().dropna().tail(context.talib_window),
                                       closes.ffill().dropna().tail(context.talib_window),
                                       context.atr_window)[-1] # [-1] gets the last value, as all talib methods are rolling calculations#
        if not context.use_average_true_range: #average_true_range
            average_true_range = 1 #divide by 1 gives... same initial number
            context.average_true_rage_multipl_factor = 1
        
        return (context.portfolio.portfolio_value * context.risk_factor)  / (average_true_range * context.average_true_rage_multipl_factor) 
    except:
        log.warn('Insufficient history to calculate risk adjusted size for {0.symbol}'.format(security))
        return 0
        

def significant_change_in_position_size(context, new_position_size, old_position_size):
    return np.abs((new_position_size - old_position_size)  / old_position_size) > context.significant_position_difference

def slope_(ts): ## new version log(log(ts))
    x = np.arange(len(ts))  
    log_ts = np.log(np.log(ts))  
    slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)  
    annualized_slope = (np.power(np.exp(slope), 250) - 1) * 100  
    return annualized_slope * (r_value ** 2)     

def slope(ts): ## new version
    x = np.arange(len(ts))  
    log_ts = np.log(ts)  
    slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)  
    annualized_slope = (np.power(np.exp(slope), 250) - 1) * 100 
    return annualized_slope * (r_value ** 2)

def slope_v(ts): # new (Vladimir)
    x = np.arange(len(ts))
    log_ts = np.log(ts) 
    slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)
    annualized_slope = ((1 + slope)**250 -1.0) * 100 
    return annualized_slope * (r_value ** 2) 

def _slope(ts): # original (James?)
    x = np.arange(len(ts))
    log_ts = np.log(ts)
    slope, intercept, r_value, p_value, std_err = stats.linregress(x, log_ts)
    annualized_slope = ((1 + slope)**250 ) * 100 
    return annualized_slope * (r_value ** 2) 


class MarketCap(CustomFactor):   
    inputs = [USEquityPricing.close, morningstar.valuation.shares_outstanding] 
    window_length = 1
    
    def compute(self, today, assets, out, close, shares):       
        out[:] = close[-1] * shares[-1]
        

def make_pipeline(context,sma_window_length, market_cap_limit):
    pipe = Pipeline()  
    
    # Now only stocks in the top N largest companies by market cap
    market_cap = MarketCap()
    top_N_market_cap = market_cap.top(market_cap_limit)
    
    #Other filters to make sure we are getting a clean universe
    is_primary_share = morningstar.share_class_reference.is_primary_share.latest
    is_not_adr = ~morningstar.share_class_reference.is_depositary_receipt.latest
    
    #### TREND FITLER ##############
    #### We don't want to trade stocks that are below their sma_window_length(100) moving average price.
    if context.use_stock_trend_filter:
        latest_price = USEquityPricing.close.latest
        sma = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=sma_window_length)
        above_sma = (latest_price > sma)
        initial_screen = (above_sma & top_N_market_cap & is_primary_share & is_not_adr)
        log.info("Init: Stock trend filter ON")
    else: #### TREND FITLER OFF  ##############
        initial_screen = (top_N_market_cap & is_primary_share & is_not_adr)
        log.info("Init: Stock trend filter OFF")

    pipe.add(market_cap, "market_cap")
    
    pipe.set_screen(initial_screen)
    
    return pipe

def make_value_stocks_pipeline(context):
    """
    df = get_fundamentals(
        .filter(fundamentals.company_reference.primary_exchange_id.in_(["NYSE", "NYS"]))
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio != None)         
        .filter(fundamentals.operation_ratios.total_debt_equity_ratio < 1.0)   
        #.filter(fundamentals.operation_ratios.quick_ratio > 1.0)
        .filter(fundamentals.operation_ratios.current_ratio > 1.5)
        .filter(fundamentals.valuation.market_cap != None)
        .filter(fundamentals.valuation.shares_outstanding != None)  
        .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK") # no pink sheets
        .filter(fundamentals.company_reference.primary_exchange_id != "OTCBB") # no pink sheets
        .filter(fundamentals.asset_classification.morningstar_sector_code != None) # require sector
        .filter(fundamentals.share_class_reference.security_type == 'ST00000001') # common stock only
        .filter(~fundamentals.share_class_reference.symbol.contains('_WI')) # drop when-issued
        .filter(fundamentals.share_class_reference.is_primary_share == True) # remove ancillary classes
        .filter(((fundamentals.valuation.market_cap*1.0) / (fundamentals.valuation.shares_outstanding*1.0)) > 1.0)  # stock price > $1
        .filter(fundamentals.share_class_reference.is_depositary_receipt == False) # !ADR/GDR
        #.filter(fundamentals.valuation.market_cap >= 10000.0E+06) #OFF
        #.filter(fundamentals.valuation.market_cap <= 250.0E+06) #OFF
        .filter(~fundamentals.company_reference.standard_name.contains(' LP')) # exclude LPs
        .filter(~fundamentals.company_reference.standard_name.contains(' L P'))
        .filter(~fundamentals.company_reference.standard_name.contains(' L.P'))
        .filter(fundamentals.balance_sheet.limited_partnership == None) # exclude LPs
        .filter(fundamentals.valuation_ratios.ev_to_ebitda != None)
        .filter(fundamentals.valuation_ratios.ev_to_ebitda >= 0.0)
        .filter(fundamentals.valuation_ratios.ev_to_ebitda <= 6.0) #NEED TO MAKE DYNAMIC (only pick bottom 10% cheapest of current market)
        #.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
        #.order_by(fundamentals.valuation.market_cap.desc())
        #.order_by(fundamentals.valuation_ratios.fcf_ratio.asc())
        .order_by(fundamentals.valuation_ratios.ps_ratio.asc())
        .limit(2000))
    """
    pipe = Pipeline(
        columns={
            'ev_to_ebitda': Fundamentals.ev_to_ebitda.latest,
            'sales_yield': Fundamentals.sales_yield.latest,
            'roic': Fundamentals.roic.latest,
            'ps_ratio': Fundamentals.ps_ratio.latest,
        },
        screen=(
        Fundamentals.primary_exchange_id.latest.element_of(["NYSE", "NYS"]) &
        Fundamentals.total_debt_equity_ratio.latest.notnull() &
        (Fundamentals.total_debt_equity_ratio.latest < 1.0) &
        # fundamentals.operation_ratios.quick_ratio > 1.0 &
        (Fundamentals.current_ratio.latest > 1.5) &
        Fundamentals.market_cap.latest.notnull() &
        Fundamentals.shares_outstanding.latest.notnull() &
        (Fundamentals.primary_exchange_id.latest != "OTCPK") &
        (Fundamentals.primary_exchange_id.latest != "OTCBB") &
        Fundamentals.morningstar_sector_code.latest.notnull() &
        (Fundamentals.security_type.latest.eq('ST00000001')) &
        (~Fundamentals.symbol.latest.has_substring('_WI')) &
        Fundamentals.is_primary_share.latest &
        (((Fundamentals.market_cap.latest * 1.0) / (Fundamentals.shares_outstanding.latest * 1.0)) > 1.0) &
        ~Fundamentals.is_depositary_receipt.latest &
        ~Fundamentals.standard_name.latest.has_substring(' LP') &
        ~Fundamentals.standard_name.latest.has_substring(' L P') &
        ~Fundamentals.standard_name.latest.has_substring(' L.P') &
        Fundamentals.limited_partnership.latest.isnull() &
        Fundamentals.ev_to_ebitda.latest.notnull() &
        (Fundamentals.ev_to_ebitda.latest >= 0.0) &
        (Fundamentals.ev_to_ebitda.latest <= 6.0)
        )
    )
    return pipe
There was a runtime error.