Implement EMA

This commit is contained in:
Alexander Zobnin
2021-05-28 10:11:41 +03:00
parent ec38c3981c
commit 31e989d28f
2 changed files with 87 additions and 11 deletions

View File

@@ -57,17 +57,18 @@ var frontendFuncMap map[string]bool
func init() {
seriesFuncMap = map[string]DataProcessingFunc{
"groupBy": applyGroupBy,
"scale": applyScale,
"offset": applyOffset,
"delta": applyDelta,
"rate": applyRate,
"movingAverage": applyMovingAverage,
"removeAboveValue": applyRemoveAboveValue,
"removeBelowValue": applyRemoveBelowValue,
"transformNull": applyTransformNull,
"percentile": applyPercentile,
"timeShift": applyTimeShiftPost,
"groupBy": applyGroupBy,
"scale": applyScale,
"offset": applyOffset,
"delta": applyDelta,
"rate": applyRate,
"movingAverage": applyMovingAverage,
"exponentialMovingAverage": applyExponentialMovingAverage,
"removeAboveValue": applyRemoveAboveValue,
"removeBelowValue": applyRemoveBelowValue,
"transformNull": applyTransformNull,
"percentile": applyPercentile,
"timeShift": applyTimeShiftPost,
}
aggFuncMap = map[string]AggDataProcessingFunc{
@@ -256,6 +257,15 @@ func applyMovingAverage(series timeseries.TimeSeries, params ...interface{}) (ti
return series.SimpleMovingAverage(n), nil
}
func applyExponentialMovingAverage(series timeseries.TimeSeries, params ...interface{}) (timeseries.TimeSeries, error) {
n, err := MustFloat64(params[0])
if err != nil {
return nil, errParsingFunctionParam(err)
}
return series.ExponentialMovingAverage(n), nil
}
func applyAggregateBy(series []*timeseries.TimeSeriesData, params ...interface{}) ([]*timeseries.TimeSeriesData, error) {
pInterval, err := MustString(params[0])
pAgg, err := MustString(params[1])

View File

@@ -12,6 +12,9 @@ func (ts TimeSeries) SimpleMovingAverage(n int) TimeSeries {
// It's not possible to calculate MA if n greater than number of points
n = int(math.Min(float64(ts.Len()), float64(n)))
// It's not a most performant way to caclulate MA, but since it's most straightforward, it's easy to read.
// Should work fine on relatively small n, which is 90% of cases. Another way is caclulate window average, then add
// next point ( (window sum + point value) / (count + 1) ) and remove the first one.
for i := n; i < ts.Len(); i++ {
windowEdgeRight := i
windowCount := 0
@@ -33,3 +36,66 @@ func (ts TimeSeries) SimpleMovingAverage(n int) TimeSeries {
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]}
emaPrev := *ts[0].Value
var emaCurrent float64
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
}