diff --git a/pkg/statistics/sharp.go b/pkg/statistics/sharp.go index ca8c81750..bf2114249 100644 --- a/pkg/statistics/sharp.go +++ b/pkg/statistics/sharp.go @@ -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 } diff --git a/pkg/statistics/sharp_test.go b/pkg/statistics/sharp_test.go new file mode 100644 index 000000000..7d373301c --- /dev/null +++ b/pkg/statistics/sharp_test.go @@ -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) +} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 3648a1d8b..3790c6075 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -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 @@ -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