Back to Community
New to Quantopian and Python -would appreciate some help with 'The Lazy Fundamental Analyst' strategy

Hi,

I'm new to both Quantopian and Python.

I'd be very grateful if somebody could take a look at my code below, and tell me how I could (i) correct and (ii) execute it as a trading strategy. I'm trying to move away from Portfolio 123 and import this strategy into Quantopian

I took the strategy from 'The Lazy Fundamental Analyst Healthcare' by Fred Piard. For reference, the strategy is labelled 'Russell 2000 Healthcare Hedged' on pg 74. Eventually, I hope to move all the strategies I find interesting in this book to Quantopian

Essentially, the strategy is as follows:

  1. Rank 20 healthcare companies (as classified by GICS) with the lowest Price/Sales
  2. The universe is the Russell 2000
  3. 50% of portfolio AUM is long these 20 healthcare stocks, 50% of AUM is short S&P index
  4. The 20 stocks each have a position size of 2.5% of portfolio AUM
  5. Portfolio is rebalanced every 4 weeks
  6. No leverage utilised

As I'm new to Python, I took some related examples (https://www.quantopian.com/posts/introducing-the-pipeline-api) I found on the forum and meshed it together. The code doesn't work so would definitely appreciate any pointers

Thank you.

from quantopian.algorithm import attach_pipeline, pipeline_output  
from quantopian.pipeline import Pipeline  
from quantopian.pipeline import CustomFactor  
from quantopian.pipeline.data.builtin import USEquityPricing  
from quantopian.pipeline.data import morningstar  
from quantopian.pipeline.classifiers.morningstar import Sector


# Create Price to Sales custom factor  
class Price_to_sales(CustomFactor):  
    # Pre-declare inputs and window_length  
    inputs = [morningstar.valuation_ratios.ps_ratio]  
    window_length = 1  
    # Compute factor1 value  
    def compute(self, today, assets, out, ps_ratio):  
        out[:] = ps_ratio[-1]

def initialize(context):  
    pipe = Pipeline()  
    attach_pipeline(pipe, 'ranked_2000')  
    my_sectors = [206] #"Healthcare"  
    sector_filter = Sector().element_of(my_sectors)  
    # Create list of all criteria for securities worth investing in  
    tradable_securities = {  
    'not_wi':not_wi,  
    'primary_share':primary_share,  
    'common_stock':common_stock,  
    'not_otc':not_otc  
    }  

    pipe_screen = (sector_filter)  

    # Create, register and name a pipeline  
    pipe = Pipeline(columns=tradable_securities, screen=pipe_screen)  

    # Add the factor defined to the pipeline  
    price_to_sales = Price_to_sales()  
    pipe.add(price_to_sales, 'price to sales')  

    # Create and apply a filter representing the top 2000 equities by MarketCap every day  
    # This is an approximation of the Russell 2000  
    mkt_cap = MarketCap()  
    top_2000 = mkt_cap.top(2000)  
    # Rank price to sales and add the rank to our pipeline  
    price_to_sales_rank = price_to_sales.rank(mask=top_2000)  
    pipe.add(price_to_sales_rank, 'ps_rank')  
    # Set a screen to ensure that only the top 2000 companies by market cap  
    # which are healthcare companies with positive PS ratios are screened  
    pipe.set_screen(top_2000 & (price_to_sales>0))  
    # Scedule my rebalance function  
    schedule_function(func=rebalance,  
                      date_rule=date_rules.month_start(days_offset=0),  
                      time_rule=time_rules.market_open(hours=0,minutes=30),  
                      half_days=True)  
    # Schedule my plotting function  
    schedule_function(func=record_vars,  
                      date_rule=date_rules.every_day(),  
                      time_rule=time_rules.market_close(),  
                      half_days=True)  
    # set my leverage  
    context.long_leverage = 0.00  
    context.short_leverage = -0.00 

def before_trading_start(context, data):  
    # Call pipelive_output to get the output  
    context.output = pipeline_output('ranked_2000')  
    # Narrow down the securities to only the 20 with lowest price to sales & update my universe  
    context.long_list = context.output.sort(['ps_rank'], ascending=False).iloc[:20]  
    context.spy = sid(8554)  
    context.short_list = context.spy   

def record_vars(context, data):  
     # Record and plot the leverage of our portfolio over time.  
    record(leverage = context.account.leverage)  
    print "Long List"  
    log.info("\n" + str(context.long_list.sort(['ps_rank'], ascending=True).head(10)))  
    print "Short List"  
    log.info("\n" + str(context.short_list)  


# This rebalancing is called according to our schedule_function settings.  
def rebalance(context, data):  
    long_weight = context.long_leverage / float(len(context.long_list))  
    short_weight = context.short_leverage / float(len(context.short_list))

    for long_stock in context.long_list.index:  
        log.info("ordering longs")  
        log.info("weight is %s" % (long_weight))  
        order_target_percent(long_stock, long_weight)  
    for short_stock in context.short_list.index:  
        log.info("ordering shorts")  
        log.info("weight is %s" % (short_weight))  
        order_target_percent(short_stock, short_weight)  
    for stock in context.portfolio.positions.iterkeys():  
        if stock not in context.long_list.index and stock not in context.short_list.index:  
            order_target(stock, 0)

13 responses

I made it work but you have to sort the exact algo yourself....

Clone Algorithm
113
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
# Backtest ID: 593f98f2163ac26e6d7c3b34
There was a runtime error.

Hi Peter,

Thanks so much. Very much appreciated as I was getting quite frustrated as a beginner.

Do you offer your services as a freelancer? The code is not finished yet and I'm now trying to add the code for IB execution. Of course, I want to learn as much as possible so myself, so happy to pay you for a full code review once I'm done

Thank you

Just keep on sharing and learning and one day you'll pay it forward by helping someone else. If you want to keep your algo more private just invite me to collaborate in the IDE and I help you out

I don't want to sound annoying, I'm still kind of learning about this, but isn't the Russell 2000 composed of the bottom 2000 stocks (by mkt cap) of the Russell 3000, which is basically all of the equities in the US market?
That way, your algorithm should pick the bottom 2000, not the top. The line I'm referring to is this:

top_2000 = mkt_cap.top(2000)  

which, to be true to the original strategy, should be

bottom_2000 = mkt_cap.bottom(2000)  

IMHO.
Be sure to change references across the code as well.
Also, that algorithm doesn't go short SPY with 50% AUM, which explains the 1.06 beta and the good performance over the period you backtested. Had it gone through 2008, I don't think it would have fared that well.
It actually seems a very odd strategy, since going long Healthcare stocks in RUT and shorting SPY isn't really a hedged strategy.
Or I might be wrong, as I said, I'm still pretty new to all of this.

That algorithm has positive alpha so a hedged strategy might have positive alpha as well. Seems like the short side would be healthcare companies with high P/S ratios, which would include lots of companies that haven't passed Phase 3 drug trials and will crash if they don't pass.

Wouldn't picking the bottom 2000 stocks by market cap return penny stocks that are too small for the Russell 2K? Penny stocks have low market caps but there are a lot of them.

I figured the correct short side would be of higher P/S healthcare stocks, and I'll probably write an algo soon if I find some time.
It just seemed interesting that the original author proposed shorting SPY as the hedge.

Apparently, ^RUT stocks are the bottom 2000 of the Russell 3000 (the 3000 largest stocks by mkt cap in the US), which comprises 98% of the equities in the US, so yeah it'll most probably include penny stocks.
It'd be interesting to see whether filtering those out made the algo work better or not.
Many of them are likely to fail, and as long as they had P/S, hence get shorted, they would likely increase alpha.

I tried making the changes people suggested... bottom(2000) and changing longs 0.5, shorts -0.5, and the algo loses money.

This is what I managed to pull together. Few changes and optimizations, and by trying with different weights, factors and data this is the best alpha I could pull.
Or better yet, the only non-negative alpha I could come up with.

Clone Algorithm
36
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
# Backtest ID: 5950ede938484d4d4acd02cf
There was a runtime error.

Hi Lorenzo et al,

Thanks so much for the input. In reply to some of the issues raised:

1) Yes, the author Fred Piard does explicitly short the SPY index in his book 'The Lazy Fundamental Analyst', and not the high P/S healthcare companies. He acknowledges this is a blunt hedging technique, rather than a beta-neutral long/short equity strategy. It is a simple strategy that has the following parameters. I removed leverage from my original post:

Rank 20 healthcare companies (as classified by GICS) in the Russell 2000 with the lowest Price/Sales
100% of portfolio AUM is long these 20 healthcare stocks, 100% of AUM is always short S&P index
100% AUM is equally distributed among the 20 stocks
Portfolio is rebalanced every 4 weeks

I'm trying to replicate his results by using Quantopian instead of Portfolio 123 which he used. As he has the backtest to 1999, I wanted to test whether my results chimed with his

2) Yes, the Russell 2000 is the bottom 2000 market cap stocks in the Russell 3000. I copied the code from an official Quantopian post (https://www.quantopian.com/posts/introducing-the-pipeline-api), and then quickly realized that the code is wrong

I assume by having both longs and shorts at 100% of AUM you meant a 2x leverage. In that case, it appears the strategy works pretty well, with a ~14% CAGR.

Clone Algorithm
36
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
# Backtest ID: 595217d7b9ec2451898b03cc
There was a runtime error.

Been playing with the code for a while and unfortunately I wouldn't recommend targeting the bottom stocks unless you're a gambling man/woman. The problem is that even if you're fishing for quality stock at the bottom of the barrel, there's the risk that the stocks being invested in disappears due to delisting. Stocks have been leaving the publicly traded market for a while.

The gains were astronomical during backtesting, however anyone trying the strategy out (without other layers of criteria) would lose there butts long term.

Isn't pending-delisting typically an easy criteria to filter for? If it's been under $1 fo newly a year, sell. Maybe bankruptcies and scams are harder to screen for. I'm curious what the ratio of delisting reasons are.

You couldn't completely eliminate the risk, but if you daily rebalanced that would definitely mitigate it.

I have a couple of issues with daily rebalancing though. Even if you go through Robinhood to avoid fees, there is still the selling fee, however small. With stocks that are as close to pink sheets as you can get, and many "cheap" stocks to pick from, and supposing an investor wanted to diversify, that's a lot of transactions. Another problem is that wash sales will happen frequently.

This may not apply to any of the contests on the site, but I personally like to trade real money and understand how fast money can disappear through fees, although minimal. I try to max out my rebalancing to about once every 2 months in order to lock in any losses for the tax year.