Minimum variance using daily vs min backtesting

I have been trying to use minimum variance optimization for a few ETFs. I have the problem of getting completely different results with daily Vs min backtests, even after I control for closing price. I merged the code from a few places - equal weight sample algorithm, David's min variance code using scipy fmin, etc, so you might find it a little inconsistent. I will post the minute based back-testing in the next post. One change here, the weights are not constrained to be positive, which needs to be changed but it still doesnt explain the differences as the log shows exactly the same numbers atleast after first rebalancing

7
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
import pandas as pd
from scipy.optimize import fmin
from math import sqrt
import datetime

def initialize(context):

set_symbol_lookup_date('2013-01-01')

context.stocks = symbols('SPY', 'MDY', 'VBR' )

context.rebalance_date = None
context.rebal_days = 28
context.price_window = 28

context.data = {
i: []  for i in context.stocks #
}
context.rebalance_hour_start = 0
context.rebalance_hour_end = 20

def handle_data(context, data):

# Get the current exchange time, in the exchange timezone
exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')

# History of prices
price_history = history(bar_count=context.price_window, frequency='1d', field='close_price')

if  context.rebalance_date == None or exchange_time > context.rebalance_date + datetime.timedelta(days=context.rebal_days):

# Check if its in the right time window
if exchange_time.hour < context.rebalance_hour_start or exchange_time.hour > context.rebalance_hour_end:
return

print "Rebalance Time - %s" % exchange_time

# Do nothing if there are open orders:
if has_orders(context):
print('has open orders - doing nothing!')
return

values = pd.DataFrame(price_history[context.stocks])
context.df = values.pct_change().dropna()
x = Opt(context, data)
weights = x.get_weights()

#log.info(weights)
for i in context.stocks:
print "Order %s at price %f at %f percent" % (i.symbol, data[i].price, weights[i] * 0.99)
order_target_percent(i, weights[i] * 0.99, limit_price=None, stop_price=None)

context.rebalance_date = exchange_time

def has_orders(context):
# Return true if there are pending orders.
has_orders = False
for sec in context.stocks:
orders = get_open_orders(sec)
if orders:
for oo in orders:
message = 'Open order for {amount} shares in {stock}'
message = message.format(amount=oo.amount, stock=sec)
log.info(message)

has_orders = True
return has_orders

class Opt():
def __init__(self, context,data):
self.context = context
self.data = data

def get_weights(self):
context = self.context
guess = np.ones(len(context.stocks) - 1,dtype=float)*(1./len(context.stocks))
#
# n-1 array sent into fmin, the last value is added in the optimizing function
opt = fmin(self.min_var, guess)

# The last weight is (1 - the sum of the others)
return {sym: np.append(opt,1-sum(opt))[i] for i,sym in enumerate(context.df)}

def min_var(self, weights):
context = self.context
weights = np.append(weights, 1 - sum(weights))
return pvar(context.df, weights)

def pvar(P, w=None):
''' Gets the variance of a returns portfolio P with weights w. '''
if w is not None:
var = 0
C = P.corr().as_matrix()
s= [i for i in P.std()]
for i in xrange(len(s)):
for j in xrange(len(s)):
var += w[i]*w[j]*s[i]*s[j]*C[i, j]
return var
return P.cov().mean().mean()
There was a runtime error.
7 responses

Here is the min based backtesting. The log says I do get the identical closing day price as the daily backtesting. But the returns are different

1
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
import pandas as pd
from scipy.optimize import fmin
from math import sqrt
import datetime

def initialize(context):

set_symbol_lookup_date('2013-01-01')

context.stocks = symbols('SPY', 'MDY', 'VBR' )

context.rebalance_date = None
context.rebal_days = 28
context.price_window = 28

context.data = {
i: []  for i in context.stocks #
}
context.rebalance_hour_start = 16
context.rebalance_hour_end = 17

def handle_data(context, data):

# Get the current exchange time, in the exchange timezone
exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')

# History of prices
price_history = history(bar_count=context.price_window, frequency='1d', field='close_price')

if  context.rebalance_date == None or exchange_time > context.rebalance_date + datetime.timedelta(days=context.rebal_days):

# Check if its in the right time window
if exchange_time.hour < context.rebalance_hour_start or exchange_time.hour > context.rebalance_hour_end:
return

print "Rebalance Time - %s" % exchange_time

# Do nothing if there are open orders:
if has_orders(context):
print('has open orders - doing nothing!')
return

values = pd.DataFrame(price_history[context.stocks])
context.df = values.pct_change().dropna()
x = Opt(context, data)
weights = x.get_weights()

#log.info(weights)
for i in context.stocks:
print "Order %s at price %f at %f percent" % (i.symbol, data[i].price, weights[i] * 0.99)
order_target_percent(i, weights[i] * 0.99, limit_price=None, stop_price=None)

context.rebalance_date = exchange_time

def has_orders(context):
# Return true if there are pending orders.
has_orders = False
for sec in context.stocks:
orders = get_open_orders(sec)
if orders:
for oo in orders:
message = 'Open order for {amount} shares in {stock}'
message = message.format(amount=oo.amount, stock=sec)
log.info(message)

has_orders = True
return has_orders

class Opt():
def __init__(self, context,data):
self.context = context
self.data = data

def get_weights(self):
context = self.context
guess = np.ones(len(context.stocks) - 1,dtype=float)*(1./len(context.stocks))
#
# n-1 array sent into fmin, the last value is added in the optimizing function
opt = fmin(self.min_var, guess)

# The last weight is (1 - the sum of the others)
return {sym: np.append(opt,1-sum(opt))[i] for i,sym in enumerate(context.df)}

def min_var(self, weights):
context = self.context
weights = np.append(weights, 1 - sum(weights))
return pvar(context.df, weights)

def pvar(P, w=None):
''' Gets the variance of a returns portfolio P with weights w. '''
if w is not None:
var = 0
C = P.corr().as_matrix()
s= [i for i in P.std()]
for i in xrange(len(s)):
for j in xrange(len(s)):
var += w[i]*w[j]*s[i]*s[j]*C[i, j]
return var
return P.cov().mean().mean()
There was a runtime error.

Looks like your log is telling you what price you're trying to order at, but not what price you're clearing at.

Default behavior on Quantopian is that minute mode orders clear at the closing price of the next minute. Daily mode orders clear at the closing price of the next day. Both of course plus default slippage.

Search on the forum for the "trade at the open" slippage model which is designed to approximate trading at the following day's opening price in daily mode.

Thanks Matt, how do I look at the clearing price for my order and print it in the log?

The easiest way to do that would be to run a full backtest and then look at the Transactions tab.

If you really need to see it in the logs instead that seems a little trickier as it doesn't appear this value is stored anywhere explicitly. You can calculate it but you need to save a lot of state variables. It seems like you would have to:

1) Save the Order object that is returned by any of the order methods in an array stored in the context object
2) In handle_data, query your array of orders to see if any are now filled which were not previously filled (so you'll also have to track previous and current states)
3) Then in context.portfolio.positions, look up the corresponding security and back out the clearing price from the weighted average cost basis (which means you will also have to know the number of shares and cost basis from the previous period for each security).

You could shortcut some of that if you knew without doubt that your algorithm was entering/exiting positions entirely (as opposed to accumulating/divesting positions partially over time) by just querying context.portfolio.positions and looking at cost basis / number of shares. But if you are going to add/exit positions partially over multiple time periods that won't work since cost basis is an average (Quantopian doesn't store individual lots right now).

Unless you have an operational need to know the clearing price in real time, using the transaction log from the full backtest seems a lot easier.

Using transaction logs makes more sense for my requirement, thanks.

I am finding it hard to sync up the minute-wise backtesting order execution with the daily backtesting order execution. I am not sure how to specify - "place the order two trading days from now at 4:58PM". Basically I am not sure how to keep track of days in the minute-wise backtesting. Are there any code examples for this use case?

You can create a custom counter to track the number of days, check out the example below.

The logs say:

2015-01-05handle_data:12INFONumber of days is 1
2015-01-06handle_data:12INFONumber of days is 2
2015-01-07handle_data:12INFONumber of days is 3
2015-01-08handle_data:12INFONumber of days is 4
2015-01-09handle_data:12INFONumber of days is 5
End of logs.

2
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
 Returns 1 Month 3 Month 6 Month 12 Month
 Alpha 1 Month 3 Month 6 Month 12 Month
 Beta 1 Month 3 Month 6 Month 12 Month
 Sharpe 1 Month 3 Month 6 Month 12 Month
 Sortino 1 Month 3 Month 6 Month 12 Month
 Volatility 1 Month 3 Month 6 Month 12 Month
 Max Drawdown 1 Month 3 Month 6 Month 12 Month
def initialize(context):
context.num_days = 0
context.current_date = None

context.stock = symbol('SPY')

def handle_data(context,data):
context.today = get_datetime('US/Eastern').date()

if context.current_date != context.today:
context.num_days +=1
log.info("Number of days is %s" % (context.num_days))
context.current_date = context.today

There was a runtime error.
Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.