8.3 KiB
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:
internal/usecase/coinglass_signal.go— 跨交易所 OI / funding / LSR 聚合,做加权平均、percentile、阈值比较。和derivatives_signal.go同形态。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:
@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。后者实际上不应用 float64(query usecase 只是从 repo 拉 entity 再返回),但 token 形态保持简单——靠 code review + 同步新增的 G12.7 强约束 query 文件不引 float64 也行(见 D3 取舍)。
D2. 新增 G12.7 / G12.8(coinglass_signal 同款约束)
镜像 G12.5 / G12.6 的形态:
@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 全 string(spec D6),mapper 出口若漏掉 FormatFloat,G12.4 会立刻 break。 - 测试:
mapper_test.go必须 table-driven 覆盖至少一个含小数的字段(funding rate、OI 数量),断言 entity.xxx 字段是string类型且形如"0.0123"——这是 mapper 行为的回归底线。
为什么不扩 G12.1 去扫 webapi:internal/repo/webapi/binance/ 下虽然没 float64(Binance 直接返回 string,mapper 无需转换),但将来若有别的数据源走相同的"JSON number → float64 → FormatFloat 回 string"路径,每个都要单独白名单会让 G12.1 越来越糙。CoinGlass mapper 的安全网是 G12.4(entity 全 string)+ unit test,结构性确保 float 不会泄漏到下游,比 grep 白名单更稳。
D4. coinglass_query.go 严格禁 float64(隐式)
coinglass_query.go 从 repo 拿到的 entity 已经是 string(D3 出口规则保证),usecase 内任何 float64 都属于 bug。G12.1 的 coinglass token 是"宽门",但 code review 阶段对 query 文件出现 float64 应该当作必改项。
理由
为什么不把 mapper 单独列 G12.9 grep
考虑过:
@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 grep(G12.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出现 float64(PR review 抓到或 G12.1 漏过)→ 立即添加 G12.9 单独守 query 文件(grepfloat64内嵌coinglass_query.go必须空)。
与其它决策的关系
- ADR-0001 D5(全链路 string):CoinGlass entity 全 string(spec D6),严格沿用,无冲突。
- ADR-0002(指标数值边界):本 ADR 是其在 v3 数据源场景的白名单扩展 + 边界形态新增,不修改 ADR-0002 的核心决策。
- ADR-0003(per-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 是兑现该承诺。