Back to Community
minimum variance portfolio w/ S&P 500 sector ETFs and TLT

Looks sorta interesting. Maybe someone would like to improve it or comment? --Grant

Clone Algorithm
607
Loading...
Backtest from to with initial capital
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
# https://www.quantopian.com/posts/long-only-minimum-variance-portfolio-using-scipy-dot-optimize-dot-minimize

import numpy as np
from pytz import timezone
import scipy

trading_freq = 20

def initialize(context):
    
    context.stocks = [ sid(19662),  # XLY Consumer Discrectionary SPDR Fund
                       sid(19656),  # XLF Financial SPDR Fund
                       sid(19658),  # XLK Technology SPDR Fund
                       sid(19655),  # XLE Energy SPDR Fund
                       sid(19661),  # XLV Health Care SPRD Fund
                       sid(19657),  # XLI Industrial SPDR Fund
                       sid(19659),  # XLP Consumer Staples SPDR Fund
                       sid(19654),  # XLB Materials SPDR Fund
                       sid(19660),  # XLU Utilities SPRD Fund
                       sid(23921)]  # TLT ISHARES 20+ YEAR TREASURY BOND
    
    context.x0 = 1.0*np.ones_like(context.stocks)/len(context.stocks)
    
    context.day_count = -1

def handle_data(context, data):
     
    # Trade only once per day
    loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
    if loc_dt.hour == 16 and loc_dt.minute == 0:
        context.day_count += 1
        pass
    else:
        return
    
    # Limit trading frequency
    if context.day_count % trading_freq != 0.0:
        return
    
    prices = history(21,'1d','price').as_matrix(context.stocks)
    ret = np.diff(prices,axis=0) # daily returns
    ret = np.divide(ret,np.amax(np.absolute(ret)))
    
    bnds = ((0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1))
    cons = ({'type': 'eq', 'fun': lambda x:  np.sum(x)-1.0})
    
    res= scipy.optimize.minimize(variance, context.x0, args=ret,method='SLSQP',constraints=cons,bounds=bnds)
    
    if res.success:
        allocation = res.x
        allocation[allocation<0]=0
        denom = np.sum(allocation)
        if denom != 0:
            allocation = allocation/denom
    else:
        allocation = context.x0
 
    context.x0 = allocation
        
    record(stocks=np.sum(allocation[0:-1]))
    record(bonds=allocation[-1])
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,allocation[i])
        
def variance(x,*args):
    
    p = np.squeeze(np.asarray(args))
    Acov = np.cov(p.T)
    
    return np.dot(x,np.dot(Acov,x))
We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.
There was a runtime error.
5 responses

Since you're already using a general optimization algorithm, why not add the no-shorts as a constraint also? Also, try out bootstrapping, that has helped a lot to reduce the jumpiness of optimized portfolio holdings over time, for me.

Thanks Simon,

The solution is already bounded by:

bnds = ((0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1))  

Do you think it would help to incorporate a no-shorts constraint, as well (x[i] >= 0)?

Also, what do you mean by bootstrapping?

Grant

I try add XIV to the portfolio, it seems approve the little performance and keep same Volatility and Max Drawdown with original portfolio.

Clone Algorithm
12
Loading...
Backtest from to with initial capital
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
# https://www.quantopian.com/posts/long-only-minimum-variance-portfolio-using-scipy-dot-optimize-dot-minimize

import numpy as np
from pytz import timezone
import scipy

trading_freq = 20

def initialize(context):
    
    context.stocks = [ sid(19662),  # XLY Consumer Discrectionary SPDR Fund
                       sid(19656),  # XLF Financial SPDR Fund
                       sid(19658),  # XLK Technology SPDR Fund
                       sid(19655),  # XLE Energy SPDR Fund
                       sid(19661),  # XLV Health Care SPRD Fund
                       sid(19657),  # XLI Industrial SPDR Fund
                       sid(19659),  # XLP Consumer Staples SPDR Fund
                       sid(19654),  # XLB Materials SPDR Fund
                       sid(19660),  # XLU Utilities SPRD Fund
                       sid(23921),
                       sid(40516)]  # TLT ISHARES 20+ YEAR TREASURY BOND
    
    context.x0 = 1.0*np.ones_like(context.stocks)/len(context.stocks)
    
    context.day_count = -1

def handle_data(context, data):
     
    # Trade only once per day
    loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
    if loc_dt.hour == 16 and loc_dt.minute == 0:
        context.day_count += 1
        pass
    else:
        return
    
    # Limit trading frequency
    if context.day_count % trading_freq != 0.0:
        return
    
    prices = history(21,'1d','price').as_matrix(context.stocks)
    ret = np.diff(prices,axis=0) # daily returns
    ret = np.divide(ret,np.amax(np.absolute(ret)))
    
    bnds = ((0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1),(0,1))
    cons = ({'type': 'eq', 'fun': lambda x:  np.sum(x)-1.0})
    
    res= scipy.optimize.minimize(variance, context.x0, args=ret,method='SLSQP',constraints=cons,bounds=bnds)
    
    if res.success:
        allocation = res.x
        allocation[allocation<0]=0
        denom = np.sum(allocation)
        if denom != 0:
            allocation = allocation/denom
    else:
        allocation = context.x0
 
    context.x0 = allocation
        
    record(stocks=np.sum(allocation[0:-1]))
    record(bonds=allocation[-1])
    
    for i,stock in enumerate(context.stocks):
        order_target_percent(stock,allocation[i])
        
def variance(x,*args):
    
    p = np.squeeze(np.asarray(args))
    Acov = np.cov(p.T)
    
    return np.dot(x,np.dot(Acov,x))
There was a runtime error.

Ah no, you shouldn't. I just noticed you were also doing:

allocation[allocation<0]=0  

which would seem to be unnecessary.

I'll have to check. I recall adding it because the bounds are not strictly obeyed. Sometimes there is a small negative value.