Fundamental Algo inspired by Benjamin Graham

Hi,

I'm trying out an algorithm that picks securities that have fulfilled a few "value" criteria (selected somewhat arbitrarily for now, but inspired by https://www.quantopian.com/posts/grahamfundmantals-algo-simple-screening-on-benjamin-graham-number-fundamentals) for several times in the past year and then holds on these stocks for as long as they still fulfill these criteria in the past year.

However, there are a few problems with the algorithm:

1. The algo stops trading mid-2011 because a sell order can't be fulfilled anymore, permanently activating the leverage protection (I prefer to keep the portfolio's cash value nonnegative). Is there a way around this?
2. Any ideas why num_positions is sometimes non-integer? Is it averaged over all trading days of a particular week?

Thanks,
Daniel

990
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
import math
import pandas as pd
import numpy as np

symbol('AAPL')

def initialize(context):
context.num_stocks = 100

context.needs_rebalance = False
context.running_day = 0
context.rebalance_frequency = 20
context.stocks_per_date = {}
context.min_holding_period = 365
context.min_signal_count = 6

# Sector mappings
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"
}

# 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 filter_stocks(context, num_stocks):
fundamental_df = get_fundamentals(
query(
# put your query in here by typing "fundamentals."
fundamentals.valuation_ratios.pe_ratio,
fundamentals.valuation_ratios.peg_ratio,
fundamentals.valuation_ratios.pb_ratio,
fundamentals.earnings_report.diluted_eps,
fundamentals.valuation_ratios.book_value_per_share,
fundamentals.asset_classification.morningstar_sector_code
)
.filter(fundamentals.valuation.market_cap >= 5e9)
.filter(fundamentals.valuation.shares_outstanding != None)
.filter(fundamentals.operation_ratios.quick_ratio >= 1.0)
.filter(fundamentals.valuation_ratios.pe_ratio > 3.0)
.filter(fundamentals.valuation_ratios.pe_ratio < 12.0)
.filter(fundamentals.valuation_ratios.pb_ratio < 2.0)

#        .filter(fundamentals.valuation.market_cap                  > 80e6   )
#        .filter(fundamentals.valuation_ratios.pe_ratio             < 11     )
#        .filter(fundamentals.valuation_ratios.pe_ratio             > 3      )
#        .filter(fundamentals.operation_ratios.roa                  > 0.1    )
#        .filter(fundamentals.balance_sheet.current_debt            < 80e6   )
#        .filter(fundamentals.valuation_ratios.book_value_per_share > 8      )

#        .filter(fundamentals.valuation_ratios.peg_ratio < 1.5)
.order_by(fundamentals.valuation.market_cap.desc())
.limit(num_stocks)
)
#print fundamental_df

current_date = get_datetime()

stocks = []
for stock in fundamental_df:
#sector = fundamental_df[stock]['morningstar_sector_code']
#pe = fundamental_df[stock]['pe_ratio']
#peg = fundamental_df[stock]['peg_ratio']
#pb = fundamental_df[stock]['pb_ratio']
#bvps = fundamental_df[stock]['book_value_per_share']
#eps = fundamental_df[stock]['diluted_eps']

#stocks.append(stock)
if not stock in context.stocks_per_date:
context.stocks_per_date[stock] = []
context.stocks_per_date[stock].append(current_date)
#print 'stock=%s, sector=%s, pe=%s, peg=%s, pb=%s, bvps=%s, eps=%s' % (stock, sector, pe, peg, pb, bvps, eps)
#         if eps != None and bvps != None:
#             if fundamental_df[stock]['morningstar_sector_code'] == 103:
#                 graham_number = math.sqrt(15 * eps * bvps)
#             else:
#                 graham_number= math.sqrt(22.5 * eps * bvps)
#graham_margin_of_safety = price / graham_number

context.fundamental_df = fundamental_df

for stock in context.stocks_per_date:
if len(context.stocks_per_date[stock]) >= context.min_signal_count:
current_signals = 0
for signal_date in context.stocks_per_date[stock]:
if (current_date - signal_date).days <= context.min_holding_period:
current_signals += 1
if current_signals >= context.min_signal_count:
stocks.append(stock)

return stocks

def rebalance(context, data):
# Exit all positions before starting new ones
for stock in context.portfolio.positions:
if stock not in context.stocks and not get_open_orders(stock):
order_target_value(stock, 0)

# Create weights for each stock
weight = create_weights(context, context.stocks)

# Rebalance all stocks to target weights
stock_symbols = []
for stock in context.stocks:
stock_symbols.append(stock.symbol)
log.info('weight=%f, symbols=%s' % (weight, stock_symbols))

for stock in context.stocks:
try:
if not get_open_orders(stock):
order_target_percent(stock, weight)
except:
pass
#            print "Unexpected error:", sys.exc_info()[0]

# track how many positions we're holding
record(num_positions = len(context.stocks))
record(cash = context.portfolio.cash)

context.running_day += 1
if context.running_day % context.rebalance_frequency != 1:
return

context.stocks = filter_stocks(context, context.num_stocks)
context.needs_rebalance = True
update_universe(context.stocks)

def create_weights(context, stocks):
"""
Takes in a list of securities and weights them all equally
"""
if len(stocks) == 0:
return 0
else:
weight = 1.0/len(stocks)
return weight

def handle_data(context, data):
"""
Code logic to run during the trading day.
handle_data() gets called every bar.
"""
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

def has_orders(context, data):
# Return true if there are pending orders.
has_orders = False
for sec in data:
orders = get_open_orders(sec)
if orders:
for oo in orders:
message = 'Open order for {amount} shares in {stock}'
message = message.format(amount=oo.amount, stock=sec)
log.info(message)

has_orders = True
return has_orders

There was a runtime error.
1 response

Nice algorithm Daniel. It looks like you are trying to order the stock TAP_A which has a really low volume, so it can't be traded. That's weird, because it has pretty a high market cap. There are a few ways to fix this — I would just try to filter out stocks with a low volume using data[stock].volume to get their volume. You could compare the volume to how many shares you are trying to trade to make sure the volume is greater than that amount.

For your second question, are you talking about the custom data plot? Yeah, the non-integers come up because there are so many data points that some have to be averaged together, like you said. There are only so many pixels!

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.