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

49
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
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

#    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:
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):
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)
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)
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 :)