Advertisement
Guest User

Untitled

a guest
Oct 28th, 2016
245
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.54 KB | None | 0 0
  1. """
  2.  
  3. Magic Formula by Joel Greenblatt
  4. 1. Establish a minimum market capitalization (usually greater than $50 million).
  5. 2. Exclude utility and financial stocks.
  6. 3. Exclude foreign companies (American Depositary Receipts).
  7. 4. Determine company's earnings yield = EBIT / enterprise value.
  8. 5. Determine company's return on capital = EBIT / (net fixed assets + working capital).
  9. 6. Rank all companies above chosen market capitalization
  10. by highest earnings yield and highest return on capital (ranked as percentages).
  11. 7. Invest in 20–30 highest ranked companies, accumulating 2–3 positions per month over a 12-month period.
  12. 8. Re-balance portfolio once per year, selling losers one week before the year-mark and winners one week after the year mark.
  13.  
  14. Continue over a long-term (5–10+ year) period.
  15.  
  16. net fixed assets = Total Assets - Total Current Assets - Total Intangibles & Goodwill
  17.  
  18.  
  19. #### Differences in the implementation here ###
  20.  
  21. Rather than rebalancing once/year, this algo accumulates positions each month
  22. and holds them for just under or over 1 year depending on whether the return is down/up.
  23.  
  24. This means that it takes a while to get up to full capacity, but the rebalancing is spread
  25. out over the course of the year rather than all at once.
  26.  
  27.  
  28. ##### To switch to the Acquirer's Multiple toggle the comments on lines 137/138 ####
  29.  
  30. """
  31.  
  32.  
  33. from datetime import timedelta
  34. import pandas as pd
  35. import numpy as np
  36.  
  37.  
  38. def initialize(context):
  39. #: Setting a few variables that we're using for our picks
  40. context.picks = None
  41. context.fundamental_dict = {}
  42. context.fundamental_data = None
  43.  
  44. #: Choices are mf -> Magic Formula and am -> Acquirer's Multiple
  45. context.ranker_type = 'mf'
  46.  
  47. #: Time period in days
  48. days = 365
  49. quarters = 4
  50. context.time_periods = [(days/quarters)*i for i in range(0,quarters + 1)]
  51.  
  52. # Total number of stocks to hold at any given time
  53. context.max_positions = 30
  54.  
  55. # Number of positions to accumulate each month
  56. context.positions_per_month = 3
  57.  
  58. context.minimum_market_cap = 500e6
  59.  
  60. # Used to track the purchase dates of each security
  61. context.entry_dates = {}
  62.  
  63. # In order to maximize post tax returns, losing positions should be sold before
  64. # the 1 year mark and winning positions should be held longer than one year
  65. context.hold_days = {
  66. 'win': timedelta(days=375),
  67. 'loss': timedelta(days=345)
  68. }
  69.  
  70. # Buy Stocks at the beginning of each month
  71. schedule_function(func=buy_stocks,
  72. time_rule=time_rules.market_open(),
  73. date_rule=date_rules.week_end())
  74.  
  75. # Look to close positions every week
  76. schedule_function(func=sell_stocks,
  77. time_rule=time_rules.market_open(),
  78. date_rule=date_rules.week_start())
  79.  
  80. # plot record variables
  81. schedule_function(func=record_vars,
  82. time_rule=time_rules.market_close(),
  83. date_rule=date_rules.every_day())
  84.  
  85. def before_trading_start(context, data):
  86. excluded_sectors = [103, 207]
  87. sector_code = fundamentals.asset_classification.morningstar_sector_code
  88. fundamental_df = get_fundamentals(
  89. query(
  90. sector_code,
  91. fundamentals.valuation.market_cap,
  92. fundamentals.valuation.enterprise_value,
  93. fundamentals.cash_flow_statement.capital_expenditure,
  94. fundamentals.operation_ratios.roic,
  95. fundamentals.income_statement.ebit,
  96. fundamentals.income_statement.ebitda,
  97. fundamentals.balance_sheet.total_assets,
  98. )
  99. .filter(fundamentals.valuation.market_cap > context.minimum_market_cap)
  100. .filter(~sector_code.in_(excluded_sectors))
  101. .order_by(fundamentals.valuation.market_cap.desc())
  102. # .limit(200)
  103. )
  104. df_length = len(fundamental_df.columns.values)
  105. if df_length < 100:
  106. log.info("Length of fundies dataframe error: %s" % len(fundamental_df.columns.values))
  107. fundamental_df = fundamental_df.dropna(axis=1)
  108.  
  109. context.fundamental_df = fundamental_df
  110.  
  111. #: On the first of every month
  112. if get_datetime().day == 1:
  113. context.fundamental_dict[get_datetime()] = context.fundamental_df
  114. context.fundamental_data = pd.Panel(context.fundamental_dict)
  115. picks = run_historical_ranker(context)
  116. #: If it's not yet time to order, fill context.fundamental_df with an empty list
  117. if picks is None:
  118. context.picks = None
  119. else:
  120. context.picks = list(picks.index)
  121.  
  122. def buy_stocks(context, data):
  123. # Only accumulate positions if there's room in the portfolio
  124. stocks_owned = position_count(context, data)
  125. now = get_datetime()
  126. stocks_bought = 0
  127.  
  128. open_orders = get_open_orders()
  129.  
  130. if context.picks is None:
  131. return
  132.  
  133. for stock in context.picks:
  134. if not data.can_trade(stock) or stock in open_orders:
  135. continue
  136. if stocks_bought >= context.positions_per_month or stocks_owned >= context.max_positions:
  137. return
  138. # Skip stocks already owned
  139. if stock in context.portfolio.positions:
  140. continue
  141. # Some securities throw an error, not sure why???
  142. #try:
  143. order_target_percent(stock, 1.0 / context.max_positions)
  144. context.entry_dates[stock] = now
  145. stocks_bought += 1
  146. stocks_owned += 1
  147. # except Exception as e:
  148. # log.debug(e)
  149.  
  150. def sell_stocks(context, data):
  151. now = get_datetime()
  152. open_orders = get_open_orders()
  153. for stock in context.portfolio.positions:
  154. if data.can_trade(stock) and stock not in open_orders:
  155. cost_basis = context.portfolio.positions[stock].cost_basis
  156. returns = data.current(stock, 'price') / cost_basis - 1
  157. if returns >= 0:
  158. entry_date = context.entry_dates[stock]
  159. if now >= entry_date + context.hold_days['win']:
  160. order_target(stock, 0)
  161. elif returns < 0:
  162. entry_date = context.entry_dates[stock]
  163. if now >= entry_date + context.hold_days['loss']:
  164. order_target(stock, 0)
  165.  
  166.  
  167. def position_count(context, data):
  168. return sum(1 for stock, position in context.portfolio.positions.items()
  169. if position.amount > 0)
  170.  
  171. def run_historical_ranker(context):
  172. """
  173. Ranks our stocks and returns the context.max_positions amount of them if available.
  174. Returns None if we currently don't have enough historical data
  175. """
  176. #: Instantiate a few variables that we need
  177. trailing_metrics = {}
  178. fund_hist = context.fundamental_data
  179. time_periods = context.time_periods
  180.  
  181. #: Check that we have data for the earliest date or before then
  182. if not check_earliest_date(time_periods, fund_hist):
  183. return None
  184.  
  185. #: Create our ranks by putting them into a dict keyed by time period and the ranking
  186. for t in time_periods:
  187. temp_fund_df = find_closest_date(fund_hist, get_datetime() - timedelta(t))
  188. if context.ranker_type == 'am':
  189. ranks = acquirers_multiple(context, temp_fund_df)
  190. elif context.ranker_type == 'mf':
  191. ranks = magic_formula_ranks(context, temp_fund_df)
  192. trailing_metrics[t] = ranks
  193.  
  194. #: Create a DataFrame that we can simply call the mean() on to get a historical average
  195. trailing_metrics_df = pd.DataFrame(trailing_metrics).transpose()
  196. trailing_mean = trailing_metrics_df.mean()
  197.  
  198. #: Magic Formula takes highest ranked stocks
  199. trailing_mean.sort(ascending=True)
  200. return trailing_mean.head(context.max_positions)
  201.  
  202. def magic_formula_ranks(context, df):
  203. """
  204. Creates the magic formula ranking for a given DataFrame
  205. """
  206. #: Create our sorting mechanisms
  207. earnings_yield_ranking = df.ix['ebit'] / df.ix['enterprise_value']
  208. roic_ranking = df.ix['roic'].copy()
  209.  
  210. #: Sort our rankings
  211. earnings_yield_ranking.sort(ascending=False)
  212. roic_ranking.sort(ascending=False)
  213.  
  214. #: Create our ranking mechanisms
  215. earnings_yield_ranking = pd.Series(range(len(earnings_yield_ranking)), index=earnings_yield_ranking.index)
  216. roic_ranking = pd.Series(range(len(roic_ranking)), index=roic_ranking.index)
  217.  
  218. return earnings_yield_ranking + roic_ranking
  219.  
  220. def acquirers_multiple(context, df):
  221. """
  222. Creates the acquirer's multiple ranking for a given DataFrame
  223. """
  224. multiples = df.ix['enterprise_value'] / (df.ix['ebitda'] + df.ix['capital_expenditure'])
  225. multiples.sort(ascending=True)
  226. return pd.Series(range(len(multiples)), index=multiples.index)
  227.  
  228. def find_closest_date(df_panel, date):
  229. """
  230. Finds the closest date if an exact match isn't possible, otherwise finds the exact match
  231. """
  232. date_index = df_panel.items.searchsorted(date)
  233. date = df_panel.items[date_index]
  234. return df_panel[date]
  235.  
  236. def check_earliest_date(time_periods, fund_history):
  237. """
  238. Finds the earliest date and makes sure that we have data for that time
  239. """
  240. earliest_date = get_datetime() - timedelta(max(time_periods))
  241. if earliest_date < min(fund_history.items):
  242. return False
  243. return True
  244.  
  245. def record_vars(context, data):
  246. record(leverage=context.account.leverage,
  247. stocks_owned=position_count(context, data))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement