Back to Community
A Value Investment Strategy That Combines Security Selection And Market Timing Signals

In this exercise, we attempt to develop an institutional quality investment cum trading strategy with small and retail investor in mind. Apart from lack of professional expertise and smaller investible amounts, individual investors also face limitations in their ability to taking short positions. Considering these restrictions, we attempt to develop a value investment strategy and augment the portfolio performance by timing its positions. Every quarter, we screen value stocks (having high book-to-market ratio) based on Piotroski F-Score algorithm (and a Modified FS-Score algorithm); and take active positions in these stocks, in the interim. We compare the performance of our algorithms with that of original Piotroski F-Score algorithm and with that of S&P 500 Value index (SPYV).

Paper: https://ssrn.com/abstract=3451859
https://github.com/NeelkanthMehta/A-Value-Investment-Strategy-That-Combines-Security-Selection-And-Market-Timing-Signals.git

Clone Algorithm
11
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
"""
THIS IS COMBINATION OF PIOTROSKI ALGOROTHM THAT SCREENS VALUE SECURITIES AND TECHINCAL INDICATORS OF RSI, MFI and ADX
1. Modified Piotroski FS-Score is an score between 0 - 10, used to assess strength of company's financial performance
   
   The score is calculated based on 9 criteria divided into 3 groups:
    
   Profitability:
       Return on Assets normalized by Total Assets (1 point if greater than 0, 0 otherwise);
       Free Cash Flow to Firm normalized by Total Assets (1 point if greater than 0, 0 otherwise);
       Growth in Return of Assets (ROA) over previous year (1 point if greater than 0, 0 otherwise);
       Free Cash Flow to firm (less) Return on Assets (1 point if greater than 0, 0 otherwise);
       Growth in Free Cash Flow to Firm over previous year (1 point if greater than 0, 0 otherwise);
   
   Leverage, Liquidity and Source of Funds
       Drop in Leverage (long-term) ratio (1 point if less than 0, 0 otherwise);
       Growth in Current ratio (1 point if greater than 0, 0 otherwise);
       No growth in the net shares outstanding (1 point if less than or equal to than 0, 0 otherwise);
   
   Operating Efficiency
       Growth in Gross Margin (1 point if greater than 0, 0 otherwise);
       Growth in Asset Turnover ratio (1 point if greater than 0, 0 otherwise);

2. Relative Strength Index (RSI):
   Classified as a momentum Oscillator, RSI charts the current and historical strength or weakness of a stock or market based on the closing prices of a recent trading period[3]
   
   a. n-period Smoothed/ Modified Moving Average --> SMMA
   b. RS = SMMA(U, n)/ SMMA(U, n)
   c. RSI = 100 - (100/ (1 + RS))

3. Money Flow Index (MFI):
   Again an Oscilator, is used to show the money flow (an approximation of the dollar value of a day's trading) over several days [4].
   Steps to calculate are:
   a. typical price = (high + low + close)/ 3
   b. money flow = typical price x volume
      Positive money flow: days when money flow is higher than previous day's money flow
      Negative money flow: days when money flow is lower than previous day's money flow
   c. money ratio = Positive money flow/ Negative money flow
   d. MFI = 100 x (Positive money flow/ (Positive money flow + Negative money flow))

4. Average Directional Movement Index (ADX):
   Indicates strength in current series of prices of a financial instrument. It is a combination of two other positive directional indicator positive/ negative Directional Indicator (or +/ -DI)[5].
   UpMove = h_t - h_(t-1)
   DownMove = l_{t-1} - l_t
   if UpMove > DownMove & UpMove > 0, then +DM = UpMove, else +DM = 0
   if DownMove > UpMove & DownMove > , then -DM = DownMove, else -DM = 0
   
   After selecting number of periods, +DI and -DI are:
       +DI = 100 x SMMA(+DM)/ Average True Range
       -DI = 100 x SMMA(-DM)/ Average True Range

ref:
    1. https://en.wikipedia.org/wiki/Piotroski_F-Score
    2. https://www.aaii.com/journal/article/simple-methods-to-improve-the-piotroski-f-score
    3. https://en.wikipedia.org/wiki/Relative_strength_index
    4. https://en.wikipedia.org/wiki/Money_flow_index
    5. https://en.wikipedia.org/wiki/Average_directional_movement_index
"""

# Importing all the necessary libraries and modules
import numpy as np
import pandas as pd
import talib
import quantopian.algorithm as algo
import quantopian.optimize as opt
from sklearn import preprocessing
from scipy.stats.mstats import winsorize
from quantopian.pipeline import factors
from quantopian.pipeline import filters
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.experimental import risk_loading_pipeline
from quantopian.pipeline.data import Fundamentals
import quantopian.pipeline.factors as Factors

# Defining universal variables
WIN_LIMIT = 0.0
MAX_GROSS_LEVERAGE = 1.0
NUM_LONG_POSITIONS = 25
NUM_SHORT_POSITIONS= 25
# MAX_LONG_POSITION__SIZE = 0.5/ NUM_LONG_POSITIONS
# MAX_SHORT_POSITION_SIZE = -0.5/ NUM_SHORT_POSITIONS

# defining winsorizing function
def preprocess(a):
    a = np.nan_to_num(a - np.nanmean(a))
    a = winsorize(a, limits=[WIN_LIMIT, WIN_LIMIT])
    return preprocessing.scale(a)


# Piotroski 9-pt criteria
class Piotroski(factors.CustomFactor):
    inputs = [
        Fundamentals.roa,
        Fundamentals.free_cash_flow,
        Fundamentals.total_assets,
        Fundamentals.cash_flow_from_continuing_operating_activities,
        Fundamentals.long_term_debt_equity_ratio,
        Fundamentals.current_ratio,
        Fundamentals.shares_outstanding,
        Fundamentals.gross_margin,
        Fundamentals.assets_turnover
    ]
    
    window_length = 252
    
    def compute(self, today, assets, out, roa, cash_flow, total_assets, cash_flow_from_ops, long_term_debt_ratio, current_ratio, shares_outstanding, gross_margin, assets_turnover):
        
        profit = (
            (roa[-1] > 0).astype(int) +
            ((cash_flow[-1]/ total_assets[-1]) > 0).astype(int) +
            ((cash_flow[-1]/ total_assets[-1]) > (cash_flow[0]/ total_assets[0])).astype(int) +
            (roa[-1] > roa[0]).astype(int) +
            (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        )
        
        leverage = (
            (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int) +
            (shares_outstanding[-1] <= shares_outstanding[0]).astype(int) +
            (current_ratio[-1] > current_ratio[0]).astype(int)
        )
        
        operating = (
            (gross_margin[-1] > gross_margin[0]).astype(int) +
            (assets_turnover[-1] > assets_turnover[0]).astype(int)
        )
        
        out[:] = profit + leverage + operating


# Money Flow Index (MFI) indicator
class MFI(Factors.CustomFactor):
    """
    Money Flow Index
    Volume Indicator
    **Default Inputs:**  USEquityPricing.high, USEquityPricing.low, USEquityPricing.close, USEquityPricing.volume
    **Default Window Length:** 15 (14 + 1-day for difference in prices)
    http://www.fmlabs.com/reference/default.htm?url=MoneyFlowIndex.htm
    """

    inputs = [USEquityPricing.high, USEquityPricing.low, USEquityPricing.close, USEquityPricing.volume]
    window_length = 15

    def compute(self, today, assets, out, high, low, close, vol):

        # calculate typical price
        typical_price = (high + low + close) / 3.

        # calculate money flow of typical price
        money_flow = typical_price * vol

        # get differences in daily typical prices
        tprice_diff = (typical_price - np.roll(typical_price, 1, axis=0))[1:]

        # create masked arrays for positive and negative money flow
        pos_money_flow = np.ma.masked_array(money_flow[1:], tprice_diff < 0, fill_value = 0.)
        neg_money_flow = np.ma.masked_array(money_flow[1:], tprice_diff > 0, fill_value = 0.)

        # calculate money ratio
        money_ratio = np.sum(pos_money_flow, axis=0) / np.sum(neg_money_flow, axis=0)

        # MFI
        out[:] = 100. - (100. / (1. + money_ratio))


# Initializing algorithms and routines therein
def initialize(context):
    """
    Called once at the start of the algorithm.
    """
    set_benchmark(symbol('SPY'))
    set_commission(commission.PerTrade(cost=0.0))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0))
    set_long_only()
    # set_max_leverage(1.1)
    
    # Rebalance every day, 1 hour after market open.
    algo.schedule_function(rebalance, algo.date_rules.every_day(), algo.time_rules.market_open(hours=1),)

    # Record tracking variables at the end of each day.
    algo.schedule_function(record_vars, algo.date_rules.every_day(), algo.time_rules.market_close(),)
    
    # Create our dynamic stock selector.
    algo.attach_pipeline(make_pipeline(), 'piotroski')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors')


# Defining a Quantopian pipeline
def make_pipeline():
    """
    A function to create our dynamic stock selector (pipeline). Documentation
    on pipeline can be found here:
    https://www.quantopian.com/help#pipeline-title
    """

    # Base universe set to the QTradableStocksUS
    base_universe = QTradableStocksUS()

    # For Piotroski, we need stocks with Book-to-Market >= 1
    book = Fundamentals.book_value_per_share.latest
    market = Fundamentals.market_cap.latest/ Fundamentals.shares_outstanding.latest
    book_to_market = book/ market
    p_universe = book_to_market >= 1
    
    # CustomFactor of Piotroski F-Score
    f_score = Piotroski()
    
    # Filtering top and bottom 25 stocks
    longs  = f_score.eq(10) | f_score.eq(9) | f_score.eq(8)  #f_score.top(25, mask=base_universe)
    shorts = f_score.eq(0) #f_score.bottom(25, mask=base_universe)
    
    universe = base_universe & p_universe & ( longs| shorts)

    pipe = Pipeline(
        columns={
            'longs': longs,
            'shorts': shorts,
            'f_score': f_score,
        },
        screen=universe
    )
    return pipe


def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    try:
        # generates output
        context.output = algo.pipeline_output('piotroski')
        
        # loads risk factors
        context.risk_loadings = algo.pipeline_output('risk_factors')
        
        # records position
        record(cash=context.portfolio.cash, asset=context.portfolio.portfolio_value, ) 

        # These are the securities that we are interested in trading each day.
        context.security_list = context.output.index.tolist()
    except Exception as e:
        print(str(e))


def rebalance(context, data):
    """
    Execute orders according to our schedule_function() timing.
    """
    #: Obtaining Prices
    # prices = data.history(context.security_list, 'price', 252, '1d')
    
    #: Compute for portfolio weights
    long_secs = context.output[context.output['longs']].index
    long_weight = (1.0/ max(len(long_secs), 1))
    
    short_secs = context.output[context.output['shorts']].index
    short_weight = 0 #(0.0/ max(len(short_secs), 1))
    
    # Open our long position.
    for security in long_secs:
        try:
            RSI_daily = factors.RSI(inputs = [USEquityPricing.close], window_length = 14)
            
            MFI_Daily = MFI()
            
            # Defining ADX
            period = 30
            H = data.history(security,'high', 2*period,'1d') #data.history should be changed right?
            L = data.history(security,'low', 2*period,'1d')
            C = data.history(security,'price', 2*period,'1d')
            ta_ADX = talib.ADX(H, L, C, period)  
            ta_nDI = talib.MINUS_DI(H, L, C, period)  
            ta_pDI = talib.PLUS_DI(H, L, C, period)  
            ADX = ta_ADX[-1]
            nDI = ta_nDI[-1]
            pDI = ta_pDI[-1]

            # defining Long and short positions position criteria
            if data.can_trade(security) and (RSI_daily < 30) and (MFI_Daily < 20) or ((ADX > 20) & (pDI > nDI)):
                log.info("Going long on stock %s"%(security.symbol))
                order_target_percent(security, long_weight)
            
            if data.can_trade(security) and (RSI_daily > 70) and (MFI_Daily > 80) or ((ADX > 25) & (pDI < nDI)):
                log.info("Going short on stock %s"%(security.symbol))
                order_target_percent(security, 0)
        except:
            pass
   
    # Closing the position
    for security in context.portfolio.positions:
        if data.can_trade(security) and security not in long_secs and security not in short_secs:
            order_target_percent(security, 0)


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


def handle_data(context, data):
    """
    This inbuilt function is called every minute.
    Here, stop loss is defined, so that it runs every minute
    
    The logic is that if a security's price is 10% lower than the price it was purchased, a stoplimit  order is triggred. 
    # """
    context.security_list = context.output.index.tolist()
    for security in context.security_list:
        current_price = data.current(security, 'price')
        position = context.portfolio.positions[security].amount
        price_position = context.portfolio.positions[security].cost_basis
        if (position > 0) and (current_price < price_position * 0.9):
            order_target_percent(security, 0, style=LimitOrder(current_price))
            log.info('Sell with stop loss hit' + str(security.symbol))
There was a runtime error.