Annualizing a Sharpe vs. Sortino Ratio

Below are functions that (I hope) calculate Sharpe and Sortino ratios.
I have made their interfaces identical.
From here, "Commonly, Sharpe Ratios on a daily, weekly or monthly basis are annualized by multiplying by the square root of the higher frequency time period. This is because the effective return is proportional to time. Assuming a Weiner process governs stock prices, variance is proportional to time. Hence standard deviation is proportional to the square root of time. So you would scale a Sharpe Ratio by multiplying by t/√t = √t, where t is the frequency you are annualizing from."
So why doesn't this logic apply to a Sortino Ratio, i.e. why don't we have an equivalent np.sqrt(N) term in the last line?
Thanks

def annualised_Sharpe(daily_ret, yearly_benchmark_rate=0.05,N=252):
MAR = yearly_benchmark_rate/N
excess_daily_ret = daily_ret - MAR
return np.sqrt(N) * excess_daily_ret.mean() / excess_daily_ret.std()

def annualised_Sortino(daily_ret, yearly_benchmark_rate=0.05, N=252):
MAR = yearly_benchmark_rate/N
excess_daily_ret = daily_ret - MAR
target_downside_deviation = np.sqrt(np.mean(minimum(excess_daily_ret,0.0)**2))
sortino = excess_daily_ret.mean() / target_downside_deviation
#return np.sqrt(N) * sortino
return sortino

2 responses

Thank you for posting those.
And with Bill's ok, this is an effort to calculate Beta, for both portfolio and individual securities. Ballpark. Needs improvement from others.

29
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 math

def initialize(context):
c = context    # for brevity
c.symbols = symbols('AAPL', 'TSLA')
c.spy_sec = symbol('SPY')            # for beta calc

c.prtflio_vals = []
c.spy_volatility = 0
c.betas = {
'prtflio': {
'track': [0, 0],
'avg'  : 0,
'now'  : 0,
}
}
for s in c.symbols:
sym = s.symbol
c.betas[sym] = {
'track': [0, 0],
'avg'  : 0,
'now'  : 0,
}

def handle_data(context, data):
c = context
period = 60
c.prices = history(60, '1d', 'close_price')
c.prtflio_vals.append(c.portfolio.portfolio_value)
c.prtflio_vals = c.prtflio_vals[-period:]  # trim, limit

logit = 0
if get_datetime().date() == get_environment('end').date():    # only log on last day
logit = 1

beta_prtflio = beta_calc(c, c.prtflio_vals)       # beta for portfolio
if logit:
log.info('prtflio beta {}'.format('%.2f' % beta_prtflio))

for s in data:
sym = s.symbol
beta_sym = beta_calc(c, c.prices[s], sym)     # beta per symbol
if logit:
log.info('    {} b {}'.format(sym, '%.2f' % beta_sym))

c = context

for s in data:
shrs = c.portfolio.positions[s].amount
if shrs and data[s].price > 1.2 * c.portfolio.positions[s].cost_basis:
order_target(s, 0)
else:
order(s, 10)

def beta_calc(c, values, sym=''):        # bbbbb
''' Calculate BETA for portfolio (if no sym)
or for symbol if specified.
'''
multiplier = .7    # arbitrary mult to bring vals into line via trial and error. Needs fix, by you.

b = c.betas

if sym and sym == 'SPY':
c.spy_volatility = volatility(c, values)
return 0.0    # Just setting SPY volatility in this case

if not c.spy_volatility: return 0.0

if sym:     # Symbol beta including SPY
sym_beta  = volatility(c, values) / c.spy_volatility
sym_beta *= multiplier
avg, num             = b[sym]['track']
avg_new              = ((avg * num) + sym_beta) / (num + 1)
b[sym]['track'] = [avg_new, num + 1]  # to calc average
b[sym]['avg']   = avg_new
b[sym]['now']   = sym_beta
return sym_beta

else:       # Portfolio beta
pbeta  = volatility(c, values) / c.spy_volatility
pbeta *= multiplier
avg, num         = b['prtflio']['track']
avg_new          = ((avg * num) + pbeta) / (num + 1)
b['prtflio']['track'] = [avg_new, num + 1]
b['prtflio']['avg']   = avg_new
b['prtflio']['now']   = pbeta

record(portfolio_beta = pbeta)

return pbeta

def volatility(c, values):        # vvvv
try:   # avoid "math domain error"
voltilty = 0
period   = len(values) - 1
if period <= 1: return 0.0
r = []    # Xbar = 1/n sum[1..n] (ln(P_t / P_t-1))
for i in xrange(1, period + 1):
r.append(math.log(values[i] / values[i-1]))
rMean = sum(r) / period    # Average of all
d = []    # Difference of each return from the mean, then square
for i in xrange(0, period):
d.append(math.pow((r[i] - rMean), 2))
# Square root of the sum over the period - 1
#   multiplied by the square root of the number of trading days in a year.
voltilty = math.sqrt(sum(d) / (period - 1)) * math.sqrt(252/period)
return float(voltilty)
except:
return 0.0


There was a runtime error.

Hi Bill,
Your Sortino ratio calculation is correct. As for annualizing the Sortino, in my opinion, it shouldn't matter as long as you are consistent with whatever you do. The annualizing factor is just a scalar multiple of the calculation result, so it can't change the outcome of a ranking system using the Sortino as long as the same scalar is used in every case. Annualizing is common practice in industry, but algorithms don't care if you do it or not. I'm not 100% what the industry standard is for the Sortino, but a sqrt(252) multiple seems reasonable to me.