Calculating a Portfolio Sortino Ratio with Target Downside Deviation
Summary
The document shows how to calculate a Sortino ratio for a weighted portfolio. It first forms a portfolio return series by applying asset weights to each period’s returns. Given a target return, or minimum acceptable return, it computes downside deviation as the square root of the average squared shortfalls below that target, then divides the portfolio’s average excess return by this downside measure. The example uses a zero target and compares the result with a calculation from an R performance-analysis package.
A small sample of ten return observations produces matching rounded results in the Python and R examples. The approach illustrates why the portfolio return series should be formed before applying the downside calculation. The document notes a small discrepancy at greater decimal precision and suggests differing numeric precision as a possible reason. It does not discuss annualization, alternative downside-deviation conventions, missing data, or how to choose a target return, so those choices need to be specified for other applications.
Key ideas
- Calculate portfolio returns from asset returns and portfolio weights before measuring downside risk.
- The Sortino ratio divides average return above a target by target downside deviation.
- Downside deviation uses squared shortfalls below the selected target and includes non-shortfall periods as zero contributions.
- A zero target is used in the example, but the target return is a user-specified choice.
- The example reports agreement between Python and R after rounding, with a small difference at higher precision.
Tags
Full text
# How to calculate Sortino ratio from a weighted portfolio with Python?
# How to calculate Sortino ratio from a weighted portfolio with Python?
In this working example I'm able to calculate a Sharpe ratio (with rf=0) from a weighted portfolio of 3 securities, but how can I modify the code bellow so it calculates a Sortino ratio ?
```
import numpy as np
from pandas_datareader import data
tickers = ['AAPL', 'MSFT', '^GSPC']
start_date = '2010-01-01'
end_date = '2016-12-31'
weights = [1/3, 1/3, 1/3]
# Fetch data
df = data.DataReader(tickers, 'yahoo', start_date, end_date)['Close']
# Calculate historical returns
returns = df.pct_change(1).dropna()
# Weighted mean returns
exp_rets = returns.mean()
mean = sum(exp_rets * weights)
# Standard deviation
var = np.dot(np.dot(weights, returns.cov()), weights)
# Sharp ratio
sr = mean / np.sqrt(var)
print(sr)
0.054270779230564975
```
## Answer by Pleb (score 4, accepted)
https://quant.stackexchange.com/a/68577
## Sortino ratio in Python:
Doing my own due dilligence, I found a paper depicting the formula for the Sortino ratio, which follows the same setup as from this link described in an earlier comment. For brewity, the formula for the Sortino ratio can be specified as:
$$ S = \frac{R - T}{TDD} $$
where $R$ is the average period return and $T$ is the target return (also referred to as mean acceptable return, MAR). Moreover,
$$ TDD = \sqrt{\frac{1}{N}\sum_{i=1}^N \min\left(0, X_i - T\right)^2} $$
denotes the target downside deviation, with $X_i$ being the $i$'th return. Using the above formula we can calculate the Sortino ratio in Python. Disregarding the first part of your code above (defining weights, getting stock data, etc), we can calculate the Sortino ratio using the following function:
```
def SortinoRatio(df, T):
"""Calculates the Sortino ratio from univariate excess returns.
Args:
df ([float]): The dataframe or pandas series of univariate excess returns.
T ([integer]): The targeted return.
"""
#downside deviation:
temp = np.minimum(0, df - T)**2
temp_expectation = np.mean(temp)
downside_dev = np.sqrt(temp_expectation)
#Sortino ratio:
sortino_ratio = np.mean(df - T) / downside_dev
return(sortino_ratio)
```
You can now construct portfolio returns, define your specified target return $T$ and feed these into the function:
```
#portfolio returns:
port_ret = returns.dot(weights)
#-------------------output:-----------------------
print(np.round(SortinoRatio(port_ret, T = 0),4))
0.0782
```
This results in a Sortino ratio of 0.0782.
## Verification:
A good way to validate the above function, is to find an already implemented function from a "respectable" source. Here, the `PerformanceAnalytics` package from `R` contains a function that calculates the Sortino ratio. Using this function on the first 10 rows of your dataframe (called ret) we get the following:
```
#R code:
ret <- matrix(
c(0.001729, 0.000323, 0.003116,
-0.015906, -0.006137, 0.000546,
-0.001849, -0.010400, 0.004001,
0.006648, 0.006897, 0.002882,
-0.008821, -0.012720, 0.001747,
-0.011375, -0.006607, -0.009381,
0.014106, 0.009312, 0.008326,
-0.005792, 0.020099, 0.002426,
-0.016712, -0.003230, -0.010823,
0.044238, 0.007777, 0.012500), nrow = 10, ncol = 3, byrow = TRUE)
weights <- c(1/3, 1/3, 1/3)
portret <- as.xts(ret %*% weights, order.by = as.Date(1:10))
#-------------------output:-----------------------
round(SortinoRatio(portret, MAR = 0), 4)
[,1]
> Sortino Ratio (MAR = 0%) 0.1664
```
Here, the function in Python gives the same result (there's a slight difference for higher decimal-points):
```
return_test = returns.head(10)@weights
#-------------------output:-----------------------
print(np.round(SortinoRatio(return_test, T = 0),4))
0.1664
```
There is a slight deviation between the two results for more decimal-points (>5) which might be attributable to different precision-based values used between the languages. Nevertheless, I hope this helps you with your implementation.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.