Back to Community
Question on calculated max drawdown

The backtest below is a simple strategy where you buy SPY when the 15 day MA is above the 63 day MA and sell when 15 day MA crosses below 63 day MA. My question: The max drawdown from this strategy is calculated to be 15.8%. How is that number calculated? I hold SPY, which should track the benchmark fairly well, for several months in some cases. I would expect the maximum drawdown to be near 100%. Is the "max" drawdown actually the average drawdown?

4 responses

Here is the backtest

Clone Algorithm
49
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
#Buy and sell SPY when 15 day MA is above 126 day MA


import datetime
import math
import numpy
import pandas

def initialize(context):
    #
    #
    #Variables for later
    context.day=None
    context.timestep=0
    context.margin_req=0 
    context.totalShorts=0
    context.totalLongs=0
    context.cash=0
    context.pauseUntil=63
            
    #
    #
    #Set constraints on borrowing
    context.pct_invested_threshold = 0.95 #Set limit on percent invested (as a decimal)
    context.init_margin=1.50 #Set initial margin requirement    
    context.maint_margin=1.25 #Set the maintenance margin requirement   


    #
    #
    #Build the Universe
    #Add SPDR SPY
    context.SPY = sid(8554)

    

    
def handle_data(context, data):

    #Update Account Information at the beginning of each frame
    update_newFrame(context, data)

    if pause(context.pauseUntil, context, data) == 0:
        return
    
    
    #Apply Trade Logic
    trade_logic(context,data)


    
#    Trade Algorithm
def trade_logic (context, data):
#    log.info("Call to trade logic with 15 MA = "+str(data[context.SPY].mavg(15))+" and 63 MA = "+str(data[context.SPY].mavg(63)))
    if data[context.SPY].mavg(15) < data[context.SPY].mavg(63):
#        log.info("15 day is below!")
        liquidate_position(context.SPY, context)
    else:
        #log.info("15 day is above!")
        nshares=math.floor(0.99*context.pct_invested_threshold * context.cash / data[context.SPY].price)
        generate_order (context.SPY, nshares, context, data)
    
    
#
#
#    Supporting Functions
#
#

    
def pause(nth_Day, mycontext, mydata):
    if mycontext.timestep == 1:    
        nshares=math.floor (mycontext.portfolio.cash / mydata[mycontext.SPY].price) 
        order(mycontext.SPY,+nshares)
        
    if mycontext.timestep < nth_Day: 
        return(0)
    elif mycontext.timestep == nth_Day:
        #Liquidate position on the last day of the pause
        liquidate_position(mycontext.SPY,mycontext)
        return(0)
    else:
        return(1)

def update_newFrame(context, data):
    #
    context.cash = context.portfolio.cash
    context.portvalue = context.portfolio.positions_value
    context.totalShorts=0
    for sym in data.keys():
        if context.portfolio.positions[sym].amount < 0:
            context.totalShorts += (context.portfolio.positions[sym].amount * data[sym].price)
        else:
            context.totalLongs += (context.portfolio.positions[sym].amount * data[sym].price)

    update_portvals(context)
    
    #Handle assigning the timestep number (1 day is 1 timestep)
    if get_datetime().day <> context.day: #look for a new day
        context.day=get_datetime().day
        context.timestep += 1
        #log.info ( "Cash: "+str(context.cash)+"; Margin Req: "+str(context.margin_req)+" Avail Cash:"+str(context.cash - context.margin_req) )
        if context.timestep>context.pauseUntil:
            if context.cash < context.margin_req: #check for margin calls daily
                generate_marginCall(context, data)
            
    
def update_portvals(context):
    #Update account information when this function is called
    context.total_equity = context.cash + context.portvalue
    context.pct_invested = (context.totalLongs-context.totalShorts) / context.total_equity
    context.pct_cash = context.cash / context.total_equity
    context.margin_req = abs(context.totalShorts * context.maint_margin)
    
        
def generate_order(sym, size, context, data):    
    #log.info("Call to generate_order")
    if size>0: #Opening a long position    

        #log.info("Buy long "+str(size)+" shares "+str(sym) )
        #log.info("Cash = "+str(context.cash)+"; Current Margin Req.="+str(context.margin_req) )                 

        #Is there enough available cash to buy the position
        if (context.cash-context.margin_req) < size * data[sym].price:
            #log.info("Trade canceled : Insufficient funds.")
            return

        #Deduct price from cash and add to portfolio value
        context.cash -= size * data[sym].price
        context.portvalue += size * data[sym].price
        context.totalLongs += size * data[sym].price
        update_portvals(context)

        #Abort the transaction if the percent invested is greater than the threshold
        #before slippage and commissions
        if context.pct_invested > context.pct_invested_threshold:
            context.cash += size * data[sym].price
            context.portvalue -= size * data[sym].price
            context.totalLongs -= size * data[sym].price
            update_portvals(context)

#            if size>100:
#                log.info("Re-generating order for "+str(size*context.pct_invested_threshold)+" instead of "+str(size))
#                generate_order(sym,size*context.pct_invested_threshold, context,data)
#            return
        
        #Abort the transaction if the investment would generate a margin call
        if context.cash < context.margin_req:
#            log.info("Invest would generate a margin call")
            context.cash += size * data[sym].price
            context.portvalue -= size * data[sym].price
            context.totalLongs -= size * data[sym].price
            update_portvals(context)
            return
    
        order(sym,size)

    else: #Opening a short position
        
        #log.info("Generating a short order for "+str(size)+" shares of "+str(sym)+" and context.cash="+str(context.cash)+" and context.margin_req="+str(context.margin_req) )
        #Is there at least enough available cash to cover the initial maintenance margin
        if (context.cash-context.margin_req) < abs(size * data[sym].price * context.init_margin):
            #log.info("Trade canceled")
            return
        
        #Deduct price from cash and add to portfolio value (note that size is negative)
        context.cash -= size * data[sym].price
        context.portvalue += size * data[sym].price
        context.totalShorts += size * data[sym].price
        update_portvals(context)
        
        #Abort the transaction if the percent invested is greater than the threshold
        #before slippage and commission
        if context.pct_invested > context.pct_invested_threshold:
            context.cash += size * data[sym].price
            context.portvalue -= size * data[sym].price
            context.totalShorts -= size * data[sym].price
            update_portvals(context)
            #log.info("Trade canceled")
            return
            
        #Abort the transaction if the investment would generate a margin call
        if context.cash < context.margin_req:
            context.cash += size * data[sym].price
            context.portvalue -= size * data[sym].price
            context.totalShorts -= size * data[sym].price
            update_portvals(context)
            #log.info("Trade canceled")
            return
        
        order(sym,size)
            
        
def generate_marginCall(context,data):
    #This function should be coded to address margin calls
    log.info("Margin call")
    

def liquidate_position(sym,context):
    if context.portfolio.positions[sym].amount is not 0:
        order(sym, -context.portfolio.positions[sym].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.

100% drawdown would be the same as saying "lost all the money". I don't see anywhere on the blue line where it goes to -100%.

But I do see some places where it could have lost up to 16% of the portfolio value. E.g. from the peak at the start of 2010 to the trough in September that year.

Definition of 'Drawdown'
The peak-to-trough decline during a specific record period of an investment, fund or commodity. A drawdown is usually quoted as the percentage between the peak and the trough.
http://www.investopedia.com/terms/d/drawdown.asp

Thanks Dennis - I must be off my game today. For some reason I confused drawdown with downside capture ratio. Whew.... second foolish mistake I made today. My apologies.

No worries :)