14 changed files with 564 additions and 62 deletions
@ -1,21 +1,65 @@
|
||||
package sig |
||||
|
||||
import "sig-pub/pkg/indicator" |
||||
import ( |
||||
"fmt" |
||||
"sig-pub/pkg/indicator" |
||||
) |
||||
|
||||
type IndicatorSummary struct { |
||||
indicator.IIndicatorSummary |
||||
indicatorContext IOffsetIndicatorContext |
||||
summaryIndicator indicator.ISummaryIndicator |
||||
summaryIndicatorNewer func() indicator.ISummaryIndicator |
||||
cachedSummaryIndicators map[string]indicator.ISummaryIndicator |
||||
} |
||||
|
||||
func NewIndicatorSummary(summaryIndicator indicator.ISummaryIndicator, indicatorContext IOffsetIndicatorContext) *IndicatorSummary { |
||||
func NewIndicatorSummary(summaryIndicatorNewer func() indicator.ISummaryIndicator, indicatorContext IOffsetIndicatorContext) *IndicatorSummary { |
||||
return &IndicatorSummary{ |
||||
summaryIndicator: summaryIndicator, |
||||
summaryIndicatorNewer: summaryIndicatorNewer, |
||||
indicatorContext: indicatorContext, |
||||
cachedSummaryIndicators: make(map[string]indicator.ISummaryIndicator), |
||||
} |
||||
} |
||||
|
||||
func (s *IndicatorSummary) Summary(offset, count int16) (summary any, ok bool) { |
||||
// Summary 0 10, 0 20, 1 30
|
||||
func (s *IndicatorSummary) Summary(offset, count int16) (summary any, rok bool) { |
||||
key := fmt.Sprintf("%d,%d", offset, count) |
||||
summaryIndicator, ok := s.cachedSummaryIndicators[key] |
||||
|
||||
if ok { |
||||
s.indicatorContext.AddOffset(offset + count + 1) |
||||
eliminater, ok := summaryIndicator.(indicator.ISummaryIndicatorEliminater) |
||||
if ok { |
||||
eliminater.Eliminate(s.indicatorContext) |
||||
} |
||||
s.indicatorContext.AddOffset(-(offset + count + 1)) |
||||
|
||||
s.indicatorContext.AddOffset(offset) |
||||
summaryIndicator.Accumulate(s.indicatorContext) |
||||
summary, ok = summaryIndicator.Summary(s.indicatorContext) |
||||
s.indicatorContext.AddOffset(-offset) |
||||
return |
||||
} |
||||
|
||||
// 初始化, 计算全量数据
|
||||
summaryIndicator = s.summaryIndicatorNewer() |
||||
if err := summaryIndicator.Init(s.indicatorContext.Input()); err != nil { |
||||
panic(err) |
||||
} |
||||
s.cachedSummaryIndicators[key] = summaryIndicator |
||||
|
||||
// 设置当前相对offset
|
||||
offset = offset + count |
||||
s.indicatorContext.AddOffset(offset) |
||||
for range count { |
||||
summaryIndicator.Accumulate(s.indicatorContext) |
||||
|
||||
offset-- |
||||
s.indicatorContext.AddOffset(-1) |
||||
} |
||||
|
||||
// 获取结果
|
||||
summary, rok = summaryIndicator.Summary(s.indicatorContext) |
||||
// 计算结束后还原offset
|
||||
s.indicatorContext.AddOffset(-offset) |
||||
return |
||||
} |
||||
|
||||
@ -1,47 +1,259 @@
|
||||
package indicator |
||||
|
||||
import "sig-pub/pkg/types" |
||||
import ( |
||||
"math" |
||||
"sig-pub/pkg/types" |
||||
"sig-pub/pkg/zlog" |
||||
) |
||||
|
||||
// VRVP 成交量分布图
|
||||
const ( |
||||
MaxRawBuckets = 2000 // 最大原始分桶数量,超过此数量将进行合并
|
||||
) |
||||
|
||||
// VRVP Volume Profile (Visible Range Volume Profile)
|
||||
// 成交量分布图: 显示特定时间段内各价格水平的成交量分布
|
||||
type VRVP struct { |
||||
buckets int // 价格行数
|
||||
minPrice float64 // 最低价
|
||||
maxPrice float64 // 最高价
|
||||
|
||||
// 流式计算状态
|
||||
baseStep float64 // 当前的基础步长
|
||||
rawBuckets map[int64]*types.VRVPBucket // 原始分桶数据, key = int64(price / baseStep)
|
||||
klines int64 // 累计K线数量
|
||||
skts, ekts int64 // 开始时间戳, 结束时间戳
|
||||
} |
||||
|
||||
func (c *VRVP) Meta() IndicatorMeta { |
||||
return IndicatorMeta{ |
||||
Name: "VRVP", |
||||
Desc: "成交量分布图", |
||||
Input: []types.InputArg{ |
||||
{Name: "window", Type: types.InputTypeUInt, Desc: "窗口大小", Default: 10}, |
||||
}, |
||||
State: []string{"sig"}, |
||||
Plots: []Plot{ |
||||
{Name: "RVI", State: "vector", Type: PlotLine, Props: PlotProps{"color": ColorGreen}}, |
||||
{Name: "Signal", State: "sig", Type: PlotLine, Props: PlotProps{"color": ColorRed}}, |
||||
{Name: "buckets", Type: types.InputTypeUInt, Desc: "价格分桶数量", Default: 24}, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
func (s *VRVP) Init(input types.Input) (err error) { // 校验参数并初始化
|
||||
// s.rate = input.Float("rate")
|
||||
// s.rate2 = input.Float("rate2")
|
||||
// Init 初始化参数
|
||||
func (v *VRVP) Init(input types.Input) (err error) { |
||||
v.buckets = input.Int("buckets") |
||||
if v.buckets <= 0 { |
||||
v.buckets = 24 |
||||
} |
||||
v.minPrice = math.MaxFloat64 |
||||
v.maxPrice = -math.MaxFloat64 |
||||
v.rawBuckets = make(map[int64]*types.VRVPBucket) |
||||
v.baseStep = 0 |
||||
return |
||||
} |
||||
|
||||
// Accumulate 计算累加值
|
||||
func (v *VRVP) Accumulate(ctx IIndicatorContext) { |
||||
func (v *VRVP) CandlePeriods(ctx IIndicatorContext) int16 { |
||||
return 1 |
||||
} |
||||
|
||||
// Summary 计算完毕获取计算结果
|
||||
func (v *VRVP) Summary(ctx IIndicatorContext) (summary any, ok bool) { |
||||
// Eliminate 清除这根K线的数据
|
||||
func (v *VRVP) Eliminate(ctx IIndicatorContext) { |
||||
k := ctx.Get(0) |
||||
high := k.HighF64() |
||||
low := k.LowF64() |
||||
vol := k.VolF64() |
||||
|
||||
if vol <= 0 { |
||||
return |
||||
} |
||||
|
||||
v.klines-- |
||||
|
||||
// 限制说明 :在流式计算模式下,无法精确回滚 minPrice 、 maxPrice 以及 baseStep 的历史变化。
|
||||
// 这意味着在长期运行后,分布图的统计范围可能会比实际存在的 K 线范围略大,但这不影响分布形状的准确性。
|
||||
|
||||
if v.baseStep <= 0 { |
||||
return |
||||
} |
||||
|
||||
// 将成交量从原始桶中移除
|
||||
startIdx := int64(math.Floor(low / v.baseStep)) |
||||
endIdx := int64(math.Floor(high / v.baseStep)) |
||||
|
||||
coveredBuckets := float64(endIdx - startIdx + 1) |
||||
volPerBucket := vol / coveredBuckets |
||||
|
||||
isUp := k.CloseF64() >= k.OpenF64() |
||||
|
||||
for i := startIdx; i <= endIdx; i++ { |
||||
bucket, exists := v.rawBuckets[i] |
||||
if !exists { |
||||
continue |
||||
} |
||||
|
||||
bucket.Volume -= volPerBucket |
||||
if isUp { |
||||
bucket.BuyVol -= volPerBucket |
||||
} else { |
||||
bucket.SellVol -= volPerBucket |
||||
} |
||||
|
||||
// 如果桶的成交量归零(考虑浮点误差),则移除该桶以节省内存
|
||||
if bucket.Volume < 1e-8 { |
||||
delete(v.rawBuckets, i) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Accumulate 累积每根K线的数据
|
||||
func (v *VRVP) Accumulate(ctx IIndicatorContext) { |
||||
k := ctx.Get(0) |
||||
high := k.HighF64() |
||||
low := k.LowF64() |
||||
vol := k.VolF64() |
||||
v.klines++ |
||||
if v.skts == 0 { |
||||
v.skts = k.Ts |
||||
} |
||||
v.ekts = k.Ts |
||||
|
||||
if vol <= 0 { |
||||
return |
||||
} |
||||
|
||||
// 更新全局极值
|
||||
if high > v.maxPrice { |
||||
v.maxPrice = high |
||||
} |
||||
if low < v.minPrice { |
||||
v.minPrice = low |
||||
} |
||||
|
||||
// 初始化 baseStep
|
||||
if v.baseStep == 0 { |
||||
// 如果 K 线有波动,使用波动的一小部分作为初始精度
|
||||
// 如果无波动(High==Low),使用价格的万分之一
|
||||
if high > low { |
||||
v.baseStep = (high - low) / 100.0 |
||||
} else { |
||||
if low > 0 { |
||||
v.baseStep = low * 0.0001 |
||||
} else { |
||||
v.baseStep = 0.01 // 默认值
|
||||
} |
||||
} |
||||
} |
||||
|
||||
// 将成交量分配到原始桶中
|
||||
startIdx := int64(math.Floor(low / v.baseStep)) |
||||
endIdx := int64(math.Floor(high / v.baseStep)) |
||||
|
||||
coveredBuckets := float64(endIdx - startIdx + 1) |
||||
volPerBucket := vol / coveredBuckets |
||||
|
||||
isUp := k.CloseF64() >= k.OpenF64() |
||||
|
||||
for i := startIdx; i <= endIdx; i++ { |
||||
bucket, exists := v.rawBuckets[i] |
||||
if !exists { |
||||
bucket = &types.VRVPBucket{ |
||||
Price: float64(i)*v.baseStep + v.baseStep/2, // 暂存中心价
|
||||
} |
||||
v.rawBuckets[i] = bucket |
||||
} |
||||
|
||||
bucket.Volume += volPerBucket |
||||
if isUp { |
||||
bucket.BuyVol += volPerBucket |
||||
} else { |
||||
bucket.SellVol += volPerBucket |
||||
} |
||||
} |
||||
|
||||
// 检查是否需要合并桶
|
||||
if len(v.rawBuckets) > MaxRawBuckets { |
||||
v.halveResolution() |
||||
} |
||||
} |
||||
|
||||
type RVVPSummary struct { |
||||
TotalVolume float64 |
||||
Buckets []PriceBucket |
||||
// halveResolution 将分辨率减半(步长翻倍)
|
||||
func (v *VRVP) halveResolution() { |
||||
newBaseStep := v.baseStep * 2 |
||||
newBuckets := make(map[int64]*types.VRVPBucket, len(v.rawBuckets)/2+1) |
||||
|
||||
for key, bucket := range v.rawBuckets { |
||||
// 计算新的 key
|
||||
// oldPrice ~= key * oldStep
|
||||
// newKey = floor(oldPrice / newStep) = floor(key * oldStep / (2 * oldStep)) = floor(key / 2)
|
||||
newKey := key >> 1 // key / 2
|
||||
|
||||
newBucket, exists := newBuckets[newKey] |
||||
if !exists { |
||||
newBucket = &types.VRVPBucket{ |
||||
Price: float64(newKey)*newBaseStep + newBaseStep/2, |
||||
} |
||||
newBuckets[newKey] = newBucket |
||||
} |
||||
|
||||
newBucket.Volume += bucket.Volume |
||||
newBucket.BuyVol += bucket.BuyVol |
||||
newBucket.SellVol += bucket.SellVol |
||||
} |
||||
|
||||
v.baseStep = newBaseStep |
||||
v.rawBuckets = newBuckets |
||||
} |
||||
|
||||
type PriceBucket struct { |
||||
Price float64 |
||||
Volume float64 |
||||
// Summary 计算最终的成交量分布结果
|
||||
// 在所有K线Accumulate完成后调用
|
||||
func (v *VRVP) Summary(ctx IIndicatorContext) (summary any, ok bool) { |
||||
zlog.Debugf("VRVP Summary: klines=%d, skts=%d, ekts=%d, minPrice=%.2f, maxPrice=%.2f, baseStep=%.2f", |
||||
v.klines, v.skts, v.ekts, v.minPrice, v.maxPrice, v.baseStep) |
||||
if v.minPrice >= v.maxPrice { |
||||
return nil, false |
||||
} |
||||
|
||||
// 计算最终的目标步长
|
||||
rangeHeight := v.maxPrice - v.minPrice |
||||
finalStep := rangeHeight / float64(v.buckets) |
||||
|
||||
// 如果 finalStep 小于当前的 baseStep,说明数据太稀疏,无法满足 rowCount 的精度要求
|
||||
// 但通常情况下,由于我们只在桶过多时才合并,baseStep 应该相对较小
|
||||
|
||||
// 初始化最终分桶
|
||||
buckets := make([]types.VRVPBucket, v.buckets) |
||||
for i := 0; i < v.buckets; i++ { |
||||
buckets[i].Price = v.minPrice + float64(i)*finalStep + finalStep/2 |
||||
} |
||||
|
||||
// 将 rawBuckets 聚合到最终 buckets
|
||||
for _, rawB := range v.rawBuckets { |
||||
// 计算原始桶对应的价格范围中心
|
||||
// 注意:rawB.Price 在合并过程中可能不再准确,重新计算更稳妥,或者在合并时更新 Price
|
||||
// 这里我们使用 key 重新计算,更准确
|
||||
// 但由于 map key 不在 value 中,我们无法直接获取 key
|
||||
// 所以我们需要遍历 map 的 key
|
||||
// 修正:在上面的循环中我们无法获得 key,所以需要修改遍历方式
|
||||
// 或者我们在 PriceBucket 中存储准确的 Price
|
||||
// 在 halveResolution 中,我们更新了 Price,所以 rawB.Price 是当前 baseStep 下的中心价
|
||||
|
||||
price := rawB.Price |
||||
|
||||
// 找到对应的最终桶索引
|
||||
idx := int((price - v.minPrice) / finalStep) |
||||
|
||||
if idx < 0 { |
||||
idx = 0 |
||||
} else if idx >= v.buckets { |
||||
idx = v.buckets - 1 |
||||
} |
||||
|
||||
buckets[idx].Volume += rawB.Volume |
||||
buckets[idx].BuyVol += rawB.BuyVol |
||||
buckets[idx].SellVol += rawB.SellVol |
||||
} |
||||
|
||||
ok, summary = true, &types.VRVPSummary{ |
||||
Klines: v.klines, |
||||
Step: finalStep, |
||||
MinPrice: v.minPrice, |
||||
MaxPrice: v.maxPrice, |
||||
Buckets: buckets, |
||||
} |
||||
return |
||||
} |
||||
|
||||
@ -0,0 +1,16 @@
|
||||
package types |
||||
|
||||
// VRVPSummary 成交量分布图指标计算结果
|
||||
type VRVPSummary struct { |
||||
Klines int64 `json:"klines"` |
||||
Step float64 `json:"step"` // 每个桶的价格高度
|
||||
MinPrice float64 `json:"minPrice"` // 统计范围最低价
|
||||
MaxPrice float64 `json:"maxPrice"` // 统计范围最高价
|
||||
Buckets []VRVPBucket `json:"buckets"` |
||||
} |
||||
type VRVPBucket struct { |
||||
Price float64 `json:"price"` // 桶代表价格
|
||||
Volume float64 `json:"volume"` // 总成交量
|
||||
BuyVol float64 `json:"buyVol"` // 主动买入量(近似)
|
||||
SellVol float64 `json:"sellVol"` // 主动卖出量(近似)
|
||||
} |
||||
Loading…
Reference in new issue