import datetime
import pytz
import pandas as pd
import numpy as np
# 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
OrderWorkingMinutes = 120
def initialize(context):
#set_slippage(slippage.FixedSlippage(spread=0.01))
#set_commission(commission.PerShare(cost=0.005, min_trade_cost=0.35))
context.peak_port_val = 0.0
context.max_dd = 0.0
#uvxy = sid(41969)
#svxy = sid(41968)
#vixy = sid(40669)
xiv = sid(40516)
vxx = sid(38054)
context.baskets = [
pd.Series({
xiv: -0.99,
vxx: -1.01
}),
]
# these will be calculated during our ordering_logic
context.desired_positions = []
context.spy = sid(8554)
context.order_cancel_working_time = datetime.timedelta(0, OrderWorkingMinutes * 60, 0)
schedule_function(ordering_logic,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_close(minutes=140),
half_days=True)
schedule_function(ordering_logic,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_close(minutes=1),
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):
basket_to_call_in = context.baskets[0]
sid_to_call_in = list(basket_to_call_in.keys())[0]
log.warn(str(get_now()) + ": CALLING IN " + str(sid_to_call_in.symbol))
# simulate the cropping of one leg by 80%, this should cause our algo to
# crop the other leg to 80% of target
order_target_percent(sid_to_call_in, basket_to_call_in[sid_to_call_in] * 0.8)
'''
def get_now():
return pd.Timestamp(get_datetime()).tz_convert('US/Eastern')
# replicate part of order_target_percent, except with an adjustable portfolio size,
# so that we can find out the correct shares given implied portfolio sizes from
# un-rebalanced legs
def percent_to_shares(context, data, sid, percentage_of_port, port_val):
cash_target = port_val * percentage_of_port
last_price = data.current(sid, 'price')
shares_target = cash_target / last_price
return int(shares_target)
def calculate_desired_basket(context, data, basket, port_val):
basket_all_traded = True
for sid in basket.index:
if not data.can_trade(sid):
basket_all_traded = False
desired_basket = pd.Series({sid: (percent_to_shares(context, data, sid, basket[sid], port_val) if basket_all_traded else 0.0) for sid in basket.index})
return desired_basket
def calculate_desired_positions(context, data, baskets):
desired_positions = [ calculate_desired_basket(context, data, basket, context.portfolio.portfolio_value) for basket in baskets ]
return desired_positions
def ordering_logic(context, data):
context.desired_positions = calculate_desired_positions(context, data, context.baskets)
rebalance(context, data)
def rebalance(context, data):
now = get_now()
for (weights, basket) in zip(context.baskets, context.desired_positions):
# this is silly, but apparently necessary for Quantopian
basket_all_traded = True
for sid in basket.index:
if not data.can_trade(sid):
basket_all_traded = False
prices = data.history(basket.index, 'price', 60, '1m')
rets = np.log(prices[basket.index]).diff().fillna(0)
spread_rets = (weights * rets).sum(axis=1)
std = spread_rets.std()
# only trade if these ETFs aren't getting fucked
if (std < 0.002):
if basket_all_traded:
for sid in basket.index:
desired_position = basket[sid]
if not tolerable(context, data, sid, desired_position, context.portfolio.positions[sid].amount):
log.info(str(now) + ": Targeting " + str(basket[sid]) + " for " + str(sid.symbol))
order_target(sid, basket[sid])
else:
log.error(str(now) + ": ABORTING REBALANCE, STD TOO HIGH")
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)
# defines tolerable departures from expected position in a stock
def tolerable(context, data, sid, a, b):
last_price = data.current(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
# this is basically an inverse of percent_to_shares
def calculate_portfolio_val_implied_by_stock_position(context, data, basket, sid):
last_price = data.current(sid, 'price')
cash_position = float(context.portfolio.positions[sid].amount) * last_price
percentage_of_port = basket[sid]
implied_portfolio_val = cash_position / percentage_of_port
return implied_portfolio_val
# this is basically an inverse of calculate_desired_basket
def calculate_portfolio_val_implied_by_basket(context, data, basket):
portfolio_vals_implied_by_legs = [ calculate_portfolio_val_implied_by_stock_position(context, data, basket, sid) for sid in basket.index ]
implied_portfolio_val = min(portfolio_vals_implied_by_legs)
return implied_portfolio_val
def calculate_new_smaller_basket(context, data, basket):
now = get_now()
implied_smaller_port_val = calculate_portfolio_val_implied_by_basket(context, data, basket)
log.error(str(now) + ": Actual account value: " + str(context.portfolio.portfolio_value) + ", Implied (smaller) account value: " + str(implied_smaller_port_val))
smaller_basket = calculate_desired_basket(context, data, basket, implied_smaller_port_val)
return smaller_basket
def print_basket(basket):
s = "{"
for p in basket.index:
s = s + p.symbol + ": " + str(basket[p]) + ","
s = s + "}"
return s
def verify_basket(context, data, basket_desired_positions, basket_desired_weights):
now = get_now()
basket_okay = True
for sid in basket_desired_positions.index:
desired_position = basket_desired_positions[sid]
if not tolerable(context, data, sid, desired_position, context.portfolio.positions[sid].amount):
basket_okay = False
new_basket = basket_desired_positions
if not basket_okay:
new_basket = calculate_new_smaller_basket(context, data, basket_desired_weights)
log.error(str(now) + ": Basket verification failed. Previous desired basket: " + print_basket(basket_desired_positions) + " New desired basket: " + print_basket(new_basket))
return (new_basket, basket_okay)
def verify_positions(context, data, desired_shares):
all_baskets_okay = True
new_desired_positions = []
for (basket_positions, basket_weights) in zip(context.desired_positions, context.baskets):
(new_desired_basket, basket_okay) = verify_basket(context, data, basket_positions, basket_weights)
if (not basket_okay):
all_baskets_okay = False
new_desired_positions.append(new_desired_basket)
return (new_desired_positions, all_baskets_okay)
def drawdown(context, data):
port_val = context.portfolio.portfolio_value
dd = 0.0
if (port_val > context.peak_port_val):
dd = 0.0
context.peak_port_val = port_val
else:
dd = 1.0 - port_val / context.peak_port_val
context.max_dd = max(dd, context.max_dd)
return dd
def handle_data(context, data):
dd = drawdown(context, data)
record(leverage=context.account.leverage)
record(max_drawdown=context.max_dd)
record(current_drawdown=dd)
now = get_now()
(sids_cancelled, fresh_orders) = cancel_all_stale(context, data)
rebalanced = False
# 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, and/or that we have been filled all the shares we wanted
(new_positions, all_positions_okay) = verify_positions(context, data, context.desired_positions)
if (not all_positions_okay):
log.error(str(now) + ": Portfolio problem, rebalancing")
context.desired_positions = new_positions
rebalance(context, data)
rebalanced = True