Files
cryptoHermes/ai/adr/0004-coinglass-numeric-boundary-extension.md
dela 1b61541ff5
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
feat: 接入 CoinGlass 清算热力图隔离链路
2026-05-25 16:57:22 +08:00

122 lines
8.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 0004 — CoinGlass 数值边界例外扩展
**状态**Accepted
**日期**2026-05-25
**决策者**:项目维护者
**适用范围**`internal/usecase/coinglass_signal.go`(新)、`internal/usecase/coinglass_query.go`(新)、`internal/repo/webapi/coinglass/mapper.go`(新)、`internal/repo/webapi/coinglass/crypto.go`(新)、`Makefile``guard` 目标
---
## 背景
v3 phase-1 CoinGlass 接入spec`ai/specs/coinglass.md`)会在两个不同位置引入 transient `float64`
1. **`internal/usecase/coinglass_signal.go`** — 跨交易所 OI / funding / LSR 聚合做加权平均、percentile、阈值比较。和 `derivatives_signal.go` 同形态。
2. **`internal/repo/webapi/coinglass/mapper.go`** — 解密后的 plaintext JSON 通过 `json.Unmarshal` 默认进 `interface{}` 时数值就是 `float64`mapper 把这些 `float64` 映射到 entity 字段spec D6 锁定:出口 `strconv.FormatFloat(_, 'f', -1, 64)` 回 string
这两个位置和 ADR-0002 当时锁定的边界形态不完全一致:
- ADR-0002 的白名单**只**覆盖 `internal/usecase/`G12.1 grep 路径),且白名单文件名 pattern 是 `indicator|derivatives_signal`
- `coinglass_signal.go` 套用同款例外,**形态相同 → 直接扩白名单**就够。
- `repo/webapi/coinglass/mapper.go` 是**新形态**webapi 层的 transient float64。当前 G12.1 不扫 `repo/`,但下游 webapi mapper 第一次出现 `float64` 也意味着 G12 的"边界守住"语义需要明文表态。
问题:怎么把 CoinGlass 的两个 float64 位置纳入 G12而不让"白名单越扩越松"失控。
---
## 决策
**沿用 ADR-0002 的"白名单 + 出口强转 string"模式,把 G12 的语义按场景扩两块:**
### D1. usecase 层白名单扩 `coinglass`
`Makefile` `guard` 的 G12.1 第三个白名单 token
```makefile
@echo "G12.1 (usecase float)..." && ! { \
grep -rn --include="*.go" "float64\|float32" internal/usecase \
| grep -vE "indicator|derivatives_signal|coinglass" \
| grep -q . ; }
```
`coinglass` 这个 token 同时覆盖 `coinglass_signal.go``coinglass_query.go`。后者实际上**不应**用 float64query usecase 只是从 repo 拉 entity 再返回),但 token 形态保持简单——靠 code review + 同步新增的 G12.7 强约束 query 文件不引 float64 也行(见 D3 取舍)。
### D2. 新增 G12.7 / G12.8coinglass_signal 同款约束)
镜像 G12.5 / G12.6 的形态:
```makefile
@echo "G12.7 (coinglass_signal no repo)..." && ! grep -q "internal/repo" internal/usecase/coinglass_signal.go
@echo "G12.8 (coinglass_signal no DB)..." && ! grep -qE "binance|postgres|pgx" internal/usecase/coinglass_signal.go
```
`coinglass_query.go` **不**列入 no-repo 守卫——query usecase 本来就要打 repo沿用 `market_query.go` 形态)。
### D3. repo/webapi/coinglass mapper 不进 G12 扫描,但必须满足出口规则
`repo/webapi/coinglass/mapper.go``crypto.go` 内部允许 transient `float64`**不**在 G12.1 的 grep 路径里G12.1 仍只扫 `internal/usecase`)。
约束改成两层兜底:
- **静态**G12.4 不变(`internal/entity` 任何位置不得 float。CoinGlass entity 全 stringspec D6mapper 出口若漏掉 FormatFloatG12.4 会立刻 break。
- **测试**`mapper_test.go` 必须 table-driven 覆盖至少一个含小数的字段funding rate、OI 数量),断言 entity.xxx 字段是 `string` 类型且形如 `"0.0123"`——这是 mapper 行为的回归底线。
**为什么不扩 G12.1 去扫 webapi**`internal/repo/webapi/binance/` 下虽然没 float64Binance 直接返回 stringmapper 无需转换),但将来若有别的数据源走相同的"JSON number → float64 → FormatFloat 回 string"路径,每个都要单独白名单会让 G12.1 越来越糙。CoinGlass mapper 的安全网是 G12.4entity 全 string+ unit test**结构性**确保 float 不会泄漏到下游,比 grep 白名单更稳。
### D4. `coinglass_query.go` 严格禁 float64隐式
`coinglass_query.go` 从 repo 拿到的 entity 已经是 stringD3 出口规则保证usecase 内任何 float64 都属于 bug。G12.1 的 `coinglass` token 是"宽门",但 code review 阶段对 query 文件出现 float64 应该当作必改项。
---
## 理由
### 为什么不把 mapper 单独列 G12.9 grep
考虑过:
```makefile
@echo "G12.9 (coinglass mapper exit)..." && grep -qE "FormatFloat\(.*'f'" internal/repo/webapi/coinglass/mapper.go
```
否决grep 只能证明"有 FormatFloat 调用",不能证明"每个 float64 字段都过了 FormatFloat"。真正能验证后者的是 table-driven 测试断言。把"伪安全"的 grep 加进 Makefile 反而稀释 G12 的可信度——所以 D3 选择"G12.4 + 单测"双兜底,明确拒绝在 Makefile 加 mapper 专用 grep。
### 为什么不为 mapper 引入 decimal
CoinGlass JSON 数值上限远低于 ADR-0002 论证过的 1e6 USD 量级funding rate ~ ±0.01、OI 数量 ~ 1e10 BTC、清算量 ~ 1e9 USD。IEEE-754 binary64 显著位数 ~15.95 decimal digits绝对误差 << CoinGlass 自身的精度承诺(它的源数据来自交易所,再聚合一道)。引入 `shopspring/decimal` 在 mapper 这种 hot path每个 endpoint 每周期数十到数百个数值字段)反而是性能税。
### 为什么单开 ADR 而不是改 ADR-0002
ADR-0002 已 Accepted且 ADR 是不可变记录。扩白名单 = 新决策,按 he-maintainer 模式开 ADR-0004 链接回 ADR-0002 更清晰将来若再有第三个数据源走类似路径V3 phase-2 ETF Flow / CME可继续开 ADR-0005链路可追。
### 与 spec §6.3 的关系
spec `ai/specs/coinglass.md` §6.3 已经口头描述了 G12.1 扩白名单 + 新增 G12.7/G12.8。本 ADR 是这条 spec 子条款的**架构层背书**——把"为什么这样扩、不这样扩的边界在哪"固化下来,避免 PR-1 实施时 reviewer 来问"为什么 mapper 不也加 G12.9"再重新讨论。
---
## 代价
- G12 白名单从 2 个 token`indicator|derivatives_signal`)变 3 个(`+coinglass`)。每多一个 token"白名单越扩越松"的诱惑越大;本 ADR 明确:**只有 ADR 批准过的形态可以加 token**。未来增源前必须先开 ADR 再改 Makefile。
- mapper 的 float64 边界靠 entity grepG12.4+ unit test 双兜底,没有 Makefile grep 直接守。若 mapper 单测被改/删而 entity 没破理论上能漏过一段时间——缓解mapper_test.go 在 PR-1 的 test plan 里列为 must-have且未来 G12.4 一旦触发立即定位到 mapper 出口未 FormatFloat。
- 比"全栈 decimal"方案差的精度:依 IEEE-754 累积误差。CoinGlass 数据用作下游 Hermes 量化信号的"参考量级"而非"成交价 / 落库价",可接受;若某天 CoinGlass 数据要进入交易决策的资金计算路径,本 ADR 需重评(见下)。
---
## 何时重新评估
- 新增第三类 webapi mapper 走"JSON float64 → string entity"路径 → 评估是否升级到通用机制codegen mapper 或 wrapper helper而不是逐个 ADR。
- CoinGlass 数据进入资金/订单决策(目前 spec 明确不进)→ 直接复用 ADR-0002 的升级路径:换 decimal 或上 Kahan 求和。
- G12.1 白名单扩到第 5 个 token → 触发"白名单失控"复评,可能改成"显式列文件名清单"代替 grep pattern。
- `coinglass_query.go` 出现 float64PR review 抓到或 G12.1 漏过)→ 立即添加 G12.9 单独守 query 文件grep `float64` 内嵌 `coinglass_query.go` 必须空)。
---
## 与其它决策的关系
- **ADR-0001 D5全链路 string**CoinGlass entity 全 stringspec D6严格沿用无冲突。
- **ADR-0002指标数值边界**:本 ADR 是其在 v3 数据源场景的**白名单扩展 + 边界形态新增**,不修改 ADR-0002 的核心决策。
- **ADR-0003per-interval 技术结构)**无直接关系CoinGlass 数据未来若并入 `TechnicalStructure.Intervals` 是另一份 ADR 的议题。
- **守卫 G12**risk-guardrails.md:196本 ADR 落地后risk-guardrails.md 的 G12 章节需要同步:白名单文件清单加 `coinglass_signal.go`,子规则列表加 G12.7 / G12.8。该同步在 PR-1 落地 Makefile 改动时一起做。
- **`ai/specs/coinglass.md` §6.3 / §16**spec §16 已经把"暂列 ADR-0004"提前注册,本 ADR 是兑现该承诺。