Tuesday 7 April 2015

Quantopian Trading Strategy: Fundamentals, Daily

Below are some examples of strategies that take advantage of Morningstar's fundamental data on US companies. 

1. Filter the top 50 companies by market capitalisation
2. Find the top two sectors with the highest avg PE ratio, then invest in the shares that have the lowest PE ratio in those sectors. 
3. Every month exit all positions before entering new ones at the start of the month
4. Record all the positions we enter. 

import pandas as pd
import numpy as np

def initialize(context): 
context.stock_weights = {}
context.days = 0 
context.sect_numbers = 2

The initialise method is compulsory for any algorithm, and is input to set up bookkeeping. Context is an initialised and empty Python dictionary and stores any defined variables and portfolio/position/account object. Here, we have set up a dictionary of shares and their weights, and state that we don't require any days before rebalancing the portfolio, and will go long in 2 sectors. 
    
context.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,
date_rule=date_rules.month_start(),
time_rule=time_rules.market_open())

def rebalance(context, data):
for stock in context.portfolio.positions:
if stock not in context.fundamental_df and stock in data:
order_target_percent(stock, 0)

    log.info("The two sectors we are buying today are %r" % context.sectors)

    weight = create_weights(context, context.stocks)

for stock in context.fundamental_df:
    if stock in data:
       if weight != 0:
         log.info("Ordering %0.0f%% percent of %s in %s"
                   % (weight * 100,
                   stock.symbol,
                   context.sector_mappings[context.fundamental_df[stock]['morningstar_sector_code']]))
order_target_percent(stock, weight)

We ensure the portfolio rebalances on the first day of every month, at market open, and that we exit all positions before we start any new ones. We then ensure shares are all rebalanced to their target weights. The log.info is what we read when the transaction occurs, with the sector names at the end of the string. i.e If we had the share in our portfolio, order the target weight of the share and output information that we are ordering x% of the share in whichever sector.
    
def before_trading_start(context): 

    num_stocks = 50
fundamental_df = get_fundamentals(
query(
fundamentals.valuation_ratios.pe_ratio,
fundamentals.asset_classification.morningstar_sector_code
)
.filter(fundamentals.valuation.market_cap != None)
.filter(fundamentals.valuation.shares_outstanding != None)
.order_by(fundamentals.valuation.market_cap.desc())
.limit(num_stocks)
)

As the name suggests, the before_trading_start method is called once before the market opens and in this case updates the universe with 50 shares and their financial performance data from get_fundamentals. get_fundamentals, here assigned the variable name fundamental_df, is a SQLAlchemy query to retrieve the shares with information about their PE ratio and industry sector from Morningstar. These results are then filtered to ensure we don't include any companies that have zero market capitalisation or shares outstanding as this would imply they are not trading anymore. We then sort the results by descending order so the shares with the largest market capitalisation listed first. The results are cut off when we reach 50 shares. 

    sector_pe_dict = {}
    for stock in fundamental_df:
        sector = fundamental_df[stock]['morningstar_sector_code']
        pe = fundamental_df[stock]['pe_ratio']
        
        if sector in sector_pe_dict:
            sector_pe_dict[sector].append(pe)
        else:
            sector_pe_dict[sector] = []

The algorithm searches for sectors that have the highest average PE ratio. I have assigned 'sector' as the variable name for the industry sector code and 'pe' as the variable name for the PE ratio so that it is easier to type later. If the sector exists then we add the PE ratio to our existing list. Otherwise, we don't. 
    
    sector_pe_dict = dict([(sectors, np.average(sector_pe_dict[sectors])) 
                               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=True)[:context.sect_numb]
    
    context.stocks = [stock for stock in fundamental_df
                      if fundamental_df[stock]['morningstar_sector_code'] in sectors]
    
    context.sectors = [context.sector_mappings[sect] for sect in sectors]

    context.fundamental_df = fundamental_df[context.stocks]
    
    update_universe(context.fundamental_df.columns.values)   
    
We are searching for the average PE ratio for each sector, sorting in ascending order, filtering shares based on the top two sectors we found in the previous step. Then we initialise a context.sectors variable, and update context.fundamental_df with the desired shares and their PE ratios. 

def create_weights(context, stocks):
       if len(stocks) == 0:
        return 0 
    else:
        weight = 1.0/len(stocks)
        return weight

We previously coded our portfolio to rebalance to target weights. Here we define that our weights for each stock is equal, as len(stocks) refers to the number of stocks. 

def handle_data(context, data):

    record(num_positions = len(context.portfolio.positions))

Handle_data is called whenever a market event occurs for our specified shares. The parameter context stores any defined states and portfolio object while data is a snapshot of our universe as of when this method was called. For instance, the dictionary can include market information about each share and is updated in the process when a query is run for fundamental data. So whenever our shares are affected we track the number of positions we hold. 




This resulted in giving me a 16.9% return, outperforming the market benchmark by 1.7%. 

PE ratio represents the market value per share over earnings per share; the price an investor is willing to pay for every dollar of earnings. Generally a low PE ratio is indicative of an undervalued stock, however this should really be compared within industries. A high PE ratio is not always a bad thing as it can reflect strong past performance. 

In order to factor in future performance, I decided to use diluted EPS growth as investment criteria instead, keeping all else constant to see its effect on my strategy. In this case I changed the listing of securities to descending as a higher EPS growth is generally better. Previously I was sorting PE ratio based on highest PE sector but lowest PE company within such sector(s) hence why i had sorted ascending order. 

import pandas as pd
import numpy as np

def initialize(context):
    context.stock_weights = {}
    context.days = 0
    context.sect_numb = 2
    
    context.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,
                      date_rule=date_rules.month_start(),
                      time_rule=time_rules.market_open())
    
def rebalance(context, data):
    
    for stock in context.portfolio.positions:
        if stock not in context.fundamental_df and stock in data:
            order_target_percent(stock, 0)

    log.info("The two sectors we are ordering today are %r" % context.sectors)

    weight = create_weights(context, context.stocks)

    for stock in context.fundamental_df:
        if stock in data:
          if weight != 0:
              log.info("Ordering %0.0f%% percent of %s in %s" 
                       % (weight * 100, 
                          stock.symbol, 
                          context.sector_mappings[context.fundamental_df[stock]['morningstar_sector_code']]))
              
          order_target_percent(stock, weight)
    
def before_trading_start(context): 
  
    num_stocks = 50
    
    fundamental_df = get_fundamentals(
        query(
            fundamentals.earnings_ratios.diluted_eps_growth,
            fundamentals.asset_classification.morningstar_sector_code
        )
        .filter(fundamentals.valuation.market_cap != None)
        .filter(fundamentals.valuation.shares_outstanding != None)
        .order_by(fundamentals.valuation.market_cap.desc())
        .limit(num_stocks)
    )

    sector_eps_dict = {}
    for stock in fundamental_df:
        sector = fundamental_df[stock]['morningstar_sector_code']
        eps = fundamental_df[stock]['diluted_eps_growth']
                
        if sector in sector_eps_dict:
            sector_eps_dict[sector].append(eps)
        else:
            sector_eps_dict[sector] = []
    
    sector_eps_dict = dict([(sectors, np.average(sector_eps_dict[sectors])) 
        for sectors in sector_eps_dict if len(sector_eps_dict[sectors]) > 0])
    
    sectors = sorted(sector_eps_dict, key=lambda x: sector_eps_dict[x], reverse=False)[:context.sect_numb]
    
    context.stocks = [stock for stock in fundamental_df
                      if fundamental_df[stock]['morningstar_sector_code'] in sectors]
    
    context.sectors = [context.sector_mappings[sect] for sect in sectors]

    context.fundamental_df = fundamental_df[context.stocks]
    
    update_universe(context.fundamental_df.columns.values)   
    
def create_weights(context, stocks):

    if len(stocks) == 0:
        return 0 
    else:
        weight = 1.0/len(stocks)
        return weight
        
def handle_data(context, data):
  
    record(num_positions = len(context.portfolio.positions))


This resulted in an even higher return of 20.9%, beating the benchmark by 5.7%. Judging from the Sharpe ratio, returns with this strategy are higher even after adjusting for risk. 

No comments:

Post a Comment