Back to Community
Need Help with First Algorithm - Small Cap Stock Picking Strategy

Hi,

I'm looking for help in finishing my first algorithm. I'm new to Python so please bear with me if my questions are on the elementary side. Below is a summary of what I'm trying accomplish followed by a list of questions.

Small Cap Stock Picking Strategy  

1. Market Cap between 15M and 150M  
2. Dividend yield >= 2%  
3. Country = USA, no ADR  
4. No OTC stocks  
5. EPS growth this year >= 10% (trying to match Finviz filter)  
6. EPS growth Q/Q >= 20% (trying to match Finviz filter)  
7. Price between $5 and $20  

Rebalance:  
1.  Rebalance monthly, equal weights  
2.  Sort by price closest to 52-week high  
3.  Take the top 5 stocks.

Questions:

  1. How is the fundamental query tied to the fundamental filters? Do I need to have a query for each filter?
  2. I'm trying to match the filter from the FINVIZ stock screener for 'EPS Growth this Year'. Is this the same as 'diluted.eps.growth'?
  3. I'm also trying to match the FINVIZ filter for 'EPS Growth Q/Q', which compares the current quarter EPS to that of the quarter one year ago. How can I add this?
  4. How do I add a filter to only buy stocks in the $5 to $20 price range?
  5. How do I sort and rank filtered stocks based on how close they are to their 52-week highs (top five stocks nearest their 52wk highs).
  6. Does this algorithm currently handle stocks that were delisted or had their symbols changed, etc?

Sorry for all of the questions. Any help that can be provided on any or all of the above would be appreciated. Other comments regarding possible errors or recommendations for improvement are also welcome.

Thanks in advance.

Clone Algorithm
123
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
"""
    Small Cap Stock Picking Strategy
    
    1. Market Cap between 15M and 150M
    2. Dividend yield >= 2%
    3. Country = USA, no ADR
    4. No OTC stocks
    5. EPS growth this year >= 10% (trying to match Finviz filter)  **** IS THIS THE SAME AS diluted.eps.growth? ****
    6. EPS growth Q/Q >= 20% (trying to match Finviz filter) **** NEED HELP TO ADD THIS ****
    7. Price between $5 and $20 **** NEED HELP TO ADD THIS ****
    
    Rebalance:
    1.  Rebalance monthly, equal weights
    2.  Sort by price closest to 52-week high **** NEED HELP TO ADD THIS ****
    3.  Take the top 5 stocks.

"""

import pandas as pd
import numpy as np
import datetime

def initialize(context):
    # Dictionary of stocks and their respective weights
    context.stock_weights = {}
    # 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):
    # Track cash to avoid leverage
    cash = context.portfolio.cash

    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in context.fundamental_df:
            if stock in data:
                order_target(stock, 0)
                cash += context.portfolio.positions[stock].amount * data[stock].price
                 
                # positions[stock].amount returns the number of shares.
                # To convert to the cash that will be available after sale, it should be multiplied with price
                 
    # Create weights for each stock
    weight = create_weights(context, context.stocks)

    # Rebalance all stocks to target weights
    for stock in context.fundamental_df:
        if weight != 0 and stock in data:
            notional = context.portfolio.portfolio_value * weight
            price = data[stock].price
            numshares = int(notional / price)
            
            # Growth companies could be trading thin: avoid them
            #if cash > price * numshares and numshares < data[stock].volume * 0.2:
            if stock in data:
                    order_target_percent(stock, weight)

                    cash -= notional
                
    
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.
    """

    num_stocks = 500
    
    # Screen stocks based on the above criteria.
    # Sort in descending order. **** Need help to change sort from dividend yield to 'closest to 52-week high' ****
    
    fundamental_df = get_fundamentals(
        query(
            # put your query in here by typing "fundamentals."
            fundamentals.operation_ratios.revenue_growth,
            fundamentals.valuation.market_cap,
            fundamentals.valuation_ratios.pe_ratio,
            fundamentals.valuation.shares_outstanding,
            fundamentals.company_reference.country_id,
        )
        .filter(fundamentals.valuation.market_cap > 15000000)
        .filter(fundamentals.valuation.market_cap < 150000000)
        .filter(fundamentals.valuation.shares_outstanding != None)
        #.filter(fundamentals.valuation_ratios.pe_ratio < 20)
        #.filter(fundamentals.valuation_ratios.fcf_per_share < 12)
        .filter(fundamentals.valuation_ratios.dividend_yield >= 0.02)
        .filter(fundamentals.company_reference.country_id == "USA")                               
        .filter(fundamentals.earnings_ratios.diluted_eps_growth > 0.1)
        .filter(fundamentals.share_class_reference.is_primary_share == True)
        .filter(fundamentals.share_class_reference.is_depositary_receipt == False)
        .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK")
        #.order_by(fundamentals.valuation.market_cap.asc())
        .order_by(fundamentals.valuation_ratios.dividend_yield.desc())
        #.order_by(fundamentals.earnings_ratios.diluted_eps_growth.desc())
        .limit(5)
    )

    # 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)   
    
    record (cash = context.portfolio.cash, asset = context.portfolio.portfolio_value)
    
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
        ## If you make the weight 100% total, you might end up buying more than 100% by the time
        ## all the orders are fulfilled.  If that happens, everytime you will be increasing leverage.
        weight = .9/len(stocks)
        return weight
        
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.
5 responses

Hi Troy,

This sounds like a job for the Pipeline API! With Pipeline, you can rank and sort stocks to define your own custom tradable universe of securities. In Pipeline, you have access to historical pricing and fundamental data. There are also some sample algos here.

Regarding your questions on Fundamentals data, there is a description for all the fundamentals data fields here. Unfortunately there is no great way to compare a quarterly fundamental value to one from a year ago, but take a look at the algo posted here for a 'hack' solution that compares the current EPS to the previous quarter's EPS.

If you use Pipeline to implement your strategy, you won't need to worry about delisted stocks, OTC, or symbol changes. Behind the scenes, the delisted + OTC stocks will not make it through the filters that you listed (price, EPS growth, etc.), and securities are referenced by a security ID (SID) that takes care of any symbol changes for you. That being said, if you hold position in a security that becomes delisted, it stays in your portfolio. Technically, you can check if a security has stopped trading if you look at the end_date attribute of the stock in data. However, this uses lookahead bias an does not translate to live trading. You're better off trying to develop a strategy that avoids holding position in companies likely to bankrupt. We realize that this is not a great solution, and we are working on developing a better one (for example, emptying your portfolio of delisted securities).

I hope this helps!

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.

Hello Troy,
Welcome to the community. I’ll only answer some of your questions that I think I can help with.
Question 2 – diluted_eps_growth: The growth in the company’s diluted earnings per share (EPS) on a percentage basis. Morningstar calculates the annualized growth percentage based on the underlying diluted EPS reported in the Income Statement within the company filings or reports. This was taken from https://www.quantopian.com/help/fundamentals.
Question 4 – Check out the algo I have shared with you, this should provide a bunch of new tools that you can use. Check out specifically “fundamentals.valuation.market_cap*1.0”
Question 5 - You may find inspiration from what is contained in the def_filter function: volume = history(30, '1d', 'volume')[df_securities].
Question 6 – Look at the extra fundamental filters I have. This should help. What Jamie vaguely referred to can be found in the df_filter function. You may want to also check out the function “has_orders” which prevents adding new orders when orders are not being filled. If you still have a prob with a stock, you may just want to add it to the context.flagged_stocks.
Also try searching the words “value” or “buffet” or “graham” or “fundamental” to try and find other examples of people using fundamental filters that might help you with your other questions.
Best of luck,

Clone Algorithm
27
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
"""
    Trading Strategy using Fundamental Data and with a Sector Choice Ranking option
    1. Filter companies by market cap and other fundamental metrics
    2. Find the top sectors that have a desired metric
    3. Every month exit all the positions before entering new ones at the month
    4. Log the positions that we need 
"""
import pandas as pd  
import numpy as np  
import talib
from datetime import timedelta

def initialize(context):  
    ##### Decide parameters here
    context.num_stocks = 20 # number of securities in portfolio
    context.sect_select = False # Ture = Additional Filter by sector, currently set to Price to earnings (PE)
    context.sect_sort_order = False #True = High, False = Low
    context.sect_numb = 11 # Number of sectors to choose portfolio postions from
    set_benchmark(symbol('SPY'))
    context.flagged_stocks = ['GLBC','BELM'] # specify securities not to trade
    set_do_not_order_list(security_lists.leveraged_etf_list) # force algo not to buy leveraged ETFs
    #set_commission(commission.PerTrade(cost=10.00)) # set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.0))
    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1)) # the effect of trading a security on the market IRL
    #####
    context.vol_min = [] # Minimum average volume per day
    context.needs_rebalance = False
    context.stock_weights = {}  # Dictionary of stocks and their respective weights 
    context.sector_mappings = { # Sector mappings
       101.0: "Basic Materials",  
       102.0: "Consumer Cyclical",  
       103.0: "Financial Services",  
       104.0: "Real Estate",  
       205.0: "Consumer Defensive",  
       206.0: "Healthcare",  
       207.0: "Utilites",  
       308.0: "Communication Services",  
       309.0: "Energy",  
       310.0: "Industrials",  
       311.0: "Technology"  
    }  
    schedule_function(rebalance, # Rebalance monthly on the first day of the month at market open
                      date_rule=date_rules.month_start(),  
                      time_rule=time_rules.market_open())
    
def rebalance(context, data):
    df_securities = df_filter(context, data)
    for stock in context.portfolio.positions: # Exit all positions before starting new ones
        if stock not in df_securities and stock in data:  
            order_target_percent(stock, 0)
    if context.sect_select == True:
        log.info("The sectors we are ordering today are %r" % context.sectors) # log info
    else:
        pass

    weight = create_weights(context, context.stocks) # Create weights for each position
        
    for stock in df_securities: # Rebalance all positions to target weights
        if stock in data:
            if weight != 0:  
                log.info("Ordering %0.0f%% percent of %s in %s" # log our position entries
                         % (weight * 95,  
                            stock.symbol,  
                            context.sector_mappings[context.fundamental_df[stock]['morningstar_sector_code']]))  
            order_target_percent(stock, (weight*0.95))
    
def before_trading_start(context, data): # called before the start of each trading day. It updates our universe with the securities and values from fetch_fundamentals
    fundamental_df = get_fundamentals(  
        query(  
            fundamentals.valuation.market_cap, # put your query in here by typing "fundamentals."
            fundamentals.valuation_ratios.pe_ratio,  
            fundamentals.valuation_ratios.pb_ratio,
            fundamentals.operation_ratios.long_term_debt_equity_ratio,
            fundamentals.operation_ratios.current_ratio,
            fundamentals.operation_ratios.roe,
            fundamentals.valuation_ratios.book_value_per_share,
            fundamentals.valuation_ratios.pcf_ratio,
            fundamentals.valuation_ratios.fcf_ratio,
            fundamentals.valuation_ratios.peg_ratio,
            fundamentals.asset_classification.morningstar_sector_code
        )  
        #.filter(fundamentals.valuation.market_cap                          < 1.5e9   ) # >100e6 is 100,000,000
        .filter(fundamentals.valuation.market_cap                           >= 100e6 ) # >25e6 is 25,000,000
        .filter(fundamentals.valuation_ratios.pe_ratio                     < 25      )
        #.filter(fundamentals.company_reference.country_id                 == "USA"   ) 
        #.filter(fundamentals.balance_sheet.current_debt                    != None   ) # < 80e6
        #.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 bulletin boards
        .filter(fundamentals.asset_classification.morningstar_sector_code  != None   ) # require sector
        .filter(fundamentals.valuation.market_cap                          != None   )
        .filter(fundamentals.valuation_ratios.pe_ratio                     != None   )
        .filter(fundamentals.valuation_ratios.pb_ratio                     != None   )
        .filter(fundamentals.valuation_ratios.book_value_per_share         != None   )
        .filter(fundamentals.operation_ratios.long_term_debt_equity_ratio  != None   )
        .filter(fundamentals.operation_ratios.current_ratio                != None   )
        .filter(fundamentals.operation_ratios.roe                          != None   )
        .filter(fundamentals.operation_ratios.roa                          != None   )
        .filter(fundamentals.valuation_ratios.pcf_ratio                    != None   )
        .filter(fundamentals.valuation_ratios.fcf_ratio                    != None   )
        .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.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
                
        .order_by(
            fundamentals.valuation_ratios.pe_ratio.asc(), # desc for highest first, asc for lowest first
            # fundamentals.whatever.desc(),
        ) 
        .limit(300)  
    )

    if context.sect_select == True:
        sector_pe_dict = {} # Find sectors with the desired PE
        for stock in fundamental_df:  
            sector = fundamental_df[stock]['morningstar_sector_code']  
            pe = fundamental_df[stock]['pe_ratio']  
                          
            if sector in sector_pe_dict: # If it exists add our PE to the existing list. Otherwise don't add it.
                sector_pe_dict[sector].append(pe) # sector_pe_dict[sector].append(whatever)
            else:  
                sector_pe_dict[sector] = []

        # print sector_pe_dict 
        sector_pe_dict = dict([(sectors, np.mean(sector_pe_dict[sectors])) # Find average PE per sector 
                                   for sectors in sector_pe_dict if len(sector_pe_dict[sectors]) > 0])
        sectors = sorted(sector_pe_dict, key=lambda x: sector_pe_dict[x], reverse=context.sect_sort_order)[:context.sect_numb] 
        context.stocks = [stock for stock in fundamental_df # Filter out only stocks within that particular sector
                          if fundamental_df[stock]['morningstar_sector_code'] in sectors]  
        context.sectors = [context.sector_mappings[sect] for sect in sectors] # Initialize a context.sectors variable, not really sure what this does
    else:
        context.stocks = [stock for stock in fundamental_df]
    context.fundamental_df = fundamental_df[context.stocks]
    update_universe(context.fundamental_df.columns.values) # for a fundamental filter this MUST be used

def df_count(context, data):
    df_count = []
    df_count = [stock for stock in context.fundamental_df]
    record(dfnum_positions = len(df_count))
    return df_count
    
def df_filter(context, data):
    df_securities = []
    df_securities = [stock for stock in df_count(context, data)]
    for stock in df_securities: 
        current_date = get_datetime() # do not enter if the position is scheduled to be removed from the exchange in the next 31 days
        if (stock.end_date - current_date).days < 31: # our capital would get tied up indefinatively if we have a position and it get's delisted
            df_securities.remove(stock)
    for stock in df_securities:
        if (stock.symbol == 'SPY') or (stock.symbol in context.flagged_stocks) or (stock in security_lists.leveraged_etf_list):
            df_securities.remove(stock)
    volume = history(30, '1d', 'volume')[df_securities]
    volume_avg = volume.mean()
    context.vol_min = (context.portfolio.portfolio_value/(context.num_stocks)) # dynamic liquidity restriction
    for stock in df_securities:
        if volume_avg[stock] < context.vol_min:
            df_securities.remove(stock)
    df_securities = df_securities[:(context.num_stocks)]
    return df_securities
        
def create_weights(context, stocks):  
    if context.num_stocks == 0: # takes in a list of securities and weights them all equally
        return 0
    else:
        weight = 1.0/context.num_stocks
        return weight 

def handle_data(context, data): # code logic to run during the trading day. handle_data() gets called every bar
    df_count(context, data)
    df_filter(context, data)
    orders = has_orders(context, data)
    if orders:
        log.warn('has open orders')
    if not orders and context.needs_rebalance:
        rebalance(context, data)
        context.needs_rebalance = False
    record(num_positions = len(context.portfolio.positions)) # track how many positions we're holding, if discrepency->look for open orders
    record(cash=context.portfolio.cash) # record cash level, do not allow to go below ZERO
def has_orders(context, data):
    has_orders = False # Return true if there are pending orders so we don't place more orders for the same position
    for stock in data:
        orders = get_open_orders(stock)
        if orders:
            for oo in orders:                  
                message = 'Open order for {amount} shares in {stock}'  
                message = message.format(amount=oo.amount, stock=stock)  
                log.info(message)

            has_orders = True
    return has_orders
There was a runtime error.

Jamie,

Thank you for the response and information. This is definitely helpful. I'll look into using Pipeline as noted, and I think the 'hack' you mentioned may be just what I need for the EPS Q/Q data!

Darth,

Thanks for responding to my post with detailed answers and the excellent Algo that I'll be keeping for future reference. Nice idea to get pricing info indirectly using market cap & shares outstanding! I will look into the functions as noted and hopefully be able to stitch together a working Algo with all of my criteria.

Thanks again,

Troy

Troy,
At the very least you have some new ideas to check out. I wouldn't mind seeing your final algo with all the elements you describe working. I like where you are headed with it.
Have a nice day.

Thanks Darth. I will plan on posting the algo when I get it working (or at least as far as I can go without needing more help!).

Troy