Back to Community
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

Clone Algorithm
7
Loading...
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

Clone Algorithm
1
Loading...
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.  
Clone Algorithm
2
Loading...
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.