Hi,
After reading Millennial Money by Patrick O'Shaughness (good read btw!!), i was inspired to see if there was any similar trading strategies on Q. To my delight, and after a simple google, i came across this wacky algo: https://www.quantopian.com/posts/patrick-oshaughnessys-millennial-money-value-investing-algorithm-number-fundamentals . However, as you will notice it uses the V1 syntax which allowed get_fundamentals.
With no experience of using Pipeline i have attempted to redesign the algorithm to incorporate the updated syntax which, according to Q, will enable a faster gathering of fundamental data. It's fair to say i have hit a slight brick wall and my python skills are simply slightly out of their depth.
I have attached my updated source code which gathers pipeline data and simply copy and pasted the end of the original algo's code on for your benefit in manipulating the code. The trouble is, i know exactly what the original code is reading, however, i do not know how to implicate this into my algo. You may notice in the original algo (with other 6000% returns) that the majority of this is derived from leverage, and i want to attempt to reduce this. If someone is able to explain where i should go next, that would be much appreciated. I'm well aware i am using many restrictions on the fundamnetal data but this can all be changed.
Open to any suggestions/advice.
Cheers,
James
Here is the code if it does not come through as i believe the backtest returned no results:
EDIT: apologies for the nasty format - I commented a lot of the script out.
""" Patrick O'Shaughnessy - Millennial Money
1. Stakeholder yield < 5%. Stakeholder yield = Cash from financing 12m / Market Cap Q1
2. ROIC > 25%
ROIC = operating_income / (invested_capital - cash)
3. CFO > Net Income (Earnings Quality)
4. EV/FCF < 15 (Value)
5. 6M Relative Strength top three-quarters of the market.
6M Relative Strength = 6M Stock Total Return / 6M Total Return S&P500 (Momentum)
6. Positions are kept at least for 1 quarter
7. implemented 15% stop loss; if the stock hits stop loss it cannot be repurchased for a half year
8. market cap > 30m
9. Debt/Equity < 0.5
10. Ebitda Margin > 15%
The positions are updated quarterly.
"""
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
import pandas as pd
import numpy as np
def initialize(context):
pipe = Pipeline ()
attach_pipeline(pipe, 'mmoney')
sector_code = morningstar.asset_classification.morningstar_sector_code.latest
pipe.add(sector_code, 'sector_code')
country_id = morningstar.company_reference.country_id.latest
pipe.add(country_id, 'country_id')
primary_exchange_id = morningstar.company_reference.primary_exchange_id.latest
pipe.add(primary_exchange_id, 'primary_exchange_id')
is_depositary_receipt = morningstar.share_class_reference.is_depositary_receipt.latest
pipe.add(is_depositary_receipt, 'is_depositary_receipt')
is_primary_share = morningstar.share_class_reference.is_primary_share.latest
pipe.add(is_primary_share, 'is_primary_share')
financing_cash_flow = morningstar.cash_flow_statement.financing_cash_flow.latest
pipe.add(financing_cash_flow, 'financing_cash_flow')
market_cap = morningstar.valuation.market_cap.latest
pipe.add(market_cap, 'market_cap')
shares_outstanding = morningstar.valuation.shares_outstanding.latest
pipe.add(shares_outstanding, 'shares_outstanding')
operating_income = morningstar.income_statement.operating_income.latest
pipe.add(operating_income, 'operating_income')
invested_capital = morningstar.balance_sheet.invested_capital.latest
pipe.add(invested_capital, 'invested_capital')
cash_and_cash_equivalents = morningstar.balance_sheet.cash_and_cash_equivalents.latest
pipe.add(cash_and_cash_equivalents, 'cash_and_cash_equivalents')
enterprise_value = morningstar.valuation.enterprise_value.latest
pipe.add(enterprise_value, 'enterprise_value')
free_cash_flow = morningstar.cash_flow_statement.free_cash_flow.latest
pipe.add(free_cash_flow, 'free_cash_flow')
total_debt_equity_ratio = morningstar.operation_ratios.total_debt_equity_ratio.latest
pipe.add(total_debt_equity_ratio, 'total_debt_equity_ratio')
ebitda_margin = morningstar.operation_ratios.ebitda_margin.latest
pipe.add(ebitda_margin, 'ebitda_margin')
context.account.leverage = 0
context.bought_for={}
context.all_current_stocks = []
context.prices = {}
context.max_num_stocks = 50
context.days = 64
context.quarter_days = 65
context.quarters_alive = {}
context.relative_strength_6m = {}
context.banned_days = {}
# No Financials (103) and Real Estate (104) Stocks, no ADR or PINK, only USA
sector_code_filter = (sector_code != 103 & 104)
country_id_filter = (country_id == "USA")
is_depositary_receipt_filter = (is_depositary_receipt == False)
is_primary_share_filter = (is_primary_share == True)
primary_exchange_id_filter = (primary_exchange_id != "OTCPK")
# Check for data correctness (i,e. avoid division by zero)
market_cap_filter = (market_cap > 30000000)
shares_outstanding_filter = (shares_outstanding > 0)
free_cash_flow_filter = (free_cash_flow > 0)
ebitda_margin_filter = (ebitda_margin > 0.15)
invested_capital_filter = (invested_capital > 0)
total_debt_equity_ratio_filter = (total_debt_equity_ratio < 0.5)
cash_and_cash_equivalents_filter = (cash_and_cash_equivalents > 0)
invested_capital_filter = (invested_capital != cash_and_cash_equivalents)
shy_filter = ((financing_cash_flow / market_cap) < 0.05)
roic_filter = ((operating_income / (invested_capital - cash_and_cash_equivalents)) > 0.25)
evfcf_filter = ((enterprise_value / free_cash_flow) < 10)
pipe.set_screen (sector_code_filter & country_id_filter & is_depositary_receipt_filter & is_primary_share_filter & primary_exchange_id_filter & market_cap_filter & shares_outstanding_filter & free_cash_flow_filter & ebitda_margin_filter & invested_capital_filter & total_debt_equity_ratio_filter & cash_and_cash_equivalents_filter & invested_capital_filter & shy_filter & roic_filter & evfcf_filter)
schedule_function(func=compute_strength_and_rebalance, date_rule=date_rules.every_day())
#schedule_function(func=rebalance, date_rule=date_rules.every_day())
def quarter_passed(context):
"""
Screener results quarterly updated
"""
return context.days % context.quarter_days == 0
def rebalance(context, data):
stock_already_bought_not_matching_latest_search = []
# Exit positions before starting new ones
#for stock in context.portfolio.positions:
exceded_required_quarters = (stock not in context.quarters_alive or context.quarters_alive[stock] >= 2)
if stock not in context.output and exceded_required_quarters:
if data.can_trade(stock):
order_target_percent(stock, 0)
if stock in context.quarters_alive:
del(context.quarters_alive[stock])
elif stock not in context.output:
stock_already_bought_not_matching_latest_search.append(stock)
# Filtering out stocks without data and applying momentum criteria
# -0.6745 approximation for the top three-quarters of the market
context.stocks = [stock for stock in context.stocks
if data.can_trade(stock) and context.relative_strength_6m[stock] > -0.6745]
# make sure to get out of delisted stocks so they don't sit stagnant in portfolio
context.stocks = [stock for stock in context.stocks
if (stock.end_date - get_datetime()).days > 100]
new_stock = [stock for stock in context.stocks
if stock not in context.portfolio.positions]
# save price for SL
save_buy_price(context, data, new_stock)
context.stocks = context.stocks + stock_already_bought_not_matching_latest_search
# remove banned stock
context.stocks = remove_banned_stock(context, context.stocks)
#if len(context.stocks) == 0:
log.info("No Stocks to buy")
return
weight = 1.0/len(context.stocks)
log.info("Ordering %0.0f%% for each of %s (%d stocks)" % (weight * 100, ', '.join(stock.symbol for stock #in context.stocks), len(context.stocks)))
# buy all stocks equally
#for stock in context.stocks:
#if data.can_trade(stock):
if stock not in context.quarters_alive:
context.quarters_alive[stock] = 0
order_target_percent(stock, weight)
def save_buy_price(context, data, new_stock):
context.prices = data.history(new_stock, 'price', 1, '1d')
for stock in new_stock:
if stock in context.banned_days and context.banned_days[stock] > 0:
continue
context.bought_for[stock] = context.prices[stock][0]
def remove_banned_stock(context, stocks):
for stock in stocks:
if stock in context.banned_days:
if context.banned_days[stock] > 0:
stocks.remove(stock)
return stocks
def compute_relative_strength(context, data):
prices = data.history(context.security_list + [symbol('SPY')], 'price', 150, '1d')
# Price % change in the last 6 months
pct_change = (prices.ix[-130] - prices.ix[0]) / prices.ix[0]
pct_change_spy = pct_change[symbol('SPY')]
pct_change = pct_change - pct_change_spy
if pct_change_spy != 0:
pct_change = pct_change / abs(pct_change_spy)
pct_change = pct_change.drop(symbol('SPY'))
context.relative_strength_6m = pct_change
def update_stocks_qarters(context):
for stock in context.quarters_alive:
context.quarters_alive[stock] +=1
def update_banned_days(context):
for stock in context.banned_days:
context.banned_days[stock] -=1
def day_trading(context, data):
context.prices = data.history(context.all_current_stocks, 'price', 1, '1d')
for stock in context.portfolio.positions:
if stock not in context.bought_for:
continue
if stock not in context.prices:
continue
if context.bought_for[stock] == 0.0:
continue
if len(context.prices[stock]) == 0:
continue
try:
if hit_stop_loss(context, stock):
log.info("%s hit stop loss" % stock.symbol)
context.banned_days[stock] = 130 #banned for half year
order_target_percent(stock, 0)
except:
pass
def hit_stop_loss(context, stock):
pct_change = (context.prices[stock][0] - context.bought_for[stock])/context.bought_for[stock]
return pct_change <= -0.15
def compute_strength_and_rebalance(context, data):
record(num_positions = len(context.portfolio.positions))
day_trading(context, data)
update_banned_days(context)
if not quarter_passed(context):
return
update_stocks_qarters(context)
#compute_relative_strength(context, data)
#rebalance(context, data)
""""""