122 lines
8.3 KiB
Markdown
122 lines
8.3 KiB
Markdown
# 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`。后者实际上**不应**用 float64(query usecase 只是从 repo 拉 entity 再返回),但 token 形态保持简单——靠 code review + 同步新增的 G12.7 强约束 query 文件不引 float64 也行(见 D3 取舍)。
|
||
|
||
### D2. 新增 G12.7 / G12.8(coinglass_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 全 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
|
||
|
||
考虑过:
|
||
|
||
```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 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 文件(grep `float64` 内嵌 `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 是兑现该承诺。
|