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 calculated 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 }