bollinger bounds

This commit is contained in:
Will Charczuk 2016-07-15 09:02:50 -07:00
parent 21b4dfddc9
commit bc2b51077b
4 changed files with 189 additions and 27 deletions

153
bollinger_band_series.go Normal file
View file

@ -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)
}

View file

@ -136,7 +136,27 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
for _, s := range c.Series { for _, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show { if s.GetStyle().IsZero() || s.GetStyle().Show {
seriesAxis := s.GetYAxis() 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() seriesLength := vp.Len()
for index := 0; index < seriesLength; index++ { for index := 0; index < seriesLength; index++ {
vx, vy := vp.GetValue(index) vx, vy := vp.GetValue(index)

View file

@ -40,8 +40,8 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
XValues: s1x, XValues: s1x,
YValues: s1y, YValues: s1y,
Style: chart.Style{ Style: chart.Style{
Show: true, Show: true,
FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64), //FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64),
}, },
} }
@ -60,32 +60,16 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
}, },
} }
s1ma := &chart.MovingAverageSeries{ s1ma := &chart.BollingerBandsSeries{
Name: "Average", Name: "BBS",
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"),
Style: chart.Style{ Style: chart.Style{
Show: true, Show: true,
StrokeColor: drawing.ColorRed, StrokeColor: chart.DefaultAxisColor,
}, FillColor: chart.DefaultAxisColor.WithAlpha(64),
Annotations: []chart.Annotation{
chart.Annotation{
X: s1malx,
Y: s1maly,
Label: fmt.Sprintf("%s - %s", "test", chart.FloatValueFormatter(s1maly)),
},
}, },
K: 2.0,
WindowSize: 10,
InnerSeries: s1,
} }
c := chart.Chart{ c := chart.Chart{
@ -121,7 +105,6 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
s1, s1,
s1ma, s1ma,
s1lv, s1lv,
s1malv,
}, },
} }

View file

@ -5,3 +5,9 @@ type ValueProvider interface {
Len() int Len() int
GetValue(index int) (float64, float64) GetValue(index int) (float64, float64)
} }
// BoundedValueProvider allows series to return a range.
type BoundedValueProvider interface {
Len() int
GetBoundedValue(index int) (x, y1, y2 float64)
}