|
| 1 | +''' |
| 2 | + This algorithm defines a long-only portfolio of 6 ETF's and rebalances all of |
| 3 | + them when any one of them is off the target by a threshold of 5%. |
| 4 | + It is based on David Swensen's rationale for portfolio construction as defined in |
| 5 | + his book: "Unconventional Success: A Fundamental Approach to Personal Investment": |
| 6 | + http://www.amazon.com/Unconventional-Success-Fundamental-Approach-Investment/dp/0743228383 . |
| 7 | + The representative ETF's are defined here: |
| 8 | + http://seekingalpha.com/article/531591-swensens-6-etf-portfolio |
| 9 | + The target percents are defined here: |
| 10 | + https://www.yalealumnimagazine.com/articles/2398/david-swensen-s-guide-to-sleeping-soundly |
| 11 | + The rebalancing strategy is defined in the book and here: |
| 12 | + http://socialize.morningstar.com/NewSocialize/forums/p/102207/102207.aspx |
| 13 | +
|
| 14 | + This is effectively a passive managment strategy or Lazy portfolio: |
| 15 | + http://en.wikipedia.org/wiki/Passive_management |
| 16 | + http://www.bogleheads.org/wiki/Lazy_portfolios |
| 17 | +
|
| 18 | + Taxes are not modelled. |
| 19 | + |
| 20 | + NOTE: This algo can run in minute-mode simulation and is compatible with LIVE TRADING. |
| 21 | +''' |
| 22 | + |
| 23 | +from __future__ import division |
| 24 | +import datetime |
| 25 | +import pytz |
| 26 | +import pandas as pd |
| 27 | +from zipline.api import order_target_percent |
| 28 | + |
| 29 | +def initialize(context): |
| 30 | + |
| 31 | + set_long_only() |
| 32 | + set_symbol_lookup_date('2005-01-01') # because EEM has multiple sid's. |
| 33 | + |
| 34 | + context.secs = symbols('TIP', 'TLT', 'VNQ', 'EEM', 'EFA', 'VTI') # Securities |
| 35 | + context.pcts = [ 0.15, 0.15, 0.15, 0.1, 0.15, 0.3 ] # Percentages |
| 36 | + context.ETFs = zip(context.secs, context.pcts) # list of tuples |
| 37 | + |
| 38 | + # Change this variable if you want to rebalance less frequently |
| 39 | + context.rebalance_days = 20 # 1 = can rebalance any day, 20 = every month |
| 40 | + |
| 41 | + # Set the trade time, if in minute mode, we trade between 10am and 3pm. |
| 42 | + context.rebalance_date = None |
| 43 | + context.rebalance_hour_start = 10 |
| 44 | + context.rebalance_hour_end = 15 |
| 45 | + |
| 46 | +def handle_data(context, data): |
| 47 | + |
| 48 | + # Get the current exchange time, in the exchange timezone |
| 49 | + exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern') |
| 50 | + |
| 51 | + # If it's a rebalance day (defined in intialize()) then rebalance: |
| 52 | + if context.rebalance_date == None or \ |
| 53 | + exchange_time >= context.rebalance_date + datetime.timedelta(days=context.rebalance_days): |
| 54 | + |
| 55 | + # Do nothing if there are open orders: |
| 56 | + if has_orders(context): |
| 57 | + print('has open orders - doing nothing!') |
| 58 | + return |
| 59 | + |
| 60 | + rebalance(context, data, exchange_time) |
| 61 | + |
| 62 | +def rebalance(context, data, exchange_time, threshold = 0.05): |
| 63 | + """ |
| 64 | + For every stock or cash position, if the target percent is off by the threshold |
| 65 | + amount (5% as a default), then place orders to adjust all positions to the target |
| 66 | + percent of the current portfolio value. |
| 67 | + """ |
| 68 | + |
| 69 | + # if the backtest is in minute mode |
| 70 | + if get_environment('data_frequency') == 'minute': |
| 71 | + # rebalance if we are in the user specified rebalance time-of-day window |
| 72 | + if exchange_time.hour < context.rebalance_hour_start or \ |
| 73 | + exchange_time.hour > context.rebalance_hour_end: |
| 74 | + return |
| 75 | + |
| 76 | + need_full_rebalance = False |
| 77 | + portfolio_value = context.portfolio.portfolio_value |
| 78 | + |
| 79 | + # rebalance if we have too much cash |
| 80 | + if context.portfolio.cash / portfolio_value > threshold: |
| 81 | + need_full_rebalance = True |
| 82 | + |
| 83 | + # or rebalance if an ETF is off by the given threshold |
| 84 | + for sid, target in context.ETFs: |
| 85 | + pos = context.portfolio.positions[sid] |
| 86 | + position_pct = (pos.amount * pos.last_sale_price) / portfolio_value |
| 87 | + # if any position is out of range then rebalance the whole portfolio |
| 88 | + if abs(position_pct - target) > threshold: |
| 89 | + need_full_rebalance = True |
| 90 | + break # don't bother checking the rest |
| 91 | + |
| 92 | + # perform the full rebalance if we flagged the need to do so |
| 93 | + # What we should do is first sell the overs and then buy the unders. |
| 94 | + if need_full_rebalance: |
| 95 | + for sid, target in context.ETFs: |
| 96 | + order_target_percent(sid, target) |
| 97 | + log.info("Rebalanced at %s" % str(exchange_time)) |
| 98 | + context.rebalance_date = exchange_time |
| 99 | + |
| 100 | + |
| 101 | +def has_orders(context): |
| 102 | + # Return true if there are pending orders. |
| 103 | + has_orders = False |
| 104 | + for sec in context.secs: |
| 105 | + orders = get_open_orders(sec) |
| 106 | + if orders: |
| 107 | + for oo in orders: |
| 108 | + message = 'Open order for {amount} shares in {stock}' |
| 109 | + message = message.format(amount=oo.amount, stock=sec) |
| 110 | + log.info(message) |
| 111 | + |
| 112 | + has_orders = True |
| 113 | + return has_orders |
0 commit comments