From 8bd5cdfe173a3797818e8fa9cada3a18fde80170 Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Sat, 16 Jul 2016 13:10:44 -0700 Subject: [PATCH] exp moving average, renaming moving average to simple moving average. --- exp_moving_average_series.go | 88 +++++++++++++++++++ exp_moving_average_series_test.go | 31 +++++++ ...ries.go => simple_moving_average_series.go | 22 ++--- ...go => simple_moving_average_series_test.go | 12 +-- testserver/main.go | 24 ++++- 5 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 exp_moving_average_series.go create mode 100644 exp_moving_average_series_test.go rename moving_average_series.go => simple_moving_average_series.go (73%) rename moving_average_series_test.go => simple_moving_average_series_test.go (85%) diff --git a/exp_moving_average_series.go b/exp_moving_average_series.go new file mode 100644 index 0000000..78b8eca --- /dev/null +++ b/exp_moving_average_series.go @@ -0,0 +1,88 @@ +package chart + +const ( + // DefaultExponentialMovingAverageSigma is the default exponential smoothing factor. + DefaultExponentialMovingAverageSigma = 0.25 +) + +// ExponentialMovingAverageSeries is a computed series. +type ExponentialMovingAverageSeries struct { + Name string + Style Style + YAxis YAxisType + + // Sigma is the 'smoothing factor' parameter. + Sigma float64 + InnerSeries ValueProvider + + valueBuffer []float64 +} + +// GetName returns the name of the time series. +func (mas ExponentialMovingAverageSeries) GetName() string { + return mas.Name +} + +// GetStyle returns the line style. +func (mas ExponentialMovingAverageSeries) GetStyle() Style { + return mas.Style +} + +// GetYAxis returns which YAxis the series draws on. +func (mas ExponentialMovingAverageSeries) GetYAxis() YAxisType { + return mas.YAxis +} + +// Len returns the number of elements in the series. +func (mas *ExponentialMovingAverageSeries) Len() int { + return mas.InnerSeries.Len() +} + +// GetSigma returns the smoothing factor for the serise. +func (mas ExponentialMovingAverageSeries) GetSigma(defaults ...float64) float64 { + if mas.Sigma == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return DefaultExponentialMovingAverageSigma + } + return mas.Sigma +} + +// GetValue gets a value at a given index. +func (mas *ExponentialMovingAverageSeries) GetValue(index int) (x float64, y float64) { + if mas.InnerSeries == nil { + return + } + if mas.valueBuffer == nil || index == 0 { + mas.valueBuffer = make([]float64, mas.InnerSeries.Len()) + } + vx, vy := mas.InnerSeries.GetValue(index) + x = vx + if index == 0 { + mas.valueBuffer[0] = vy + y = vy + return + } + + sig := mas.GetSigma() + mas.valueBuffer[index] = sig*vy + ((1.0 - sig) * mas.valueBuffer[index-1]) + y = mas.valueBuffer[index] + return +} + +// GetLastValue computes the last moving average value but walking back window size samples, +// and recomputing the last moving average chunk. +func (mas ExponentialMovingAverageSeries) GetLastValue() (x float64, y float64) { + if mas.InnerSeries == nil { + return + } + + return 0.0, 0.0 //this one is going to be a bitch. +} + +// Render renders the series. +func (mas *ExponentialMovingAverageSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { + style := mas.Style.WithDefaultsFrom(defaults) + DrawLineSeries(r, canvasBox, xrange, yrange, style, mas) +} diff --git a/exp_moving_average_series_test.go b/exp_moving_average_series_test.go new file mode 100644 index 0000000..09bd9f3 --- /dev/null +++ b/exp_moving_average_series_test.go @@ -0,0 +1,31 @@ +package chart + +import ( + "math" + "testing" + + "github.com/blendlabs/go-assert" +) + +func TestExponentialMovingAverageSeries(t *testing.T) { + assert := assert.New(t) + + mockSeries := mockValueProvider{ + Seq(1.0, 10.0), + Seq(10, 1.0), + } + assert.Equal(10, mockSeries.Len()) + + mas := &ExponentialMovingAverageSeries{ + InnerSeries: mockSeries, + } + + var yvalues []float64 + for x := 0; x < mas.Len(); x++ { + _, y := mas.GetValue(x) + yvalues = append(yvalues, y) + } + + assert.Equal(10.0, yvalues[0]) + assert.True(math.Abs(yvalues[9]-3.77) < 0.01) +} diff --git a/moving_average_series.go b/simple_moving_average_series.go similarity index 73% rename from moving_average_series.go rename to simple_moving_average_series.go index 2fe075a..4b3a701 100644 --- a/moving_average_series.go +++ b/simple_moving_average_series.go @@ -5,8 +5,8 @@ const ( DefaultMovingAverageWindowSize = 5 ) -// MovingAverageSeries is a computed series. -type MovingAverageSeries struct { +// SimpleMovingAverageSeries is a computed series. +type SimpleMovingAverageSeries struct { Name string Style Style YAxis YAxisType @@ -18,27 +18,27 @@ type MovingAverageSeries struct { } // GetName returns the name of the time series. -func (mas MovingAverageSeries) GetName() string { +func (mas SimpleMovingAverageSeries) GetName() string { return mas.Name } // GetStyle returns the line style. -func (mas MovingAverageSeries) GetStyle() Style { +func (mas SimpleMovingAverageSeries) GetStyle() Style { return mas.Style } // GetYAxis returns which YAxis the series draws on. -func (mas MovingAverageSeries) GetYAxis() YAxisType { +func (mas SimpleMovingAverageSeries) GetYAxis() YAxisType { return mas.YAxis } // Len returns the number of elements in the series. -func (mas *MovingAverageSeries) Len() int { +func (mas *SimpleMovingAverageSeries) Len() int { return mas.InnerSeries.Len() } // GetValue gets a value at a given index. -func (mas *MovingAverageSeries) GetValue(index int) (x float64, y float64) { +func (mas *SimpleMovingAverageSeries) GetValue(index int) (x float64, y float64) { if mas.InnerSeries == nil { return } @@ -57,7 +57,7 @@ func (mas *MovingAverageSeries) GetValue(index int) (x float64, y float64) { // GetLastValue computes the last moving average value but walking back window size samples, // and recomputing the last moving average chunk. -func (mas MovingAverageSeries) GetLastValue() (x float64, y float64) { +func (mas SimpleMovingAverageSeries) GetLastValue() (x float64, y float64) { if mas.InnerSeries == nil { return } @@ -78,7 +78,7 @@ func (mas MovingAverageSeries) GetLastValue() (x float64, y float64) { } // GetWindowSize returns the window size. -func (mas MovingAverageSeries) GetWindowSize(defaults ...int) int { +func (mas SimpleMovingAverageSeries) GetWindowSize(defaults ...int) int { if mas.WindowSize == 0 { if len(defaults) > 0 { return defaults[0] @@ -88,7 +88,7 @@ func (mas MovingAverageSeries) GetWindowSize(defaults ...int) int { return mas.WindowSize } -func (mas MovingAverageSeries) getAverage(valueBuffer *RingBuffer) float64 { +func (mas SimpleMovingAverageSeries) getAverage(valueBuffer *RingBuffer) float64 { var accum float64 valueBuffer.Each(func(v interface{}) { if typed, isTyped := v.(float64); isTyped { @@ -99,7 +99,7 @@ func (mas MovingAverageSeries) getAverage(valueBuffer *RingBuffer) float64 { } // Render renders the series. -func (mas *MovingAverageSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { +func (mas *SimpleMovingAverageSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { style := mas.Style.WithDefaultsFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, mas) } diff --git a/moving_average_series_test.go b/simple_moving_average_series_test.go similarity index 85% rename from moving_average_series_test.go rename to simple_moving_average_series_test.go index 7d22776..7ef513d 100644 --- a/moving_average_series_test.go +++ b/simple_moving_average_series_test.go @@ -21,7 +21,7 @@ func (m mockValueProvider) GetValue(index int) (x, y float64) { return } -func TestMovingAverageSeriesGetValue(t *testing.T) { +func TestSimpleMovingAverageSeriesGetValue(t *testing.T) { assert := assert.New(t) mockSeries := mockValueProvider{ @@ -30,7 +30,7 @@ func TestMovingAverageSeriesGetValue(t *testing.T) { } assert.Equal(10, mockSeries.Len()) - mas := &MovingAverageSeries{ + mas := &SimpleMovingAverageSeries{ InnerSeries: mockSeries, WindowSize: 10, } @@ -52,7 +52,7 @@ func TestMovingAverageSeriesGetValue(t *testing.T) { assert.Equal(6.0, yvalues[8]) } -func TestMovingAverageSeriesGetLastValueWindowOverlap(t *testing.T) { +func TestSimpleMovingAverageSeriesGetLastValueWindowOverlap(t *testing.T) { assert := assert.New(t) mockSeries := mockValueProvider{ @@ -61,7 +61,7 @@ func TestMovingAverageSeriesGetLastValueWindowOverlap(t *testing.T) { } assert.Equal(10, mockSeries.Len()) - mas := &MovingAverageSeries{ + mas := &SimpleMovingAverageSeries{ InnerSeries: mockSeries, WindowSize: 15, } @@ -78,7 +78,7 @@ func TestMovingAverageSeriesGetLastValueWindowOverlap(t *testing.T) { assert.Equal(yvalues[len(yvalues)-1], ly) } -func TestMovingAverageSeriesGetLastValue(t *testing.T) { +func TestSimpleMovingAverageSeriesGetLastValue(t *testing.T) { assert := assert.New(t) mockSeries := mockValueProvider{ @@ -87,7 +87,7 @@ func TestMovingAverageSeriesGetLastValue(t *testing.T) { } assert.Equal(100, mockSeries.Len()) - mas := &MovingAverageSeries{ + mas := &SimpleMovingAverageSeries{ InnerSeries: mockSeries, WindowSize: 10, } diff --git a/testserver/main.go b/testserver/main.go index 4002b38..c6be9d1 100644 --- a/testserver/main.go +++ b/testserver/main.go @@ -60,7 +60,7 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult { }, } - s1ma := &chart.BollingerBandsSeries{ + s1bb := &chart.BollingerBandsSeries{ Name: "BBS", Style: chart.Style{ Show: true, @@ -72,6 +72,24 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult { InnerSeries: s1, } + s1sma := &chart.SimpleMovingAverageSeries{ + Style: chart.Style{ + Show: true, + StrokeColor: drawing.ColorRed, + StrokeDashArray: []float64{5.0, 5.0}, + }, + InnerSeries: s1, + } + + s1ema := &chart.ExponentialMovingAverageSeries{ + Style: chart.Style{ + Show: true, + StrokeColor: drawing.ColorBlue, + StrokeDashArray: []float64{5.0, 5.0}, + }, + InnerSeries: s1, + } + c := chart.Chart{ Title: "A Test Chart", TitleStyle: chart.Style{ @@ -102,9 +120,11 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult { }, }, Series: []chart.Series{ + s1bb, s1, - s1ma, s1lv, + s1sma, + s1ema, }, }