Back to Community
RSI percent rebalancing strategy
Clone Algorithm
156
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 numpy as np
import collections

rsiPeriods   = 14
rsiIndicator = ta.RSI(timeperiod = rsiPeriods)

def initialize(context):
    context.stocks = [sid(4922), sid(62), sid(43694), sid(8580), sid(25555),\
                      sid(20680), sid(328),   sid(14328), sid(368),   sid(16841),\
                      sid(9883),  sid(337),   sid(38650), sid(739),   sid(27533),\
                      sid(3806),  sid(18529), sid(1209),  sid(1406), sid(1419),\
                      sid(15101), sid(17632), sid(39095), sid(1637),  sid(1900),\
                      sid(32301), sid(18870), sid(14014), sid(25317), sid(36930),\
                      sid(12652), sid(26111), sid(24819), sid(24482), sid(2618),\
                      sid(2663),  sid(27543), sid(1787) , sid(2696),  sid(42950),\
                      sid(20208), sid(2853),  sid(8816),  sid(12213),  sid(3212),\
                      sid(9736),  sid(23906), sid(26578), sid(22316), sid(13862)]
    
    context.dayCount = 0

    set_commission(commission.PerTrade(cost=1.0))
    set_slippage(TradeAtTheOpenSlippageModel(.1))

def handle_data(context, data):
    context.dayCount += 1
    if (context.dayCount < rsiPeriods or context.dayCount % 10 != 0):
        return
    
    ranked = {}
    rsiValues = rsiIndicator(data)
    rsiValuesMean = np.mean(rsiValues)
    if (rsiValuesMean == None or np.isnan(rsiValuesMean)):
        return
    rsiValuesStd  = np.std(rsiValues)
    record(RSIMean = rsiValuesMean)
    
    for stock in context.stocks:
        if stock not in data:
            continue
        rsi = rsiValues[stock]
        rsiZScore = (rsi - rsiValuesMean) / rsiValuesStd
        ranked[stock] = rsiZScore

    # dictionary sorted by value
    ranked = collections.OrderedDict(sorted(ranked.items(), key=lambda t: t[1]))
    
    # Figure out where the long bias percent should end
    longBiasPct = rsiValuesMean / 100.0
    tradeCount  = len(ranked)
    rsiSplit = longBiasPct * tradeCount
    shortRsiSplit = tradeCount - rsiSplit
    
    record(LongBias = longBiasPct, RsiSplit = rsiSplit, ShortRsiSplit = shortRsiSplit)
    longTotalWeight = (rsiSplit * (rsiSplit + 1)) / 2
    shortTotalWeight = (shortRsiSplit * (shortRsiSplit + 1)) / 2
    totalLongFraction = 0
    totalShortFraction = 0
    l = 0
    s = 0
    
    # Now buy long those up ranked RSI securities
    # and sell short those down ranked RSI securities
    # Split at the boundary of RSI percent
    for i in range(0, tradeCount):
        stock = ranked.keys()[i]
        if stock not in data:
            continue
            
        if (i <= rsiSplit):
            longWeight = rsiSplit - l
            longFraction = (longWeight / longTotalWeight) * longBiasPct
            totalLongFraction += longFraction
            order_target_percent(stock, longFraction)
            #print(" Long Symbol: {0} %: {1}".format(stock.symbol, longFraction))
            l += 1
        else:
            shortWeight = shortRsiSplit - s
            shortFraction = -(shortWeight / shortTotalWeight) * (1.0 - longBiasPct)
            totalShortFraction += shortFraction
            order_target_percent(stock, shortFraction)
            #print("Short symbol: {0} %: {1}".format(stock.symbol, shortFraction))
            s += 1

    #print("long %: {0}  short %: {1}".format(totalLongFraction, totalShortFraction))
    
########################################################    
class TradeAtTheOpenSlippageModel(slippage.SlippageModel):
    def __init__(self, fractionOfOpenCloseRange):
        self.fractionOfOpenCloseRange = fractionOfOpenCloseRange

    def process_order(self, trade_bar, order):
        openPrice = trade_bar.open_price
        closePrice = trade_bar.price
        ocRange = closePrice - openPrice
        ocRange = ocRange * self.fractionOfOpenCloseRange
        if (ocRange != 0.0):
            targetExecutionPrice = openPrice + ocRange
        else:
            targetExecutionPrice = openPrice
            
        # Create the transaction using the new price we've calculated.
        return slippage.create_transaction(
            trade_bar,
            order,
            targetExecutionPrice,
            order.amount
        )
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
14 responses

its strange, I clone your strategy but my backtest result not same as your

Clone Algorithm
2
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 numpy as np
import collections

rsiPeriods       = 10
rsiMAPeriods     = 5
rebalancePeriods = 15

rsiIndicator = ta.RSI(timeperiod = rsiPeriods)

def initialize(context):
    context.stocks = [sid(4922), sid(62), sid(43694), sid(8580), sid(25555),\
                      sid(20680), sid(328),   sid(14328), sid(368),   sid(16841),\
                      sid(9883),  sid(337),   sid(38650), sid(739),   sid(27533),\
                      sid(3806),  sid(18529), sid(1209),  sid(1406), sid(1419),\
                      sid(15101), sid(17632), sid(39095), sid(1637),  sid(1900),\
                      sid(32301), sid(18870), sid(14014), sid(25317), sid(36930),\
                      sid(12652), sid(26111), sid(24819), sid(24482), sid(2618),\
                      sid(2663),  sid(27543), sid(1787) , sid(2696),  sid(42950),\
                      sid(20208), sid(2853),  sid(8816),  sid(12213),  sid(3212),\
                      sid(9736),  sid(23906), sid(26578), sid(22316), sid(13862)]
    
    context.dayCount = 0
    context.RollingRSIValues = collections.deque(maxlen = rsiMAPeriods)
    set_commission(commission.PerTrade(cost=1.0))
    set_slippage(TradeAtTheOpenSlippageModel(.1))

def handle_data(context, data):
    context.dayCount += 1
    if (context.dayCount < rsiPeriods + rsiMAPeriods or context.dayCount % rebalancePeriods != 0):
        return
    
    ranked = {}
    rsiValues = rsiIndicator(data)
    rsiValuesMean = np.mean(rsiValues)
    if (rsiValuesMean == None or np.isnan(rsiValuesMean)):
        return
    context.RollingRSIValues.append(rsiValuesMean)
    rsiMeanMean = np.mean(np.array(context.RollingRSIValues))
    rsiValuesStd  = np.std(rsiValues)
    record(RSIMean = rsiValuesMean, RSIMeanAvg = rsiMeanMean)

    for stock in context.stocks:
        if stock not in data:
            continue
        rsi = rsiValues[stock]
        rsiZScore = (rsi - rsiValuesMean) / rsiValuesStd
        ranked[stock] = rsiZScore

    # dictionary sorted by value
    ranked = collections.OrderedDict(sorted(ranked.items(), key=lambda t: t[1]))
    
    # Figure out where the long bias percent should end
    rsiMeanAdjusted = rsiMeanMean #rsiValuesMean + (rsiValuesMean - rsiMeanMean)
    longBiasPct = rsiMeanAdjusted / 100.0
    
    tradeCount  = len(ranked)
    longRsiSplit = longBiasPct * tradeCount
    shortRsiSplit = tradeCount - longRsiSplit
    
    ### Try weighting heavier to the top and bottom
    longRsiSplit /= 2
    shortRsiSplit /= 2    
    
    record(LongBias = longBiasPct, RsiSplit = longRsiSplit, ShortRsiSplit = shortRsiSplit)
    longTotalWeight = (longRsiSplit * (longRsiSplit + 1)) / 2
    shortTotalWeight = (shortRsiSplit * (shortRsiSplit + 1)) / 2
    totalLongFraction = 0
    totalShortFraction = 0
    l = 0
    s = 0
    
    # Now buy long those up ranked RSI securities
    # and sell short those down ranked RSI securities
    # Split at the boundary of RSI percent
    for i in range(0, tradeCount):
        stock = ranked.keys()[i]
        if stock not in data:
            continue
            
        if (i > longRsiSplit and i <= longRsiSplit * 2):
            longWeight = longRsiSplit - l
            longFraction = (longWeight / longTotalWeight) * longBiasPct
            totalLongFraction += longFraction
            order_target_percent(stock, longFraction)
            #print(" Long Symbol: {0} %: {1}".format(stock.symbol, longFraction))
            #print("  Long i:{0} fraction: {1}".format(i, longFraction))
            l += 1
        elif(i >= tradeCount - (shortRsiSplit * 2) and i < tradeCount - shortRsiSplit):
            shortWeight = shortRsiSplit - s
            shortFraction = -(shortWeight / shortTotalWeight) * (1.0 - longBiasPct)
            totalShortFraction += shortFraction
            order_target_percent(stock, shortFraction)
            #print("Short symbol: {0} %: {1}".format(stock.symbol, shortFraction))
            #print(" Short i:{0} fraction: {1}".format(i, shortFraction))
            s += 1

    #print("long %: {0}  short %: {1}".format(totalLongFraction, totalShortFraction))
    
########################################################    
class TradeAtTheOpenSlippageModel(slippage.SlippageModel):
    def __init__(self, fractionOfOpenCloseRange):
        self.fractionOfOpenCloseRange = fractionOfOpenCloseRange

    def process_order(self, trade_bar, order):
        openPrice = trade_bar.open_price
        closePrice = trade_bar.price
        ocRange = closePrice - openPrice
        ocRange = ocRange * self.fractionOfOpenCloseRange
        if (ocRange != 0.0):
            targetExecutionPrice = openPrice + ocRange
        else:
            targetExecutionPrice = openPrice
            
        # Create the transaction using the new price we've calculated.
        return slippage.create_transaction(
            trade_bar,
            order,
            targetExecutionPrice,
            order.amount
        )
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Works here.

P.

Clone Algorithm
35
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 numpy as np
import collections

rsiPeriods       = 10
rsiMAPeriods     = 5
rebalancePeriods = 15

rsiIndicator = ta.RSI(timeperiod = rsiPeriods)

def initialize(context):
    context.stocks = [sid(4922), sid(62), sid(43694), sid(8580), sid(25555),\
                      sid(20680), sid(328),   sid(14328), sid(368),   sid(16841),\
                      sid(9883),  sid(337),   sid(38650), sid(739),   sid(27533),\
                      sid(3806),  sid(18529), sid(1209),  sid(1406), sid(1419),\
                      sid(15101), sid(17632), sid(39095), sid(1637),  sid(1900),\
                      sid(32301), sid(18870), sid(14014), sid(25317), sid(36930),\
                      sid(12652), sid(26111), sid(24819), sid(24482), sid(2618),\
                      sid(2663),  sid(27543), sid(1787) , sid(2696),  sid(42950),\
                      sid(20208), sid(2853),  sid(8816),  sid(12213),  sid(3212),\
                      sid(9736),  sid(23906), sid(26578), sid(22316), sid(13862)]
    
    context.dayCount = 0
    context.RollingRSIValues = collections.deque(maxlen = rsiMAPeriods)
    set_commission(commission.PerTrade(cost=1.0))
    set_slippage(TradeAtTheOpenSlippageModel(.1))

def handle_data(context, data):
    context.dayCount += 1
    if (context.dayCount < rsiPeriods + rsiMAPeriods or context.dayCount % rebalancePeriods != 0):
        return
    
    ranked = {}
    rsiValues = rsiIndicator(data)
    rsiValuesMean = np.mean(rsiValues)
    if (rsiValuesMean == None or np.isnan(rsiValuesMean)):
        return
    context.RollingRSIValues.append(rsiValuesMean)
    rsiMeanMean = np.mean(np.array(context.RollingRSIValues))
    rsiValuesStd  = np.std(rsiValues)
    record(RSIMean = rsiValuesMean, RSIMeanAvg = rsiMeanMean)

    for stock in context.stocks:
        if stock not in data:
            continue
        rsi = rsiValues[stock]
        rsiZScore = (rsi - rsiValuesMean) / rsiValuesStd
        ranked[stock] = rsiZScore

    # dictionary sorted by value
    ranked = collections.OrderedDict(sorted(ranked.items(), key=lambda t: t[1]))
    
    # Figure out where the long bias percent should end
    rsiMeanAdjusted = rsiMeanMean #rsiValuesMean + (rsiValuesMean - rsiMeanMean)
    longBiasPct = rsiMeanAdjusted / 100.0
    
    tradeCount  = len(ranked)
    longRsiSplit = longBiasPct * tradeCount
    shortRsiSplit = tradeCount - longRsiSplit
    
    ### Try weighting heavier to the top and bottom
    longRsiSplit /= 2
    shortRsiSplit /= 2    
    
    record(LongBias = longBiasPct, RsiSplit = longRsiSplit, ShortRsiSplit = shortRsiSplit)
    longTotalWeight = (longRsiSplit * (longRsiSplit + 1)) / 2
    shortTotalWeight = (shortRsiSplit * (shortRsiSplit + 1)) / 2
    totalLongFraction = 0
    totalShortFraction = 0
    l = 0
    s = 0
    
    # Now buy long those up ranked RSI securities
    # and sell short those down ranked RSI securities
    # Split at the boundary of RSI percent
    for i in range(0, tradeCount):
        stock = ranked.keys()[i]
        if stock not in data:
            continue
            
        if (i > longRsiSplit and i <= longRsiSplit * 2):
            longWeight = longRsiSplit - l
            longFraction = (longWeight / longTotalWeight) * longBiasPct
            totalLongFraction += longFraction
            order_target_percent(stock, longFraction)
            #print(" Long Symbol: {0} %: {1}".format(stock.symbol, longFraction))
            #print("  Long i:{0} fraction: {1}".format(i, longFraction))
            l += 1
        elif(i >= tradeCount - (shortRsiSplit * 2) and i < tradeCount - shortRsiSplit):
            shortWeight = shortRsiSplit - s
            shortFraction = -(shortWeight / shortTotalWeight) * (1.0 - longBiasPct)
            totalShortFraction += shortFraction
            order_target_percent(stock, shortFraction)
            #print("Short symbol: {0} %: {1}".format(stock.symbol, shortFraction))
            #print(" Short i:{0} fraction: {1}".format(i, shortFraction))
            s += 1

    #print("long %: {0}  short %: {1}".format(totalLongFraction, totalShortFraction))
    
########################################################    
class TradeAtTheOpenSlippageModel(slippage.SlippageModel):
    def __init__(self, fractionOfOpenCloseRange):
        self.fractionOfOpenCloseRange = fractionOfOpenCloseRange

    def process_order(self, trade_bar, order):
        openPrice = trade_bar.open_price
        closePrice = trade_bar.price
        ocRange = closePrice - openPrice
        ocRange = ocRange * self.fractionOfOpenCloseRange
        if (ocRange != 0.0):
            targetExecutionPrice = openPrice + ocRange
        else:
            targetExecutionPrice = openPrice
            
        # Create the transaction using the new price we've calculated.
        return slippage.create_transaction(
            trade_bar,
            order,
            targetExecutionPrice,
            order.amount
        )
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Its so stranged, What was wrong for me,I just did Clone Algorithm and then Click "Run Full Backtest",
Why I cannot reproduce the same result!

Strangely, starting the backtest on 2002-01-03 causes the big difference

P

Clone Algorithm
35
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 numpy as np
import collections

rsiPeriods       = 10
rsiMAPeriods     = 5
rebalancePeriods = 15

rsiIndicator = ta.RSI(timeperiod = rsiPeriods)

def initialize(context):
    context.stocks = [sid(4922), sid(62), sid(43694), sid(8580), sid(25555),\
                      sid(20680), sid(328),   sid(14328), sid(368),   sid(16841),\
                      sid(9883),  sid(337),   sid(38650), sid(739),   sid(27533),\
                      sid(3806),  sid(18529), sid(1209),  sid(1406), sid(1419),\
                      sid(15101), sid(17632), sid(39095), sid(1637),  sid(1900),\
                      sid(32301), sid(18870), sid(14014), sid(25317), sid(36930),\
                      sid(12652), sid(26111), sid(24819), sid(24482), sid(2618),\
                      sid(2663),  sid(27543), sid(1787) , sid(2696),  sid(42950),\
                      sid(20208), sid(2853),  sid(8816),  sid(12213),  sid(3212),\
                      sid(9736),  sid(23906), sid(26578), sid(22316), sid(13862)]
    
    context.dayCount = 0
    context.RollingRSIValues = collections.deque(maxlen = rsiMAPeriods)
    set_commission(commission.PerTrade(cost=1.0))
    set_slippage(TradeAtTheOpenSlippageModel(.1))

def handle_data(context, data):
    context.dayCount += 1
    if (context.dayCount < rsiPeriods + rsiMAPeriods or context.dayCount % rebalancePeriods != 0):
        return
    
    ranked = {}
    rsiValues = rsiIndicator(data)
    rsiValuesMean = np.mean(rsiValues)
    if (rsiValuesMean == None or np.isnan(rsiValuesMean)):
        return
    context.RollingRSIValues.append(rsiValuesMean)
    rsiMeanMean = np.mean(np.array(context.RollingRSIValues))
    rsiValuesStd  = np.std(rsiValues)
    record(RSIMean = rsiValuesMean, RSIMeanAvg = rsiMeanMean)

    for stock in context.stocks:
        if stock not in data:
            continue
        rsi = rsiValues[stock]
        rsiZScore = (rsi - rsiValuesMean) / rsiValuesStd
        ranked[stock] = rsiZScore

    # dictionary sorted by value
    ranked = collections.OrderedDict(sorted(ranked.items(), key=lambda t: t[1]))
    
    # Figure out where the long bias percent should end
    rsiMeanAdjusted = rsiMeanMean #rsiValuesMean + (rsiValuesMean - rsiMeanMean)
    longBiasPct = rsiMeanAdjusted / 100.0
    
    tradeCount  = len(ranked)
    longRsiSplit = longBiasPct * tradeCount
    shortRsiSplit = tradeCount - longRsiSplit
    
    ### Try weighting heavier to the top and bottom
    longRsiSplit /= 2
    shortRsiSplit /= 2    
    
    record(LongBias = longBiasPct, RsiSplit = longRsiSplit, ShortRsiSplit = shortRsiSplit)
    longTotalWeight = (longRsiSplit * (longRsiSplit + 1)) / 2
    shortTotalWeight = (shortRsiSplit * (shortRsiSplit + 1)) / 2
    totalLongFraction = 0
    totalShortFraction = 0
    l = 0
    s = 0
    
    # Now buy long those up ranked RSI securities
    # and sell short those down ranked RSI securities
    # Split at the boundary of RSI percent
    for i in range(0, tradeCount):
        stock = ranked.keys()[i]
        if stock not in data:
            continue
            
        if (i > longRsiSplit and i <= longRsiSplit * 2):
            longWeight = longRsiSplit - l
            longFraction = (longWeight / longTotalWeight) * longBiasPct
            totalLongFraction += longFraction
            order_target_percent(stock, longFraction)
            #print(" Long Symbol: {0} %: {1}".format(stock.symbol, longFraction))
            #print("  Long i:{0} fraction: {1}".format(i, longFraction))
            l += 1
        elif(i >= tradeCount - (shortRsiSplit * 2) and i < tradeCount - shortRsiSplit):
            shortWeight = shortRsiSplit - s
            shortFraction = -(shortWeight / shortTotalWeight) * (1.0 - longBiasPct)
            totalShortFraction += shortFraction
            order_target_percent(stock, shortFraction)
            #print("Short symbol: {0} %: {1}".format(stock.symbol, shortFraction))
            #print(" Short i:{0} fraction: {1}".format(i, shortFraction))
            s += 1

    #print("long %: {0}  short %: {1}".format(totalLongFraction, totalShortFraction))
    
########################################################    
class TradeAtTheOpenSlippageModel(slippage.SlippageModel):
    def __init__(self, fractionOfOpenCloseRange):
        self.fractionOfOpenCloseRange = fractionOfOpenCloseRange

    def process_order(self, trade_bar, order):
        openPrice = trade_bar.open_price
        closePrice = trade_bar.price
        ocRange = closePrice - openPrice
        ocRange = ocRange * self.fractionOfOpenCloseRange
        if (ocRange != 0.0):
            targetExecutionPrice = openPrice + ocRange
        else:
            targetExecutionPrice = openPrice
            
        # Create the transaction using the new price we've calculated.
        return slippage.create_transaction(
            trade_bar,
            order,
            targetExecutionPrice,
            order.amount
        )
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

In my environment , I cannot set the backtest start date from 2002-01-04, after I manual input or click from calendar at 2002-01-04 then it always be set back to 2002-01-03.

Interesting. I have 'felt' this issue before. My time zone is usually EST+5 and sometimes I can't run a backtest to the same end date as a US user i.e. later today they will run a backtest ending 02/19/2014 but I will still be limited to 02/18/2014 for a while. You seem to be seeing the problem at the other end. What is your time zone?

@Dan: has this come up before? I've never mentioned it before.

@AM: Why such a huge difference with a one day shift in start date?

P.

My time zone on GMT+8.

Hi Anony,

I added an additional function (capital_invested) and some plotting--perhaps useful.

Grant

Clone Algorithm
67
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 numpy as np
import collections

rsiPeriods       = 63
rsiMAPeriods     = 21
rsiIndicator = ta.RSI(timeperiod = rsiPeriods)

def initialize(context):
    ### Bottom half of NQ100
    #context.stocks=[sid(3951),sid(8655),sid(25339),sid(4246),sid(43405),sid(27357),sid(32046),sid(4485),sid(43919),sid(4668),sid(8677),sid(22802),sid(3450),sid(5061),sid(5121),sid(5149),sid(5166),sid(23709),sid(13905),sid(19926),sid(19725),sid(8857),sid(5767),sid(5787),sid(19917),sid(6295),sid(6413),sid(6546),sid(20281),sid(6683),sid(26169),sid(6872),sid(11901),sid(13940),sid(7061),sid(15581),sid(24518),sid(7272),sid(39840),sid(7671),sid(27872),sid(8017),sid(38817),sid(8045),sid(8132),sid(8158),sid(24124),sid(8344),sid(8352)]
    ### Correlated Group
    #context.stocks=[sid(8572),sid(27676),sid(1620),sid(28016),sid(2079),sid(2855),sid(32715),sid(32611),sid(3676),sid(4117),sid(4453),sid(35036),sid(12691),sid(36346),sid(4589),sid(32146),sid(12350),sid(4849),sid(4914),sid(4974),sid(27026),sid(5328),sid(5387),sid(5442),sid(5520),sid(5634),sid(5651),sid(5822),sid(23151),sid(5956),sid(6030),sid(6116),sid(6583),sid(24873),sid(7457),sid(20940),sid(7949),sid(18221),sid(32393),sid(8329)]
    ### Negative Correlated Group
    context.stocks=[sid(262),sid(328),sid(14328),sid(693),sid(700),sid(754),sid(1745),sid(18870),sid(2079),sid(25317),sid(16348),sid(15789),sid(2263),sid(2427),sid(27543),sid(20208),sid(3455),sid(3642),sid(3735),sid(4010),sid(8831),sid(4117),sid(4315),sid(12691),sid(4849),sid(3450),sid(4954),sid(4974),sid(27026),sid(5520),sid(5651),sid(19917),sid(5822),sid(6295),sid(6976),sid(7254),sid(357),sid(7990),sid(8132),sid(14848)]
   
    context.dayCount = 0
    context.PandL = 0
    context.RollingRSIValues = collections.deque(maxlen = rsiMAPeriods)
    context.priorRollingRSIValue = 0
    
    set_commission(commission.PerTrade(cost=1.0))
    set_slippage(TradeAtTheOpenSlippageModel(.1))

def handle_data(context, data):
    
    ##############################################################
    # plot capital invested (e.g. how much capital is invested)
    capital = capital_invested(context, data)
    record(capital_invested = capital)

    # plot cash
    cash = context.portfolio.cash
    record(cash = cash)
    
    # plot positions value
    positions_value = context.portfolio.positions_value
    record(positions_value = positions_value)
    ##############################################################  
    
    context.dayCount += 1
    if (context.dayCount < rsiPeriods + rsiMAPeriods):
        return
   
    if (context.portfolio.pnl < context.PandL):
        context.PandL = context.portfolio.pnl
        return
    context.PandL = context.portfolio.pnl
    record(PnL = context.PandL)
    
    ranked = {}
    banked = {}
    rsiValues = rsiIndicator(data)
    rsiMean = np.mean(rsiValues)
    context.RollingRSIValues.append(rsiMean)
    rsiMeanMean = np.mean(np.array(context.RollingRSIValues))
    # record(rsiMean=rsiMean, rsiMeanMean=rsiMeanMean)
    
    for stock in context.stocks:
        if stock not in data:
            continue
        rsi = rsiValues[stock]
        if (np.isnan(rsi)):
            continue
        if (rsiMeanMean > context.priorRollingRSIValue):
            if (rsi > 30):
                ranked[stock] = rsi
            else:
                banked[stock] = rsi
        else:
            if (rsi > 50):
                ranked[stock] = rsi
            else:
                banked[stock] = rsi
                
    context.priorRollingRSIValue = rsiMeanMean
    
    # dictionary sorted by value
    ranked = collections.OrderedDict(sorted(ranked.items(), key=lambda t: t[1]))
    banked = collections.OrderedDict(sorted(banked.items(), key=lambda t: t[1]))    
    # Inverted banked
    if (len(banked) > 0):
        items = banked.items()
        items.reverse()
        banked = collections.OrderedDict(items)
        
    longTradeCount  = len(ranked)
    longTradeCountWeight = (longTradeCount * (longTradeCount + 1)) / 2
    longWeightFraction = float(longTradeCount) / float(len(context.stocks))
    shortTradeCount  = len(banked)
    shortTradeCountWeight = (shortTradeCount * (shortTradeCount + 1)) / 2
    shortWeightFraction = float(shortTradeCount) / float(len(context.stocks))

    ### Long
    for i in range(0, longTradeCount):
        stock = ranked.keys()[i]
        rsi =  ranked.values()[i]
        longFraction = float(longTradeCount - i) / float(longTradeCountWeight)
        order_target_percent(stock, longFraction * longWeightFraction)
    
    ### Short
    for i in range(0, shortTradeCount):
        stock = banked.keys()[i]
        rsi =  banked.values()[i]
        shortFraction = float(shortTradeCount - i) / float(shortTradeCountWeight)
        order_target_percent(stock, -shortFraction * shortWeightFraction)

########################################################    
class TradeAtTheOpenSlippageModel(slippage.SlippageModel):
    def __init__(self, fractionOfOpenCloseRange):
        self.fractionOfOpenCloseRange = fractionOfOpenCloseRange

    def process_order(self, trade_bar, order):
        openPrice = trade_bar.open_price
        closePrice = trade_bar.price
        ocRange = closePrice - openPrice
        ocRange = ocRange * self.fractionOfOpenCloseRange
        if (ocRange != 0.0):
            targetExecutionPrice = openPrice + ocRange
        else:
            targetExecutionPrice = openPrice
            
        # Create the transaction using the new price we've calculated.
        return slippage.create_transaction(
            trade_bar,
            order,
            targetExecutionPrice,
            order.amount
        )
    
##############################################################
    
def capital_invested(context, data):

    # get a sum total of capital spent or borrowed for all current positions
    capital = 0.0  # initialize to zero

    # check every stock in current positions (also works with set_universe)
    for stock in context.portfolio.positions:
        
        # get amount of shares in current position for this stock
        amount = context.portfolio.positions[stock].amount
        
        # get the cost basis of the shares (how much we spent on average per share)
        cost_basis = context.portfolio.positions[stock].cost_basis

        # check if position is a short trade (negative amount)
        amount = max(amount, -amount) # change amount to a positive number
                
        # add dollar amount to the 'spent' total
        capital += amount * cost_basis
    
    # return amount of capital tied up in positions
    return capital
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

I was curious why changing the backtest date by one day, from Jan 3 to Jan 4 2002, changed the algorithm results from 200% to 600% (!). So I started digging around.

I began to debug by limiting the backtest to the year 2002 and discovered the following:

  • The algorithm begins to trade in August 2002 (see the custom charts and logs)
  • It trades on a 15-day interval cycle. This set of 15 days changes if you start the backtest on Jan 3 or Jan 4 ( see logs)
  • Based on the comment above, it's sensitive to the start date
  • I noticed interesting activity in November 2002, so decided to investingate further

    • The stock driving the November portfolio returns is ALXN
    • I investigated the entry and exit points for ALXN depending on the start dates. Based on the cycle mentioned above, it took different positions per date.
      • Jan 3 backtest [ALXN details]: on 11/21/2002 BUY 22556 shares and on 12/15/2002 SELL -32003 shares -> Take a long position
      • Jan 4 backtest [ALXN details]: on 11/24/2002 SELL -17320 shares and on 12/16/2002 BUY 7423 shares -> Take a short position
      • ok, so now we found the major points of discrepancies!
  • Since this algo is based on RSI, I looked at the RSI behavior for ALXN. The RSI is different between the runs, as you can see on the custom graphs

    • Implication: RSI for ALXN is radically different depending on the day you start the backtest and which periods you cycle through. Important to note that RSI is a path dependent indicator. This is compounded as the backtest continues to run to 2014.

Feel free to clone the attached backtest and compare the runs on Jan 3 and Jan 4. Note that the custom graph only allows you to record 5 variables at a time, but you can uncomment any of my lines to track the behavior. To see more details on the custom graph, you can click on the variable name to add/remove it from the chart.

Clone Algorithm
5
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 numpy as np
import collections
import pandas as pd

rsiPeriods       = 10
rsiMAPeriods     = 5
rebalancePeriods = 15

rsiIndicator = ta.RSI(timeperiod = rsiPeriods)

def initialize(context):
    context.stocks = [sid(4922), sid(62), sid(43694), sid(8580), sid(25555),\
                      sid(20680), sid(328),   sid(14328), sid(368),   sid(16841),\
                      sid(9883),  sid(337),   sid(38650), sid(739),   sid(27533),\
                      sid(3806),  sid(18529), sid(1209),  sid(1406), sid(1419),\
                      sid(15101), sid(17632), sid(39095), sid(1637),  sid(1900),\
                      sid(32301), sid(18870), sid(14014), sid(25317), sid(36930),\
                      sid(12652), sid(26111), sid(24819), sid(24482), sid(2618),\
                      sid(2663),  sid(27543), sid(1787) , sid(2696),  sid(42950),\
                      sid(20208), sid(2853),  sid(8816),  sid(12213),  sid(3212),\
                      sid(9736),  sid(23906), sid(26578), sid(22316), sid(13862)]
    
    context.dayCount = 0
    context.RollingRSIValues = collections.deque(maxlen = rsiMAPeriods)
    set_commission(commission.PerTrade(cost=1.0))
    set_slippage(TradeAtTheOpenSlippageModel(.1))

def handle_data(context, data):
    #record(port_val= context.portfolio.portfolio_value)
    #record(cash = context.portfolio.cash)
    #record(Pos_value = context.portfolio.positions_value)
    record(alxn_shares = context.portfolio.positions[sid(14328)].amount)
    record(alxn_price = context.portfolio.positions[sid(14328)].last_sale_price)
     
    #exchange_time = pd.Timestamp(get_datetime()).tz_convert('US/Eastern')
    
    context.dayCount += 1
    if (context.dayCount < rsiPeriods + rsiMAPeriods or context.dayCount % rebalancePeriods != 0):
        log.debug('Not trading - return 15')
        return
    
    ranked = {}
    rsiValues = rsiIndicator(data)
    rsiValuesMean = np.mean(rsiValues)
    if (rsiValuesMean == None or np.isnan(rsiValuesMean)):
        log.debug('return 37 - ' + str(rsiValuesMean) + ' - ' + str(np.isnan(rsiValuesMean)))
        return
    context.RollingRSIValues.append(rsiValuesMean)
    rsiMeanMean = np.mean(np.array(context.RollingRSIValues))
    rsiValuesStd  = np.std(rsiValues)
    #record(RSIMean = rsiValuesMean, RSIMeanAvg = rsiMeanMean)

    for stock in context.stocks:
        if stock not in data:
            continue
        rsi = rsiValues[stock]
        if stock == sid(14328):  ## want to record the RSI for ALXN
            record (rsi_alxn = rsi)
        rsiZScore = (rsi - rsiValuesMean) / rsiValuesStd
        ranked[stock] = rsiZScore

    # dictionary sorted by value
    ranked = collections.OrderedDict(sorted(ranked.items(), key=lambda t: t[1]))
    
    # Figure out where the long bias percent should end
    rsiMeanAdjusted = rsiMeanMean #rsiValuesMean + (rsiValuesMean - rsiMeanMean)
    longBiasPct = rsiMeanAdjusted / 100.0
    
    tradeCount  = len(ranked)
    longRsiSplit = longBiasPct * tradeCount
    shortRsiSplit = tradeCount - longRsiSplit
    
    ### Try weighting heavier to the top and bottom
    longRsiSplit /= 2
    shortRsiSplit /= 2    
    
    #record(LongBias = longBiasPct, RsiSplit = longRsiSplit, ShortRsiSplit = shortRsiSplit)
    longTotalWeight = (longRsiSplit * (longRsiSplit + 1)) / 2
    shortTotalWeight = (shortRsiSplit * (shortRsiSplit + 1)) / 2
    totalLongFraction = 0
    totalShortFraction = 0
    l = 0
    s = 0
    
    # Now buy long those up ranked RSI securities
    # and sell short those down ranked RSI securities
    # Split at the boundary of RSI percent
    for i in range(0, tradeCount):
        stock = ranked.keys()[i]
        if stock not in data:
            continue
            
        if (i > longRsiSplit and i <= longRsiSplit * 2):
            longWeight = longRsiSplit - l
            longFraction = (longWeight / longTotalWeight) * longBiasPct
            totalLongFraction += longFraction
            order_target_percent(stock, longFraction)
            #log.info("Ordered long stock for  " + str(stock))
            l += 1
            
            #Suspicious trade activity happens in November, lets investigate
            if get_datetime().month ==11:
                log.debug("Long Returns for Nov " + str(get_datetime()) + "  " + str(stock.symbol) + "  "+ str(data[stock].returns()))
            
        elif(i >= tradeCount - (shortRsiSplit * 2) and i < tradeCount - shortRsiSplit):
            shortWeight = shortRsiSplit - s
            shortFraction = -(shortWeight / shortTotalWeight) * (1.0 - longBiasPct)
            totalShortFraction += shortFraction
            order_target_percent(stock, shortFraction)
            #log.info("Ordered short stock for  " + str(stock))
            s += 1
            #Suspicious trade activity happens in November, lets investigate
            if get_datetime().month ==11:
               log.debug("SHORT Returns for Nov " + str(get_datetime()) +"  "+ str(stock.symbol) + "  "+ str(data[stock].returns()))
            

    
########################################################    
class TradeAtTheOpenSlippageModel(slippage.SlippageModel):
    def __init__(self, fractionOfOpenCloseRange):
        self.fractionOfOpenCloseRange = fractionOfOpenCloseRange

    def process_order(self, trade_bar, order):
        openPrice = trade_bar.open_price
        closePrice = trade_bar.price
        ocRange = closePrice - openPrice
        ocRange = ocRange * self.fractionOfOpenCloseRange
        if (ocRange != 0.0):
            targetExecutionPrice = openPrice + ocRange
        else:
            targetExecutionPrice = openPrice
            
        # Create the transaction using the new price we've calculated.
        return slippage.create_transaction(
            trade_bar,
            order,
            targetExecutionPrice,
            order.amount
        )
    
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
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.

Hello Alisa,

What does it mean to say "RSI is a path dependent indicator".

P

A path dependent indicator means that future calculations depend on the history of the algo and decisions made in the past. It may depend on the portfolio characteristics, current positions, and trade behavior. Thus, if any of these details change, your indicator value will be different. And it may not be consistent from backtest to backtest if you change the dates.

Hello AM,

RSI calculated via TA-Lib as implemented by Quantopian will produce the same value irrespective of the start date. See attached run with different start dates. See also https://www.quantopian.com/posts/ta-lib-rsi-using-something-other-than-close-price and https://www.quantopian.com/posts/ta-dot-macd-values-dont-match-google-or-yahoo-finance-charts

Hello Alisa,

Your algo shows 18.24 as the RSI form Aug 29 to Sep 19 whereas mine shows values from 42.18 through to 64.65. I don't uderstand why.

P.

Clone Algorithm
3
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
ALXN = sid(14328)
RSI = ta.RSI(timeperiod=10)

def initialize(context):
    pass

def handle_data(context, data):
    record(ALXN=data[ALXN].close_price)
    record(RSI=RSI(data)[ALXN])
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Hello AM,

I think the argument can be made the Quantopian solution is better for walk-forward testing.

That set aside, what cause the very different results with a one day change in start date?

P.

@Peter, I had cloned an earlier post of the algo, before it was updated by AM. That's likely the source of difference