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 骨架。
This commit is contained in:
dela
2026-05-24 20:10:13 +08:00
parent 05dc2ef0e7
commit c6c25d1a45
2 changed files with 126 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# 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 的可机械验证形式

View File

@@ -193,6 +193,35 @@ grep -rn "FundingRepo\|OIRepo\|LSRepo\|TakerRepo" internal/controller
---
## G12. 指标计算的 float64 边界
**规则**`float64``internal/usecase/` 下**只允许出现在 `indicator.go` 内部**作为 transient 计算变量;进入 `entity` / 返回上层之前必须 `strconv.FormatFloat(_, 'f', -1, 64)` 转回 string。`internal/usecase/indicator.go` 同时**禁止** import `internal/repo/...` 的任何子包。
**为什么**G2 已经预留"指标计算中间过程可用 float64"的例外但没有可机械验证的边界。Milestone 6 之后会有更多指标加入,必须把例外明文化、可 grep 化,避免某天 float64 悄悄出现在 entity 字段或被 repo 持久化。决策细节见 `ai/adr/0002-indicator-numeric-boundary.md`
**怎么验证**
```bash
# usecase 下除 indicator.go 之外不得出现 float64
grep -rn --include="*.go" "float64\|float32" internal/usecase | grep -v indicator
# 期望:无输出
# indicator.go 不得 import 任何 repo 子包
grep -n "internal/repo" internal/usecase/indicator.go
# 期望:无输出
# indicator.go 不得 import binance / postgres / pgx
grep -nE "binance|postgres|pgx" internal/usecase/indicator.go
# 期望:无输出(注释中的 ADR 引用不算)
# entity 字段任何位置不得 float
grep -rn --include="*.go" "float64\|float32" internal/entity
# 期望:无输出
```
**例外**:无。如果未来某个指标输入/输出确实需要 decimal 精度(累加型大数据集),先升级 ADR-0002 再改实现。
---
## 升级守护规则的时机
同一类错误第二次出现 → 写一条新规则进来。修改本文件 = 改动了项目契约,需要在 PR 描述里说明原因并 @ 维护者。