为什么:Milestone 6 引入 transient float64 用于 pivot/percentile/median 计算。G2 早已预留"统计中间过程可用 float64"的例外,但缺可机械验证 的边界。本提交把例外落地为: - ADR-0002 记录"为什么不用 shopspring/decimal" - G12 给出 4 条 grep 守卫(usecase float 仅限 indicator.go、 indicator.go 不依赖 repo、不依赖 binance/postgres/pgx、entity 仍禁 float) 代码无改动;下一 commit 开始铺 indicator.go 骨架。
4.3 KiB
ADR 0002 — 指标计算的数值边界
状态:Accepted
日期:2026-05-24
决策者:项目维护者
适用范围:internal/usecase/indicator.go 及其后续衍生指标实现
背景
Milestone 6 引入技术指标计算(pivot S/R、percentile range、LSR 穿越中位数)。这些算法需要:
- 浮点排序(percentile、median)
- 浮点比较(cluster threshold 0.3%)
- 线性插值(percentile interpolation、LSR 穿越价位)
而 entity.Kline.High / Low / Close 和 entity.LongShortRatio.Ratio 都是 string(见 ADR-0001 D5 / 守卫 G2)——全链路 string 保留 Binance 原始精度。
问题:指标内部要做数学运算,但项目硬规则禁止 float64 表示价格。如何调和?
候选方案:
shopspring/decimal— 任意精度十进制,Go 量化生态事实标准math/big.Float— stdlib,任意精度二进制strconv.ParseFloat+ transientfloat64— stdlib,IEEE-754 binary64
决策
选 方案 3:strconv + transient float64,输出前 FormatFloat 转回 string。
边界规则:
float64只出现在internal/usecase/indicator.go内部- 进入
entity.TechnicalLevel/entity.TechnicalStructure前必须strconv.FormatFloat(f, 'f', -1, 64)转回 string - entity / DTO / persistence / transport 全部保持 string(不变)
- 任何其它 usecase 文件出现
float64视为违规
这是 ai/risk-guardrails.md G2 例外条款("纯统计/分析(如指标计算的中间过程)可以用 float64,但结果落库或返回 API 时必须转回 string")的具体落地。守卫 G12 把这条例外细化成可机械验证的命令。
理由
精度足够
BTC/ETH USDⓈ-M Futures 报价上限 ≈ 1e6 USD。
IEEE-754 binary64 在该量级下:
- 显著位数 ~15.95 decimal digits
- 相对误差 < 2.22e-16
- 绝对误差 < 1e-9 USD
而 Binance 价格步长(tickSize):BTCUSDT 0.10、ETHUSDT 0.01。float64 累积误差比 tickSize 小至少 7 个数量级。指标输出(support price、percentile)的有效位数远不到这个程度。
不引入 direct dep
shopspring/decimal 是稳定的好库,但:
- 当前
go.mod没有它,引入 = direct dep + 间接依赖链增长 - 排序 / percentile / median 在 decimal 上写起来要构造 sort.Interface、手写比较,啰嗦
- benchmark 上 decimal 加减乘除比 float64 慢 ~10-30x;排序整段 N=300 K 线大概累计差几十微秒——非热路径,但累积可见
math/big.Float 比 decimal 更难写(没有 Less / Cmp 的舒适接口),且十进制⇄二进制反复转换本身就有"看起来对但实际错位"的风险。
G2 已预留例外
守卫 G2 原文(risk-guardrails.md:43):
例外:纯统计/分析(如指标计算的中间过程)可以用 float64,但结果落库或返回 API 时必须转回 string。
本 ADR 并非破坏既有规则,而是把这条已写好的例外具体化、可验证化。
代价
- 指标内部计算精度严格依赖 IEEE-754 行为;如果未来要做"百万级订单簿累加成交价"这类对绝对精度敏感的统计,本决策需重新评估。
- 任何走出
indicator.go的 float 值都是 bug;依赖 G12 grep + code review 双重把关。 - transient float64 → string 转回时
FormatFloat(_, 'f', -1, 64)会给出最短可往返表示。对于0.300000这种 Binance 原样字符串经一次解析+格式化后可能变成0.3——这点要在 indicator 测试中明确容忍,并在 ADR-0001 D5(全链路 string)之外形成补充:indicator 输出的 string 是"计算结果 string",不一定字符等于任何输入 string。
何时重新评估
- 指标扩展到累加型(VWAP 总和、OI 加权累计)且数据点 ≥ 1e8 级别 → 评估 Kahan 求和或上 decimal
- 出现"测试断言失败但人眼看着正确"的浮点尾差 case → 升级前先尝试调整测试容差,无法解决再换 decimal
- 任何对外公布的"指标精度承诺"高于 1e-6 → 当前方案不够,必须换 decimal
与其它决策的关系
- ADR-0001 D5(全链路 string):本 ADR 是其在指标计算场景的具体例外说明,不冲突
- 守卫 G2:本 ADR 是 G2 例外条款的落地
- 守卫 G12(本次新增):本 ADR 的可机械验证形式