feature: add Sortino ratio

This commit is contained in:
Raphanus Lo 2022-08-18 23:26:54 +08:00
parent 555295f305
commit 747839212a
5 changed files with 100 additions and 9 deletions

View File

@ -7,20 +7,14 @@ import (
// Sharpe: Calcluates the sharpe ratio of access returns
//
// @param returns (Series): Series of profit/loss percentage every specific interval
// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy)
// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy, 1 for annually)
// @param annualize (bool): return annualize sharpe?
// @param smart (bool): return smart sharpe ratio
func Sharpe(returns Series, periods int, annualize bool, smart bool) float64 {
data := returns
num := data.Length()
divisor := Stdev(data, data.Length(), 1)
var divisor = Stdev(data, data.Length(), 1)
if smart {
sum := 0.
coef := math.Abs(Correlation(data, Shift(data, 1), num-1))
for i := 1; i < num; i++ {
sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i))
}
divisor = divisor * math.Sqrt(1.+2.*sum)
divisor *= autocorrPenalty(returns)
}
result := Mean(data) / divisor
if annualize {
@ -28,3 +22,17 @@ func Sharpe(returns Series, periods int, annualize bool, smart bool) float64 {
}
return result
}
func avgReturnRate(returnRate float64, periods int) float64 {
return math.Pow(1.+returnRate, 1./float64(periods)) - 1.
}
func autocorrPenalty(data Series) float64 {
num := data.Length()
coef := math.Abs(Correlation(data, Shift(data, 1), num-1))
var sum = 0.
for i := 1; i < num; i++ {
sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i))
}
return math.Sqrt(1. + 2.*sum)
}

44
pkg/types/sortino.go Normal file
View File

@ -0,0 +1,44 @@
package types
import (
"math"
)
// Sortino: Calcluates the sotino ratio of access returns
//
// ROI_excess E[ROI] - ROI_risk_free
// sortino = ---------- = -----------------------
// risk sqrt(E[ROI_drawdown^2])
//
// @param returns (Series): Series of profit/loss percentage every specific interval
// @param riskFreeReturns (float): risk-free return rate of year
// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy, 1 for annually)
// @param annualize (bool): return annualize sortino?
// @param smart (bool): return smart sharpe ratio
func Sortino(returns Series, riskFreeReturns float64, periods int, annualize bool, smart bool) float64 {
avgRiskFreeReturns := 0.
excessReturn := Mean(returns)
if riskFreeReturns > 0. && periods > 0 {
avgRiskFreeReturns = avgReturnRate(riskFreeReturns, periods)
excessReturn -= avgRiskFreeReturns
}
num := returns.Length()
var sum = 0.
for i := 0; i < num; i++ {
exRet := returns.Index(i) - avgRiskFreeReturns
if exRet < 0 {
sum += exRet * exRet
}
}
var risk = math.Sqrt(sum / float64(num))
if smart {
risk *= autocorrPenalty(returns)
}
result := excessReturn / risk
if annualize {
return result * math.Sqrt(float64(periods))
}
return result
}

26
pkg/types/sortino_test.go Normal file
View File

@ -0,0 +1,26 @@
package types
import (
"github.com/stretchr/testify/assert"
"testing"
)
/*
python
import quantstats as qx
import pandas as pd
print(qx.stats.sortino(pd.Series([0.01, -0.03, 0.1, -0.02, 0.001]), 0.00, 0, False, False))
print(qx.stats.sortino(pd.Series([0.01, -0.03, 0.1, -0.02, 0.001]), 0.03, 252, False, False))
print(qx.stats.sortino(pd.Series([0.01, -0.03, 0.1, -0.02, 0.001]), 0.03, 252, True, False))
*/
func TestSortino(t *testing.T) {
var a Series = &Float64Slice{0.01, -0.03, 0.1, -0.02, 0.001}
output := Sortino(a, 0.03, 0, false, false)
assert.InDelta(t, output, 0.75661, 0.0001)
output = Sortino(a, 0.03, 252, false, false)
assert.InDelta(t, output, 0.74597, 0.0001)
output = Sortino(a, 0.03, 252, true, false)
assert.InDelta(t, output, 11.84192, 0.0001)
}

View File

@ -125,6 +125,18 @@ func (s *IntervalProfitCollector) GetSharpe() float64 {
return Sharpe(Minus(s.Profits, 1.), s.Profits.Length(), true, false)
}
// Get sortino value with the interval of profit collected.
// No risk-free return rate and smart sortino OFF for the calculated result.
func (s *IntervalProfitCollector) GetSortino() float64 {
if s.tmpTime.IsZero() {
panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
}
if s.Profits == nil {
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
}
return Sortino(Minus(s.Profits, 1.), 0., s.Profits.Length(), true, false)
}
func (s *IntervalProfitCollector) GetOmega() float64 {
return Omega(Minus(s.Profits, 1.))
}
@ -132,6 +144,7 @@ func (s *IntervalProfitCollector) GetOmega() float64 {
func (s IntervalProfitCollector) MarshalYAML() (interface{}, error) {
result := make(map[string]interface{})
result["Sharpe Ratio"] = s.GetSharpe()
result["Sortino Ratio"] = s.GetSortino()
result["Omega Ratio"] = s.GetOmega()
result["Profitable Count"] = s.GetNumOfProfitableIntervals()
result["NonProfitable Count"] = s.GetNumOfNonProfitableIntervals()