14 changed files with 564 additions and 62 deletions
@ -1,21 +1,65 @@ |
|||||||
package sig |
package sig |
||||||
|
|
||||||
import "sig-pub/pkg/indicator" |
import ( |
||||||
|
"fmt" |
||||||
|
"sig-pub/pkg/indicator" |
||||||
|
) |
||||||
|
|
||||||
type IndicatorSummary struct { |
type IndicatorSummary struct { |
||||||
indicator.IIndicatorSummary |
indicator.IIndicatorSummary |
||||||
indicatorContext IOffsetIndicatorContext |
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{ |
return &IndicatorSummary{ |
||||||
summaryIndicator: summaryIndicator, |
summaryIndicatorNewer: summaryIndicatorNewer, |
||||||
indicatorContext: indicatorContext, |
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 |
return |
||||||
} |
} |
||||||
|
|||||||
@ -1,47 +1,259 @@ |
|||||||
package indicator |
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 { |
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 { |
func (c *VRVP) Meta() IndicatorMeta { |
||||||
return IndicatorMeta{ |
return IndicatorMeta{ |
||||||
Name: "VRVP", |
Name: "VRVP", |
||||||
|
Desc: "成交量分布图", |
||||||
Input: []types.InputArg{ |
Input: []types.InputArg{ |
||||||
{Name: "window", Type: types.InputTypeUInt, Desc: "窗口大小", Default: 10}, |
{Name: "buckets", Type: types.InputTypeUInt, Desc: "价格分桶数量", Default: 24}, |
||||||
}, |
|
||||||
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}}, |
|
||||||
}, |
}, |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
func (s *VRVP) Init(input types.Input) (err error) { // 校验参数并初始化
|
// Init 初始化参数
|
||||||
// s.rate = input.Float("rate")
|
func (v *VRVP) Init(input types.Input) (err error) { |
||||||
// s.rate2 = input.Float("rate2")
|
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 |
return |
||||||
} |
} |
||||||
|
|
||||||
// Accumulate 计算累加值
|
func (v *VRVP) CandlePeriods(ctx IIndicatorContext) int16 { |
||||||
|
return 1 |
||||||
|
} |
||||||
|
|
||||||
|
// 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) { |
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() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 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 |
||||||
} |
} |
||||||
|
|
||||||
// Summary 计算完毕获取计算结果
|
newBucket.Volume += bucket.Volume |
||||||
|
newBucket.BuyVol += bucket.BuyVol |
||||||
|
newBucket.SellVol += bucket.SellVol |
||||||
|
} |
||||||
|
|
||||||
|
v.baseStep = newBaseStep |
||||||
|
v.rawBuckets = newBuckets |
||||||
|
} |
||||||
|
|
||||||
|
// Summary 计算最终的成交量分布结果
|
||||||
|
// 在所有K线Accumulate完成后调用
|
||||||
func (v *VRVP) Summary(ctx IIndicatorContext) (summary any, ok bool) { |
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 |
||||||
|
} |
||||||
|
|
||||||
return |
// 计算最终的目标步长
|
||||||
|
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 |
||||||
} |
} |
||||||
|
|
||||||
type RVVPSummary struct { |
buckets[idx].Volume += rawB.Volume |
||||||
TotalVolume float64 |
buckets[idx].BuyVol += rawB.BuyVol |
||||||
Buckets []PriceBucket |
buckets[idx].SellVol += rawB.SellVol |
||||||
} |
} |
||||||
|
|
||||||
type PriceBucket struct { |
ok, summary = true, &types.VRVPSummary{ |
||||||
Price float64 |
Klines: v.klines, |
||||||
Volume float64 |
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