diff --git a/bollinger_band_series.go b/bollinger_band_series.go new file mode 100644 index 0000000..2894cdf --- /dev/null +++ b/bollinger_band_series.go @@ -0,0 +1,153 @@ +package chart + +import "math" + +// BollingerBandsSeries draws bollinger bands for an inner series. +// Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev. +type BollingerBandsSeries struct { + Name string + Style Style + YAxis YAxisType + + WindowSize int + K float64 + InnerSeries ValueProvider + + valueBuffer *RingBuffer +} + +// GetName returns the name of the time series. +func (bbs BollingerBandsSeries) GetName() string { + return bbs.Name +} + +// GetStyle returns the line style. +func (bbs BollingerBandsSeries) GetStyle() Style { + return bbs.Style +} + +// GetYAxis returns which YAxis the series draws on. +func (bbs BollingerBandsSeries) GetYAxis() YAxisType { + return bbs.YAxis +} + +// GetWindowSize returns the window size. +func (bbs BollingerBandsSeries) GetWindowSize(defaults ...int) int { + if bbs.WindowSize == 0 { + if len(defaults) > 0 { + return defaults[0] + } + return DefaultMovingAverageWindowSize + } + return bbs.WindowSize +} + +// Len returns the number of elements in the series. +func (bbs *BollingerBandsSeries) Len() int { + return bbs.InnerSeries.Len() +} + +// GetBoundedValue gets the bounded value for the series. +func (bbs *BollingerBandsSeries) GetBoundedValue(index int) (x, y1, y2 float64) { + if bbs.InnerSeries == nil { + return + } + if bbs.valueBuffer == nil || index == 0 { + bbs.valueBuffer = NewRingBufferWithCapacity(bbs.GetWindowSize()) + } + if bbs.valueBuffer.Len() >= bbs.GetWindowSize() { + bbs.valueBuffer.Dequeue() + } + px, py := bbs.InnerSeries.GetValue(index) + bbs.valueBuffer.Enqueue(py) + x = px + + ay := bbs.getAverage(bbs.valueBuffer) + std := bbs.getStdDev(bbs.valueBuffer) + + y1 = ay + (bbs.K * std) + y2 = ay - (bbs.K * std) + return +} + +// Render renders the series. +func (bbs BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { + s := bbs.Style.WithDefaultsFrom(defaults) + + r.SetStrokeColor(s.GetStrokeColor()) + r.SetStrokeDashArray(s.GetStrokeDashArray()) + r.SetStrokeWidth(s.GetStrokeWidth()) + r.SetFillColor(s.GetFillColor()) + + cb := canvasBox.Bottom + cl := canvasBox.Left + + v0x, v0y1, v0y2 := bbs.GetBoundedValue(0) + x0 := cl + xrange.Translate(v0x) + y0 := cb - yrange.Translate(v0y1) + + var vx, vy1, vy2 float64 + var x, y int + + xvalues := make([]float64, bbs.Len()) + xvalues[0] = v0x + y2values := make([]float64, bbs.Len()) + y2values[0] = v0y2 + + r.MoveTo(x0, y0) + for i := 1; i < bbs.Len(); i++ { + vx, vy1, vy2 = bbs.GetBoundedValue(i) + + xvalues[i] = vx + y2values[i] = vy2 + + x = cl + xrange.Translate(vx) + y = cb - yrange.Translate(vy1) + if i > bbs.GetWindowSize() { + r.LineTo(x, y) + } else { + r.MoveTo(x, y) + } + } + y = cb - yrange.Translate(vy2) + r.LineTo(x, y) + for i := bbs.Len() - 1; i >= bbs.GetWindowSize(); i-- { + vx, vy2 = xvalues[i], y2values[i] + x = cl + xrange.Translate(vx) + y = cb - yrange.Translate(vy2) + r.LineTo(x, y) + } + r.Close() + r.FillStroke() +} + +func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 { + var accum float64 + valueBuffer.Each(func(v interface{}) { + if typed, isTyped := v.(float64); isTyped { + accum += typed + } + }) + return accum / float64(valueBuffer.Len()) +} + +func (bbs BollingerBandsSeries) getVariance(valueBuffer *RingBuffer) float64 { + if valueBuffer.Len() == 0 { + return 0 + } + + var variance float64 + m := bbs.getAverage(valueBuffer) + + valueBuffer.Each(func(v interface{}) { + if n, isTyped := v.(float64); isTyped { + variance += (float64(n) - m) * (float64(n) - m) + } + }) + + return variance / float64(valueBuffer.Len()) +} + +func (bbs BollingerBandsSeries) getStdDev(valueBuffer *RingBuffer) float64 { + return math.Pow(bbs.getVariance(valueBuffer), 0.5) +} diff --git a/chart.go b/chart.go index dd96c14..bf82791 100644 --- a/chart.go +++ b/chart.go @@ -136,7 +136,27 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { for _, s := range c.Series { if s.GetStyle().IsZero() || s.GetStyle().Show { seriesAxis := s.GetYAxis() - if vp, isValueProvider := s.(ValueProvider); isValueProvider { + if bvp, isBoundedValueProvider := s.(BoundedValueProvider); isBoundedValueProvider { + seriesLength := bvp.Len() + for index := 0; index < seriesLength; index++ { + vx, vy1, vy2 := bvp.GetBoundedValue(index) + + minx = math.Min(minx, vx) + maxx = math.Max(maxx, vx) + + if seriesAxis == YAxisPrimary { + miny = math.Min(miny, vy1) + miny = math.Min(miny, vy2) + maxy = math.Max(maxy, vy1) + maxy = math.Max(maxy, vy2) + } else if seriesAxis == YAxisSecondary { + minya = math.Min(minya, vy1) + minya = math.Min(minya, vy2) + maxya = math.Max(maxya, vy1) + maxya = math.Max(maxya, vy2) + } + } + } else if vp, isValueProvider := s.(ValueProvider); isValueProvider { seriesLength := vp.Len() for index := 0; index < seriesLength; index++ { vx, vy := vp.GetValue(index) diff --git a/testserver/main.go b/testserver/main.go index 43856d3..cd0e6a9 100644 --- a/testserver/main.go +++ b/testserver/main.go @@ -40,8 +40,8 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult { XValues: s1x, YValues: s1y, Style: chart.Style{ - Show: true, - FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64), + Show: true, + //FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64), }, } @@ -60,32 +60,16 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult { }, } - s1ma := &chart.MovingAverageSeries{ - Name: "Average", - Style: chart.Style{ - Show: true, - StrokeColor: drawing.ColorRed, - StrokeDashArray: []float64{5, 1, 1}, - }, - WindowSize: 10, - InnerSeries: s1, - } - - s1malx, s1maly := s1ma.GetLastValue() - - s1malv := chart.AnnotationSeries{ - Name: fmt.Sprintf("Last Value"), + s1ma := &chart.BollingerBandsSeries{ + Name: "BBS", Style: chart.Style{ Show: true, - StrokeColor: drawing.ColorRed, - }, - Annotations: []chart.Annotation{ - chart.Annotation{ - X: s1malx, - Y: s1maly, - Label: fmt.Sprintf("%s - %s", "test", chart.FloatValueFormatter(s1maly)), - }, + StrokeColor: chart.DefaultAxisColor, + FillColor: chart.DefaultAxisColor.WithAlpha(64), }, + K: 2.0, + WindowSize: 10, + InnerSeries: s1, } c := chart.Chart{ @@ -121,7 +105,6 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult { s1, s1ma, s1lv, - s1malv, }, } diff --git a/value_provider.go b/value_provider.go index 3119b7a..1ff8e82 100644 --- a/value_provider.go +++ b/value_provider.go @@ -5,3 +5,9 @@ type ValueProvider interface { Len() int GetValue(index int) (float64, float64) } + +// BoundedValueProvider allows series to return a range. +type BoundedValueProvider interface { + Len() int + GetBoundedValue(index int) (x, y1, y2 float64) +}