Skip to content

Commit

Permalink
feature: add volume weighted average price (vwap) indicator (c9s#211)
Browse files Browse the repository at this point in the history
  • Loading branch information
narumiruna authored May 7, 2021
1 parent 7cb425d commit 3f39131
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 0 deletions.
4 changes: 4 additions & 0 deletions pkg/indicator/ewma.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func KLineClosePriceMapper(k types.KLine) float64 {
return k.Close
}

func KLineTypicalPriceMapper(k types.KLine) float64 {
return (k.High + k.Low + k.Close) / float64(3)
}

func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) {
for _, k := range kLines {
prices = append(prices, f(k))
Expand Down
107 changes: 107 additions & 0 deletions pkg/indicator/vwap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package indicator

import (
"time"

"github.com/c9s/bbgo/pkg/types"
)

/*
vwap implements the volume weighted average price (VWAP) indicator:
The basics of VWAP
- https://www.investopedia.com/terms/v/vwap.asp
- https://academy.binance.com/en/articles/volume-weighted-average-price-vwap-explained
*/
//go:generate callbackgen -type VWAP
type VWAP struct {
types.IntervalWindow
Values Float64Slice
WeightedSum float64
VolumeSum float64
EndTime time.Time

UpdateCallbacks []func(value float64)
}

func (inc *VWAP) calculateVWAP(kLines []types.KLine, priceF KLinePriceMapper) (vwap float64) {
for i, k := range kLines {
inc.update(k, priceF, 1.0) // add kline

// if window size is not zero, then we do not apply sliding window method
if inc.Window != 0 && len(inc.Values) >= inc.Window {
inc.update(kLines[i-inc.Window], priceF, -1.0) // pop kline
}
vwap = inc.WeightedSum / inc.VolumeSum
inc.Values.Push(vwap)
}

return vwap
}

func (inc *VWAP) update(kLine types.KLine, priceF KLinePriceMapper, multiplier float64) {
// multiplier = 1 or -1
price := priceF(kLine)
volume := kLine.Volume

inc.WeightedSum += multiplier * price * volume
inc.VolumeSum += multiplier * volume
}

func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) {
if len(kLines) < inc.Window {
return
}

var priceF = KLineTypicalPriceMapper
var dataLen = len(kLines)

// init the values from the kline data
var from = 1
if len(inc.Values) == 0 {
// for the first value, we should use the close price
price := priceF(kLines[0])
volume := kLines[0].Volume

inc.Values = []float64{price}
inc.WeightedSum = price * volume
inc.VolumeSum = volume
} else {
// update vwap with the existing values
for i := dataLen - 1; i > 0; i-- {
var k = kLines[i]
if k.EndTime.After(inc.EndTime) {
from = i
} else {
break
}
}
}

// update vwap
for i := from; i < dataLen; i++ {
inc.update(kLines[i], priceF, 1.0) // add kline

if i >= inc.Window {
inc.update(kLines[i-inc.Window], priceF, -1.0) // pop kline
}
vwap := inc.WeightedSum / inc.VolumeSum

inc.Values.Push(vwap)
inc.EmitUpdate(vwap)

inc.EndTime = kLines[i].EndTime
}
}

func (inc *VWAP) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}

inc.calculateAndUpdate(window)
}

func (inc *VWAP) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
15 changes: 15 additions & 0 deletions pkg/indicator/vwap_callbacks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions pkg/indicator/vwap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package indicator

import (
"testing"

"github.com/c9s/bbgo/pkg/types"
)

var randomPrices = []float64{0.6046702879796195, 0.9405190880450124, 0.6645700532184904, 0.4377241871869802, 0.4246474970712657, 0.6868330728671094, 0.06564701921747622, 0.15652925473279125, 0.09697951891448456, 0.3009218605852871}
var randomVolumes = []float64{0.5152226285020653, 0.8136499609900968, 0.21427387258237493, 0.380667189299686, 0.31806817433032986, 0.4688998449024232, 0.2830441511804452, 0.2931118573368158, 0.6790946759202162, 0.2185630525927643}

func Test_calculateVWAP(t *testing.T) {
buildKLines := func(prices, volumes []float64) (kLines []types.KLine) {
for i, p := range prices {
kLines = append(kLines, types.KLine{High: p, Low: p, Close: p, Volume: volumes[i]})
}
return kLines
}

tests := []struct {
name string
kLines []types.KLine
window int
want float64
}{
{
name: "trivial_case",
kLines: buildKLines([]float64{0}, []float64{1}),
window: 0,
want: 0.0,
},
{
name: "easy_case",
kLines: buildKLines([]float64{1, 2, 3}, []float64{4, 5, 6}),
window: 0,
want: (1*4 + 2*5 + 3*6) / float64(4+5+6),
},
{
name: "window_case",
kLines: buildKLines([]float64{1, 2, 3, 4}, []float64{4, 5, 6, 7}),
window: 3,
want: (2*5 + 3*6 + 4*7) / float64(5+6+7),
},
{
name: "random_case",
kLines: buildKLines(randomPrices, randomVolumes),
window: 0,
want: 0.48727133857423566,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vwap := VWAP{IntervalWindow: types.IntervalWindow{Window: tt.window}}
priceF := KLineTypicalPriceMapper
got := vwap.calculateVWAP(tt.kLines, priceF)
if got != tt.want {
t.Errorf("calculateVWAP() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 3f39131

Please sign in to comment.