Files
cryptoHermes/internal/usecase/indicator.go
dela 6d412b2326 feat(indicator): ports + 骨架 + 完整测试 table(红灯)
为什么:先把测试与骨架同时落地,让"红灯先行"作为 commit 3 实现的
靶子。骨架函数全部返回零值,所有断言失败但无编译错误也无 panic。

变更:
- ports.go 新增 IndicatorComputer 接口;接收已 fetch 的 slice、
  避免与 MarketContextUsecase 重复 IO(G1 + G12)
- internal/usecase/indicator.go:常量、pivotPoint、9 个 helper +
  Compute 全部签名就位,实现先返零值
- internal/usecase/indicator_test.go:parsePrice/formatPrice/
  percentile/medianFloat/pivot{Highs,Lows}/clusterLevels/
  rangeHighLow/longShortCrossings/Compute 全部 table-driven
  覆盖(含 V-shape、双顶 0.18%/0.45%、三聚类、N=1/2/20 percentile、
  穿越保留 N、malformed 跳过等)
- go.mod 把 stretchr/testify v1.8.1 从 indirect 提升为 direct
2026-05-24 20:16:48 +08:00

135 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package usecase: indicator.go 实现技术指标计算Milestone 6
//
// 范围v2 首版):
// - Support / Resistancepivot 局部极值 + 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 7NumPy 默认rank = (n-1)*pfloor + 小数权重插值。
// n==0 返回 0n==1 返回该唯一值。
func percentile(sortedAsc []float64, p float64) float64 {
return 0
}
// rangeHighLow 计算 RangeHighP95和 RangeLowP5
// 解析失败的价位被跳过,不污染百分位计算。
// 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
}