Skip to content

Commit a7d1822

Browse files
committed
Update swensen.py
1 parent 1322f71 commit a7d1822

File tree

1 file changed

+113
-0
lines changed

1 file changed

+113
-0
lines changed

swensen.py

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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

Comments
 (0)