mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-27 17:25:16 +00:00
195 lines
4.2 KiB
Go
195 lines
4.2 KiB
Go
package indicatorv2
|
|
|
|
import (
|
|
"math"
|
|
|
|
"golang.org/x/exp/slices"
|
|
"gonum.org/v1/gonum/floats"
|
|
"gonum.org/v1/gonum/stat"
|
|
|
|
bbgofloats "github.com/c9s/bbgo/pkg/datatype/floats"
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
)
|
|
|
|
// DefaultValueAreaPercentage is the percentage of the total volume used to calculate the value area.
|
|
const DefaultValueAreaPercentage = 0.68
|
|
|
|
type VolumeProfileStream struct {
|
|
*types.Float64Series
|
|
VP VolumeProfileDetails
|
|
window int
|
|
}
|
|
|
|
// VolumeProfileDetails is a histogram of market price and volume.
|
|
// Intent is to show the price points with most volume during a period.
|
|
// The profile gives key features such as:
|
|
//
|
|
// Point of control (POC)
|
|
//
|
|
// Value area high (VAH)
|
|
//
|
|
// Value area low (VAL)
|
|
//
|
|
// Session High/Low
|
|
type VolumeProfileDetails struct {
|
|
|
|
// Bins is the histogram bins.
|
|
Bins []float64
|
|
|
|
// Hist is the histogram values.
|
|
Hist []float64
|
|
|
|
// POC is the point of control.
|
|
POC float64
|
|
|
|
// VAH is the value area high.
|
|
VAH float64
|
|
|
|
// VAL is the value area low.
|
|
VAL float64
|
|
|
|
// High is the highest price in the profile.
|
|
High float64
|
|
|
|
// Low is the lowest price in the profile.
|
|
Low float64
|
|
}
|
|
|
|
// VolumeLevel is a price and volume pair used to build a volume profile.
|
|
type VolumeLevel struct {
|
|
|
|
// Price is the market price, typically the high/low average of the kline.
|
|
Price float64
|
|
|
|
// Volume is the total buy and sell volume at the price.
|
|
Volume float64
|
|
}
|
|
|
|
func VolumeProfile(source KLineSubscription, window int) *VolumeProfileStream {
|
|
prices := HLC3(source)
|
|
volumes := Volumes(source)
|
|
|
|
s := &VolumeProfileStream{
|
|
Float64Series: types.NewFloat64Series(),
|
|
window: window,
|
|
}
|
|
|
|
source.AddSubscriber(func(v types.KLine) {
|
|
if source.Length() < window {
|
|
s.PushAndEmit(0)
|
|
return
|
|
}
|
|
var nBins = 10
|
|
// nBins = int(math.Floor((prices.Slice.Max()-prices.Slice.Min())/binWidth)) + 1
|
|
s.VP.High = prices.Slice.Max()
|
|
s.VP.Low = prices.Slice.Min()
|
|
sortedPrices, sortedVolumes := buildVolumeLevel(prices.Slice, volumes.Slice)
|
|
s.VP.Bins = make([]float64, nBins)
|
|
s.VP.Bins = floats.Span(s.VP.Bins, s.VP.Low, s.VP.High+1)
|
|
s.VP.Hist = stat.Histogram(nil, s.VP.Bins, sortedPrices, sortedVolumes)
|
|
|
|
pocIdx := floats.MaxIdx(s.VP.Hist)
|
|
s.VP.POC = midBin(s.VP.Bins, pocIdx)
|
|
|
|
// TODO the results are of by small difference whereas it is expected they work the same
|
|
// vaTotalVol := volumes.Sum() * DefaultValueAreaPercentage
|
|
// Calculate Value Area with POC as the centre point\
|
|
vaTotalVol := floats.Sum(volumes.Slice) * DefaultValueAreaPercentage
|
|
|
|
vaCumVol := s.VP.Hist[pocIdx]
|
|
var vahVol, valVol float64
|
|
vahIdx, valIdx := pocIdx+1, pocIdx-1
|
|
stepVAH, stepVAL := true, true
|
|
|
|
for (vaCumVol <= vaTotalVol) &&
|
|
(vahIdx <= len(s.VP.Hist)-1 && valIdx >= 0) {
|
|
|
|
if stepVAH {
|
|
vahVol = 0
|
|
for vahVol == 0 && vahIdx+1 < len(s.VP.Hist)-1 {
|
|
vahVol = s.VP.Hist[vahIdx] + s.VP.Hist[vahIdx+1]
|
|
vahIdx += 2
|
|
}
|
|
stepVAH = false
|
|
}
|
|
|
|
if stepVAL {
|
|
valVol = 0
|
|
for valVol == 0 && valIdx-1 >= 0 {
|
|
valVol = s.VP.Hist[valIdx] + s.VP.Hist[valIdx-1]
|
|
valIdx -= 2
|
|
}
|
|
stepVAL = false
|
|
}
|
|
|
|
switch {
|
|
case vahVol > valVol:
|
|
vaCumVol += vahVol
|
|
stepVAH, stepVAL = true, false
|
|
case vahVol < valVol:
|
|
vaCumVol += valVol
|
|
stepVAH, stepVAL = false, true
|
|
case vahVol == valVol:
|
|
vaCumVol += valVol + vahVol
|
|
stepVAH, stepVAL = true, true
|
|
}
|
|
|
|
if vahIdx >= len(s.VP.Hist)-1 {
|
|
stepVAH = false
|
|
}
|
|
|
|
if valIdx <= 0 {
|
|
stepVAL = false
|
|
}
|
|
}
|
|
|
|
s.VP.VAH = midBin(s.VP.Bins, vahIdx)
|
|
s.VP.VAL = midBin(s.VP.Bins, valIdx)
|
|
|
|
})
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *VolumeProfileStream) Truncate() {
|
|
s.Slice = s.Slice.Truncate(5000)
|
|
}
|
|
|
|
func buildVolumeLevel(p, v bbgofloats.Slice) (sortedp, sortedv bbgofloats.Slice) {
|
|
var levels []VolumeLevel
|
|
for i := range p {
|
|
levels = append(levels, VolumeLevel{
|
|
Price: p[i],
|
|
Volume: v[i],
|
|
})
|
|
}
|
|
|
|
slices.SortStableFunc(levels, func(i, j VolumeLevel) bool {
|
|
return i.Price < j.Price
|
|
})
|
|
|
|
for _, v := range levels {
|
|
sortedp.Append(v.Price)
|
|
sortedv.Append(v.Volume)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func midBin(bins []float64, idx int) float64 {
|
|
|
|
if len(bins) == 0 {
|
|
return math.NaN()
|
|
}
|
|
|
|
if idx >= len(bins)-1 {
|
|
return bins[len(bins)-1]
|
|
}
|
|
|
|
if idx < 0 {
|
|
return bins[0]
|
|
}
|
|
|
|
return stat.Mean([]float64{bins[idx], bins[idx+1]}, nil)
|
|
}
|