/
moving_average.go
131 lines (115 loc) · 3.8 KB
/
moving_average.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
package timeseries
import "math"
func (ts TimeSeries) SimpleMovingAverage(n int) TimeSeries {
if ts.Len() == 0 {
return ts
}
sma := []TimePoint{ts[0]}
// It's not possible to calculate MA if n greater than number of points
n = int(math.Min(float64(ts.Len()), float64(n)))
// Initial window, use simple moving average
windowCount := 0
var windowSum float64 = 0
for i := n; i > 0; i-- {
point := ts[n-i]
if point.Value != nil {
windowSum += *point.Value
windowCount++
}
}
if windowCount > 0 {
windowAvg := windowSum / float64(windowCount)
// Actually, we should set timestamp from datapoints[n-1] and start calculation of SMA from n.
// But in order to start SMA from first point (not from Nth) we should expand time range and request N additional
// points outside left side of range. We can't do that, so this trick is used for pretty view of first N points.
// We calculate AVG for first N points, but then start from 2nd point, not from Nth. In general, it means we
// assume that previous N points (0-N, 0-(N-1), ..., 0-1) have the same average value as a first N points.
sma[0] = TimePoint{Time: ts[0].Time, Value: &windowAvg}
}
for i := 1; i < ts.Len(); i++ {
leftEdge := int(math.Max(0, float64(i-n)))
point := ts[i]
leftPoint := ts[leftEdge]
// Remove left value
if leftPoint.Value != nil {
if windowCount > 0 {
if i < n {
windowSum -= windowSum / float64(windowCount)
} else {
windowSum -= *leftPoint.Value
}
windowCount--
}
}
// Insert next value
if point.Value != nil {
windowSum += *point.Value
windowCount++
windowAvg := windowSum / float64(windowCount)
value := windowAvg
sma = append(sma, TimePoint{Time: point.Time, Value: &value})
} else {
sma = append(sma, TimePoint{Time: point.Time, Value: nil})
}
}
return sma
}
func (ts TimeSeries) ExponentialMovingAverage(an float64) TimeSeries {
if ts.Len() == 0 {
return ts
}
// It's not possible to calculate MA if n greater than number of points
an = math.Min(float64(ts.Len()), an)
// alpha coefficient should be between 0 and 1. If provided n <= 1, then use it as alpha directly. Otherwise, it's a
// number of points in the window and alpha calculted from this information.
var a float64
var n int
ema := []TimePoint{ts[0]}
var emaCurrent float64
var emaPrev float64 = 0
if ts[0].Value != nil {
emaPrev = *ts[0].Value
}
if an > 1 {
// Calculate a from window size
a = 2 / (an + 1)
n = int(an)
// Initial window, use simple moving average
windowCount := 0
var windowSum float64 = 0
for i := n; i > 0; i-- {
point := ts[n-i]
if point.Value != nil {
windowSum += *point.Value
windowCount++
}
}
if windowCount > 0 {
windowAvg := windowSum / float64(windowCount)
// Actually, we should set timestamp from datapoints[n-1] and start calculation of EMA from n.
// But in order to start EMA from first point (not from Nth) we should expand time range and request N additional
// points outside left side of range. We can't do that, so this trick is used for pretty view of first N points.
// We calculate AVG for first N points, but then start from 2nd point, not from Nth. In general, it means we
// assume that previous N values (0-N, 0-(N-1), ..., 0-1) have the same average value as a first N values.
ema[0] = TimePoint{Time: ts[0].Time, Value: &windowAvg}
emaPrev = windowAvg
n = 1
}
} else {
// Use predefined a and start from 1st point (use it as initial EMA value)
a = an
n = 1
}
for i := n; i < ts.Len(); i++ {
point := ts[i]
if point.Value != nil {
emaCurrent = a*(*point.Value) + (1-a)*emaPrev
emaPrev = emaCurrent
value := emaCurrent
ema = append(ema, TimePoint{Time: point.Time, Value: &value})
} else {
ema = append(ema, TimePoint{Time: point.Time, Value: nil})
}
}
return ema
}