// Package usecase: indicator.go 实现技术指标计算(Milestone 6)。 // // 范围(v2 首版): // - Support / Resistance:pivot 局部极值 + 0.3% 价格聚类 // - RangeHigh / RangeLow:近 N 根 P95(High) / P5(Low) 线性插值 // - LongShortLine:近 N 根 LSR 穿越 1.0 的价位中位数 // // 数值边界:本文件内部允许 transient float64,但输出到 entity 前 // 必须 FormatFloat 转回 string。详见 ai/adr/0002-indicator-numeric-boundary.md // 与守卫 G12。 // // 算法参数固定为常量;不读取 config——Milestone 6 不引入参数面。 package usecase import ( "sort" "strconv" "cryptoHermes/internal/entity" ) const ( // Pivot 窗口:左右各 5 根 K 线 pivotLeft = 5 pivotRight = 5 // 价格聚类阈值:两个 pivot 价位相对距离 < 0.3% → 视为同一 level clusterPctThreshold = 0.003 // LongShortLine 保留近 N 次穿越 longShortCrossKeep = 20 // Range 百分位 rangePercentileHi = 0.95 rangePercentileLo = 0.05 // TechnicalLevel.Source 值 sourceSupportPivot = "pivot_low" sourceResistancePivot = "pivot_high" ) // IndicatorUsecase 实现 IndicatorComputer 接口。 // 零状态结构体——所有方法纯函数。 type IndicatorUsecase struct{} // NewIndicatorUsecase 构造器;无依赖。 func NewIndicatorUsecase() *IndicatorUsecase { return &IndicatorUsecase{} } // pivotPoint 是 pivot 检测的内部中间结构,价格已经被 parse 成 float64。 type pivotPoint struct { Price float64 Index int } // Compute 是 IndicatorComputer 的唯一对外方法。 // klines 必须按 OpenTime 升序排好(KlineRepository.FindRecent 已保证)。 // longShort 同样升序;可能为空。 // // 当前实现为骨架(commit 2):返回零值结构,让 indicator_test.go 全红。 // commit 3 填实算法。 func (u *IndicatorUsecase) Compute( klines []entity.Kline, longShort []entity.LongShortRatio, ) entity.TechnicalStructure { _ = klines _ = longShort return entity.TechnicalStructure{ Support: []entity.TechnicalLevel{}, Resistance: []entity.TechnicalLevel{}, } } // parsePrice 用 strconv 把 string 价格解析成 float64。 // 失败(空 / NaN / 非数字)返回 ok=false,调用者应跳过该值不 panic。 func parsePrice(s string) (float64, bool) { _ = sort.Float64s // keep import; will be used in commit 3 _ = strconv.Itoa return 0, false } // formatPrice 把 transient float64 转回 string。 // 用 'f' / prec=-1 / bitSize=64 让 FormatFloat 给出最短可往返表示。 func formatPrice(f float64) string { return "" } // pivotHighs 在 klines 上扫描,返回所有 left=L、right=R 的 swing high。 // swing high 条件:klines[i].High 严格大于 [i-L, i-1] 和 [i+1, i+R] 内所有 high。 // 边界:i < L 或 i > len-R-1 跳过。 func pivotHighs(klines []entity.Kline, left, right int) []pivotPoint { return nil } // pivotLows 对称镜像 pivotHighs,按 .Low 找局部低点。 func pivotLows(klines []entity.Kline, left, right int) []pivotPoint { return nil } // clusterLevels 把 pivot 点按价格相对距离聚合到同一 level。 // 算法:对 points 按价格升序排序;从最小往上扫,若当前点价格与当前 cluster // 的均价相对距离 < pctThreshold,则合并;否则起新 cluster。 // 每个 cluster 输出 TechnicalLevel{Price: formatPrice(均价), Strength: strconv.Itoa(len), Source: source}。 // 输出按 Strength 降序、Price 降序(同 Strength 时高价在前)。 func clusterLevels(points []pivotPoint, pctThreshold float64, source string) []entity.TechnicalLevel { return nil } // percentile 在升序排好的 sorted 上算线性插值的 p 分位(p∈[0,1])。 // 经典 type 7(NumPy 默认):rank = (n-1)*p,floor + 小数权重插值。 // n==0 返回 0;n==1 返回该唯一值。 func percentile(sortedAsc []float64, p float64) float64 { return 0 } // rangeHighLow 计算 RangeHigh(P95)和 RangeLow(P5)。 // 解析失败的价位被跳过,不污染百分位计算。 // klines 为空时返回 (nil, nil)。 func rangeHighLow(klines []entity.Kline) (high *string, low *string) { return nil, nil } // longShortCrossings 在 ls 上扫描 ratio == 1.0 的穿越点。 // "穿越" 定义:相邻两条 ratio 一上一下穿过 1.0(含相等边界,但单边贴 1.0 不算)。 // 对每个穿越点,按时间戳找最近的 kline,用 (open+close)/2 作为穿越价位估计。 // 保留最近 keepN 次穿越的价位,取中位数;无穿越或全部 parse 失败 → nil。 func longShortCrossings(ls []entity.LongShortRatio, klines []entity.Kline, keepN int) *string { return nil } // medianFloat 计算切片中位数;空切片返回 0。会改写传入 slice(内部 sort)。 // 调用者若不希望被改,应先 copy。 func medianFloat(xs []float64) float64 { return 0 }