Here is what I came up with (with the baskets removed, just add in your baskets at the top). I hate this sort of code, completely not what I want to be doing.
import datetime
import pytz
import pandas as pd
from zipline.utils.tradingcalendar import get_early_closes
def initialize(context):
set_slippage(slippage.FixedSlippage(spread=0.00))
set_commission(commission.PerShare(cost=0.0035, min_trade_cost=0.35))
A = sid(???)
B = sid(???)
C = sid(???)
D = sid(???)
E = sid(???)
F = sid(???)
context.baskets = [
{
A: +0.2,
B: -0.2,
},
{
C: +0.15,
D: -0.15,
},
{
E: +0.15,
F: -0.15,
}
]
# these will be calculated during our ordering_logic
context.desired_positions = []
context.spy = sid(8554)
start_date = context.spy.security_start_date
end_date = context.spy.security_end_date
context.early_closes = get_early_closes(start_date, end_date).date
# this is the grace period that we give orders to work. Should be short for IB
# since we don't want to be unhedged for long, but unfortunately, might need to
# be very long for Quantopian backtesting, since they do not fill during no-trade
# bars
context.order_cancel_working_time = datetime.timedelta(0,30*60,0)
schedule_function(ordering_logic,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_close(minutes=40),
half_days=True)
# TEST FUNCTION DO NOT RELEASE!!!
schedule_function(simulate_call_in,
date_rules.month_start(days_offset=10),
time_rules.market_open(minutes=30),
half_days=True)
# TEST FUNCTION
def simulate_call_in(context, data):
sid_to_call_in = list(context.baskets[0].keys())[0]
log.warn(str(get_now()) + ": CALLING IN " + str(sid_to_call_in.symbol))
order_target(sid_to_call_in, 0.0)
def get_now():
return pd.Timestamp(get_datetime()).tz_convert('US/Eastern')
def percent_to_shares(context, data, sid, percentage_of_port):
port_val = context.portfolio.portfolio_value
cash_target = port_val * percentage_of_port
last_price = data[sid]['price']
shares_target = cash_target / last_price
return int(shares_target)
def calculate_desired_basket(context, data, basket):
basket_all_traded = True
for sid in basket:
if sid not in data:
basket_all_traded = False
desired_basket = {sid: (percent_to_shares(context,data,sid,basket[sid]) if basket_all_traded else 0.0) for sid in basket}
return desired_basket
def calculate_desired_positions(context, data, baskets):
desired_positions = [ calculate_desired_basket(context, data, basket) for basket in baskets ]
return desired_positions
def ordering_logic(context, data):
record( leverage=context.account.leverage )
context.desired_positions = calculate_desired_positions(context, data, context.baskets)
rebalance(context, data)
def rebalance(context, data):
now = get_now()
for basket in context.desired_positions:
# this is silly, but apparently necessary for Quantopian
basket_all_traded = True
for sid in basket:
if sid not in data:
basket_all_traded = False
if basket_all_traded:
for sid in basket:
log.info(str(now) + ": Targeting " + str(basket[sid]) + " for " + str(sid.symbol))
order_target(sid, basket[sid])
def cancel_all_stale(context, data):
now = get_now()
sids_cancelled = set()
fresh_orders = False
open_orders = get_open_orders()
for security, orders in open_orders.iteritems():
for oo in orders:
if ((get_datetime() - oo.dt) > context.order_cancel_working_time):
log.warn(str(now) + ": Cancelling order placed at " + str(oo.dt) + " for " + str(oo.amount) + " shares of " + str(oo.sid.symbol) + "!")
sids_cancelled.add(oo.sid)
cancel_order(oo)
else:
fresh_orders = True
#log.info(str(now) + ": NOT CANCELLING order for " + str(oo.amount) + " shares of " + str(oo.sid.symbol) + " because it's fresh!")
return (sids_cancelled, fresh_orders)
def tolerable(context, data, sid, a, b):
last_price = data[sid]['price']
a_cash = a*last_price
b_cash = b*last_price
cash_diff = abs(a_cash - b_cash)
port_value = context.portfolio.portfolio_value
diff = cash_diff / port_value
tol = 0.01 # 1% of port value is ok
return diff < tol
def verify_basket(context, data, basket_desired_positions):
basket_okay = True
for sid in basket_desired_positions:
desired_position = basket_desired_positions[sid]
if not tolerable(context, data, sid, desired_position, context.portfolio.positions[sid].amount):
basket_okay = False
new_basket = {sid: (basket_desired_positions[sid] if basket_okay else 0.0) for sid in basket_desired_positions}
return (new_basket, basket_okay)
def verify_positions(context, data, desired_shares):
all_baskets_okay = True
new_desired_positions = []
for basket in context.desired_positions:
(new_desired_basket, basket_okay) = verify_basket(context, data, basket)
if (not basket_okay):
all_baskets_okay = False
new_desired_positions.append(new_desired_basket)
return (new_desired_positions, all_baskets_okay)
def handle_data(context, data):
now = get_now()
(sids_cancelled, fresh_orders) = cancel_all_stale(context, data)
# only bother checking out portfolio if we actually have one
if (len(context.portfolio.positions) > 0):
# if this is the same minute as our ordering_logic, we won't cancel those orders, they have
# one minute to work.
if (fresh_orders == False):
# if we cancelled some orders, presumably held because of no shorts available, give them a minute
# to cancel
if (len(sids_cancelled) == 0):
# if there was nothing to cancel and nothing fresh, double check that our positions haven't been
# changed from underneath us
(new_positions, all_positions_okay) = verify_positions(context, data, context.desired_positions)
if (not all_positions_okay):
log.error(str(now) + ": Portfolios not converging, backing out of unbalanced baskets!")
context.desired_positions = new_positions
rebalance(context, data)
Note that without all this crappy error handling code that we are trying to do every minute, this algorithm would otherwise be nothing but a list and dictionary comprehension calling order_target_percent! Literally 10 lines of code! This is insane!
Anyway, basically, we try to proactively rebalance once per day, and give those orders N minutes to work. After those minutes are up, we cancel the orders, and give those orders a minute to cancel. This is how we catch short fails.
If we have no working orders and nothing to cancel, we check to make sure our portfolio is all what we intended. If it's not, due to a short fail not filling, or a short getting called in from underneath us, we log an error, set that entire hedged basket to 0 and rebalance towards it. Those orders will clearly have to abide by the same logic, but that's okay, we'll be 0 on that basket for the rest of the day. The next morning, we try again. Or that's the theory anyway! Comments welcome.