The Code

All we’ll really be needing to get our script started is initialize — and here’s what it looks like for our script. (If you’d rather see the code in your own IDE, you can grab the code in full on our Github.)

Our script’s very first function. Go ahead and put this into GrahamFundamentals.py if you want to follow along.

To kick things off, we’ll be doing from pylivetrader import * to get access to all the features, like order submission, it provides. We then go ahead and put the sectors that we’re interested in trading into the context object, which just means that we can access them later in other functions. For the sake of simplicity, we want our algorithm to rebalance on the first of the month every three months. schedule_function lets us tell pylivetrader that we’re going to want one of our methods run eventually, and we tell it that we want it to try to run at the first of every month. You can see Quantopian’s documentation on how to configure the timing of your functions here.

The function we’re scheduling is called try_rebalance . It’s a simple function that sees if it’s time to shake up our portfolio by checking the context.months_until_rebalance countdown.

Simple enough.

The math here is pretty easy to follow — we run our algorithm’s actual logic, stored in update_target_securities and rebalance , once every three months. Let’s take a look at the first of those functions, where we’ll be grabbing all our data and figuring out which stocks we want to buy. There’s a lot going on here, and I’ll break it down one piece at a time.

Now we’re really getting into it.

Here’s where we start the real work. We’re going to be processing each sector separately, since, as I mentioned above, we want to eventually weight our investments accordingly. To do that, we’ll first need to get the data for the stocks in each sector. I’ll go over build_sector_fundamentals below, but for now, just think of it as some magic that gives us back a dataframe that has all the information we’ll need for the sector we’re in, sorted by stock. Similarly, filter_fundamental_df is a bit of magic that will remove the stocks that don’t meet all of our criteria. We’ll come back to these.

With all our information put together, we’re going to figure out how much of each stock we want to buy. We calculate a stock’s sector contribution by comparing its market cap against the total market cap of the other stocks in that sector we want to buy, and we store that in a new column on the dataframe, sector_contributions . If you add all the sector contributions for every stock in a given sector together, you’ll get 1, so context.total_sector_contributions will be 1/len(context.sectors) , assuming we order at least one stock from every sector. Once that’s sorted out, we merge all our dataframes together using pd.concat so we can easily query all the data at once later.

Let’s put off the data filtering a little longer and look at how we compose our orders once we’ve got enough information to choose and weight our positions. We’ll do this in the rebalance function.

Here’s where the money moves.

We might have some existing positions that we no longer wish to keep. Maybe a company experienced some great growth and its PE ratio rose over 9.0, and it’s time to take our profit. We see which positions we have in our portfolio by looking through context.portfolio.positions , which is created for us by pylivetrader. For those we don’t want around anymore, we call order_target_percent . This is a method pylivetrader provides us, and it will set our portfolio’s total investment in a given security to the percent we tell it to. So, if we have $50,000 and no AAPL stock, and we say order_target_percent(symbol('AAPL'), 10) , we’ll wind up buying roughly $5,000 of AAPL. When we tell it to order “zero percent” of something, we’re telling it to liquidate our existing position.

Once our assets are freed up, we look through the stocks we actually do want to buy, and we submit buy orders for each, again using order_target_percent . This time, we tell it to determine the percentage of our portfolio to invest based on on get_weight . As discussed above, this is going to go through and create a weight based on the stock’s size within its own sector. With that, pylivetrader’s job is done — the stocks are bought and sold every few months, and you get to watch the script’s performance in real time on Alpaca’s dashboard.

Of course, I still haven’t gone over the “magic” I mentioned earlier. Let’s take a look at how those methods filter all the stocks out there down to just the few we want.

Data collection’s a dirty job, but some code’s got to do it.

Broadly, this code accesses an IEX endpoint that provides us the list of stocks in a given sector, then breaks those lists down into chunks of 100, as IEX won’t allow you to ask for information on more stocks than that in a single query. We have to extend iexfinance a bit with the SectorCollection class to access that endpoint, as that library is more focused on the Stocks endpoint, but it’s not a huge chore. After that, we hit the IEX endpoints we need — financials, quote, stats, and earnings — for each batch of stocks, and we wrap it all up in a dictionary. At the end, we transform that dictionary into a dataframe using the pd.DataFrame.from_dict method, which helpfully saves us the hassle of having to declare the dataframe format ourselves.

We’re almost done, but first, let’s take a look at this method’s helper functions that validate the data.

Not the most interesting code in the world, but it makes sure we’re not investing based on bad data.

This block of code is a little lengthy and pretty straightforward — we make sure that all our data is present, save it in a dictionary, and give it back to the caller. I will note that eps_good is doing some work that could be done as part of the dataframe filtering, but I felt that it was cleaner to just handle the earnings information separately rather than add a column for every quarter to the dataframe. We make sure that a company hasn’t had any negative earnings per share reports in the last year, and if it clears that check, we move forward with our analysis of it. (If the validation methods return False , it discards the stock and doesn’t even add it to the dataframe we’ll filter.)

Finally, let’s bring it back around, and we’ll encode the rules I discussed earlier in the form of a dataframe filter.

The finishing touch — the rest of Graham’s rules for our investment model.

This returns a view of the dataframe with only the rows for stocks that meet the criteria. And there you have it! Put all those methods together, and you’re ready to go. Again, you can find a full copy of the script on our GitHub here if you don’t want to piece it together yourself.