forked from c9s/bbgo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ewma.go
145 lines (114 loc) · 3.41 KB
/
ewma.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package indicator
import (
"math"
"time"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/types"
)
const MaxEWMAValues = 1_000
const EWMAValueTruncateSize = 500
//go:generate callbackgen -type EWMA
type EWMA struct {
types.IntervalWindow
Values types.Float64Slice
LastOpenTime time.Time
UpdateCallbacks []func(value float64)
}
func (inc *EWMA) Update(value float64) {
var multiplier = 2.0 / float64(1+inc.Window)
if len(inc.Values) == 0 {
inc.Values.Push(value)
return
} else if len(inc.Values) > MaxEWMAValues {
inc.Values = inc.Values[EWMAValueTruncateSize:]
}
ema := (1-multiplier)*inc.Last() + multiplier*value
inc.Values.Push(ema)
}
func (inc *EWMA) Last() float64 {
if len(inc.Values) == 0 {
return 0
}
return inc.Values[len(inc.Values)-1]
}
func (inc *EWMA) calculateAndUpdate(allKLines []types.KLine) {
if len(allKLines) < inc.Window {
// we can't calculate
return
}
var priceF = KLineClosePriceMapper
var dataLen = len(allKLines)
var multiplier = 2.0 / (float64(inc.Window) + 1)
// init the values from the kline data
var from = 1
if len(inc.Values) == 0 {
// for the first value, we should use the close price
inc.Values = []float64{priceF(allKLines[0])}
} else {
// from = len(inc.Values)
// update ewma with the existing values
for i := dataLen - 1; i > 0; i-- {
var k = allKLines[i]
if k.StartTime.After(inc.LastOpenTime) {
from = i
} else {
break
}
}
}
for i := from; i < dataLen; i++ {
var k = allKLines[i]
var ewma = priceF(k)*multiplier + (1-multiplier)*inc.Values[i-1]
inc.Values.Push(ewma)
inc.LastOpenTime = k.StartTime
inc.EmitUpdate(ewma)
}
if len(inc.Values) != dataLen {
log.Warnf("%s EMA (%d) value length (%d) != all kline data length (%d)", inc.Interval, inc.Window, len(inc.Values), dataLen)
}
v1 := math.Floor(inc.Values[len(inc.Values)-1]*100.0) / 100.0
v2 := math.Floor(CalculateKLinesEMA(allKLines, priceF, inc.Window)*100.0) / 100.0
if v1 != v2 {
log.Warnf("ACCUMULATED %s EMA (%d) %f != EMA %f", inc.Interval, inc.Window, v1, v2)
}
}
func CalculateKLinesEMA(allKLines []types.KLine, priceF KLinePriceMapper, window int) float64 {
var multiplier = 2.0 / (float64(window) + 1)
return ewma(MapKLinePrice(allKLines, priceF), multiplier)
}
// see https://www.investopedia.com/ask/answers/122314/what-exponential-moving-average-ema-formula-and-how-ema-calculated.asp
func ewma(prices []float64, multiplier float64) float64 {
var end = len(prices) - 1
if end == 0 {
return prices[0]
}
return prices[end]*multiplier + (1-multiplier)*ewma(prices[:end], multiplier)
}
type KLinePriceMapper func(k types.KLine) float64
func KLineOpenPriceMapper(k types.KLine) float64 {
return k.Open
}
func KLineClosePriceMapper(k types.KLine) float64 {
return k.Close
}
func KLineTypicalPriceMapper(k types.KLine) float64 {
return (k.High + k.Low + k.Close) / float64(3)
}
func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) {
for _, k := range kLines {
prices = append(prices, f(k))
}
return prices
}
type KLineWindowUpdater interface {
OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow))
}
func (inc *EWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}
inc.calculateAndUpdate(window)
}
func (inc *EWMA) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}