Quantopian's community platform is shutting down. Please read this post for more information and download your code.
Back to Community
Using #Fundamentals growth ranking for healthy growth stock picking strategy

I am excited for the newly added fundamental data function! It is much easier than before to test fundamental strategies that comes up to our minds.

I experimented with a couple of valuation ratios, but what seemed to work was the basic growth rate. This strategy invests evenly to top 100 highest growing stocks, rebalancing monthly.

A restriction I added was that ROIC (return on invested capital) should be above 10%, i.e. companies are making healthy returns on money at hand rather than generating revenue by burning cash

I am looking forward to fundamental data becoming available for live trading!

13 responses

Good work Naoki.
While unrelated to your algo, I noticed an exciting one day portfolio gain of $60K+ from 2008-09-19 to 2008-09-22 due to CDII rising from $0.05 to $5.51. From my clone run, it looked like the 12000+ CDII shares were purchased on 2008-07-01 at $0.10 . Looking at finance.yahoo.com I see CDII prices in the $4 to $7 range in that period; I didnt see any days in the penny-stock range.

I wonder if there is an error in the Quantopian database for CDII in that period or what?

Thanks guys. I submitted a ticket to look into the pricing issue.

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.

Naoki, this is great. Thanks so much for sharing. (I found this via Lesson 3 of the Quantopian Tutorial.)

I cloned it and ran a backtest just now without making any changes, and I ran into this runtime error. The backtest stopped at Feb 26, 2004.

OrderSecurityOutsideUniverse: 0036 You have placed an order with an unexpected security: Security(10583 [MLNK]). Please declare all securities in the initialize function by calling sid(123) for each security that will be used later in your algorithm.
There was a runtime error on line 29.

I couldn't figure out why this is happening. Anyone running into this too?

I suspected at first that it might be because MLNK is outside the universe as of Feb 26, 2004, but then with some logging, I observed that this isn't a problem for other securities that are not in the current context.fundamental_df. Any ideas?

You need to protect against there not being pricing data for thinly traded stocks. The way to typically do this is to check for the stock's presence in the data object (if stock in data) prior to your ordering.

Here's a backtest with the extra checks thrown in.

The line 29 is removing the stock from portfolio when it is no longer in the top growth group.
That is why when you saw an error, MLNK was out of the universe.

It is not in universe, but it is usually still in data because you hold the stock. But it was not trading Feb 26, 2004 at the minute you tried to sell.

As Josh pointed out, it is always a good thing to check if the stock is trading in that particular day and minute for backtesting, which I did not for this line.
Thank you for finding this out, A. Roy, and for the proper fix, Josh.

Hello, I am new here, and wondering if I can ask a few basic questions.

  1. Inside "rebalance" function, exit and entry algo have following respective lines:
    cash += context.portfolio.positions[stock].amount
    cash -= notional - context.portfolio.positions[stock].amount
    The first line is to take account of the added $ amount when you sell existing stocks, and the second line is the deducted $ amount when you buy new stocks. However, I don't understand why you subtract notional AND $ amount.

  2. Inside "create_weight" function, the way you define weight ensures that 10% of your portfolio (=stock+cash) is always cash, right?
    weight = .9/len(stocks)

Hello,

  1. You are right. First line is deducing the $ amount when selling the stock, next line is adding the $ amount when buying. But I made some misunderstanding and a mistake in the original code. Please see the comment in the updated version attached.

  2. Yes, 10% of portfolio is cash. This is to avoid accidental leveraging. These growth stocks are more volatile and less liquid, and if you buy them 100%, you might end up buying over 100%. If you repeat this multiple times, leveraging just goes up and up. 90% stock 10% safeguards it.

undamental_df = get_fundamentals(  
        **query(  
            # put your query in here by typing "fundamentals."  
            fundamentals.operation_ratios.revenue_growth,  
            fundamentals.asset_classification.morningstar_sector_code**  
        )  
        .filter(fundamentals.valuation.market_cap > 0)  
        .filter(fundamentals.operation_ratios.roic > 0.1)  
        .filter(fundamentals.valuation.shares_outstanding != None)  
        .order_by(fundamentals.operation_ratios.revenue_growth.desc())  
        .limit(num_stocks)  
    )  

Hi Naoki,
I don't fully understand what the code inside query does specifically the morningstar_sector_code. Could you shed some light on that for me? Thank You

There are a lot of people who do not know and I can see that is the case here so I will answer instead. Good question.

The query section populates the dataframe with the particular values.
Since those values are not being used, there's no need to collect them.
That's the case in a lot of algorithms.
When the values are not being used, just leave query empty like this:

    fundamental_df = get_fundamentals(  
        query()  
        .filter(fundamentals.valuation.market_cap > 0)  
        .filter(fundamentals.operation_ratios.roic > 0.1)  
        .filter(fundamentals.valuation.shares_outstanding != None)  
        .order_by(fundamentals.operation_ratios.revenue_growth.desc())  
        .limit(num_stocks)  
    )  

To see that, try as is, with a breakpoint on line 88 (in the algo above), and type fundamental_df in the debugger console window (hit [Enter]).
Then make that just query() and repeat.
Maybe someone here or from Q can round that out with some finer points in exploring pandas dataframes (technically a panel in this case, although also a dataframe, I think, a single dataframe inside a panel, there could be more than one unless I'm mistaken, something like that).
Query and filters can be entirely different from each other.

This is an example of combining a couple of dataframes (or panels), using the values from query, and assembling a score from those values.

    c = context  
    f = fundamentals  
    panel1 = get_fundamentals(  
        query(  
            f.cash_flow_statement.cash_flow_from_continuing_operating_activities,  
            f.operation_ratios.roa,  
            f.cash_flow_statement.operating_cash_flow,  
            f.valuation_ratios.ev_to_ebitda,  
            f.operation_ratios.current_ratio,  
            f.valuation_ratios.pb_ratio,  
            #f.valuation_ratios.pe_ratio,  
        )  
        .filter(f.share_class_reference.is_primary_share == True)  
        .filter(f.share_class_reference.is_depositary_receipt == False)  
        .filter(f.operation_ratios.roa > .002)  
        .filter(f.valuation.market_cap > 3000e6)  
        .filter(f.cash_flow_statement.cash_flow_from_continuing_operating_activities > 0)  
        .order_by(f.valuation.market_cap.desc())  
        .limit(120)  # 30-40.6  80-36.7  300-29.1  
    )  
    panel = panel1  
    '''  
    # Use to add some manually  
    panel2 = get_fundamentals(    # Fundamentals for specific symbols  
        query(  
            f.cash_flow_statement.cash_flow_from_continuing_operating_activities,  
            f.operation_ratios.roa,  
            f.cash_flow_statement.operating_cash_flow,  
            f.valuation_ratios.ev_to_ebitda,  
            f.operation_ratios.current_ratio,  
            f.valuation_ratios.pb_ratio,  
        )  
        .filter(f.company_reference.primary_symbol.in_(['TSLA']))  
        #.filter(f.company_reference.primary_symbol.in_(['ENLK', 'PKD', 'TSLA', 'NNBR', 'OWW']))  
    )  
    panel = panel1.append(panel2)               # creates dupes  
    panel = panel.groupby(panel.index).sum()    # dedupe  
    '''  
    # Normalize the values 0 to 100 in relation to each other  
    for i in range(len(panel)): # 0 and 1 for two elements in query above, they are indexes  
        value_list = panel.iloc[i, :].values  
        top    = max(value_list)  
        bottom = min(value_list)  
        count  = 0  
        for v in value_list:    # each value across all stocks  
            prcnt = 100 * (v - bottom) / (top - bottom)  # where this value sits, percentage  
            panel[panel.columns[count]][i] = prcnt  
            count += 1  
    c.fstocks = []  
    for s in panel.columns.values:  
        if s.symbol in c.excludes: continue  
        if '_' in s.symbol: continue  
        c.fstocks.append(s)  
        # panel[s][1] is operation_ratios.roa  
        # Try multiplying these also, instead of adding them  
        score =     panel[s][0] + panel[s][1] + panel[s][2] + panel[s][3] + panel[s][4] - panel[s][5]  
        if np.isnan(score):  
            c.fscore[s] = 0  
        else:  
            c.fscore[s] = score  
    update_universe( c.fstocks )  

Just curious, why do you care about the ROIC if you rebalance monthly? I'm thinking the ROIC is a good caution against midterm to longterm health of the company but if you rebalance monthly won't it sell the stock and buy another upwardly mobile stock?

Another version inspired by the author's aglo.

Has anyone live traded this algo? If yes, what were the results like?

When I cloned this I couldn't run it because it is based on API 1.0, so I tried to change it to API 2.0, but then the results are not near as good (worse than the benchmark). Can anyone tell me what I did wrong?