feat(indicator): per-interval S/R + Range + LSLine + Donchian 箱体(ADR-0003)
按 ADR-0003 把 Support / Resistance / Range / LSLine 从顶层下沉进 IntervalTechnicals,每个周期独立计算;顶层 5 字段保留为 Intervals[primary] 镜像(单一来源,不双写)以维持向后兼容的 JSON shape。 新增 BoxBreak(Donchian 风格,lookback=20 根不含当前根,K 线 < 21 → nil), 落到 IntervalTechnicals.Box。 IndicatorComputer.Compute 签名升级到 longShortByInterval map: market_context 改为并发拉取 15m/1h/4h/1d 的全局 LSR,1w 不在 Binance LSR 支持列表 → 加 warning lsr_unsupported_interval:1w。 测试:boxBreak 6 个 case(inside/break_up/break_down/insufficient/2 个 parse 失败)+ per-interval Support/Resistance/Range 跨周期断言 + 镜像不变量 + 缺 LSR key → LongShortLine nil。indicator.go 平均覆盖 97.26%(boxBreak 100%)。
This commit is contained in:
@@ -51,8 +51,14 @@ type TechnicalStructure struct {
|
||||
}
|
||||
|
||||
type IntervalTechnicals struct {
|
||||
Bollinger *Bollinger `json:"bollinger,omitempty"`
|
||||
Vegas *Vegas `json:"vegas,omitempty"`
|
||||
Bollinger *Bollinger `json:"bollinger,omitempty"`
|
||||
Vegas *Vegas `json:"vegas,omitempty"`
|
||||
Support []TechnicalLevel `json:"support,omitempty"`
|
||||
Resistance []TechnicalLevel `json:"resistance,omitempty"`
|
||||
RangeHigh *string `json:"rangeHigh,omitempty"`
|
||||
RangeLow *string `json:"rangeLow,omitempty"`
|
||||
LongShortLine *string `json:"longShortLine,omitempty"`
|
||||
Box *BoxBreak `json:"box,omitempty"`
|
||||
}
|
||||
|
||||
// Bollinger 是 20 周期、2 倍标准差的布林带。
|
||||
@@ -74,6 +80,16 @@ type Vegas struct {
|
||||
Trend string `json:"trend"`
|
||||
}
|
||||
|
||||
// BoxBreak 是 Donchian 风格的 N 根 lookback 箱体突破信号。
|
||||
// BoxHigh / BoxLow 取最近 LookbackBars 根(不含当前根)的最高高 / 最低低;
|
||||
// Status: inside / break_up / break_down,由最后一根 K 线收盘相对箱体边判定。
|
||||
type BoxBreak struct {
|
||||
BoxHigh string `json:"boxHigh"`
|
||||
BoxLow string `json:"boxLow"`
|
||||
Status string `json:"status"`
|
||||
LookbackBars string `json:"lookbackBars"`
|
||||
}
|
||||
|
||||
type TechnicalLevel struct {
|
||||
Price string `json:"price"`
|
||||
Strength string `json:"strength,omitempty"`
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
// Package usecase: indicator.go 实现技术指标计算(Milestone 6)。
|
||||
// Package usecase: indicator.go 实现技术指标计算。
|
||||
//
|
||||
// 范围(v2 首版):
|
||||
// 范围(v2.x):每个周期独立产出一组 IntervalTechnicals,包含
|
||||
// - Support / Resistance:pivot 局部极值 + 0.3% 价格聚类
|
||||
// - RangeHigh / RangeLow:近 N 根 P95(High) / P5(Low) 线性插值
|
||||
// - LongShortLine:近 N 根 LSR 穿越 1.0 的价位中位数
|
||||
// - Bollinger(per interval):SMA20 + 2σ
|
||||
// - Vegas(per interval):EMA12/144/169 三线 + 趋势判定
|
||||
// - LongShortLine:同周期 LSR 穿越 1.0 的价位中位数(LSR 不可用的周期为 nil)
|
||||
// - Bollinger:SMA20 + 2σ
|
||||
// - Vegas:EMA12/144/169 三线 + 趋势判定
|
||||
// - Box:Donchian 风格 20 根 lookback 箱体突破
|
||||
//
|
||||
// 顶层 TechnicalStructure.{Support, Resistance, RangeHigh, RangeLow, LongShortLine}
|
||||
// 是 Intervals[primaryInterval] 的镜像(向后兼容,详见 ai/adr/0003-*.md)。
|
||||
//
|
||||
// 数值边界:本文件内部允许 transient float64,但输出到 entity 前
|
||||
// 必须 FormatFloat 转回 string。详见 ai/adr/0002-indicator-numeric-boundary.md
|
||||
// 与守卫 G12。
|
||||
//
|
||||
// 算法参数固定为常量;不读取 config——Milestone 6 不引入参数面。
|
||||
// 算法参数固定为常量;不读取 config。
|
||||
package usecase
|
||||
|
||||
import (
|
||||
@@ -51,6 +55,9 @@ const (
|
||||
vegasFast = 12
|
||||
vegasMid = 144
|
||||
vegasSlow = 169
|
||||
|
||||
// Box(Donchian-style):取最近 boxLookback 根(不含当前根)的 high/low
|
||||
boxLookback = 20
|
||||
)
|
||||
|
||||
// IndicatorUsecase 实现 IndicatorComputer 接口。
|
||||
@@ -68,50 +75,56 @@ type pivotPoint struct {
|
||||
|
||||
// Compute 是 IndicatorComputer 的唯一对外方法。
|
||||
// klinesByInterval 每个 value 必须按 OpenTime 升序排好(KlineRepository.FindRecent 已保证)。
|
||||
// primaryInterval 指定 support/resistance/range/longShortLine 用哪个周期;
|
||||
// 其它周期(包括 primary 自己)都会参与 Intervals 里的 Bollinger / Vegas。
|
||||
// longShort 同样升序;可能为空。
|
||||
// primaryInterval 决定顶层 TechnicalStructure.{Support, Resistance, Range*, LongShortLine}
|
||||
// 镜像哪个周期;所有周期都会独立计算 IntervalTechnicals。
|
||||
// longShortByInterval 同样升序;可能整体为 nil 或某个 key 缺失(如 1w,Binance 不支持)。
|
||||
func (u *IndicatorUsecase) Compute(
|
||||
klinesByInterval map[string][]entity.Kline,
|
||||
primaryInterval string,
|
||||
longShort []entity.LongShortRatio,
|
||||
longShortByInterval map[string][]entity.LongShortRatio,
|
||||
) entity.TechnicalStructure {
|
||||
primary := klinesByInterval[primaryInterval]
|
||||
highs := pivotHighs(primary, pivotLeft, pivotRight)
|
||||
lows := pivotLows(primary, pivotLeft, pivotRight)
|
||||
resistance := clusterLevels(highs, clusterPctThreshold, sourceResistancePivot)
|
||||
support := clusterLevels(lows, clusterPctThreshold, sourceSupportPivot)
|
||||
intervals := make(map[string]entity.IntervalTechnicals, len(klinesByInterval))
|
||||
for iv, ks := range klinesByInterval {
|
||||
intervals[iv] = computeIntervalTechnicals(ks, longShortByInterval[iv])
|
||||
}
|
||||
|
||||
// 顶层镜像 primary 周期;缺 primary 时给出空骨架(向后兼容 v2.0 JSON shape)。
|
||||
primary := intervals[primaryInterval]
|
||||
support := primary.Support
|
||||
if support == nil {
|
||||
support = []entity.TechnicalLevel{}
|
||||
}
|
||||
resistance := primary.Resistance
|
||||
if resistance == nil {
|
||||
resistance = []entity.TechnicalLevel{}
|
||||
}
|
||||
hi, lo := rangeHighLow(primary)
|
||||
lsLine := longShortCrossings(longShort, primary, longShortCrossKeep)
|
||||
|
||||
intervals := make(map[string]entity.IntervalTechnicals, len(klinesByInterval))
|
||||
for iv, ks := range klinesByInterval {
|
||||
intervals[iv] = computeIntervalTechnicals(ks)
|
||||
}
|
||||
|
||||
return entity.TechnicalStructure{
|
||||
Support: support,
|
||||
Resistance: resistance,
|
||||
RangeHigh: hi,
|
||||
RangeLow: lo,
|
||||
LongShortLine: lsLine,
|
||||
RangeHigh: primary.RangeHigh,
|
||||
RangeLow: primary.RangeLow,
|
||||
LongShortLine: primary.LongShortLine,
|
||||
Intervals: intervals,
|
||||
}
|
||||
}
|
||||
|
||||
// computeIntervalTechnicals 在单周期 K 线上算 Bollinger 与 Vegas。
|
||||
// 任一项数据不足则对应字段为 nil。
|
||||
func computeIntervalTechnicals(klines []entity.Kline) entity.IntervalTechnicals {
|
||||
// computeIntervalTechnicals 在单周期 K 线 + 同周期 LSR 上算所有指标。
|
||||
// 任一项数据不足则对应字段为 nil / 空切片。
|
||||
func computeIntervalTechnicals(klines []entity.Kline, ls []entity.LongShortRatio) entity.IntervalTechnicals {
|
||||
closes := parseCloses(klines)
|
||||
highs := pivotHighs(klines, pivotLeft, pivotRight)
|
||||
lows := pivotLows(klines, pivotLeft, pivotRight)
|
||||
hi, lo := rangeHighLow(klines)
|
||||
return entity.IntervalTechnicals{
|
||||
Bollinger: bollinger(closes),
|
||||
Vegas: vegas(closes),
|
||||
Bollinger: bollinger(closes),
|
||||
Vegas: vegas(closes),
|
||||
Support: clusterLevels(lows, clusterPctThreshold, sourceSupportPivot),
|
||||
Resistance: clusterLevels(highs, clusterPctThreshold, sourceResistancePivot),
|
||||
RangeHigh: hi,
|
||||
RangeLow: lo,
|
||||
LongShortLine: longShortCrossings(ls, klines, longShortCrossKeep),
|
||||
Box: boxBreak(klines, boxLookback),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,3 +543,46 @@ func vegas(closes []float64) *entity.Vegas {
|
||||
Trend: trend,
|
||||
}
|
||||
}
|
||||
|
||||
// boxBreak 计算 Donchian 风格的箱体突破。
|
||||
// 取最近 lookback 根(不含当前根)的 High 最大值与 Low 最小值组成箱体,
|
||||
// 用当前根 Close 判断 status:> BoxHigh → break_up;< BoxLow → break_down;其余 inside。
|
||||
// 需要 len(klines) >= lookback+1;任一价格解析失败 → nil。
|
||||
func boxBreak(klines []entity.Kline, lookback int) *entity.BoxBreak {
|
||||
if lookback <= 0 || len(klines) < lookback+1 {
|
||||
return nil
|
||||
}
|
||||
n := len(klines)
|
||||
window := klines[n-lookback-1 : n-1]
|
||||
hi, lo := math.Inf(-1), math.Inf(1)
|
||||
for _, k := range window {
|
||||
h, ok1 := parsePrice(k.High)
|
||||
l, ok2 := parsePrice(k.Low)
|
||||
if !ok1 || !ok2 {
|
||||
return nil
|
||||
}
|
||||
if h > hi {
|
||||
hi = h
|
||||
}
|
||||
if l < lo {
|
||||
lo = l
|
||||
}
|
||||
}
|
||||
closeVal, ok := parsePrice(klines[n-1].Close)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
status := "inside"
|
||||
switch {
|
||||
case closeVal > hi:
|
||||
status = "break_up"
|
||||
case closeVal < lo:
|
||||
status = "break_down"
|
||||
}
|
||||
return &entity.BoxBreak{
|
||||
BoxHigh: formatPrice(hi),
|
||||
BoxLow: formatPrice(lo),
|
||||
Status: status,
|
||||
LookbackBars: strconv.Itoa(lookback),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,3 +631,169 @@ func TestCompute_IntervalsPopulated(t *testing.T) {
|
||||
require.NotNil(t, out.Intervals["4h"].Bollinger)
|
||||
require.NotNil(t, out.Intervals["4h"].Vegas)
|
||||
}
|
||||
|
||||
// ---- boxBreak ---------------------------------------------------------
|
||||
|
||||
// mkFlatKline 生成单根 K 线,all 价位相同(用于构造箱体内 baseline)。
|
||||
func mkFlatKline(idx int64, price float64) entity.Kline {
|
||||
s := strconv.FormatFloat(price, 'f', -1, 64)
|
||||
return mkKline(idx, s, s, s, s)
|
||||
}
|
||||
|
||||
func TestBoxBreak_Inside(t *testing.T) {
|
||||
// 21 根:前 20 根 high=110、low=90 → box [90,110];最后一根 close=100 → inside
|
||||
klines := make([]entity.Kline, 21)
|
||||
for i := 0; i < 20; i++ {
|
||||
klines[i] = mkKline(int64(i), "100", "110", "90", "100")
|
||||
}
|
||||
klines[20] = mkKline(20, "100", "101", "99", "100")
|
||||
got := boxBreak(klines, boxLookback)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, "110", got.BoxHigh)
|
||||
require.Equal(t, "90", got.BoxLow)
|
||||
require.Equal(t, "inside", got.Status)
|
||||
require.Equal(t, "20", got.LookbackBars)
|
||||
}
|
||||
|
||||
func TestBoxBreak_BreakUp(t *testing.T) {
|
||||
// 前 20 根 [90,110];最后一根 close=120 > 110 → break_up
|
||||
klines := make([]entity.Kline, 21)
|
||||
for i := 0; i < 20; i++ {
|
||||
klines[i] = mkKline(int64(i), "100", "110", "90", "100")
|
||||
}
|
||||
klines[20] = mkKline(20, "115", "121", "114", "120")
|
||||
got := boxBreak(klines, boxLookback)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, "break_up", got.Status)
|
||||
// 当前根不参与箱体计算:边界仍是 110/90
|
||||
require.Equal(t, "110", got.BoxHigh)
|
||||
require.Equal(t, "90", got.BoxLow)
|
||||
}
|
||||
|
||||
func TestBoxBreak_BreakDown(t *testing.T) {
|
||||
klines := make([]entity.Kline, 21)
|
||||
for i := 0; i < 20; i++ {
|
||||
klines[i] = mkKline(int64(i), "100", "110", "90", "100")
|
||||
}
|
||||
klines[20] = mkKline(20, "92", "93", "85", "85")
|
||||
got := boxBreak(klines, boxLookback)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, "break_down", got.Status)
|
||||
require.Equal(t, "90", got.BoxLow)
|
||||
}
|
||||
|
||||
func TestBoxBreak_InsufficientData(t *testing.T) {
|
||||
// 仅 20 根(< lookback+1)→ nil
|
||||
klines := make([]entity.Kline, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
klines[i] = mkFlatKline(int64(i), 100)
|
||||
}
|
||||
require.Nil(t, boxBreak(klines, boxLookback))
|
||||
require.Nil(t, boxBreak(nil, boxLookback), "nil 输入")
|
||||
require.Nil(t, boxBreak(klines, 0), "lookback=0")
|
||||
require.Nil(t, boxBreak(klines, -1), "negative lookback")
|
||||
}
|
||||
|
||||
func TestBoxBreak_MalformedPrice(t *testing.T) {
|
||||
// 窗口内任意根价格 parse 失败 → 整体返回 nil
|
||||
klines := make([]entity.Kline, 21)
|
||||
for i := 0; i < 20; i++ {
|
||||
klines[i] = mkKline(int64(i), "100", "110", "90", "100")
|
||||
}
|
||||
klines[5] = mkKline(5, "100", "abc", "90", "100") // bad high
|
||||
klines[20] = mkKline(20, "100", "101", "99", "100")
|
||||
require.Nil(t, boxBreak(klines, boxLookback))
|
||||
}
|
||||
|
||||
func TestBoxBreak_MalformedClose(t *testing.T) {
|
||||
// 当前根 close parse 失败 → nil
|
||||
klines := make([]entity.Kline, 21)
|
||||
for i := 0; i < 20; i++ {
|
||||
klines[i] = mkKline(int64(i), "100", "110", "90", "100")
|
||||
}
|
||||
klines[20] = mkKline(20, "100", "101", "99", "xyz")
|
||||
require.Nil(t, boxBreak(klines, boxLookback))
|
||||
}
|
||||
|
||||
// ---- Compute per-interval(ADR-0003 镜像不变量)-------------------------
|
||||
|
||||
// TestCompute_PerIntervalSRAndMirror 是 ADR-0003 的核心守卫:
|
||||
// (a) 每个有足够 kline 的周期都独立产出 Support/Resistance/Range;
|
||||
// (b) 顶层 5 字段必须 == Intervals[primaryInterval] 的对应字段(单一来源镜像)。
|
||||
// 同时覆盖:缺 LSR key 的周期 LongShortLine 为 nil(对应 1w warning 的算法侧契约)。
|
||||
func TestCompute_PerIntervalSRAndMirror(t *testing.T) {
|
||||
// V-shape 21 根:1 个 pivotLow、0 个 pivotHigh
|
||||
vshape := func() []entity.Kline {
|
||||
out := make([]entity.Kline, 21)
|
||||
for i := 0; i < 21; i++ {
|
||||
var p float64
|
||||
if i <= 10 {
|
||||
p = 100 - float64(i)*1.0
|
||||
} else {
|
||||
p = 90 + float64(i-10)*1.0
|
||||
}
|
||||
out[i] = mkKline(int64(i),
|
||||
strconv.FormatFloat(p, 'f', -1, 64),
|
||||
strconv.FormatFloat(p+0.5, 'f', -1, 64),
|
||||
strconv.FormatFloat(p, 'f', -1, 64),
|
||||
strconv.FormatFloat(p+0.25, 'f', -1, 64),
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
// 4h 周期带 LSR 穿越(0.9 → 1.1)
|
||||
lsr4h := []entity.LongShortRatio{mkLSR(0, "0.9"), mkLSR(1, "1.1")}
|
||||
|
||||
u := NewIndicatorUsecase()
|
||||
out := u.Compute(
|
||||
map[string][]entity.Kline{
|
||||
"1h": vshape(),
|
||||
"4h": vshape(),
|
||||
"1w": vshape(), // 1w 无 LSR(模拟 Binance 不支持)
|
||||
},
|
||||
"4h",
|
||||
map[string][]entity.LongShortRatio{
|
||||
"1h": {mkLSR(0, "0.5"), mkLSR(1, "0.7")}, // 全在 1 下方 → 无穿越
|
||||
"4h": lsr4h,
|
||||
// 故意不放 "1w"
|
||||
},
|
||||
)
|
||||
|
||||
// (a) per-interval Support 非空
|
||||
require.NotEmpty(t, out.Intervals["1h"].Support, "1h V-shape 必有 pivot low")
|
||||
require.NotEmpty(t, out.Intervals["4h"].Support)
|
||||
require.NotEmpty(t, out.Intervals["1w"].Support)
|
||||
require.NotNil(t, out.Intervals["4h"].RangeHigh)
|
||||
require.NotNil(t, out.Intervals["4h"].RangeLow)
|
||||
|
||||
// LongShortLine:4h 有穿越 → 非 nil;1h 无穿越 → nil;1w 缺 key → nil
|
||||
require.NotNil(t, out.Intervals["4h"].LongShortLine, "4h 有穿越")
|
||||
require.Nil(t, out.Intervals["1h"].LongShortLine, "1h LSR 不穿越 1.0")
|
||||
require.Nil(t, out.Intervals["1w"].LongShortLine, "1w 缺 LSR key → nil")
|
||||
|
||||
// (b) 顶层镜像 primary=4h(ADR-0003 单一来源不变量)
|
||||
// 注意:顶层把 nil 归一为空切片(v2.0 JSON shape),所以用 ElementsMatch
|
||||
// 而不是严格 Equal 来表达"语义同源"。RangeHigh/Low/LongShortLine 是 *string
|
||||
// 共享同一指针,直接 Equal 即可。
|
||||
require.ElementsMatch(t, out.Intervals["4h"].Support, out.Support, "顶层 Support 与 Intervals[primary] 同源")
|
||||
require.ElementsMatch(t, out.Intervals["4h"].Resistance, out.Resistance)
|
||||
require.Equal(t, out.Intervals["4h"].RangeHigh, out.RangeHigh)
|
||||
require.Equal(t, out.Intervals["4h"].RangeLow, out.RangeLow)
|
||||
require.Equal(t, out.Intervals["4h"].LongShortLine, out.LongShortLine)
|
||||
}
|
||||
|
||||
// TestCompute_MissingPrimaryGivesEmptySkeleton:primary 不在 Intervals 时,
|
||||
// 顶层 Support/Resistance 必须是空切片(非 nil,向后兼容 v2.0 JSON shape)。
|
||||
func TestCompute_MissingPrimaryGivesEmptySkeleton(t *testing.T) {
|
||||
u := NewIndicatorUsecase()
|
||||
out := u.Compute(
|
||||
map[string][]entity.Kline{"1h": ascending(5, 100, 1)},
|
||||
"4h", // primary 不存在
|
||||
nil,
|
||||
)
|
||||
require.NotNil(t, out.Support)
|
||||
require.NotNil(t, out.Resistance)
|
||||
require.Empty(t, out.Support)
|
||||
require.Empty(t, out.Resistance)
|
||||
require.Nil(t, out.RangeHigh, "primary 缺席 → 顶层 RangeHigh 也为 nil")
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ var supportedSymbols = map[string]bool{
|
||||
|
||||
var supportedIntervals = []string{"15m", "1h", "4h", "1d", "1w"}
|
||||
|
||||
// lsrSupportedIntervals 是 Binance Futures Global LSR 接口支持的 period 集合。
|
||||
// 1w 不在列表内 → 该周期 IntervalTechnicals.LongShortLine 为 nil,外加 warning。
|
||||
var lsrSupportedIntervals = []string{"15m", "1h", "4h", "1d"}
|
||||
|
||||
const (
|
||||
klineWindowSize = 300
|
||||
klineMinForOK = 200
|
||||
@@ -164,10 +168,31 @@ func (u *MarketContextUsecase) Build(ctx context.Context, symbol string) (*entit
|
||||
addWarn("OI history query failed: " + err.Error())
|
||||
}
|
||||
|
||||
globalLS, err := u.lsRepo.FindRecent(ctx, symbol, derivativePeriod, entity.RatioTypeGlobalAccount, longShortLen)
|
||||
if err != nil {
|
||||
addWarn("global long/short query failed: " + err.Error())
|
||||
globalLSByInterval := make(map[string][]entity.LongShortRatio, len(lsrSupportedIntervals))
|
||||
var lsMu sync.Mutex
|
||||
lsG, lsCtx := errgroup.WithContext(ctx)
|
||||
for _, iv := range lsrSupportedIntervals {
|
||||
iv := iv
|
||||
lsG.Go(func() error {
|
||||
rows, err := u.lsRepo.FindRecent(lsCtx, symbol, iv, entity.RatioTypeGlobalAccount, longShortLen)
|
||||
if err != nil {
|
||||
addWarn(fmt.Sprintf("global long/short query failed for %s: %v", iv, err))
|
||||
return nil
|
||||
}
|
||||
lsMu.Lock()
|
||||
globalLSByInterval[iv] = rows
|
||||
lsMu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
_ = lsG.Wait()
|
||||
for _, iv := range supportedIntervals {
|
||||
if !lsrSupports(iv) {
|
||||
addWarn("lsr_unsupported_interval:" + iv)
|
||||
}
|
||||
}
|
||||
globalLS := globalLSByInterval[derivativePeriod]
|
||||
|
||||
topLS, err := u.lsRepo.FindRecent(ctx, symbol, derivativePeriod, entity.RatioTypeTopTraderPosition, longShortLen)
|
||||
if err != nil {
|
||||
addWarn("top trader position long/short query failed: " + err.Error())
|
||||
@@ -202,7 +227,7 @@ func (u *MarketContextUsecase) Build(ctx context.Context, symbol string) (*entit
|
||||
Snapshot: snapshot,
|
||||
Klines: klines,
|
||||
Derivatives: derivBundle,
|
||||
Technical: u.indicator.Compute(klines, derivativePeriod, globalLS),
|
||||
Technical: u.indicator.Compute(klines, derivativePeriod, globalLSByInterval),
|
||||
DataQuality: entity.DataQuality{
|
||||
Source: "binance",
|
||||
Warnings: warnings,
|
||||
@@ -218,3 +243,12 @@ func (u *MarketContextUsecase) Build(ctx context.Context, symbol string) (*entit
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func lsrSupports(interval string) bool {
|
||||
for _, iv := range lsrSupportedIntervals {
|
||||
if iv == interval {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -59,14 +59,15 @@ type TakerVolumeRepository interface {
|
||||
// 重复 IO。
|
||||
//
|
||||
// klinesByInterval 是按周期分组的 K 线 map(每周期一份升序切片);
|
||||
// primaryInterval 指定哪个周期参与 support/resistance/range/longShortLine
|
||||
// 的计算(其它周期只参与 Intervals 多周期指标如 Bollinger / Vegas)。
|
||||
// primaryInterval 决定顶层 TechnicalStructure 镜像哪个周期;所有周期
|
||||
// 都会独立填充进 Intervals。longShortByInterval 按周期分组的 LSR map
|
||||
// (某周期缺失时,该周期 IntervalTechnicals.LongShortLine 为 nil)。
|
||||
// 实现可见 internal/usecase/indicator.go。
|
||||
type IndicatorComputer interface {
|
||||
Compute(
|
||||
klinesByInterval map[string][]entity.Kline,
|
||||
primaryInterval string,
|
||||
longShort []entity.LongShortRatio,
|
||||
longShortByInterval map[string][]entity.LongShortRatio,
|
||||
) entity.TechnicalStructure
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user