mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
Merge pull request #1181 from c9s/feature/v2indicator
FEATURE: [indicator] add new ATR, RMA indicators with the new API design
This commit is contained in:
commit
b86e1a9282
|
@ -15,3 +15,9 @@ func TestHigher(t *testing.T) {
|
||||||
out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0)
|
out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0)
|
||||||
assert.Equal(t, []float64{13.0, 15.0}, out)
|
assert.Equal(t, []float64{13.0, 15.0}, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLSM(t *testing.T) {
|
||||||
|
slice := Slice{1., 2., 3., 4.}
|
||||||
|
slope := LSM(slice)
|
||||||
|
assert.Equal(t, 1.0, slope)
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package floats
|
package floats
|
||||||
|
|
||||||
func (s Slice) Pivot(left, right int, f func(a, pivot float64) bool) (float64, bool) {
|
func (s Slice) Pivot(left, right int, f func(a, pivot float64) bool) (float64, bool) {
|
||||||
return CalculatePivot(s, left, right, f)
|
return FindPivot(s, left, right, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CalculatePivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) {
|
func FindPivot(values Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) {
|
||||||
length := len(values)
|
length := len(values)
|
||||||
|
|
||||||
if right == 0 {
|
if right == 0 {
|
||||||
|
|
|
@ -73,7 +73,10 @@ func (s Slice) Add(b Slice) (c Slice) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Slice) Sum() (sum float64) {
|
func (s Slice) Sum() (sum float64) {
|
||||||
return floats.Sum(s)
|
for _, v := range s {
|
||||||
|
sum += v
|
||||||
|
}
|
||||||
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Slice) Mean() (mean float64) {
|
func (s Slice) Mean() (mean float64) {
|
||||||
|
@ -97,6 +100,18 @@ func (s Slice) Tail(size int) Slice {
|
||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Slice) Average() float64 {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
for _, value := range s {
|
||||||
|
total += value
|
||||||
|
}
|
||||||
|
return total / float64(len(s))
|
||||||
|
}
|
||||||
|
|
||||||
func (s Slice) Diff() (values Slice) {
|
func (s Slice) Diff() (values Slice) {
|
||||||
for i, v := range s {
|
for i, v := range s {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
@ -171,19 +186,49 @@ func (s Slice) Addr() *Slice {
|
||||||
func (s Slice) Last() float64 {
|
func (s Slice) Last() float64 {
|
||||||
length := len(s)
|
length := len(s)
|
||||||
if length > 0 {
|
if length > 0 {
|
||||||
return (s)[length-1]
|
return s[length-1]
|
||||||
}
|
}
|
||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Index fetches the element from the end of the slice
|
||||||
|
// WARNING: it does not start from 0!!!
|
||||||
func (s Slice) Index(i int) float64 {
|
func (s Slice) Index(i int) float64 {
|
||||||
length := len(s)
|
length := len(s)
|
||||||
if length-i <= 0 || i < 0 {
|
if i < 0 || length-1-i < 0 {
|
||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
return (s)[length-i-1]
|
return s[length-1-i]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Slice) Length() int {
|
func (s Slice) Length() int {
|
||||||
return len(s)
|
return len(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Slice) LSM() float64 {
|
||||||
|
return LSM(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LSM is the least squares method for linear regression
|
||||||
|
func LSM(values Slice) float64 {
|
||||||
|
var sumX, sumY, sumXSqr, sumXY = .0, .0, .0, .0
|
||||||
|
|
||||||
|
end := len(values) - 1
|
||||||
|
for i := end; i >= 0; i-- {
|
||||||
|
val := values[i]
|
||||||
|
per := float64(end - i + 1)
|
||||||
|
sumX += per
|
||||||
|
sumY += val
|
||||||
|
sumXSqr += per * per
|
||||||
|
sumXY += val * per
|
||||||
|
}
|
||||||
|
|
||||||
|
length := float64(len(values))
|
||||||
|
slope := (length*sumXY - sumX*sumY) / (length*sumXSqr - sumX*sumX)
|
||||||
|
|
||||||
|
average := sumY / length
|
||||||
|
tail := average - slope*sumX/length + slope
|
||||||
|
head := tail + slope*(length-1)
|
||||||
|
slope2 := (tail - head) / (length - 1)
|
||||||
|
return slope2
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
// Code generated by "callbackgen -type EWMAStream"; DO NOT EDIT.
|
|
||||||
|
|
||||||
package indicator
|
|
||||||
|
|
||||||
import ()
|
|
|
@ -1,6 +1,40 @@
|
||||||
package indicator
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/bbgo/pkg/datatype/floats"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
//go:generate callbackgen -type Float64Updater
|
//go:generate callbackgen -type Float64Updater
|
||||||
type Float64Updater struct {
|
type Float64Updater struct {
|
||||||
updateCallbacks []func(v float64)
|
updateCallbacks []func(v float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Float64Series struct {
|
||||||
|
types.SeriesBase
|
||||||
|
Float64Updater
|
||||||
|
slice floats.Slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFloat64Series(v ...float64) Float64Series {
|
||||||
|
s := Float64Series{}
|
||||||
|
s.slice = v
|
||||||
|
s.SeriesBase.Series = s.slice
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Float64Series) Last() float64 {
|
||||||
|
return f.slice.Last()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Float64Series) Index(i int) float64 {
|
||||||
|
length := len(f.slice)
|
||||||
|
if length == 0 || length-i-1 < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return f.slice[length-i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Float64Series) Length() int {
|
||||||
|
return len(f.slice)
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,12 @@ package indicator
|
||||||
|
|
||||||
import ()
|
import ()
|
||||||
|
|
||||||
func (F *Float64Updater) OnUpdate(cb func(v float64)) {
|
func (f *Float64Updater) OnUpdate(cb func(v float64)) {
|
||||||
F.updateCallbacks = append(F.updateCallbacks, cb)
|
f.updateCallbacks = append(f.updateCallbacks, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (F *Float64Updater) EmitUpdate(v float64) {
|
func (f *Float64Updater) EmitUpdate(v float64) {
|
||||||
for _, cb := range F.updateCallbacks {
|
for _, cb := range f.updateCallbacks {
|
||||||
cb(v)
|
cb(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
package indicator
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/c9s/bbgo/pkg/datatype/floats"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
NEW INDICATOR DESIGN:
|
|
||||||
|
|
||||||
klines := kLines(marketDataStream)
|
|
||||||
closePrices := closePrices(klines)
|
|
||||||
macd := MACD(klines, {Fast: 12, Slow: 10})
|
|
||||||
|
|
||||||
equals to:
|
|
||||||
|
|
||||||
klines := KLines(marketDataStream)
|
|
||||||
closePrices := ClosePrice(klines)
|
|
||||||
fastEMA := EMA(closePrices, 7)
|
|
||||||
slowEMA := EMA(closePrices, 25)
|
|
||||||
macd := Subtract(fastEMA, slowEMA)
|
|
||||||
signal := EMA(macd, 16)
|
|
||||||
histogram := Subtract(macd, signal)
|
|
||||||
*/
|
|
||||||
|
|
||||||
type Float64Source interface {
|
|
||||||
types.Series
|
|
||||||
OnUpdate(f func(v float64))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Float64Subscription interface {
|
|
||||||
types.Series
|
|
||||||
AddSubscriber(f func(v float64))
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:generate callbackgen -type EWMAStream
|
|
||||||
type EWMAStream struct {
|
|
||||||
Float64Updater
|
|
||||||
types.SeriesBase
|
|
||||||
|
|
||||||
slice floats.Slice
|
|
||||||
|
|
||||||
window int
|
|
||||||
multiplier float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func EWMA2(source Float64Source, window int) *EWMAStream {
|
|
||||||
s := &EWMAStream{
|
|
||||||
window: window,
|
|
||||||
multiplier: 2.0 / float64(1+window),
|
|
||||||
}
|
|
||||||
|
|
||||||
s.SeriesBase.Series = s.slice
|
|
||||||
|
|
||||||
if sub, ok := source.(Float64Subscription); ok {
|
|
||||||
sub.AddSubscriber(s.calculateAndPush)
|
|
||||||
} else {
|
|
||||||
source.OnUpdate(s.calculateAndPush)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EWMAStream) calculateAndPush(v float64) {
|
|
||||||
v2 := s.calculate(v)
|
|
||||||
s.slice.Push(v2)
|
|
||||||
s.EmitUpdate(v2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EWMAStream) calculate(v float64) float64 {
|
|
||||||
last := s.slice.Last()
|
|
||||||
m := s.multiplier
|
|
||||||
return (1.0-m)*last + m*v
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubtractStream struct {
|
|
||||||
Float64Updater
|
|
||||||
types.SeriesBase
|
|
||||||
|
|
||||||
a, b, c floats.Slice
|
|
||||||
i int
|
|
||||||
}
|
|
||||||
|
|
||||||
func Subtract(a, b Float64Source) *SubtractStream {
|
|
||||||
s := &SubtractStream{}
|
|
||||||
s.SeriesBase.Series = s.c
|
|
||||||
|
|
||||||
a.OnUpdate(func(v float64) {
|
|
||||||
s.a.Push(v)
|
|
||||||
s.calculate()
|
|
||||||
})
|
|
||||||
b.OnUpdate(func(v float64) {
|
|
||||||
s.b.Push(v)
|
|
||||||
s.calculate()
|
|
||||||
})
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SubtractStream) calculate() {
|
|
||||||
if s.a.Length() != s.b.Length() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.a.Length() > s.c.Length() {
|
|
||||||
var numNewElems = s.a.Length() - s.c.Length()
|
|
||||||
var tailA = s.a.Tail(numNewElems)
|
|
||||||
var tailB = s.b.Tail(numNewElems)
|
|
||||||
var tailC = tailA.Sub(tailB)
|
|
||||||
for _, f := range tailC {
|
|
||||||
s.c.Push(f)
|
|
||||||
s.EmitUpdate(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -64,13 +64,13 @@ func (inc *PivotLow) PushK(k types.KLine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) {
|
func calculatePivotHigh(highs floats.Slice, left, right int) (float64, bool) {
|
||||||
return floats.CalculatePivot(highs, left, right, func(a, pivot float64) bool {
|
return floats.FindPivot(highs, left, right, func(a, pivot float64) bool {
|
||||||
return a < pivot
|
return a < pivot
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculatePivotLow(lows floats.Slice, left, right int) (float64, bool) {
|
func calculatePivotLow(lows floats.Slice, left, right int) (float64, bool) {
|
||||||
return floats.CalculatePivot(lows, left, right, func(a, pivot float64) bool {
|
return floats.FindPivot(lows, left, right, func(a, pivot float64) bool {
|
||||||
return a > pivot
|
return a > pivot
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package indicator
|
package indicator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/c9s/bbgo/pkg/datatype/floats"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -10,10 +9,8 @@ type KLineSubscription interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PriceStream struct {
|
type PriceStream struct {
|
||||||
types.SeriesBase
|
Float64Series
|
||||||
Float64Updater
|
|
||||||
|
|
||||||
slice floats.Slice
|
|
||||||
mapper KLineValueMapper
|
mapper KLineValueMapper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,12 +23,16 @@ func Price(source KLineSubscription, mapper KLineValueMapper) *PriceStream {
|
||||||
|
|
||||||
source.AddSubscriber(func(k types.KLine) {
|
source.AddSubscriber(func(k types.KLine) {
|
||||||
v := s.mapper(k)
|
v := s.mapper(k)
|
||||||
s.slice.Push(v)
|
s.PushAndEmit(v)
|
||||||
s.EmitUpdate(v)
|
|
||||||
})
|
})
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PriceStream) PushAndEmit(v float64) {
|
||||||
|
s.slice.Push(v)
|
||||||
|
s.EmitUpdate(v)
|
||||||
|
}
|
||||||
|
|
||||||
func ClosePrices(source KLineSubscription) *PriceStream {
|
func ClosePrices(source KLineSubscription) *PriceStream {
|
||||||
return Price(source, KLineClosePriceMapper)
|
return Price(source, KLineClosePriceMapper)
|
||||||
}
|
}
|
||||||
|
|
48
pkg/indicator/subtract.go
Normal file
48
pkg/indicator/subtract.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/c9s/bbgo/pkg/datatype/floats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SubtractStream subscribes 2 upstream data, and then subtract these 2 values
|
||||||
|
type SubtractStream struct {
|
||||||
|
Float64Series
|
||||||
|
|
||||||
|
a, b floats.Slice
|
||||||
|
i int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract creates the SubtractStream object
|
||||||
|
// subtract := Subtract(longEWMA, shortEWMA)
|
||||||
|
func Subtract(a, b Float64Source) *SubtractStream {
|
||||||
|
s := &SubtractStream{
|
||||||
|
Float64Series: NewFloat64Series(),
|
||||||
|
}
|
||||||
|
|
||||||
|
a.OnUpdate(func(v float64) {
|
||||||
|
s.a.Push(v)
|
||||||
|
s.calculate()
|
||||||
|
})
|
||||||
|
b.OnUpdate(func(v float64) {
|
||||||
|
s.b.Push(v)
|
||||||
|
s.calculate()
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubtractStream) calculate() {
|
||||||
|
if s.a.Length() != s.b.Length() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.a.Length() > s.slice.Length() {
|
||||||
|
var numNewElems = s.a.Length() - s.slice.Length()
|
||||||
|
var tailA = s.a.Tail(numNewElems)
|
||||||
|
var tailB = s.b.Tail(numNewElems)
|
||||||
|
var tailC = tailA.Sub(tailB)
|
||||||
|
for _, f := range tailC {
|
||||||
|
s.slice.Push(f)
|
||||||
|
s.EmitUpdate(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
pkg/indicator/types.go
Normal file
17
pkg/indicator/types.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import "github.com/c9s/bbgo/pkg/types"
|
||||||
|
|
||||||
|
type Float64Calculator interface {
|
||||||
|
Calculate(x float64) float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Float64Source interface {
|
||||||
|
types.Series
|
||||||
|
OnUpdate(f func(v float64))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Float64Subscription interface {
|
||||||
|
types.Series
|
||||||
|
AddSubscriber(f func(v float64))
|
||||||
|
}
|
|
@ -1 +1,15 @@
|
||||||
package indicator
|
package indicator
|
||||||
|
|
||||||
|
func max(x, y int) int {
|
||||||
|
if x > y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
19
pkg/indicator/v2.go
Normal file
19
pkg/indicator/v2.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
/*
|
||||||
|
NEW INDICATOR DESIGN:
|
||||||
|
|
||||||
|
klines := kLines(marketDataStream)
|
||||||
|
closePrices := closePrices(klines)
|
||||||
|
macd := MACD(klines, {Fast: 12, Slow: 10})
|
||||||
|
|
||||||
|
equals to:
|
||||||
|
|
||||||
|
klines := KLines(marketDataStream)
|
||||||
|
closePrices := ClosePrice(klines)
|
||||||
|
fastEMA := EMA(closePrices, 7)
|
||||||
|
slowEMA := EMA(closePrices, 25)
|
||||||
|
macd := Subtract(fastEMA, slowEMA)
|
||||||
|
signal := EMA(macd, 16)
|
||||||
|
histogram := Subtract(macd, signal)
|
||||||
|
*/
|
12
pkg/indicator/v2_atr.go
Normal file
12
pkg/indicator/v2_atr.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
type ATRStream struct {
|
||||||
|
// embedded struct
|
||||||
|
*RMAStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func ATR2(source KLineSubscription, window int) *ATRStream {
|
||||||
|
tr := TR2(source)
|
||||||
|
rma := RMA2(tr, window, true)
|
||||||
|
return &ATRStream{RMAStream: rma}
|
||||||
|
}
|
81
pkg/indicator/v2_atr_test.go
Normal file
81
pkg/indicator/v2_atr_test.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
||||||
|
"low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84],
|
||||||
|
"close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99,
|
||||||
|
|
||||||
|
39693.79, 39827.96, 40074.94, 40059.84]
|
||||||
|
}
|
||||||
|
|
||||||
|
high = pd.Series(data['high'])
|
||||||
|
low = pd.Series(data['low'])
|
||||||
|
close = pd.Series(data['close'])
|
||||||
|
result = ta.atr(high, low, close, length=14)
|
||||||
|
print(result)
|
||||||
|
*/
|
||||||
|
func Test_ATR2(t *testing.T) {
|
||||||
|
var bytes = []byte(`{
|
||||||
|
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
||||||
|
"low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84],
|
||||||
|
"close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, 39693.79, 39827.96, 40074.94, 40059.84]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var buildKLines = func(bytes []byte) (kLines []types.KLine) {
|
||||||
|
var prices map[string][]fixedpoint.Value
|
||||||
|
_ = json.Unmarshal(bytes, &prices)
|
||||||
|
for i, h := range prices["high"] {
|
||||||
|
kLine := types.KLine{High: h, Low: prices["low"][i], Close: prices["close"][i]}
|
||||||
|
kLines = append(kLines, kLine)
|
||||||
|
}
|
||||||
|
return kLines
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
kLines []types.KLine
|
||||||
|
window int
|
||||||
|
want float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test_binance_btcusdt_1h",
|
||||||
|
kLines: buildKLines(bytes),
|
||||||
|
window: 14,
|
||||||
|
want: 367.913903,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
stream := &types.StandardStream{}
|
||||||
|
|
||||||
|
kLines := KLines(stream)
|
||||||
|
atr := ATR2(kLines, tt.window)
|
||||||
|
|
||||||
|
for _, k := range tt.kLines {
|
||||||
|
stream.EmitKLineClosed(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := atr.Last()
|
||||||
|
diff := math.Trunc((got-tt.want)*100) / 100
|
||||||
|
if diff != 0 {
|
||||||
|
t.Errorf("ATR2() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
36
pkg/indicator/v2_ewma.go
Normal file
36
pkg/indicator/v2_ewma.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
type EWMAStream struct {
|
||||||
|
Float64Series
|
||||||
|
|
||||||
|
window int
|
||||||
|
multiplier float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func EWMA2(source Float64Source, window int) *EWMAStream {
|
||||||
|
s := &EWMAStream{
|
||||||
|
Float64Series: NewFloat64Series(),
|
||||||
|
window: window,
|
||||||
|
multiplier: 2.0 / float64(1+window),
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub, ok := source.(Float64Subscription); ok {
|
||||||
|
sub.AddSubscriber(s.calculateAndPush)
|
||||||
|
} else {
|
||||||
|
source.OnUpdate(s.calculateAndPush)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EWMAStream) calculateAndPush(v float64) {
|
||||||
|
v2 := s.calculate(v)
|
||||||
|
s.slice.Push(v2)
|
||||||
|
s.EmitUpdate(v2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EWMAStream) calculate(v float64) float64 {
|
||||||
|
last := s.slice.Last()
|
||||||
|
m := s.multiplier
|
||||||
|
return (1.0-m)*last + m*v
|
||||||
|
}
|
69
pkg/indicator/v2_rma.go
Normal file
69
pkg/indicator/v2_rma.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
type RMAStream struct {
|
||||||
|
// embedded structs
|
||||||
|
Float64Series
|
||||||
|
|
||||||
|
// config fields
|
||||||
|
Adjust bool
|
||||||
|
|
||||||
|
window int
|
||||||
|
counter int
|
||||||
|
sum, previous float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func RMA2(source Float64Source, window int, adjust bool) *RMAStream {
|
||||||
|
s := &RMAStream{
|
||||||
|
Float64Series: NewFloat64Series(),
|
||||||
|
window: window,
|
||||||
|
Adjust: adjust,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub, ok := source.(Float64Subscription); ok {
|
||||||
|
sub.AddSubscriber(s.calculateAndPush)
|
||||||
|
} else {
|
||||||
|
source.OnUpdate(s.calculateAndPush)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RMAStream) calculateAndPush(v float64) {
|
||||||
|
v2 := s.calculate(v)
|
||||||
|
s.slice.Push(v2)
|
||||||
|
s.EmitUpdate(v2)
|
||||||
|
s.truncate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RMAStream) calculate(x float64) float64 {
|
||||||
|
lambda := 1 / float64(s.window)
|
||||||
|
tmp := 0.0
|
||||||
|
if s.counter == 0 {
|
||||||
|
s.sum = 1
|
||||||
|
tmp = x
|
||||||
|
} else {
|
||||||
|
if s.Adjust {
|
||||||
|
s.sum = s.sum*(1-lambda) + 1
|
||||||
|
tmp = s.previous + (x-s.previous)/s.sum
|
||||||
|
} else {
|
||||||
|
tmp = s.previous*(1-lambda) + x*lambda
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.counter++
|
||||||
|
|
||||||
|
if s.counter < s.window {
|
||||||
|
// we can use x, but we need to use 0. to make the same behavior as the result from python pandas_ta
|
||||||
|
s.slice.Push(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.slice.Push(tmp)
|
||||||
|
s.previous = tmp
|
||||||
|
|
||||||
|
return tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RMAStream) truncate() {
|
||||||
|
if len(s.slice) > MaxNumOfRMA {
|
||||||
|
s.slice = s.slice[MaxNumOfRMATruncateSize-1:]
|
||||||
|
}
|
||||||
|
}
|
56
pkg/indicator/v2_rsi.go
Normal file
56
pkg/indicator/v2_rsi.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
type RSIStream struct {
|
||||||
|
// embedded structs
|
||||||
|
Float64Series
|
||||||
|
|
||||||
|
// config fields
|
||||||
|
window int
|
||||||
|
|
||||||
|
// private states
|
||||||
|
source Float64Source
|
||||||
|
}
|
||||||
|
|
||||||
|
func RSI2(source Float64Source, window int) *RSIStream {
|
||||||
|
s := &RSIStream{
|
||||||
|
source: source,
|
||||||
|
Float64Series: NewFloat64Series(),
|
||||||
|
window: window,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sub, ok := source.(Float64Subscription); ok {
|
||||||
|
sub.AddSubscriber(s.calculateAndPush)
|
||||||
|
} else {
|
||||||
|
source.OnUpdate(s.calculateAndPush)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RSIStream) calculate(_ float64) float64 {
|
||||||
|
var gainSum, lossSum float64
|
||||||
|
var sourceLen = s.source.Length()
|
||||||
|
var limit = min(s.window, sourceLen)
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
value := s.source.Index(i)
|
||||||
|
prev := s.source.Index(i + 1)
|
||||||
|
change := value - prev
|
||||||
|
if change >= 0 {
|
||||||
|
gainSum += change
|
||||||
|
} else {
|
||||||
|
lossSum += -change
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
avgGain := gainSum / float64(limit)
|
||||||
|
avgLoss := lossSum / float64(limit)
|
||||||
|
rs := avgGain / avgLoss
|
||||||
|
rsi := 100.0 - (100.0 / (1.0 + rs))
|
||||||
|
return rsi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RSIStream) calculateAndPush(x float64) {
|
||||||
|
rsi := s.calculate(x)
|
||||||
|
s.slice.Push(rsi)
|
||||||
|
s.EmitUpdate(rsi)
|
||||||
|
}
|
87
pkg/indicator/v2_rsi_test.go
Normal file
87
pkg/indicator/v2_rsi_test.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/datatype/floats"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_RSI2(t *testing.T) {
|
||||||
|
// test case from https://school.stockcharts.com/doku.php?id=technical_indicators:relative_strength_index_rsi
|
||||||
|
var data = []byte(`[44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41, 46.22, 45.64, 46.21, 46.25, 45.71, 46.45, 45.78, 45.35, 44.03, 44.18, 44.22, 44.57, 43.42, 42.66, 43.13]`)
|
||||||
|
var values []float64
|
||||||
|
err := json.Unmarshal(data, &values)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
values []float64
|
||||||
|
window int
|
||||||
|
want floats.Slice
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "RSI",
|
||||||
|
values: values,
|
||||||
|
window: 14,
|
||||||
|
want: floats.Slice{
|
||||||
|
100.000000,
|
||||||
|
99.439336,
|
||||||
|
99.440090,
|
||||||
|
98.251826,
|
||||||
|
98.279242,
|
||||||
|
98.297781,
|
||||||
|
98.307626,
|
||||||
|
98.319149,
|
||||||
|
98.334036,
|
||||||
|
98.342426,
|
||||||
|
97.951933,
|
||||||
|
97.957908,
|
||||||
|
97.108036,
|
||||||
|
97.147514,
|
||||||
|
70.464135,
|
||||||
|
70.020964,
|
||||||
|
69.831224,
|
||||||
|
80.567686,
|
||||||
|
73.333333,
|
||||||
|
59.806295,
|
||||||
|
62.528217,
|
||||||
|
60.000000,
|
||||||
|
48.477752,
|
||||||
|
53.878407,
|
||||||
|
48.952381,
|
||||||
|
43.862816,
|
||||||
|
37.732919,
|
||||||
|
32.263514,
|
||||||
|
32.718121,
|
||||||
|
38.142620,
|
||||||
|
31.748252,
|
||||||
|
25.099602,
|
||||||
|
30.217670,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// RSI2()
|
||||||
|
prices := &PriceStream{}
|
||||||
|
rsi := RSI2(prices, tt.window)
|
||||||
|
|
||||||
|
t.Logf("data length: %d", len(tt.values))
|
||||||
|
for _, price := range tt.values {
|
||||||
|
prices.PushAndEmit(price)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, floats.Slice(tt.values), prices.slice)
|
||||||
|
|
||||||
|
if assert.Equal(t, len(tt.want), len(rsi.slice)) {
|
||||||
|
for i, v := range tt.want {
|
||||||
|
assert.InDelta(t, v, rsi.slice[i], 0.000001, "Expected rsi.slice[%d] to be %v, but got %v", i, v, rsi.slice[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSubtract(t *testing.T) {
|
func Test_v2_Subtract(t *testing.T) {
|
||||||
stream := &types.StandardStream{}
|
stream := &types.StandardStream{}
|
||||||
kLines := KLines(stream)
|
kLines := KLines(stream)
|
||||||
closePrices := ClosePrices(kLines)
|
closePrices := ClosePrices(kLines)
|
||||||
|
@ -25,6 +25,6 @@ func TestSubtract(t *testing.T) {
|
||||||
t.Logf("slowEMA: %+v", slowEMA.slice)
|
t.Logf("slowEMA: %+v", slowEMA.slice)
|
||||||
|
|
||||||
assert.Equal(t, len(subtract.a), len(subtract.b))
|
assert.Equal(t, len(subtract.a), len(subtract.b))
|
||||||
assert.Equal(t, len(subtract.a), len(subtract.c))
|
assert.Equal(t, len(subtract.a), len(subtract.slice))
|
||||||
assert.InDelta(t, subtract.c[0], subtract.a[0]-subtract.b[0], 0.0001)
|
assert.InDelta(t, subtract.slice[0], subtract.a[0]-subtract.b[0], 0.0001)
|
||||||
}
|
}
|
47
pkg/indicator/v2_tr.go
Normal file
47
pkg/indicator/v2_tr.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This TRStream calculates the ATR first
|
||||||
|
type TRStream struct {
|
||||||
|
// embedded struct
|
||||||
|
Float64Series
|
||||||
|
|
||||||
|
// private states
|
||||||
|
previousClose float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TR2(source KLineSubscription) *TRStream {
|
||||||
|
s := &TRStream{
|
||||||
|
Float64Series: NewFloat64Series(),
|
||||||
|
}
|
||||||
|
|
||||||
|
source.AddSubscriber(func(k types.KLine) {
|
||||||
|
s.calculateAndPush(k.High.Float64(), k.Low.Float64(), k.Close.Float64())
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TRStream) calculateAndPush(high, low, cls float64) {
|
||||||
|
if s.previousClose == .0 {
|
||||||
|
s.previousClose = cls
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trueRange := high - low
|
||||||
|
hc := math.Abs(high - s.previousClose)
|
||||||
|
lc := math.Abs(low - s.previousClose)
|
||||||
|
if trueRange < hc {
|
||||||
|
trueRange = hc
|
||||||
|
}
|
||||||
|
if trueRange < lc {
|
||||||
|
trueRange = lc
|
||||||
|
}
|
||||||
|
|
||||||
|
s.previousClose = cls
|
||||||
|
s.EmitUpdate(trueRange)
|
||||||
|
}
|
82
pkg/indicator/v2_tr_test.go
Normal file
82
pkg/indicator/v2_tr_test.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package indicator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
python
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pandas_ta as ta
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
||||||
|
"low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84],
|
||||||
|
"close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99,
|
||||||
|
|
||||||
|
39693.79, 39827.96, 40074.94, 40059.84]
|
||||||
|
}
|
||||||
|
|
||||||
|
high = pd.Series(data['high'])
|
||||||
|
low = pd.Series(data['low'])
|
||||||
|
close = pd.Series(data['close'])
|
||||||
|
result = ta.atr(high, low, close, length=14)
|
||||||
|
print(result)
|
||||||
|
*/
|
||||||
|
func Test_TR_and_RMA(t *testing.T) {
|
||||||
|
var bytes = []byte(`{
|
||||||
|
"high": [40145.0, 40186.36, 40196.39, 40344.6, 40245.48, 40273.24, 40464.0, 40699.0, 40627.48, 40436.31, 40370.0, 40376.8, 40227.03, 40056.52, 39721.7, 39597.94, 39750.15, 39927.0, 40289.02, 40189.0],
|
||||||
|
"low": [39870.71, 39834.98, 39866.31, 40108.31, 40016.09, 40094.66, 40105.0, 40196.48, 40154.99, 39800.0, 39959.21, 39922.98, 39940.02, 39632.0, 39261.39, 39254.63, 39473.91, 39555.51, 39819.0, 40006.84],
|
||||||
|
"close": [40105.78, 39935.23, 40183.97, 40182.03, 40212.26, 40149.99, 40378.0, 40618.37, 40401.03, 39990.39, 40179.13, 40097.23, 40014.72, 39667.85, 39303.1, 39519.99, 39693.79, 39827.96, 40074.94, 40059.84]
|
||||||
|
}`)
|
||||||
|
|
||||||
|
var buildKLines = func(bytes []byte) (kLines []types.KLine) {
|
||||||
|
var prices map[string][]fixedpoint.Value
|
||||||
|
_ = json.Unmarshal(bytes, &prices)
|
||||||
|
for i, h := range prices["high"] {
|
||||||
|
kLine := types.KLine{High: h, Low: prices["low"][i], Close: prices["close"][i]}
|
||||||
|
kLines = append(kLines, kLine)
|
||||||
|
}
|
||||||
|
return kLines
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
kLines []types.KLine
|
||||||
|
window int
|
||||||
|
want float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test_binance_btcusdt_1h",
|
||||||
|
kLines: buildKLines(bytes),
|
||||||
|
window: 14,
|
||||||
|
want: 367.913903,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
stream := &types.StandardStream{}
|
||||||
|
|
||||||
|
kLines := KLines(stream)
|
||||||
|
atr := TR2(kLines)
|
||||||
|
rma := RMA2(atr, tt.window, true)
|
||||||
|
|
||||||
|
for _, k := range tt.kLines {
|
||||||
|
stream.EmitKLineClosed(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := rma.Last()
|
||||||
|
diff := math.Trunc((got-tt.want)*100) / 100
|
||||||
|
if diff != 0 {
|
||||||
|
t.Errorf("RMA(TR()) = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -341,7 +341,7 @@ func (s *FailedBreakHigh) detectMacdDivergence() {
|
||||||
var histogramPivots floats.Slice
|
var histogramPivots floats.Slice
|
||||||
for i := pivotWindow; i > 0 && i < len(histogramValues); i++ {
|
for i := pivotWindow; i > 0 && i < len(histogramValues); i++ {
|
||||||
// find positive histogram and the top
|
// find positive histogram and the top
|
||||||
pivot, ok := floats.CalculatePivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool {
|
pivot, ok := floats.FindPivot(histogramValues[0:i], pivotWindow, pivotWindow, func(a, pivot float64) bool {
|
||||||
return pivot > 0 && pivot > a
|
return pivot > 0 && pivot > a
|
||||||
})
|
})
|
||||||
if ok {
|
if ok {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user