-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: add volume weighted average price (vwap) indicator (c9s#211)
- Loading branch information
1 parent
7cb425d
commit 3f39131
Showing
4 changed files
with
188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |