mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #886 from COLDTURNIP/feature/sortino_ratio
Add Sortino ratio
This commit is contained in:
commit
bcd524361d
|
@ -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
44
pkg/types/sortino.go
Normal 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
26
pkg/types/sortino_test.go
Normal 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)
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue
Block a user