diff --git a/pkg/types/sharp.go b/pkg/types/sharpe.go similarity index 55% rename from pkg/types/sharp.go rename to pkg/types/sharpe.go index f60f00eaf..1d504aa75 100644 --- a/pkg/types/sharp.go +++ b/pkg/types/sharpe.go @@ -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) +} diff --git a/pkg/types/sharp_test.go b/pkg/types/sharpe_test.go similarity index 100% rename from pkg/types/sharp_test.go rename to pkg/types/sharpe_test.go diff --git a/pkg/types/sortino.go b/pkg/types/sortino.go new file mode 100644 index 000000000..e3bc172b3 --- /dev/null +++ b/pkg/types/sortino.go @@ -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 +} diff --git a/pkg/types/sortino_test.go b/pkg/types/sortino_test.go new file mode 100644 index 000000000..aa58b218a --- /dev/null +++ b/pkg/types/sortino_test.go @@ -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) +} diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index 2d49aec1d..72b07a65f 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -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()