Files
cryptoHermes/ai/adr/0002-indicator-numeric-boundary.md
dela c6c25d1a45 chore(harness): ADR-0002 + G12 立指标计算的 float64 边界
为什么: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 骨架。
2026-05-24 20:10:13 +08:00

98 lines
4.3 KiB
Markdown
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.
# 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` 表示价格。如何调和?
候选方案:
1. **`shopspring/decimal`** — 任意精度十进制Go 量化生态事实标准
2. **`math/big.Float`** — stdlib任意精度二进制
3. **`strconv.ParseFloat` + transient `float64`** — stdlibIEEE-754 binary64
---
## 决策
**方案 3strconv + 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 价格步长tickSizeBTCUSDT 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 的可机械验证形式