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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user