diff --git a/pkg/indicator/ad.go b/pkg/indicator/ad.go index ad2ba859fd..ad4c0fa898 100644 --- a/pkg/indicator/ad.go +++ b/pkg/indicator/ad.go @@ -22,7 +22,7 @@ type AD struct { UpdateCallbacks []func(value float64) } -func (inc *AD) update(kLine types.KLine) { +func (inc *AD) Update(kLine types.KLine) { close := kLine.Close.Float64() high := kLine.High.Float64() low := kLine.Low.Float64() @@ -42,16 +42,15 @@ func (inc *AD) Last() float64 { } func (inc *AD) calculateAndUpdate(kLines []types.KLine) { - for i, k := range kLines { + for _, k := range kLines { if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { continue } - - inc.update(k) - inc.EmitUpdate(inc.Last()) - inc.EndTime = kLines[i].EndTime.Time() + inc.Update(k) } + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } func (inc *AD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { diff --git a/pkg/indicator/macd.go b/pkg/indicator/macd.go index 8df24bca4c..e13ea931c9 100644 --- a/pkg/indicator/macd.go +++ b/pkg/indicator/macd.go @@ -31,12 +31,12 @@ type MACD struct { func (inc *MACD) calculateMACD(kLines []types.KLine, priceF KLinePriceMapper) float64 { for _, kline := range kLines { - inc.update(kline, priceF) + inc.Update(kline, priceF) } return inc.Values[len(inc.Values)-1] } -func (inc *MACD) update(kLine types.KLine, priceF KLinePriceMapper) { +func (inc *MACD) Update(kLine types.KLine, priceF KLinePriceMapper) { if len(inc.Values) == 0 { inc.FastEWMA = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.ShortPeriod}} inc.SlowEWMA = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.LongPeriod}} @@ -67,18 +67,15 @@ func (inc *MACD) calculateAndUpdate(kLines []types.KLine) { var priceF = KLineClosePriceMapper - var index = len(kLines) - 1 - var kline = kLines[index] - if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) { - return - } - - for i, kLine := range kLines { - inc.update(kLine, priceF) - inc.EmitUpdate(inc.Values[len(inc.Values)-1]) - inc.EndTime = kLines[i].EndTime.Time() + for _, k := range kLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + continue + } + inc.Update(k, priceF) } + inc.EmitUpdate(inc.Values[len(inc.Values)-1]) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } func (inc *MACD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/obv.go b/pkg/indicator/obv.go index e5ce760466..164af32db5 100644 --- a/pkg/indicator/obv.go +++ b/pkg/indicator/obv.go @@ -22,7 +22,7 @@ type OBV struct { UpdateCallbacks []func(value float64) } -func (inc *OBV) update(kLine types.KLine, priceF KLinePriceMapper) { +func (inc *OBV) Update(kLine types.KLine, priceF KLinePriceMapper) { price := priceF(kLine) volume := kLine.Volume.Float64() @@ -49,16 +49,14 @@ func (inc *OBV) Last() float64 { func (inc *OBV) calculateAndUpdate(kLines []types.KLine) { var priceF = KLineClosePriceMapper - for i, k := range kLines { + for _, k := range kLines { if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { continue } - - inc.update(k, priceF) - inc.EmitUpdate(inc.Last()) - inc.EndTime = kLines[i].EndTime.Time() + inc.Update(k, priceF) } - + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } func (inc *OBV) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { if inc.Interval != interval { diff --git a/pkg/indicator/stoch.go b/pkg/indicator/stoch.go index 0d088f2750..86e7e8f530 100644 --- a/pkg/indicator/stoch.go +++ b/pkg/indicator/stoch.go @@ -60,15 +60,15 @@ func (inc *STOCH) calculateAndUpdate(kLines []types.KLine) { return } - for i, k := range kLines { + for _, k := range kLines { if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { continue } - inc.update(k) - inc.EmitUpdate(inc.LastK(), inc.LastD()) - inc.EndTime = kLines[i].EndTime.Time() } + + inc.EmitUpdate(inc.LastK(), inc.LastD()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } func (inc *STOCH) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index fbdb1de92a..6ac4646b43 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -19,81 +19,54 @@ Volume-Weighted Average Price (VWAP) Explained type VWAP struct { types.IntervalWindow Values types.Float64Slice + Prices types.Float64Slice + Volumes types.Float64Slice WeightedSum float64 VolumeSum float64 - EndTime time.Time + 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) +func (inc *VWAP) Last() float64 { + if len(inc.Values) == 0 { + return 0.0 } - - return vwap + return inc.Values[len(inc.Values)-1] } -func (inc *VWAP) update(kLine types.KLine, priceF KLinePriceMapper, multiplier float64) { - // multiplier = 1 or -1 +func (inc *VWAP) Update(kLine types.KLine, priceF KLinePriceMapper) { price := priceF(kLine) volume := kLine.Volume.Float64() - inc.WeightedSum += multiplier * price * volume - inc.VolumeSum += multiplier * volume -} + inc.Prices.Push(price) + inc.Volumes.Push(volume) -func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) { - if len(kLines) < inc.Window { - return + if inc.Window != 0 && len(inc.Prices) > inc.Window { + popIndex := len(inc.Prices) - inc.Window - 1 + inc.WeightedSum -= inc.Prices[popIndex] * inc.Volumes[popIndex] + inc.VolumeSum -= inc.Volumes[popIndex] } - var priceF = KLineTypicalPriceMapper - var dataLen = len(kLines) + inc.WeightedSum += price * volume + inc.VolumeSum += volume - // 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.Float64() - - 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 - } - } - } + vwap := inc.WeightedSum / inc.VolumeSum + inc.Values.Push(vwap) +} - // update vwap - for i := from; i < dataLen; i++ { - inc.update(kLines[i], priceF, 1.0) // add kline +func (inc *VWAP) calculateAndUpdate(kLines []types.KLine) { + var priceF = KLineTypicalPriceMapper - if i >= inc.Window { - inc.update(kLines[i-inc.Window], priceF, -1.0) // pop kline + for _, k := range kLines { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + continue } - vwap := inc.WeightedSum / inc.VolumeSum - - inc.Values.Push(vwap) - inc.EmitUpdate(vwap) - - inc.EndTime = kLines[i].EndTime.Time() + inc.Update(k, priceF) } + + inc.EmitUpdate(inc.Last()) + inc.EndTime = kLines[len(kLines)-1].EndTime.Time() } func (inc *VWAP) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { @@ -107,3 +80,11 @@ func (inc *VWAP) handleKLineWindowUpdate(interval types.Interval, window types.K func (inc *VWAP) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } + +func CalculateVWAP(klines []types.KLine, priceF KLinePriceMapper, window int) float64 { + vwap := VWAP{IntervalWindow: types.IntervalWindow{Window: window}} + for _, k := range klines { + vwap.Update(k, priceF) + } + return vwap.Last() +} diff --git a/pkg/indicator/vwap_test.go b/pkg/indicator/vwap_test.go index 4a89fc27da..d168bb938f 100644 --- a/pkg/indicator/vwap_test.go +++ b/pkg/indicator/vwap_test.go @@ -9,16 +9,16 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -var trivialPrices = []byte(`[0]`) -var trivialVolumes = []byte(`[1]`) -var easyPrices = []byte(`[1, 2, 3]`) -var easyVolumes = []byte(`[4, 5, 6]`) -var windowPrices = []byte(`[1, 2, 3, 4]`) -var windowVolumes = []byte(`[4, 5, 6, 7]`) -var randomPrices = []byte(`[0.6046702879796195, 0.9405190880450124, 0.6645700532184904, 0.4377241871869802, 0.4246474970712657, 0.6868330728671094, 0.06564701921747622, 0.15652925473279125, 0.09697951891448456, 0.3009218605852871]`) -var randomVolumes = []byte(`[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) { + var trivialPrices = []byte(`[0]`) + var trivialVolumes = []byte(`[1]`) + var easyPrices = []byte(`[1, 2, 3]`) + var easyVolumes = []byte(`[4, 5, 6]`) + var windowPrices = []byte(`[1, 2, 3, 4]`) + var windowVolumes = []byte(`[4, 5, 6, 7]`) + var randomPrices = []byte(`[0.6046702879796195, 0.9405190880450124, 0.6645700532184904, 0.4377241871869802, 0.4246474970712657, 0.6868330728671094, 0.06564701921747622, 0.15652925473279125, 0.09697951891448456, 0.3009218605852871]`) + var randomVolumes = []byte(`[0.5152226285020653, 0.8136499609900968, 0.21427387258237493, 0.380667189299686, 0.31806817433032986, 0.4688998449024232, 0.2830441511804452, 0.2931118573368158, 0.6790946759202162, 0.2185630525927643]`) + buildKLines := func(pb, vb []byte) (kLines []types.KLine) { var prices, volumes []fixedpoint.Value _ = json.Unmarshal(pb, &prices) @@ -63,9 +63,8 @@ func Test_calculateVWAP(t *testing.T) { 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) + got := CalculateVWAP(tt.kLines, priceF, tt.window) diff := math.Trunc((got-tt.want)*100) / 100 if diff != 0 { t.Errorf("calculateVWAP() = %v, want %v", got, tt.want) diff --git a/pkg/types/float_slice.go b/pkg/types/float_slice.go index 8b11aabc6f..35b9b58560 100644 --- a/pkg/types/float_slice.go +++ b/pkg/types/float_slice.go @@ -105,3 +105,15 @@ func (s Float64Slice) DivScalar(x float64) Float64Slice { } return values } + +func (s Float64Slice) ElementwiseProduct(other Float64Slice) Float64Slice { + var values Float64Slice + for i, v := range s { + values.Push(v * other[i]) + } + return values +} + +func (s Float64Slice) Dot(other Float64Slice) float64 { + return s.ElementwiseProduct(other).Sum() +}