Back to Community
Deployed two XIV/UVXY/TQQQ strategies for paper trading ... will make it live trading after a month

This is the first strategy

Clone Algorithm
306
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
import talib
from math import trunc  
from pytz import timezone      
import numpy as np
import pandas as pd
import datetime
import math
import time
import re
import functools
import itertools

from zipline.utils import tradingcalendar

History = 128

def initialize(context):
    
    vixUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv'
    vxvUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vxvdailyprices.csv'
    nokoUrl = 'http://52.15.233.150/noko.csv'  
    
    fetch_csv(nokoUrl, 
              symbol='v1', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX1, 
              post_func=shift_data)
    
    fetch_csv(nokoUrl, 
              symbol='v2', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX2, 
              post_func=shift_data)

    fetch_csv(vixUrl, 
              symbol='v', 
              skiprows=1,
              date_column='Date', 
              pre_func=addFieldsVIX,
              post_func=shift_data)

    fetch_csv(vxvUrl, 
              symbol='vxv', 
              skiprows=2,
              date_column='Date', 
              pre_func=addFieldsVXV,
              post_func=shift_data)

    context.xiv = sid(40516)
    context.tqqq = sid(39214)
    context.uvxy = sid(41969)
    context.spyg = sid(22009)
    
    context.vix = -1
    
    set_slippage(slippage.FixedSlippage(spread=0.0))
    
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours = 0, minutes = 1))
    
def my_rebalance(context, data):  

    update_indices(context, data)     
    last_vix = data.current('v', 'Close')
    last_vx1 = data.current('v1','Close')  
    last_vx2 = data.current('v2','Close')      
    last_vxv = data.current('vxv', 'Close')
    
        
    log.info("Vix %f" %last_vix)
    log.info("Vxv %f" %last_vxv)
    log.info("V1 %f" %last_vx1)
    log.info("V2 %f" %last_vx2)

               
    # Calculating the gap between spot vix and the first month vix future
    last_ratio_v_v1 = last_vix/last_vx1

    # Calculating the contango ratio of the front and second month VIX Futures 
    last_ratio_v1_v2 = last_vx1/last_vx2

    # Blending the previous two ratios together using a weighted average
    ratio_weight = 0.7
    last_ratio = (ratio_weight*last_ratio_v_v1) + ((1-ratio_weight)*last_ratio_v1_v2) - 1
    
    vix_vxv_ratio = last_vix/last_vxv
    
    # Retrieve SPY prices for technical indicators
    prices = data.history(context.spyg, 'open', 40, '1d')
    
    # Retrieve SPY MACD data
    macda, signal, hist = talib.MACD(prices, fastperiod=12,slowperiod=26,signalperiod=9)
    macd = macda[-1] - signal[-1]
    
    # Caluclate how much vix moved the previous day
    if (context.vix <> -1) : 
        vix_ratio = last_vix/context.vix -1
    else :
        vix_ratio = 0
    context.vix = last_vix
    
    xiv_history = data.history(context.xiv, 'price', 2, '1d')  
    
    xiv_ratio = xiv_history[1]/xiv_history[0] - 1
    
    # Setting thresholds
    threshold_vix_too_low = 10.76   # 0 
    threshold_xiv = -0.049          # 1
    threshold_vxv_xiv = 0.87        # 2 
    threshold_uvxy = 0.049          # 3
    threshold_macd = -0.55          # 3
    threshold_vxv_uvxy = 1.3        # 4 
    threshold_vix_high = 19.9       # 5 
    threshold_vc_low = -0.148       # 6
    threshold_vc_high = 0.046       # 8
    threshold_vc_high_2 = -0.06     # 8
    threshold_xiv_ratio = -0.053    # 10 
    threshold_uvxy_ratio = 0.08     # 11 
    
    if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY witht he hope of a spike
        case = 0
        target_sid = context.uvxy
            
    elif last_ratio < threshold_xiv: # if contango is high, invest in XIV to gain from decay
        case = 1
        target_sid = context.xiv
            
    elif vix_vxv_ratio < threshold_vxv_xiv: # if short term vol is low compared to mid term, invest in XIV to gain from decay
        case = 2
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy and macd > threshold_macd: # if backwardation is high, invest in UVXY to gain from decay
        case = 3
        target_sid = context.uvxy

    elif vix_vxv_ratio > threshold_vxv_uvxy: # if short term vol is high compared to mid term, invest in UVXY to gain from growth
        case = 4
        target_sid = context.uvxy

    elif last_vix > threshold_vix_high: # if VIX is too high, invest in XIV expecting that VIX will drop
        case = 5
        target_sid = context.xiv

    elif vix_ratio < threshold_vc_low: # Vix down sharply, invest in XIV expecting that futures curve gets pulled down
        case = 6
        target_sid = context.xiv
            
    elif vix_ratio > threshold_vc_high: # Vix up sharply, invest in UVXY expecting that futures curve gets pulled up
        case = 7
        target_sid = context.uvxy

    elif vix_ratio > threshold_vc_high_2: #have to think
        case = 8
        target_sid = context.xiv

    else:
        case = 9
        target_sid = context.uvxy
        
    if (target_sid == context.xiv and xiv_ratio < threshold_xiv_ratio) : 
        # indicators say XIV but it just dropped overnight, so got for TQQQ
        case = 10
        target_sid = context.tqqq 
        
    elif (target_sid == context.uvxy and xiv_ratio > threshold_uvxy_ratio) :
        # indicators say UVXY but it just dropped overnight, so got for TQQQ
        case = 11
        target_sid = context.tqqq
        
    if target_sid == context.xiv :
        log.info("CASE XIV %d" %case)
    elif target_sid == context.uvxy :
        log.info("CASE UVXY %d" %case)
    else :
        log.info("CASE TQQQ %d" %case)
            
    if target_sid not in context.portfolio.positions :
        order_target_percent(context.tqqq,0)
        order_target_percent(context.uvxy,0)
        order_target_percent(context.xiv,0)
        order_target_percent(target_sid,1)
        
def update_indices(context, data):
    context.fetch_failed = False
    context.vix_vals = unpack_from_data(context, data, 'v')    
    context.vxv_vals = unpack_from_data(context, data, 'vxv')  
    context.vx1_vals = unpack_from_data(context, data, 'v1')
    context.vx2_vals = unpack_from_data(context, data, 'v2')

def fix_close(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%m/%d/%Y')))
    df = df.sort_values(by='Date', ascending=True)
    return df

def fix_closeVX(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%Y-%m-%d')))
    df = df.sort_values(by='Date', ascending=True)
    return df


def subsequent_trading_date(date):
    tdays = tradingcalendar.trading_days
    last_date = pd.to_datetime(date)
    last_dt = tradingcalendar.canonicalize_datetime(last_date)
    next_dt = tdays[tdays.searchsorted(last_dt) + 1]
    return next_dt

def add_last_bar(df):
    last_date = df.index[-1]
    subsequent_date = subsequent_trading_date(last_date)
    blank_row = pd.Series({}, index=df.columns, name=subsequent_date)
    # add today, and shift all previous data up to today. This 
    # should result in the same data frames as in backtest
    df = df.append(blank_row).shift(1).dropna(how='all')
    return df

def shift_data(df):
    log.info("Pre-Shift")
    df = add_last_bar(df)
    df.fillna(method='ffill') 
    df['PrevCloses'] = my_rolling_apply_series(df['Close'], to_csv_str, History)
    dates = pd.Series(df.index)
    dates.index = df.index
    df['PrevDates'] = my_rolling_apply_series(dates, to_csv_str, History)
    return df

def unpack_from_data(context, data, sym):
    try:
        v = data.current(sym, 'PrevCloses')
        i = data.current(sym, 'PrevDates')
        return from_csv_strs(i,v,True).apply(float)
    except:
        log.warn("Unable to unpack historical {s} data.".format(s=sym))
        context.fetch_failed = True

def addFieldsVIX(df):
    log.info("VIX: Pre-Massage")
    df = fix_close(df,'VIX Close')
    log.info("VIX: Post-Massage")
    return df

def addFieldsVXV(df):
    log.info("VXV: Pre-Massage")
    df.rename(columns={'Unnamed: 0': 'Date'}, inplace=True)
    df = fix_close(df,'CLOSE')
    log.info("VXV: Post-Massage")
    return df

def addFieldsVX1(df):
    log.info("VX1: Pre-Massage")
    df = fix_closeVX(df,'F1')
    log.info("VX1: Post-Massage")
    return df

def addFieldsVX2(df):
    log.info("VX2: Pre-Massage")
    df = fix_closeVX(df,'F2')
    log.info("VX2: Post-Massage")
    return df

# convert a series of values to a comma-separated string of said values
def to_csv_str(s):
    return functools.reduce(lambda x,y: x+','+y, pd.Series(s).apply(str))

# a specific instance of rolling apply, for Series of any type (not just numeric,
# ala pandas.rolling_apply), where the index of the series is set to the indices
# of the last elements of each subset
def my_rolling_apply_series(s_in, f, n):
    s_out = pd.Series([f(s_in[i:i+n]) for i in range(0,len(s_in)-(n-1))]) 
    s_out.index = s_in.index[n-1:]
    return s_out

# reconstitutes a Series from two csv-encoded strings, one of the index, one of the values
def from_csv_strs(x, y, idx_is_date):
    s = pd.Series(y.split(','),index=x.split(','))
    if (idx_is_date):
        s.index = s.index.map(lambda x: pd.Timestamp(x))
    return s
            
There was a runtime error.
36 responses

And this is the second one.

Clone Algorithm
85
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
import talib
from math import trunc  
from pytz import timezone      
import numpy as np
import pandas as pd
import datetime
import math
import time
import re
import functools
import itertools

from zipline.utils import tradingcalendar

History = 128

def initialize(context):
    
    vixUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv'
    vxvUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vxvdailyprices.csv'
    nokoUrl = 'http://52.15.233.150/noko.csv'  
    
    fetch_csv(nokoUrl, 
              symbol='v1', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX1, 
              post_func=shift_data)
    
    fetch_csv(nokoUrl, 
              symbol='v2', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX2, 
              post_func=shift_data)

    fetch_csv(vixUrl, 
              symbol='v', 
              skiprows=1,
              date_column='Date', 
              pre_func=addFieldsVIX,
              post_func=shift_data)

    fetch_csv(vxvUrl, 
              symbol='vxv', 
              skiprows=2,
              date_column='Date', 
              pre_func=addFieldsVXV,
              post_func=shift_data)

    context.xiv = sid(40516)
    context.tqqq = sid(39214)
    context.uvxy = sid(41969)
    context.spyg = sid(22009)
    
    context.vix = -1
    
    set_slippage(slippage.FixedSlippage(spread=0.0))
    
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours = 0, minutes = 1))
    
def my_rebalance(context, data):  

    update_indices(context, data)     
    last_vix = data.current('v', 'Close')
    last_vx1 = data.current('v1','Close')  
    last_vx2 = data.current('v2','Close')      
    last_vxv = data.current('vxv', 'Close')
    
        
    log.info("Vix %f" %last_vix)
    log.info("Vxv %f" %last_vxv)
    log.info("V1 %f" %last_vx1)
    log.info("V2 %f" %last_vx2)


    # Calculating the gap between spot vix and the first month vix future
    last_ratio_v_v1 = last_vix/last_vx1

    # Calculating the contango ratio of the front and second month VIX Futures 
    last_ratio_v1_v2 = last_vx1/last_vx2

    # Blending the previous two ratios together using a weighted average
    ratio_weight = 0.7
    last_ratio = (ratio_weight*last_ratio_v_v1) + ((1-ratio_weight)*last_ratio_v1_v2) - 1
    
    vix_vxv_ratio = last_vix/last_vxv
    
    # Retrieve SPY prices for technical indicators
    prices = data.history(context.spyg, 'open', 40, '1d')
    
    # Retrieve SPY MACD data
    macda, signal, hist = talib.MACD(prices, fastperiod=12,slowperiod=26,signalperiod=9)
    macd = macda[-1] - signal[-1]
    
    # Caluclate how much vix moved the previous day
    if (context.vix <> -1) : 
        vix_ratio = last_vix/context.vix -1
    else :
        vix_ratio = 0
    context.vix = last_vix
    
    xiv_history = data.history(context.xiv, 'price', 2, '1d')  
    
    xiv_ratio = xiv_history[1]/xiv_history[0] - 1
    
    # Setting thresholds
    threshold_vix_too_low = 10.8 # first percentile, very rare case, very likely VIX will pop so we want to be in UVXY
    
    threshold_xiv = -0.049 # 60th percentile, as we want to be primarily in XIV
    threshold_uvxy = -threshold_xiv # symmetry 

    threshold_vxv_xiv = 0.79 # first percentile, very rare case
    threshold_vxv_uvxy = 2 - threshold_vxv_xiv # symmetry 
    
    threshold_macd = -0.55 # first percentile, we want to pass 99% of the cases here
    
    threshold_vix_high = 19.9 # 85th percentile, we are already going to be in XIV 60% of the time, so need to be careful
    # no more than total 75% of the time in XIV
    
    threshold_vix_low = 11.5 # 5th percentile, very rare case, may be VIX will pop so we want to be in UVXY

    threshold_vc_low = -0.148 # 1.5th percentile, very rare, we are already going to be in XIV 60% of the time, so need to be careful
    # no more than total 75% of the time in XIV
    
    threshold_vc_high = 0.046 #80th percentile, we will hardly be in UVXY (actually 51 is 80th percentile)
    # so we need to pick up some cases where which is biased towards UVXY to get to at least 15% of the time in UVXY

    threshold_vc_low_2 = 0.0 #50th percentile, below this we go for UVXY in lieu of any other signals
    threshold_vc_high_2 = -0.06 # testing this, this is fitted  

    # relaxing condition to invest in XIV from 60th to 62nd percentile (~5%), absolute value also relaxes ~5% from -0.049 to -0.046
    threshold_xiv_2 = -0.046 # 62th percentile
    # this is to check for one last time if no signals have it, before we default to TQQQ
    
    threshold_uvxy_2 = -0.008 # 85th percentile, taking a risk to invest in UVXY when there's hardly any contango or backwardation
    # this is to check for one last time if no signals have it, before we default to TQQQ
    
    threshold_xiv_ratio = -0.06 # first percentile
    # if XIV has dropepd that much we probably don't want to be long XIVeven though other signals say yes, default to TQQQ
    threshold_uvxy_ratio = -threshold_xiv_ratio # symmetry
    # if XIV has gone up that much we probably don't want to be long UVXY even though other signals say yes, default to TQQQ
                    
    if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY witht he hope of a spike
        log.info("CASE UVXY 0")
        target_sid = context.uvxy

    elif last_ratio < threshold_xiv: # if contango is high, invest in XIV to gain from decay
        log.info("CASE XIV 1")
        target_sid = context.xiv

    elif vix_vxv_ratio < threshold_vxv_xiv: # if short term vol is low compared to mid term, invest in XIV to gain from decay
        log.info("CASE XIV 2")
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy and macd > threshold_macd: # if backwardation is high, invest in UVXY to gain from decay
        log.info("CASE UVXY 3")
        target_sid = context.uvxy

    elif vix_vxv_ratio > threshold_vxv_uvxy: # if short term vol is high compared to mid term, invest in UVXY to gain from growth
        log.info("CASE UVXY 4")
        target_sid = context.uvxy

    elif last_vix > threshold_vix_high: # if VIX is too high, invest in XIV expecting that VIX will drop
        log.info("CASE XIV 5")
        target_sid = context.xiv
            
    elif last_vix < threshold_vix_low: # if VIX is too low, invest in XIV expecting that VIX will rise
        log.info("CASE UVXY 6")
        target_sid = context.uvxy

    elif vix_ratio < threshold_vc_low: # Vix down sharply, invest in XIV expecting that futures curve gets pulled down
        log.info("CASE XIV 7")
        target_sid = context.xiv
            
    elif vix_ratio > threshold_vc_high: # Vix up sharply, invest in UVXY expecting that futures curve gets pulled up
        log.info("CASE UVXY 8")
        target_sid = context.uvxy

    elif vix_ratio > threshold_vc_high_2: #have to think
        log.info("CASE XIV 9")
        target_sid = context.xiv
            
    elif vix_ratio < threshold_vc_low_2: # vix dropped. There are no other signals. in the absence of any signal, try UVXY
        log.info("CASE UVXY 10")
        target_sid = context.uvxy

    elif last_ratio < threshold_xiv_2: # before we give up on XIV and move to TQQQ let's relax the contango conditions a bit
        log.info("CASE XIV 11")
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy_2: # before we give up on UVXY and move to TQQQ let's relax the backwardation conditions a bit
        log.info("CASE UVXY 12")
        target_sid = context.uvxy

    else:
        log.info("CASE TQQQ 13")
        target_sid = context.tqqq
        
    if (target_sid == context.xiv and xiv_ratio < threshold_xiv_ratio) : 
        # indicators say XIV but it just dropped overnight, so got for TQQQ
        log.info("CASE TQQQ 14")
        target_sid = context.tqqq 
        
    elif (target_sid == context.uvxy and xiv_ratio > threshold_uvxy_ratio) :
        # indicators say UVXY but it just dropped overnight, so got for TQQQ
        log.info("CASE TQQQ 15")
        target_sid = context.tqqq             
    if target_sid not in context.portfolio.positions :
        order_target_percent(context.tqqq,0)
        order_target_percent(context.uvxy,0)
        order_target_percent(context.xiv,0)
        order_target_percent(target_sid,1)
        
def update_indices(context, data):
    context.fetch_failed = False
    context.vix_vals = unpack_from_data(context, data, 'v')    
    context.vxv_vals = unpack_from_data(context, data, 'vxv')  
    context.vx1_vals = unpack_from_data(context, data, 'v1')
    context.vx2_vals = unpack_from_data(context, data, 'v2')

def fix_close(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%m/%d/%Y')))
    df = df.sort_values(by='Date', ascending=True)
    return df

def fix_closeVX(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%Y-%m-%d')))
    df = df.sort_values(by='Date', ascending=True)
    return df


def subsequent_trading_date(date):
    tdays = tradingcalendar.trading_days
    last_date = pd.to_datetime(date)
    last_dt = tradingcalendar.canonicalize_datetime(last_date)
    next_dt = tdays[tdays.searchsorted(last_dt) + 1]
    return next_dt

def add_last_bar(df):
    last_date = df.index[-1]
    subsequent_date = subsequent_trading_date(last_date)
    blank_row = pd.Series({}, index=df.columns, name=subsequent_date)
    # add today, and shift all previous data up to today. This 
    # should result in the same data frames as in backtest
    df = df.append(blank_row).shift(1).dropna(how='all')
    return df

def shift_data(df):
    log.info("Pre-Shift")
    df = add_last_bar(df)
    df.fillna(method='ffill') 
    df['PrevCloses'] = my_rolling_apply_series(df['Close'], to_csv_str, History)
    dates = pd.Series(df.index)
    dates.index = df.index
    df['PrevDates'] = my_rolling_apply_series(dates, to_csv_str, History)
    return df

def unpack_from_data(context, data, sym):
    try:
        v = data.current(sym, 'PrevCloses')
        i = data.current(sym, 'PrevDates')
        return from_csv_strs(i,v,True).apply(float)
    except:
        log.warn("Unable to unpack historical {s} data.".format(s=sym))
        context.fetch_failed = True

def addFieldsVIX(df):
    log.info("VIX: Pre-Massage")
    df = fix_close(df,'VIX Close')
    log.info("VIX: Post-Massage")
    return df

def addFieldsVXV(df):
    log.info("VXV: Pre-Massage")
    df.rename(columns={'Unnamed: 0': 'Date'}, inplace=True)
    df = fix_close(df,'CLOSE')
    log.info("VXV: Post-Massage")
    return df

def addFieldsVX1(df):
    log.info("VX1: Pre-Massage")
    df = fix_closeVX(df,'F1')
    log.info("VX1: Post-Massage")
    return df

def addFieldsVX2(df):
    log.info("VX2: Pre-Massage")
    df = fix_closeVX(df,'F2')
    log.info("VX2: Post-Massage")
    return df

# convert a series of values to a comma-separated string of said values
def to_csv_str(s):
    return functools.reduce(lambda x,y: x+','+y, pd.Series(s).apply(str))

# a specific instance of rolling apply, for Series of any type (not just numeric,
# ala pandas.rolling_apply), where the index of the series is set to the indices
# of the last elements of each subset
def my_rolling_apply_series(s_in, f, n):
    s_out = pd.Series([f(s_in[i:i+n]) for i in range(0,len(s_in)-(n-1))]) 
    s_out.index = s_in.index[n-1:]
    return s_out

# reconstitutes a Series from two csv-encoded strings, one of the index, one of the values
def from_csv_strs(x, y, idx_is_date):
    s = pd.Series(y.split(','),index=x.split(','))
    if (idx_is_date):
        s.index = s.index.map(lambda x: pd.Timestamp(x))
    return s
            
There was a runtime error.

If real trading is like the backtesting, wow! Is there overfitting or look ahead bias going on?

What is nokoUrl = 'http://52.15.233.150/noko.csv'? Specifically, is that data going to continue to be available? It looks similar to vixcentral.com page.

I am paper trading the first one, to see what it does.

    # Setting thresholds  
    threshold_vix_too_low = 10.76   # 0  
    threshold_xiv = -0.049          # 1  
    threshold_vxv_xiv = 0.87        # 2  
    threshold_uvxy = 0.049          # 3  
    threshold_macd = -0.55          # 3  
    threshold_vxv_uvxy = 1.3        # 4  
    threshold_vix_high = 19.9       # 5  
    threshold_vc_low = -0.148       # 6  
    threshold_vc_high = 0.046       # 8  
    threshold_vc_high_2 = -0.06     # 8  
    threshold_xiv_ratio = -0.053    # 10  
    threshold_uvxy_ratio = 0.08     # 11  

Good luck

Wonder how much someone would lose if he/she would stick with this algorithms vix_too_low hard coded threshold and VIX eventually goes to 6 or below?

Macro:

To save me the effort of comparing the two algos, what are the differences?

The strategies are excellent but I do share some of Luke's discomfort with the hard-coded thresholds, at least for absolute values such as vix_too_low. It would be great if we could instead use a ratio such as the current level compared to the moving average.

As we are not using Pipeline at all, we can completely switch to US Futures calendar and utilize Futures API provided by Q. VX Futures are available at Q since somewhere from July 2012. So, for backtesting since then, as well as current live trading, we can completely get rid of Futures fetcher from that strange noko.csv site. Hope, at some point we can replace VIX and VXV fetcher with some index API provided by Q.
Otherwise, cleaner code is attached.
Remember to run it on Futures Calendar.
Macro, let me know if attached algo would work for live trading. Hope it will.

Clone Algorithm
110
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
import talib
from math import trunc  
from pytz import timezone      
import numpy as np
import pandas as pd
import datetime
import math
import time
import re
import functools
import itertools

from zipline.utils import tradingcalendar

#Max_Wager in $ we want to invest into this algorithm
Max_Wager = 0 #5000.
#Or set above Max_Wager to 0 and set Max_Wager_Percent, as percent of portfolio
Max_Wager_Percent = 100.

History = 128
# Setting thresholds
threshold_vix_too_low = 10.8 # first percentile, very rare case, very likely VIX will pop so we want to be in UVXY
    
threshold_xiv = -0.049 # 60th percentile, as we want to be primarily in XIV
threshold_uvxy = -threshold_xiv # symmetry 

threshold_vxv_xiv = 0.79 # first percentile, very rare case
threshold_vxv_uvxy = 2 - threshold_vxv_xiv # symmetry 
    
threshold_macd = -0.55 # first percentile, we want to pass 99% of the cases here
    
threshold_vix_high = 19.9 # 85th percentile, we are already going to be in XIV 60% of the time, so need to be careful
# no more than total 75% of the time in XIV
    
threshold_vix_low = 11.5 # 5th percentile, very rare case, may be VIX will pop so we want to be in UVXY

threshold_vc_low = -0.148 # 1.5th percentile, very rare, we are already going to be in XIV 60% of the time, so need to be careful
# no more than total 75% of the time in XIV
    
threshold_vc_high = 0.046 #80th percentile, we will hardly be in UVXY (actually 51 is 80th percentile)
# so we need to pick up some cases where which is biased towards UVXY to get to at least 15% of the time in UVXY

threshold_vc_low_2 = 0.0 #50th percentile, below this we go for UVXY in lieu of any other signals
threshold_vc_high_2 = -0.06 # testing this, this is fitted  

# relaxing condition to invest in XIV from 60th to 62nd percentile (~5%), absolute value also relaxes ~5% from -0.049 to -0.046
threshold_xiv_2 = -0.046 # 62th percentile
# this is to check for one last time if no signals have it, before we default to TQQQ
    
threshold_uvxy_2 = -0.008 # 85th percentile, taking a risk to invest in UVXY when there's hardly any contango or backwardation
# this is to check for one last time if no signals have it, before we default to TQQQ
    
threshold_xiv_ratio = -0.06 # first percentile
# if XIV has dropepd that much we probably don't want to be long XIVeven though other signals say yes, default to TQQQ
threshold_uvxy_ratio = -threshold_xiv_ratio # symmetry
# if XIV has gone up that much we probably don't want to be long UVXY even though other signals say yes, default to TQQQ

# Blending two V1 and V2 ratios  together using a weighted average
ratio_weight = 0.7

def initialize(context):
    
    vixUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv'
    vxvUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vxvdailyprices.csv'

    fetch_csv(vixUrl, 
              symbol='v', 
              skiprows=1,
              date_column='Date', 
              pre_func=addFieldsVIX,
              post_func=shift_data)

    fetch_csv(vxvUrl, 
              symbol='vxv', 
              skiprows=2,
              date_column='Date', 
              pre_func=addFieldsVXV,
              post_func=shift_data)

    context.xiv = sid(40516)
    context.tqqq = sid(39214)
    context.uvxy = sid(41969)
    context.spyg = sid(22009)
    context.vix = -1
    context.futures = continuous_future('VX')
    
    set_slippage(slippage.FixedSlippage(spread=0.0))
    
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours = 0, minutes = 1))
    
def my_rebalance(context, data):  
    
    update_indices(context, data)     
    last_vix = data.current('v', 'Close')
    last_vxv = data.current('vxv', 'Close')

    cl_chain = data.current_chain(context.futures)
    front = cl_chain[0]
    second = cl_chain[1]

    last_vx1 = data.current(front, 'price')
    last_vx2 = data.current(second, 'price')
    
    log.info("%s %5.3f %s %5.3f Vix %5.3f Vxv %5.3f" %(front, last_vx1, second, last_vx2, last_vix, last_vxv))

    # Calculating the gap ratio between spot vix and the first month vix future
    last_ratio_v_v1 = last_vix/last_vx1
    # Calculating the contango ratio of the front and second month VIX Futures 
    last_ratio_v1_v2 = last_vx1/last_vx2
    # average of above 2 using ratio_weight (more to front month)
    last_ratio = (ratio_weight*last_ratio_v_v1) + ((1-ratio_weight)*last_ratio_v1_v2) - 1
    # VIX to VXV ratio
    vix_vxv_ratio = last_vix/last_vxv
    
    # Retrieve SPY prices for technical indicators
    prices = data.history(context.spyg, 'open', 40, '1d')
    # Retrieve SPY MACD data
    macda, signal, hist = talib.MACD(prices, fastperiod=12,slowperiod=26,signalperiod=9)
    macd = macda[-1] - signal[-1]
    
    # Calculate how much vix moved the previous day
    if (context.vix <> -1) : 
        vix_ratio = last_vix/context.vix -1
    else :
        vix_ratio = 0
    context.vix = last_vix
   
    #change in XIV from previous day
    xiv_history = data.history(context.xiv, 'price', 2, '1d')  
    xiv_ratio = xiv_history[1]/xiv_history[0] - 1
                    
    if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY witht he hope of a spike
        log.info("CASE UVXY 0")
        target_sid = context.uvxy

    elif last_ratio < threshold_xiv: # if contango is high, invest in XIV to gain from decay
        log.info("CASE XIV 1")
        target_sid = context.xiv

    elif vix_vxv_ratio < threshold_vxv_xiv: # if short term vol is low compared to mid term, invest in XIV to gain from decay
        log.info("CASE XIV 2")
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy and macd > threshold_macd: # if backwardation is high, invest in UVXY to gain from decay
        log.info("CASE UVXY 3")
        target_sid = context.uvxy

    elif vix_vxv_ratio > threshold_vxv_uvxy: # if short term vol is high compared to mid term, invest in UVXY to gain from growth
        log.info("CASE UVXY 4")
        target_sid = context.uvxy

    elif last_vix > threshold_vix_high: # if VIX is too high, invest in XIV expecting that VIX will drop
        log.info("CASE XIV 5")
        target_sid = context.xiv
            
    elif last_vix < threshold_vix_low: # if VIX is too low, invest in XIV expecting that VIX will rise
        log.info("CASE UVXY 6")
        target_sid = context.uvxy

    elif vix_ratio < threshold_vc_low: # Vix down sharply, invest in XIV expecting that futures curve gets pulled down
        log.info("CASE XIV 7")
        target_sid = context.xiv
            
    elif vix_ratio > threshold_vc_high: # Vix up sharply, invest in UVXY expecting that futures curve gets pulled up
        log.info("CASE UVXY 8")
        target_sid = context.uvxy

    elif vix_ratio > threshold_vc_high_2: #have to think
        log.info("CASE XIV 9")
        target_sid = context.xiv
            
    elif vix_ratio < threshold_vc_low_2: # vix dropped. There are no other signals. in the absence of any signal, try UVXY
        log.info("CASE UVXY 10")
        target_sid = context.uvxy

    elif last_ratio < threshold_xiv_2: # before we give up on XIV and move to TQQQ let's relax the contango conditions a bit
        log.info("CASE XIV 11")
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy_2: # before we give up on UVXY and move to TQQQ let's relax the backwardation conditions a bit
        log.info("CASE UVXY 12")
        target_sid = context.uvxy

    else:
        log.info("CASE TQQQ 13")
        target_sid = context.tqqq
        
    if (target_sid == context.xiv and xiv_ratio < threshold_xiv_ratio) : 
        # indicators say XIV but it just dropped overnight, so got for TQQQ
        log.info("CASE TQQQ 14")
        target_sid = context.tqqq 
        
    elif (target_sid == context.uvxy and xiv_ratio > threshold_uvxy_ratio) :
        # indicators say UVXY but it just dropped overnight, so got for TQQQ
        log.info("CASE TQQQ 15")
        target_sid = context.tqqq
    
    
    
    if target_sid not in context.portfolio.positions:
        
        Wager = Max_Wager
        if Wager == 0.:
            Wager = Max_Wager_Percent/100.*context.portfolio.portfolio_value
        Weight = Wager/context.portfolio.portfolio_value
        log.info("Wager %5.2f Weight %5.2f" %(Wager, Weight))
        
        order_target_percent(context.tqqq,0)
        order_target_percent(context.uvxy,0)
        order_target_percent(context.xiv,0)
        order_target_percent(target_sid, Weight)
        
def update_indices(context, data):
    context.fetch_failed = False
    context.vix_vals = unpack_from_data(context, data, 'v') 
    context.vxv_vals = unpack_from_data(context, data, 'vxv')  

def fix_close(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%m/%d/%Y')))
    df = df.sort_values(by='Date', ascending=True)
    return df

def subsequent_trading_date(date):
    tdays = tradingcalendar.trading_days
    last_date = pd.to_datetime(date)
    last_dt = tradingcalendar.canonicalize_datetime(last_date)
    next_dt = tdays[tdays.searchsorted(last_dt) + 1]
    return next_dt

def add_last_bar(df):
    last_date = df.index[-1]
    subsequent_date = subsequent_trading_date(last_date)
    blank_row = pd.Series({}, index=df.columns, name=subsequent_date)
    # add today, and shift all previous data up to today. This 
    # should result in the same data frames as in backtest
    df = df.append(blank_row).shift(1).dropna(how='all')
    return df

def shift_data(df):
    log.info("Pre-Shift")
    df = add_last_bar(df)
    df.fillna(method='ffill') 
    df['PrevCloses'] = my_rolling_apply_series(df['Close'], to_csv_str, History)
    dates = pd.Series(df.index)
    dates.index = df.index
    df['PrevDates'] = my_rolling_apply_series(dates, to_csv_str, History)
    return df

def unpack_from_data(context, data, sym):
    try:
        v = data.current(sym, 'PrevCloses')
        i = data.current(sym, 'PrevDates')
        return from_csv_strs(i,v,True).apply(float)
    except:
        log.warn("Unable to unpack historical {s} data.".format(s=sym))
        context.fetch_failed = True

def addFieldsVIX(df):
    #log.info("VIX: Pre-Massage")
    df = fix_close(df,'VIX Close')
    #log.info("VIX: Post-Massage")
    return df

def addFieldsVXV(df):
    #log.info("VXV: Pre-Massage")
    df.rename(columns={'Unnamed: 0': 'Date'}, inplace=True)
    df = fix_close(df,'CLOSE')
    #log.info("VXV: Post-Massage")
    return df

# convert a series of values to a comma-separated string of said values
def to_csv_str(s):
    return functools.reduce(lambda x,y: x+','+y, pd.Series(s).apply(str))

# a specific instance of rolling apply, for Series of any type (not just numeric,
# ala pandas.rolling_apply), where the index of the series is set to the indices
# of the last elements of each subset
def my_rolling_apply_series(s_in, f, n):
    s_out = pd.Series([f(s_in[i:i+n]) for i in range(0,len(s_in)-(n-1))]) 
    s_out.index = s_in.index[n-1:]
    return s_out

# reconstitutes a Series from two csv-encoded strings, one of the index, one of the values
def from_csv_strs(x, y, idx_is_date):
    s = pd.Series(y.split(','),index=x.split(','))
    if (idx_is_date):
        s.index = s.index.map(lambda x: pd.Timestamp(x))
    return s
            
There was a runtime error.

@Igor Tebelev:
Does not work for live trading.
"Futures not allowed for live trading" error upon attempt to paper trade or broker trade.

Crazy! Would love to know: what particular reason Q has for that?

Wow this is amazing. I'm surprised you aren't keeping these strategies to yourself, but hey I'm not complaining, would love an update once you go live. Thanks for sharing.

I've been seeing a lot of vix strategies on here, is there a book anyone would suggest reading to learn about developing these vix strategies ? Thanks !

The blogosphere is more helpful then books. look for VIX or XIV posts on http://quantocracy.com/

BTW: Futures are not tradeable yet but will be soon according to Q. Soon is a stretcheable term though. For now we have to stick with fetcher, as soon as futures are allowed in the live environment (even only as a signal) we can change the code to work with that

^Thanks for the info Peter

It beat long XIV by a couple % points YTD (pretty much by being right 5/17). Add a stop loss, so you don't go from 70% to 48% or 108% to 90% in 2 days each.

Clone Algorithm
42
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
import talib
from math import trunc  
from pytz import timezone      
import numpy as np
import pandas as pd
import datetime
import math
import time
import re
import functools
import itertools

from zipline.utils import tradingcalendar

History = 128

def initialize(context):
    
    vixUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv'
    vxvUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vxvdailyprices.csv'
    nokoUrl = 'http://52.15.233.150/noko.csv'  
    
    fetch_csv(nokoUrl, 
              symbol='v1', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX1, 
              post_func=shift_data)
    
    fetch_csv(nokoUrl, 
              symbol='v2', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX2, 
              post_func=shift_data)

    fetch_csv(vixUrl, 
              symbol='v', 
              skiprows=1,
              date_column='Date', 
              pre_func=addFieldsVIX,
              post_func=shift_data)

    fetch_csv(vxvUrl, 
              symbol='vxv', 
              skiprows=2,
              date_column='Date', 
              pre_func=addFieldsVXV,
              post_func=shift_data)

    context.xiv = sid(40516)
    context.tqqq = sid(39214)
    context.uvxy = sid(41969)
    context.spyg = sid(22009)
    
    set_benchmark(sid(40516)) 
    
    context.vix = -1
    
    set_slippage(slippage.FixedSlippage(spread=0.0))
    
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours = 0, minutes = 1))
    
def my_rebalance(context, data):  

    update_indices(context, data)     
    last_vix = data.current('v', 'Close')
    last_vx1 = data.current('v1','Close')  
    last_vx2 = data.current('v2','Close')      
    last_vxv = data.current('vxv', 'Close')
    
        
    log.info("Vix %f" %last_vix)
    log.info("Vxv %f" %last_vxv)
    log.info("V1 %f" %last_vx1)
    log.info("V2 %f" %last_vx2)

               
    # Calculating the gap between spot vix and the first month vix future
    last_ratio_v_v1 = last_vix/last_vx1

    # Calculating the contango ratio of the front and second month VIX Futures 
    last_ratio_v1_v2 = last_vx1/last_vx2

    # Blending the previous two ratios together using a weighted average
    ratio_weight = 0.7
    last_ratio = (ratio_weight*last_ratio_v_v1) + ((1-ratio_weight)*last_ratio_v1_v2) - 1
    
    vix_vxv_ratio = last_vix/last_vxv
    
    # Retrieve SPY prices for technical indicators
    prices = data.history(context.spyg, 'open', 40, '1d')
    
    # Retrieve SPY MACD data
    macda, signal, hist = talib.MACD(prices, fastperiod=12,slowperiod=26,signalperiod=9)
    macd = macda[-1] - signal[-1]
    
    # Caluclate how much vix moved the previous day
    if (context.vix <> -1) : 
        vix_ratio = last_vix/context.vix -1
    else :
        vix_ratio = 0
    context.vix = last_vix
    
    xiv_history = data.history(context.xiv, 'price', 2, '1d')  
    
    xiv_ratio = xiv_history[1]/xiv_history[0] - 1
    
    # Setting thresholds
    threshold_vix_too_low = 10.76   # 0 
    threshold_xiv = -0.049          # 1
    threshold_vxv_xiv = 0.87        # 2 
    threshold_uvxy = 0.049          # 3
    threshold_macd = -0.55          # 3
    threshold_vxv_uvxy = 1.3        # 4 
    threshold_vix_high = 19.9       # 5 
    threshold_vc_low = -0.148       # 6
    threshold_vc_high = 0.046       # 8
    threshold_vc_high_2 = -0.06     # 8
    threshold_xiv_ratio = -0.053    # 10 
    threshold_uvxy_ratio = 0.08     # 11 
    
    if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY witht he hope of a spike
        case = 0
        target_sid = context.uvxy
            
    elif last_ratio < threshold_xiv: # if contango is high, invest in XIV to gain from decay
        case = 1
        target_sid = context.xiv
            
    elif vix_vxv_ratio < threshold_vxv_xiv: # if short term vol is low compared to mid term, invest in XIV to gain from decay
        case = 2
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy and macd > threshold_macd: # if backwardation is high, invest in UVXY to gain from decay
        case = 3
        target_sid = context.uvxy

    elif vix_vxv_ratio > threshold_vxv_uvxy: # if short term vol is high compared to mid term, invest in UVXY to gain from growth
        case = 4
        target_sid = context.uvxy

    elif last_vix > threshold_vix_high: # if VIX is too high, invest in XIV expecting that VIX will drop
        case = 5
        target_sid = context.xiv

    elif vix_ratio < threshold_vc_low: # Vix down sharply, invest in XIV expecting that futures curve gets pulled down
        case = 6
        target_sid = context.xiv
            
    elif vix_ratio > threshold_vc_high: # Vix up sharply, invest in UVXY expecting that futures curve gets pulled up
        case = 7
        target_sid = context.uvxy

    elif vix_ratio > threshold_vc_high_2: #have to think
        case = 8
        target_sid = context.xiv

    else:
        case = 9
        target_sid = context.uvxy
        
    if (target_sid == context.xiv and xiv_ratio < threshold_xiv_ratio) : 
        # indicators say XIV but it just dropped overnight, so got for TQQQ
        case = 10
        target_sid = context.tqqq 
        
    elif (target_sid == context.uvxy and xiv_ratio > threshold_uvxy_ratio) :
        # indicators say UVXY but it just dropped overnight, so got for TQQQ
        case = 11
        target_sid = context.tqqq
        
    if target_sid == context.xiv :
        log.info("CASE XIV %d" %case)
    elif target_sid == context.uvxy :
        log.info("CASE UVXY %d" %case)
    else :
        log.info("CASE TQQQ %d" %case)
            
    if target_sid not in context.portfolio.positions :
        order_target_percent(context.tqqq,0)
        order_target_percent(context.uvxy,0)
        order_target_percent(context.xiv,0)
        order_target_percent(target_sid,1)
        
def update_indices(context, data):
    context.fetch_failed = False
    context.vix_vals = unpack_from_data(context, data, 'v')    
    context.vxv_vals = unpack_from_data(context, data, 'vxv')  
    context.vx1_vals = unpack_from_data(context, data, 'v1')
    context.vx2_vals = unpack_from_data(context, data, 'v2')

def fix_close(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%m/%d/%Y')))
    df = df.sort_values(by='Date', ascending=True)
    return df

def fix_closeVX(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%Y-%m-%d')))
    df = df.sort_values(by='Date', ascending=True)
    return df


def subsequent_trading_date(date):
    tdays = tradingcalendar.trading_days
    last_date = pd.to_datetime(date)
    last_dt = tradingcalendar.canonicalize_datetime(last_date)
    next_dt = tdays[tdays.searchsorted(last_dt) + 1]
    return next_dt

def add_last_bar(df):
    last_date = df.index[-1]
    subsequent_date = subsequent_trading_date(last_date)
    blank_row = pd.Series({}, index=df.columns, name=subsequent_date)
    # add today, and shift all previous data up to today. This 
    # should result in the same data frames as in backtest
    df = df.append(blank_row).shift(1).dropna(how='all')
    return df

def shift_data(df):
    log.info("Pre-Shift")
    df = add_last_bar(df)
    df.fillna(method='ffill') 
    df['PrevCloses'] = my_rolling_apply_series(df['Close'], to_csv_str, History)
    dates = pd.Series(df.index)
    dates.index = df.index
    df['PrevDates'] = my_rolling_apply_series(dates, to_csv_str, History)
    return df

def unpack_from_data(context, data, sym):
    try:
        v = data.current(sym, 'PrevCloses')
        i = data.current(sym, 'PrevDates')
        return from_csv_strs(i,v,True).apply(float)
    except:
        log.warn("Unable to unpack historical {s} data.".format(s=sym))
        context.fetch_failed = True

def addFieldsVIX(df):
    log.info("VIX: Pre-Massage")
    df = fix_close(df,'VIX Close')
    log.info("VIX: Post-Massage")
    return df

def addFieldsVXV(df):
    log.info("VXV: Pre-Massage")
    df.rename(columns={'Unnamed: 0': 'Date'}, inplace=True)
    df = fix_close(df,'CLOSE')
    log.info("VXV: Post-Massage")
    return df

def addFieldsVX1(df):
    log.info("VX1: Pre-Massage")
    df = fix_closeVX(df,'F1')
    log.info("VX1: Post-Massage")
    return df

def addFieldsVX2(df):
    log.info("VX2: Pre-Massage")
    df = fix_closeVX(df,'F2')
    log.info("VX2: Post-Massage")
    return df

# convert a series of values to a comma-separated string of said values
def to_csv_str(s):
    return functools.reduce(lambda x,y: x+','+y, pd.Series(s).apply(str))

# a specific instance of rolling apply, for Series of any type (not just numeric,
# ala pandas.rolling_apply), where the index of the series is set to the indices
# of the last elements of each subset
def my_rolling_apply_series(s_in, f, n):
    s_out = pd.Series([f(s_in[i:i+n]) for i in range(0,len(s_in)-(n-1))]) 
    s_out.index = s_in.index[n-1:]
    return s_out

# reconstitutes a Series from two csv-encoded strings, one of the index, one of the values
def from_csv_strs(x, y, idx_is_date):
    s = pd.Series(y.split(','),index=x.split(','))
    if (idx_is_date):
        s.index = s.index.map(lambda x: pd.Timestamp(x))
    return s
            
There was a runtime error.

Will this have issues with existing positions in an account?

The algorithms are down 8% and 11% over a month, while XIV is basically flat. The choppy up and down market is leading to a lot of trades, which is a killer. I am not deploying it with real money yet. Will wait for another month.

Thank you Macro Investor for sharing these algorithms; they've taught me a lot.

Are some of the last month's losses due to Case 0? Your fixed threshold (10.76) has worked for much of the history of the VIX but last month instead of dipping below the threshold the VIX lingered.

I added a condition to Case 0 which also requires that the VIX is below a ratio to its 200 SMA. The idea is to adjust to the VIX trend and over the last month it kept the algorithm out of Case 0 almost entirely, ending up more or less flat like XIV. This modification doesn't feel like it's overfitting or adding unneeded complication since it's letting the VIX price action set the level where we look for a spike up.

@Stephen, can you share the ratio or code you added?

@Peter

Here's Macro's algorithm from his first post in this thread with the Case 0 modification.

Clone Algorithm
69
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
import talib
from math import trunc  
from pytz import timezone      
import numpy as np
import pandas as pd
import datetime
import math
import time
import re
import functools
import itertools

from zipline.utils import tradingcalendar

History = 128

def initialize(context):
    
    vixUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv'
    vxvUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vxvdailyprices.csv'
    nokoUrl = 'http://52.15.233.150/noko.csv'  
    
    fetch_csv(nokoUrl, 
              symbol='v1', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX1, 
              post_func=shift_data)
    
    fetch_csv(nokoUrl, 
              symbol='v2', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX2, 
              post_func=shift_data)

    fetch_csv(vixUrl, 
              symbol='v', 
              skiprows=1,
              date_column='Date', 
              pre_func=addFieldsVIX,
              post_func=shift_data)

    fetch_csv(vxvUrl, 
              symbol='vxv', 
              skiprows=2,
              date_column='Date', 
              pre_func=addFieldsVXV,
              post_func=shift_data)

    context.xiv = sid(40516)
    context.tqqq = sid(39214)
    context.uvxy = sid(41969)
    context.spyg = sid(22009)
    
    context.vix = -1
    
    context.leverage = 1.0
    
    set_slippage(slippage.FixedSlippage(spread=0.0))
    
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours = 0, minutes = 1))
    
def my_rebalance(context, data):  

    update_indices(context, data)     
    last_vix = data.current('v', 'Close')
    last_vix_200ma_ratio = data.current('v', '200ma Ratio')
    last_vx1 = data.current('v1','Close')  
    last_vx2 = data.current('v2','Close')      
    last_vxv = data.current('vxv', 'Close')
    
        
    log.info("Vix %f" %last_vix)
    log.info("Vxv %f" %last_vxv)
    log.info("V1 %f" %last_vx1)
    log.info("V2 %f" %last_vx2)

               
    # Calculating the gap between spot vix and the first month vix future
    last_ratio_v_v1 = last_vix/last_vx1

    # Calculating the contango ratio of the front and second month VIX Futures 
    last_ratio_v1_v2 = last_vx1/last_vx2

    # Blending the previous two ratios together using a weighted average
    ratio_weight = 0.7
    last_ratio = (ratio_weight*last_ratio_v_v1) + ((1-ratio_weight)*last_ratio_v1_v2) - 1
    
    vix_vxv_ratio = last_vix/last_vxv
    
    # Retrieve SPY prices for technical indicators
    prices = data.history(context.spyg, 'open', 40, '1d')
    
    # Retrieve SPY MACD data
    macda, signal, hist = talib.MACD(prices, fastperiod=12,slowperiod=26,signalperiod=9)
    macd = macda[-1] - signal[-1]
    
    # Caluclate how much vix moved the previous day
    if (context.vix <> -1) : 
        vix_ratio = last_vix/context.vix -1
    else :
        vix_ratio = 0
    context.vix = last_vix
    
    xiv_history = data.history(context.xiv, 'price', 2, '1d')  
    
    xiv_ratio = xiv_history[1]/xiv_history[0] - 1
    
    # Setting thresholds
    threshold_vix_too_low = 10.76   # 0 
    threshold_vix_200ma_ratio_low = 0.79  # 0  Ratio of current price to 200 day moving average. Use to double check threshold_vix_too_low (case 0)
    threshold_xiv = -0.049          # 1
    threshold_vxv_xiv = 0.87        # 2 
    threshold_uvxy = 0.049          # 3
    threshold_macd = -0.55          # 3
    threshold_vxv_uvxy = 1.3        # 4 
    threshold_vix_high = 19.9       # 5 
    threshold_vc_low = -0.148       # 6
    threshold_vc_high = 0.046       # 8
    threshold_vc_high_2 = -0.06     # 8
    threshold_xiv_ratio = -0.053    # 10 
    threshold_uvxy_ratio = 0.08     # 11 
    
    #if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY witht he hope of a spike
    # [ Modification from original: also require VIX to be below a ratio of its 200 SMA ]
    if last_vix < threshold_vix_too_low and last_vix_200ma_ratio < threshold_vix_200ma_ratio_low : 
        case = 0
        target_sid = context.uvxy
            
    elif last_ratio < threshold_xiv: # if contango is high, invest in XIV to gain from decay
        case = 1
        target_sid = context.xiv
            
    elif vix_vxv_ratio < threshold_vxv_xiv: # if short term vol is low compared to mid term, invest in XIV to gain from decay
        case = 2
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy and macd > threshold_macd: # if backwardation is high, invest in UVXY to gain from decay
        case = 3
        target_sid = context.uvxy

    elif vix_vxv_ratio > threshold_vxv_uvxy: # if short term vol is high compared to mid term, invest in UVXY to gain from growth
        case = 4
        target_sid = context.uvxy

    elif last_vix > threshold_vix_high: # if VIX is too high, invest in XIV expecting that VIX will drop
        case = 5
        target_sid = context.xiv

    elif vix_ratio < threshold_vc_low: # Vix down sharply, invest in XIV expecting that futures curve gets pulled down
        case = 6
        target_sid = context.xiv
            
    elif vix_ratio > threshold_vc_high: # Vix up sharply, invest in UVXY expecting that futures curve gets pulled up
        case = 7
        target_sid = context.uvxy

    elif vix_ratio > threshold_vc_high_2: #have to think
        case = 8
        target_sid = context.xiv

    else:
        case = 9
        target_sid = context.uvxy
        
    if (target_sid == context.xiv and xiv_ratio < threshold_xiv_ratio) : 
        # indicators say XIV but it just dropped overnight, so got for TQQQ
        case = 10
        target_sid = context.tqqq 
        
    elif (target_sid == context.uvxy and xiv_ratio > threshold_uvxy_ratio) :
        # indicators say UVXY but it just dropped overnight, so got for TQQQ
        case = 11
        target_sid = context.tqqq
        
    if target_sid == context.xiv :
        log.info("CASE XIV %d" %case)
    elif target_sid == context.uvxy :
        log.info("CASE UVXY %d" %case)
    else :
        log.info("CASE TQQQ %d" %case)
           
    record(Case=case)
    
    if target_sid not in context.portfolio.positions :
        order_target_percent(context.tqqq,0)
        order_target_percent(context.uvxy,0)
        order_target_percent(context.xiv,0)
        order_target_percent(target_sid,context.leverage)
        
def update_indices(context, data):
    context.fetch_failed = False
    context.vix_vals = unpack_from_data(context, data, 'v')    
    context.vxv_vals = unpack_from_data(context, data, 'vxv')  
    context.vx1_vals = unpack_from_data(context, data, 'v1')
    context.vx2_vals = unpack_from_data(context, data, 'v2')

def fix_close(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%m/%d/%Y')))
    df = df.sort_values(by='Date', ascending=True)
    return df

def fix_closeVX(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%Y-%m-%d')))
    df = df.sort_values(by='Date', ascending=True)
    return df


def subsequent_trading_date(date):
    tdays = tradingcalendar.trading_days
    last_date = pd.to_datetime(date)
    last_dt = tradingcalendar.canonicalize_datetime(last_date)
    next_dt = tdays[tdays.searchsorted(last_dt) + 1]
    return next_dt

def add_last_bar(df):
    last_date = df.index[-1]
    subsequent_date = subsequent_trading_date(last_date)
    blank_row = pd.Series({}, index=df.columns, name=subsequent_date)
    # add today, and shift all previous data up to today. This 
    # should result in the same data frames as in backtest
    df = df.append(blank_row).shift(1).dropna(how='all')
    return df

def shift_data(df):
    log.info("Pre-Shift")
    df = add_last_bar(df)
    df.fillna(method='ffill') 
    df['PrevCloses'] = my_rolling_apply_series(df['Close'], to_csv_str, History)
    dates = pd.Series(df.index)
    dates.index = df.index
    df['PrevDates'] = my_rolling_apply_series(dates, to_csv_str, History)
    return df

def unpack_from_data(context, data, sym):
    try:
        v = data.current(sym, 'PrevCloses')
        i = data.current(sym, 'PrevDates')
        return from_csv_strs(i,v,True).apply(float)
    except:
        log.warn("Unable to unpack historical {s} data.".format(s=sym))
        context.fetch_failed = True

def addFieldsVIX(df):
    #log.info("VIX: Pre-Massage")
    df = fix_close(df,'VIX Close')
    df['200ma'] = df['Close'].rolling(200).mean()
    df['200ma Ratio'] = df['Close'] / df['200ma']
    #log.info("VIX: Post-Massage")
    return df

def addFieldsVXV(df):
    log.info("VXV: Pre-Massage")
    df.rename(columns={'Unnamed: 0': 'Date'}, inplace=True)
    df = fix_close(df,'CLOSE')
    log.info("VXV: Post-Massage")
    return df

def addFieldsVX1(df):
    log.info("VX1: Pre-Massage")
    df = fix_closeVX(df,'F1')
    log.info("VX1: Post-Massage")
    return df

def addFieldsVX2(df):
    log.info("VX2: Pre-Massage")
    df = fix_closeVX(df,'F2')
    log.info("VX2: Post-Massage")
    return df

# convert a series of values to a comma-separated string of said values
def to_csv_str(s):
    return functools.reduce(lambda x,y: x+','+y, pd.Series(s).apply(str))

# a specific instance of rolling apply, for Series of any type (not just numeric,
# ala pandas.rolling_apply), where the index of the series is set to the indices
# of the last elements of each subset
def my_rolling_apply_series(s_in, f, n):
    s_out = pd.Series([f(s_in[i:i+n]) for i in range(0,len(s_in)-(n-1))]) 
    s_out.index = s_in.index[n-1:]
    return s_out

# reconstitutes a Series from two csv-encoded strings, one of the index, one of the values
def from_csv_strs(x, y, idx_is_date):
    s = pd.Series(y.split(','),index=x.split(','))
    if (idx_is_date):
        s.index = s.index.map(lambda x: pd.Timestamp(x))
    return s
            
There was a runtime error.

Treat the algo with caution.
Even with the version improved by Stephen, the return of this algo could be to volatile as in year 2013.
From 1/1/2013 to 1/1/2014, this algo returned 93%, while you could earn 107% if you just hold xiv with no transaction. And the DD was about 50%.
It must be frustrating during that year.

Clone Algorithm
10
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
import talib
from math import trunc  
from pytz import timezone      
import numpy as np
import pandas as pd
import datetime
import math
import time
import re
import functools
import itertools

from zipline.utils import tradingcalendar

History = 128

def initialize(context):
    
    vixUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vixcurrent.csv'
    vxvUrl = 'http://www.cboe.com/publish/scheduledtask/mktdata/datahouse/vxvdailyprices.csv'
    nokoUrl = 'http://52.15.233.150/noko.csv'  
    
    fetch_csv(nokoUrl, 
              symbol='v1', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX1, 
              post_func=shift_data)
    
    fetch_csv(nokoUrl, 
              symbol='v2', 
              date_column='Date', 
              date_format='%Y-%m-%d', 
              pre_func=addFieldsVX2, 
              post_func=shift_data)

    fetch_csv(vixUrl, 
              symbol='v', 
              skiprows=1,
              date_column='Date', 
              pre_func=addFieldsVIX,
              post_func=shift_data)

    fetch_csv(vxvUrl, 
              symbol='vxv', 
              skiprows=2,
              date_column='Date', 
              pre_func=addFieldsVXV,
              post_func=shift_data)
    # set_commission(commission.PerTrade(cost=4.95))
    context.xiv = sid(40516)
    context.tqqq = sid(39214)
    context.uvxy = sid(41969)
    context.spyg = sid(22009)
    set_benchmark(sid(40516))
    context.vix = -1
    
    set_slippage(slippage.FixedSlippage(spread=0.05))
    
    schedule_function(my_rebalance, date_rules.every_day(), time_rules.market_open(hours = 0, minutes = 1))
    
def my_rebalance(context, data):  

    update_indices(context, data)     
    last_vix = data.current('v', 'Close')
    last_vx1 = data.current('v1','Close')  
    last_vx2 = data.current('v2','Close')      
    last_vxv = data.current('vxv', 'Close')
    last_vix_200ma_ratio = data.current('v', '200ma Ratio')
        
    log.info("Vix %f" %last_vix)
    log.info("Vxv %f" %last_vxv)
    log.info("V1 %f" %last_vx1)
    log.info("V2 %f" %last_vx2)

               
    # Calculating the gap between spot vix and the first month vix future
    last_ratio_v_v1 = last_vix/last_vx1

    # Calculating the contango ratio of the front and second month VIX Futures 
    last_ratio_v1_v2 = last_vx1/last_vx2

    # Blending the previous two ratios together using a weighted average
    ratio_weight = 0.7
    last_ratio = (ratio_weight*last_ratio_v_v1) + ((1-ratio_weight)*last_ratio_v1_v2) - 1
    
    vix_vxv_ratio = last_vix/last_vxv
    
    # Retrieve SPY prices for technical indicators
    prices = data.history(context.spyg, 'open', 40, '1d')
    
    # Retrieve SPY MACD data
    macda, signal, hist = talib.MACD(prices, fastperiod=12,slowperiod=26,signalperiod=9)
    macd = macda[-1] - signal[-1]
    
    # Caluclate how much vix moved the previous day
    if (context.vix <> -1) : 
        vix_ratio = last_vix/context.vix -1
    else :
        vix_ratio = 0
    context.vix = last_vix
    
    xiv_history = data.history(context.xiv, 'price', 2, '1d')  
    
    xiv_ratio = xiv_history[1]/xiv_history[0] - 1
    
    # Setting thresholds
    threshold_vix_too_low = 10.76   # 0 
    threshold_vix_200ma_ratio_low = 0.8  # 0  Ratio of current price to 200 day moving average. Use to double check threshold_vix_too_low (case 0)
    threshold_xiv = -0.049          # 1
    threshold_vxv_xiv = 0.87        # 2 
    threshold_uvxy = 0.049          # 3
    threshold_macd = -0.55          # 3
    threshold_vxv_uvxy = 1.3        # 4 
    threshold_vix_high = 19.9       # 5 
    threshold_vc_low = -0.148       # 6
    threshold_vc_high = 0.046       # 8
    threshold_vc_high_2 = -0.06     # 8
    threshold_xiv_ratio = -0.053    # 10 
    threshold_uvxy_ratio = 0.08     # 11 
    
    # if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY witht he hope of a spike
    #     case = 0
    #     target_sid = context.uvxy
    #if last_vix < threshold_vix_too_low: # if VIX is too low, invest in UVXY with the hope of a spike
    if last_vix < threshold_vix_too_low and last_vix_200ma_ratio < threshold_vix_200ma_ratio_low :   
        case = 0
        target_sid = context.uvxy
        
    elif last_ratio < threshold_xiv: # if contango is high, invest in XIV to gain from decay
        case = 1
        target_sid = context.xiv
            
    elif vix_vxv_ratio < threshold_vxv_xiv: # if short term vol is low compared to mid term, invest in XIV to gain from decay
        case = 2
        target_sid = context.xiv

    elif last_ratio > threshold_uvxy and macd > threshold_macd: # if backwardation is high, invest in UVXY to gain from decay
        case = 3
        target_sid = context.uvxy

    elif vix_vxv_ratio > threshold_vxv_uvxy: # if short term vol is high compared to mid term, invest in UVXY to gain from growth
        case = 4
        target_sid = context.uvxy

    elif last_vix > threshold_vix_high: # if VIX is too high, invest in XIV expecting that VIX will drop
        case = 5
        target_sid = context.xiv

    elif vix_ratio < threshold_vc_low: # Vix down sharply, invest in XIV expecting that futures curve gets pulled down
        case = 6
        target_sid = context.xiv
            
    elif vix_ratio > threshold_vc_high: # Vix up sharply, invest in UVXY expecting that futures curve gets pulled up
        case = 7
        target_sid = context.uvxy

    elif vix_ratio > threshold_vc_high_2: #have to think
        case = 8
        target_sid = context.xiv

    else:
        case = 9
        target_sid = context.uvxy
        
    if (target_sid == context.xiv and xiv_ratio < threshold_xiv_ratio) : 
        # indicators say XIV but it just dropped overnight, so got for TQQQ
        case = 10
        target_sid = context.tqqq 
        
    elif (target_sid == context.uvxy and xiv_ratio > threshold_uvxy_ratio) :
        # indicators say UVXY but it just dropped overnight, so got for TQQQ
        case = 11
        target_sid = context.tqqq
        
    if target_sid == context.xiv :
        log.info("CASE XIV %d" %case)
    elif target_sid == context.uvxy :
        log.info("CASE UVXY %d" %case)
    else :
        log.info("CASE TQQQ %d" %case)
            
    if target_sid not in context.portfolio.positions :
        order_target_percent(context.tqqq,0)
        order_target_percent(context.uvxy,0)
        order_target_percent(context.xiv,0)
        order_target_percent(target_sid,1)
        
def update_indices(context, data):
    context.fetch_failed = False
    context.vix_vals = unpack_from_data(context, data, 'v')    
    context.vxv_vals = unpack_from_data(context, data, 'vxv')  
    context.vx1_vals = unpack_from_data(context, data, 'v1')
    context.vx2_vals = unpack_from_data(context, data, 'v2')

def fix_close(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%m/%d/%Y')))
    df = df.sort_values(by='Date', ascending=True)
    return df

def fix_closeVX(df,closeField):
    df = df.rename(columns={closeField:'Close'})
    # remove spurious asterisks
    df['Date'] = df['Date'].apply(lambda dt: re.sub('\*','',dt))
    # convert date column to timestamps
    df['Date'] = df['Date'].apply(lambda dt: pd.Timestamp(datetime.datetime.strptime(dt,'%Y-%m-%d')))
    df = df.sort_values(by='Date', ascending=True)
    return df


def subsequent_trading_date(date):
    tdays = tradingcalendar.trading_days
    last_date = pd.to_datetime(date)
    last_dt = tradingcalendar.canonicalize_datetime(last_date)
    next_dt = tdays[tdays.searchsorted(last_dt) + 1]
    return next_dt

def add_last_bar(df):
    last_date = df.index[-1]
    subsequent_date = subsequent_trading_date(last_date)
    blank_row = pd.Series({}, index=df.columns, name=subsequent_date)
    # add today, and shift all previous data up to today. This 
    # should result in the same data frames as in backtest
    df = df.append(blank_row).shift(1).dropna(how='all')
    return df

def shift_data(df):
    log.info("Pre-Shift")
    df = add_last_bar(df)
    df.fillna(method='ffill') 
    df['PrevCloses'] = my_rolling_apply_series(df['Close'], to_csv_str, History)
    dates = pd.Series(df.index)
    dates.index = df.index
    df['PrevDates'] = my_rolling_apply_series(dates, to_csv_str, History)
    return df

def unpack_from_data(context, data, sym):
    try:
        v = data.current(sym, 'PrevCloses')
        i = data.current(sym, 'PrevDates')
        return from_csv_strs(i,v,True).apply(float)
    except:
        log.warn("Unable to unpack historical {s} data.".format(s=sym))
        context.fetch_failed = True

def addFieldsVIX(df):
    log.info("VIX: Pre-Massage")
    df = fix_close(df,'VIX Close')
    df['200ma'] = df['Close'].rolling(200).mean()
    df['200ma Ratio'] = df['Close'] / df['200ma']
    log.info("VIX: Post-Massage")
    return df

def addFieldsVXV(df):
    log.info("VXV: Pre-Massage")
    df.rename(columns={'Unnamed: 0': 'Date'}, inplace=True)
    df = fix_close(df,'CLOSE')
    log.info("VXV: Post-Massage")
    return df

def addFieldsVX1(df):
    log.info("VX1: Pre-Massage")
    df = fix_closeVX(df,'F1')
    log.info("VX1: Post-Massage")
    return df

def addFieldsVX2(df):
    log.info("VX2: Pre-Massage")
    df = fix_closeVX(df,'F2')
    log.info("VX2: Post-Massage")
    return df

# convert a series of values to a comma-separated string of said values
def to_csv_str(s):
    return functools.reduce(lambda x,y: x+','+y, pd.Series(s).apply(str))

# a specific instance of rolling apply, for Series of any type (not just numeric,
# ala pandas.rolling_apply), where the index of the series is set to the indices
# of the last elements of each subset
def my_rolling_apply_series(s_in, f, n):
    s_out = pd.Series([f(s_in[i:i+n]) for i in range(0,len(s_in)-(n-1))]) 
    s_out.index = s_in.index[n-1:]
    return s_out

# reconstitutes a Series from two csv-encoded strings, one of the index, one of the values
def from_csv_strs(x, y, idx_is_date):
    s = pd.Series(y.split(','),index=x.split(','))
    if (idx_is_date):
        s.index = s.index.map(lambda x: pd.Timestamp(x))
    return s
            
There was a runtime error.

I agree that it's a volatile algo (no pun). I don't think I'd trade it live except with leverage at 0.6 or lower which reduces the drawdown.

By the way, the mod I added only affects the last few months. It has no effect in 2013.

Stephen,
Thanks for pointing that out.
I didn't know the mod doesn't work in 2013. Why was that? Because it was already over 200 trading days after XIV release. Actually the 200 moving average was applied on VIX, which was available long time before.

The first condition in Case 0, as written by Macro Investor, requires that VIX is below 10.76. My mod added another VIX criterion to that. However, the first condition was never met in 2013 since VIX stayed above 10.76, and my mod wasn't a factor.

Thank you Macro and all for these great algos!!

I have been live trading at IB the one with the 200sma suggested by Stephen for a couple of weeks.

My concern is regarding the fixed threshold (10.76) of case 0. The VIX has never been this low, and at this levels maybe it's better to try the algo without the case 0 at all. I have backtested it without this case, and it still yields an awesome performance from 2012 till now. And of course last weeks perform much better without case 0. If VIX continues to be at this levels, holding UVXY will not be a good thing...

Case 0 was reached a few times in the past (in my backtests), and those cases worked well also without the case 0.

What do you think?

Ron,

Case 0 is not working well in current market. How are we sure the other cases would still work? They are not well tested in this low volatility environment.

How was your backtest performance in 2017? YTD of xiv is 100%.

Hi Zheng,

The performance YTD with case 0 is 95.7%, compared with the performance without case 0 which is 60.4%.

So far so good. The problem started when the VIX began to be at the levels of the last weeks (something that has never happened in the backtest).

Since I am live trading with IB (starting 7/13), I've been 11% up, but then I got in case 0, being now up 3.2%. Without the case 0, at this levels of VIX, I would have been 11.8% up.

I think there is a good chance for the VIX to keep going down in this environment. In this case, it will be better to remove case 0, or substitute it for another mean reversion indicator (not hardcoded).

Any suggestions?

Any suggestions?

Don't trade any algorithm with hardcoded buy/sell levels. It's only a matter of time until it blows up your account

^ Luke is right in general. You can keep modifying the thresholds to give better performance but really its the performance based on the period you are testing on. The market is dynamic so you need dynamic thresholds.

The only case that uses a fixed price level is case 0 which I attempted to make dynamic/adaptive by combining it with an r200MA threshold. All the other cases use ratios compared to fixed thresholds, making them somewhat adaptive since the ratio is the relationship of the prices rather than the prices themselves. I agree that in theory dynamic thresholds could be more robust but testing would be required to confirm that in practice.

When making changes, especially ones that add to the complexity of the algorithm, I always worry about unintentionally overfitting. Having said that I think it's certainly worthwhile to experiment with.

Dear Stephen,

Thank you for your answer an for your contributions to the algo.

My point is that the theory behind this algo is the contango and backwardation effect. There is no fundament (in my opinion) for a different behave when vix is “too low” or “too high”. This is a mean reversion assumption, that was fitted with the backtesting, setting the hardcoded thresholds. I agree that your r200MA makes it somewhat adapttive, but its still tied to the hardcoded vix too low. Adding to that, we do not have data to backtest the VIX at the actual levels, nor below 9 or even less.

There is also a negative effect of holding a leveraged ETF when it is volatile and goes up and down.

I feel more confortable taking away this improvement from the algo. Backtesting the algo without the vix too low or too high cases, also delivers high performance (with a big DD too).

I will try it live, without this limit cases, with a small amount of my trading capital (5% to 10%).

@Stephen,

I am shutting down the server for noko.csv today due to Quantopian's upcoming end of support for live trading. Use the futures calendar for VX1 and VX2 futures prices. Here is a code sample:

def initialize(context):  

    context.vx1 = continuous_future('VX', offset=0, roll='calendar', adjustment=None)  
    context.vx2 = continuous_future('VX', offset=1, roll='calendar', adjustment=None)  
    context.vx3 = continuous_future('VX', offset=2, roll='calendar', adjustment=None)

def ordering_logic(context, data):  
    # Calculating the contango ratio of the front and second month VIX Futures settlements  
    v1_close = data.history(  
        assets=context.vx1,  
        fields='price',  
        bar_count=360,  
        frequency='1m'  
        )[175]  
    v2_close = data.history(  
        assets=context.vx2,  
        fields='price',  
        bar_count=360,  
        frequency='1m'  
        )[175]  
    v3_close = data.history(  
        assets=context.vx3,  
        fields='price',  
        bar_count=360,  
        frequency='1m'  
        )[175]

    schedule_function(ordering_logic,  
                      date_rule=date_rules.every_day(),  
                      time_rule=time_rules.market_close(minutes=180),  
                      half_days=True,  
                      calendar=calendars.US_EQUITIES)

Thanks Honver. The code you provided is very helpful although I notice that the VX futures values returned from Quantopian don't quite match the data from vixcentral.com. For example the Quantopian data might have 12.6 while vixcentral.com has 12.625. I suspect it may have something to do with the difference between the futures and equities calendars?

The difference is due to what time the data is being sampled from. Vixcentral uses the last close, but the Quantopian data uses the bar that is specified from the combination of the frequency and [barsAgo] parameters.

I see... that makes sense.

My algo is based on daily data, using the previous day's VX futures close:

v1_close = data.history(  
    assets=context.vx1,  
    fields='close',  
    bar_count=2,  
    frequency='1d'  
    )[0]

And that's the VX1 close that I'm finding differs slightly from the vixcentral data.

My backtest has the US Futures calendar selected (apparently required in order to access the futures data) but I'm trading equities. So that my trades happen during equities market hours I'm using calendar=calendars.US_EQUITIES as a parameter for schedule_function().

Can someone post an updated version of this model using a different security then XIV since it is no longer listed?

There was a runtime error.
Exception:Problem reaching http://52.15.233.150/noko.csv
Line: 28 ininitialize
post_func=shift_data)