Back to Community
Buy Red Candle, Hold 12 weeks, Sell (Stocks and Commodities, April 2017, pg 18)

This month in Stocks and Commodities magazine (The title of the article is Playing with Numbers by Domenico D'Errico) an algorithm for trading was presented that:
1. On a weekly basis
2. If a red candle forms (close - open < 0), buy security
3. Sell after 12 weeks

I made one very minor changes:
1. If leverage > desired_leverage sell to bring to desired_leverage

Because QT doesn't have GTC orders there is a lot of code that pollutes meaning that needs to be written.
1. If partial fill reissue order for remaining shares
2. If failed order try to reissue it

Enhancements
1. More than one stock is allowed to be traded. Statically allocated to 1/number_of_stocks as a percentage of portfolio.

This algorithm for SPY from 2002 thru 3/21/2017 outperforms the SPY. The drawdown is still too high to be useful but it is an interesting concept.

Clone Algorithm
15
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
'''
Stocks and Commodities April 2017 pg 18
Playing with Numbers (title)
Algorithm is buy red candle on 12 week basis
by Domenico D'Errico

Algorithm (as best as I can interpret):
On a weekly basis
if close < open buy SPY
hold for 12 weeks and sell
repeat

Modifications:
Can have more than 1 stock. Add to pipeline sid_list.

Quantopian no GTC orders:
failed_order_list only works for static (1/number_of_stocks) position allocations
need to re-order for partial fills

John Glossner
[email protected]
http://Linkedin.com/in/glossner

MIT License

2017 03 22 Initial version
'''

from quantopian.pipeline import Pipeline, CustomFilter
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import CustomFactor

import numpy as np
import pandas as pd
import datetime

        
def initialize(context):
    context.desired_leverage = 1.0         #Change up or down as desired
    
    #Avoid first 30 minutes of trading
    schedule_function(trade,     date_rules.week_start(), time_rules.market_open(minutes=30))
    schedule_function(rebalance, date_rules.every_day(),  time_rules.market_open(minutes=30))
    schedule_function(plots,     date_rules.every_day(),  time_rules.market_close(minutes=1))

    attach_pipeline(my_pipeline(context), 'my_pipeline')

    context.red_candles = pd.DataFrame()
    context.position_entered_date = {}     #Equity():date. Used to count time for 12 weeks.
    context.exit_list = {}                 #Equity():info. If the order fails or gets split we need to re-place it.
    context.failed_order_list = {}         #Equity():percent. To replace orders.
    return
    
    
def before_trading_start(context, data):
    ''' 
    Runs daily before the start of trading 
    Get pipeline output
    Determine red candles
    '''
    context.output = pipeline_output('my_pipeline').dropna()
    context.red_candles = context.output['red_candle']
    context.red_candles = context.red_candles[ context.red_candles < 0 ]    #Only select when close < open
    
    return
 

def trade(context, data):
    ''' 
    frequency: week_start()
    Buy on red candle. Hold 12 weeks then sell it.
    '''

    #No positions in portfolio. Open for first time
    for security in context.red_candles.index:
        if not in_portfolio(context, data, security):
            if allow_trade(context, data, security):
                portfolio_percent = context.desired_leverage/len(context.output.index)
                ord = order_target_percent(security, portfolio_percent )
                if ord:
                    log_order(context, data, ord, security, "trade(): No positions")
                    context.position_entered_date[security] = get_datetime()    #Log date of order
                else:
                    context.failed_order_list[security] = portfolio_percent
        return
    
    #If position 12 weeks old sell it
    if len(context.portfolio.positions) != 0:          
        for pos in context.portfolio.positions:
            if get_datetime() - context.position_entered_date[pos] >= datetime.timedelta(weeks=12):
                if allow_trade(context, data, pos):
                    ord = order_target_percent(pos, 0)
                    log_order(context, data, ord, pos, "trade(): 12 week old position")
                    if ord: context.exit_list[pos] = 'position > 12 weeks old'    #Add to exit_list
                        
                    #Remove from time counter list even if order fails b/c now in exit order list
                    if context.position_entered_date[pos]:
                        del context.position_entered_date[pos]    #remove key and value from dictionary                  
    return

def rebalance(context, data):
    '''
    frequency: daily because Quantopian doesn't have GTC orders. 
    Makes sure paritally filled/failed exit orders go to 0
    Places orders to bring portfolio up/down to desired percentage of value if out_of_bounds()
    Accounts for partially filled orders
    Ensures leverage stays at desired leverage
    
    Note: might want to put all this in handle_data() but I put it here for simulation speed
    '''
    #Handle exit list - for orders partially sold (or failed) when placed
    for security in context.exit_list.copy(): #Copy b/c can't iterate through a dictionary and delete keys
        if context.portfolio.positions[security].amount != 0:
            if allow_trade(context, data, security):    #Keep placing trades until 0 in exit_list
                ord = order_target(security, 0)
                log_order(context, data, ord, security, "rebalance() - handle exit list. Get out of position")
                if not ord: continue    #order failed
            else: #In exit list but amount = 0
                del context.exit_list[security]
    
    #Handle failed orders
    for security in context.failed_order_list.copy():
        if allow_trade(context, data, security):
            ord = order_target(security, context.failed_order_list[security])   
            log_order(context, data, ord, security, "rebalance() - handle failed orders")
            if not ord: continue
            del context.failed_order_list[security]
    
    #Leverage too high
    if context.account.leverage > context.desired_leverage:
        for pos in context.portfolio.positions:  
            if allow_trade(context, data, pos) and over_levered_position(context, data, pos):
                ord=order_target_percent(pos, context.desired_leverage/len(context.output.index))
                log_order(context, data, ord, pos, "rebalance() - leverage too high")
   
    #Correct for no GTC orders. Also rebalances if outside a band range                   
    if len(context.portfolio.positions) != 0 :                  
        for pos in context.portfolio.positions:  
            if allow_trade(context, data, pos) and out_of_bands(context, data, pos):
                ord=order_target_percent(pos, context.desired_leverage/len(context.output.index))
                #don't have to check order because it will replace automatically
                log_order(context, data, ord, pos, "rebalance() - GTC correction. Keep portfolio invested")
    return    

def allow_trade(context, data, security):
    '''
    Logic for placing trades
    - can trade, no open orders, not in exit list, not a failed order
    '''
    if data.can_trade(security) and \
       not get_open_orders(security) and \
       security not in context.exit_list and \
       security not in context.failed_order_list : return True
    return False

def over_levered_position(context, data, security):
    '''
    Very simple algo to lower leverage
    Choose most levered position, if pos is that position return True
    '''
    security_percent = percent_of_portfolio(context, data, security) 
    
    if security_percent < context.desired_leverage / len(context.output) : return False    #Only lower position

    for pos in context.portfolio.positions:
        position_percent = percent_of_portfolio(context, data, pos)
        if position_percent > security_percent : return False
    return True

def percent_of_portfolio(context, data, pos):
    '''
    Return the percent of a portfolio of a specific security 
    '''
    return context.portfolio.positions[pos].amount * \
           data.current(pos, 'price') / context.portfolio.portfolio_value
    
    
def log_order(context, data, order, security, info):
    '''
    Log an order to the console
    info: Any string to print out
    '''
    if order:
        if get_order(order).amount > 0:
            log.info(str(info) + '  ' + '++++ placing order for ' + str( get_order(order).amount) + '  ' + str(security))
        else:
            log.info(str(info) + '  ' + '---- Exiting Order for ' + str( get_order(order).amount) + '  ' + str(security))  
    else:    #empty order 
         log.info(str(info) + '  ' +  '!!!!! WARNING: Order FAILED for ' + str(security))
    return


def plots(context, data):
    '''
    Custom plot on Quantopian Chart
    '''
    record(leverage=context.account.leverage)
    return

def in_portfolio(context, data, security):
    '''
    Determine if a specific security is in the portfolio
    '''
    for pos in context.portfolio.positions:
        if pos.sid == security.sid  : return True
    return False

def out_of_bands(context, data, pos, band_percent=0.05):
    '''
    Quantopian when rebalancing will place very small trades.
    This inhibits trades when the position is within band_percent of the desired value
    '''
    if len(context.output) == 0: return False
    
    total_securities = len(context.output)
    target_position_percent = context.desired_leverage/total_securities
    guard_low =  (1.0 - band_percent)*target_position_percent
    guard_high = (1.0 + band_percent)*target_position_percent

    position_percent =  context.portfolio.positions[pos].amount * \
                        data.current(pos, 'price') / context.portfolio.portfolio_value

    if position_percent <  guard_low or position_percent > guard_high: return True
    
    return False


def in_exit_list(context, data, security):
    '''
    Determine if a specific security is in the exit list
    The function is general (any list) but it is named for clarity
    '''
    if security in context.exit_list : return True
    return False

def my_pipeline(context):
    # Custom filter to return only sids in the list
    my_sid_filter = SidInList(
        sid_list = (                                      
            symbol('SPY').sid,  # S&P500 ETF
            #symbol('XLU').sid,  # XLU Utilities              
        )
    )
        
    pipe = Pipeline(
            columns = {
                    'red_candle' : USEquityPricing.close.latest - USEquityPricing.open.latest,  #close - open. negative value is red candle
                    },
            screen = my_sid_filter,
            )
    
    return pipe

class SidInList(CustomFilter):
    """
    Filter returns True for any SID included in parameter tuple passed at creation.
    Usage: my_filter = SidInList(sid_list=(23911, 46631))
    https://www.quantopian.com/posts/using-a-specific-list-of-securities-in-pipeline
    """    
    inputs = []
    window_length = 1
    params = ('sid_list',)

    def compute(self, today, assets, out, sid_list):
        out[:] = np.in1d(assets, sid_list)       
        
        
'''
MIT License

Copyright (c) 2017 John Glossner

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
There was a runtime error.