Skip to content
forked from c9s/bbgo

Commit

Permalink
feature: add sharpe function implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
zenixls2 committed Jun 29, 2022
1 parent d8d77ce commit 69533c0
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 4 deletions.
24 changes: 22 additions & 2 deletions pkg/statistics/sharp.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
package statistics

import (
"math"

"github.com/c9s/bbgo/pkg/types"
)

// Sharpe: Calcluates the sharpe ratio of access returns
//
// @param rf (float): Risk-free rate expressed as a yearly (annualized) return
// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy)
// @param annualize (bool): return annualize sharpe?
// @param smart (bool): return smart sharpe ratio
func Sharpe(returns types.Series, rf float64, periods int, annualize bool, smart bool) {
func Sharpe(returns types.Series, periods int, annualize bool, smart bool) float64 {
data := returns
num := data.Length()
if types.Lowest(data, num) >= 0 && types.Highest(data, num) > 1 {
data = types.PercentageChange(returns)
}
divisor := types.Stdev(data, data.Length(), 1)
if smart {
sum := 0.
coef := math.Abs(types.Correlation(data, types.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)
}
result := types.Mean(data) / divisor
if annualize {
return result * math.Sqrt(float64(periods))
}
return result
}
27 changes: 27 additions & 0 deletions pkg/statistics/sharp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package statistics

import (
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
"testing"
)

/*
python
import quantstats as qx
import pandas as pd
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 0, False, False))
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, False, False))
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, True, False))
*/
func TestSharpe(t *testing.T) {
var a types.Series = &types.Float64Slice{0.01, 0.1, 0.001}
output := Sharpe(a, 0, false, false)
assert.InDelta(t, output, 0.67586, 0.0001)
output = Sharpe(a, 252, false, false)
assert.InDelta(t, output, 0.67586, 0.0001)
output = Sharpe(a, 252, true, false)
assert.InDelta(t, output, 10.7289, 0.0001)
}
96 changes: 94 additions & 2 deletions pkg/types/indicator.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,14 +633,24 @@ func PercentageChange(a Series, offset ...int) Series {
return &PercentageChangeResult{a, o}
}

func Stdev(a Series, length int) float64 {
func Stdev(a Series, params ...int) float64 {
length := a.Length()
if len(params) > 0 {
if params[0] < length {
length = params[0]
}
}
ddof := 0
if len(params) > 1 {
ddof = params[1]
}
avg := Mean(a, length)
s := .0
for i := 0; i < length; i++ {
diff := a.Index(i) - avg
s += diff * diff
}
return math.Sqrt(s / float64(length))
return math.Sqrt(s / float64(length - ddof))
}

type CorrFunc func(Series, Series, int) float64
Expand Down Expand Up @@ -780,4 +790,86 @@ func Skew(a Series, length int) float64 {
return l * math.Sqrt(l-1) / (l - 2) * sum3 / math.Pow(sum2, 1.5)
}

type ShiftResult struct {
a Series
offset int
}

func (inc *ShiftResult) Last() float64 {
if inc.offset < 0 {
return 0
}
if inc.offset > inc.a.Length() {
return 0
}
return inc.a.Index(inc.offset)
}
func (inc *ShiftResult) Index(i int) float64 {
if inc.offset + i < 0 {
return 0
}
if inc.offset + i > inc.a.Length() {
return 0
}
return inc.a.Index(inc.offset + i)
}

func (inc *ShiftResult) Length() int {
return inc.a.Length() - inc.offset
}

func Shift(a Series, offset int) Series {
return &ShiftResult{a, offset}
}

type RollingResult struct {
a Series
window int
}

type SliceView struct {
a Series
start int
length int
}

func (s *SliceView) Last() float64 {
return s.a.Index(s.start)
}
func (s *SliceView) Index(i int) float64 {
if i >= s.length {
return 0
}
return s.a.Index(i+s.start)
}

func (s *SliceView) Length() int {
return s.length
}

var _ Series = &SliceView{}

func (r *RollingResult) Last() Series {
return &SliceView{r.a, 0, r.window}
}

func (r *RollingResult) Index(i int) Series {
if i * r.window > r.a.Length() {
return nil
}
return &SliceView{r.a, i*r.window, r.window}
}

func (r *RollingResult) Length() int {
mod := r.a.Length() % r.window
if mod > 0 {
return r.a.Length() / r.window + 1
} else {
return r.a.Length() / r.window
}
}

func Rolling(a Series, window int) *RollingResult {
return &RollingResult{a, window}
}
// TODO: ta.linreg

0 comments on commit 69533c0

Please sign in to comment.