Back to Community
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
# Add Period correction??
#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.

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

def initialize(context):
    c = context    # for brevity
    c.symbols = symbols('AAPL', 'TSLA')
    c.spy_sec = symbol('SPY')            # for beta calc
    
    schedule_function(trade, date_rules.every_day(), time_rules.market_open())
    
    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))
        
def trade(context, data):
    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.