# ADR 0001 — 架构基础决策 **状态**:Accepted **日期**:2026-05-24 **决策者**:项目维护者 **适用范围**:cryptoHermes v1 全部代码 --- ## 背景 cryptoHermes 是给上游 Hermes 量化分析引擎用的 Binance Futures 行情网关。在 MVP 启动前需要锁定一组基础技术决策,避免后续 PR 反复重新评估。 需求边界(来自 `docs/dev.md` 第 1 章): - 只读公共行情,不下单、不接私钥 - BTCUSDT / ETHUSDT,分钟到周级 K 线 + 衍生品(funding / OI / 多空比 / taker volume) - 历史数据必须自存(Binance 历史 OI / 多空比官方只保留 30 天) - Hermes 只调用 `/v1/market/context` 一个聚合接口 - 部署形态:单体服务 + Postgres,国内开发机通过 HTTPS_PROXY 访问 Binance --- ## 决策 ### D1. Web 框架:Fiber v2 **选了 Fiber v2**(而不是 net/http 标准库、Gin、Echo、Chi)。 **理由**: - 性能足够(fasthttp 底层),单体网关不需要再调优 - API 与 Express/Gin 相近,团队上手快 - 内置 recover/logger/req-id 中间件 - v2 稳定,社区活跃 **代价**: - 基于 fasthttp,不兼容 `net/http` 中间件生态 - 错误类型用 `*fiber.Error`,需要在 controller 包一层 **何时重新评估**:如果需要 HTTP/2 server push、需要复用 `net/http` 生态中间件、或者 v2 长期不维护——届时考虑迁回标准库。 --- ### D2. 数据库:TimescaleDB on PG 16 **选了 TimescaleDB**(而不是纯 Postgres、InfluxDB、ClickHouse)。 **理由**: - 时序数据天然适合 hypertable:5 张表全部按时间分片,查询近期数据走最新 chunk,旧数据自动归档 - SQL 接口和普通 PG 完全兼容,pgx 直连,无需新驱动 - 没有引入新的运维栈(开发机用 Docker 起 `timescale/timescaledb:latest-pg16` 即可) - Chunk 间隔可按表调(K 线 1 周、funding 30 天) **代价**: - 必须跑 `SELECT create_hypertable(...)` 才能享受时序优化,新表容易忘 - 升级 PG 大版本需要看 Timescale 兼容矩阵 **何时重新评估**:如果数据量进入 TB 级、需要列存压缩或者 OLAP 复杂聚合——考虑 ClickHouse。 --- ### D3. Architecture:Clean Architecture(4 层 + ports.go 接口边界) **4 层**:`controller / usecase / repo / entity`。 **接口边界**:`internal/usecase/ports.go` 是唯一定义对外接口的地方。 **理由**: - 上游 Hermes 只通过 REST 调用本服务,但 Binance 这个数据源大概率会扩展到 CoinGlass / ETF / CME。把数据源抽象成 `MarketDataProvider` / `DerivativesProvider` 接口,新增数据源只需要新建 `internal/repo/webapi//`,不动业务编排。 - 单测可以 mock 接口,不需要起真 DB / 真 Binance。 - `internal/app/app.go` 是唯一的组合根,DI 集中可见。 **强制约束**(见 `ai/risk-guardrails.md` G6): ``` controller 不可直接 import binance/postgres usecase 不可直接 import binance/postgres ``` **代价**: - 多一层 `ports.go`,每个新接口要写两次(接口 + 实现) - 对小型项目可能略 overkill,但本项目已经规划到 v3(多数据源),早做减少返工 **何时重新评估**:如果 v2 之后接口数量爆炸(> 30 个)→ 考虑按子域拆分 `ports.go` 为多个文件。 --- ### D4. 传输层:REST-only(v1 不上 gRPC / 消息队列 / WebSocket) **选了 REST**(而不是 gRPC、NATS、WS 推送)。 **理由**: - Hermes 是同语言 Go 服务,但跨服务调用频率是分钟级(拿一次 context),REST + JSON 完全够 - gRPC 多一份 proto 维护负担,团队/工具收益不明显 - WebSocket 推送是独立工程量(重连、消息丢失、订阅状态机),v1 不进入 - Cron + REST 拉取模型对运维友好(任何 HTTP 监控就够) **代价**: - 如果未来 Hermes 需要 sub-second 行情,REST 拉模型撑不住 **何时重新评估**:Hermes 接入实盘策略后,如果发现"分钟级"延迟成为瓶颈 → 写新 ADR 引入 WS。 --- ### D5. 价格 / 成交量:全链路 `string` **所有金额字段端到端用 `string`**:Binance DTO 是 string、entity 是 string、JSON 响应是 string。落库时由 PG 转 `NUMERIC(36,18)`。 **理由**: - Binance 原样返回字符串,避免 float64 转换丢精度 - `108000.12` 这种数 float64 表示就会丢尾数;累积统计后偏差更大 - 上游量化分析对价格精度敏感,宁可让 Hermes 自己决定怎么解析 **代价**: - 加减乘除需要 PG 端做或者引入 decimal 库 - JSON 响应里 price 是字符串,前端/客户端要适配 **强制约束**:见 `ai/risk-guardrails.md` G2。 --- ### D6. 数据源边界:只用 Binance USDⓈ-M Futures 公共 API **v1 不引入 CoinGlass、Glassnode、ETF Flow、CME、链上数据**。 **理由**: - Binance 一家覆盖 80% 的需求(K 线 / 衍生品 / 多空比) - 引入新数据源等于引入新的鉴权、限流、错误模式、SLA——v1 优先把一条链路打通 - 路线图 v2 / v3 才扩展数据源(README "路线图" 段) **强制约束**: - 新增数据源前必须先写 `ai/specs/.md`(见 AGENTS.md §10) - 不可使用 Binance 的私有签名接口(见 G1) --- ### D7. 配置层:cleanenv(yaml + env override) **选了 cleanenv**(而不是 viper、koanf、env-only)。 **理由**: - 一份 yaml 默认值 + 环境变量覆盖,开发和部署都方便 - 比 viper 轻,没有多余抽象 - `env-default:` tag 让默认值跟字段定义放在一起,可读性好 **代价**:无明显缺点。如果后续需要热重载(不需要)才会考虑换。 --- ### D8. 限流:`golang.org/x/time/rate` token bucket 挂在 `pkg/httpclient/client.go` 里,默认 RPS=20 / Burst=40,从配置读。 **理由**: - 标准库附属,零外部依赖 - token bucket 适合突发场景 - Binance IP 限 2400 weight/min,20 req/s × 60s = 1200 req/min,留出一半余量给重试和 backfill **强制约束**:所有外部 HTTP 调用走 `pkg/httpclient`,见 G5。 --- ### D9. Scheduler:robfig/cron v3 **选了 robfig/cron v3**(而不是自己写 ticker、go-co-op/gocron)。 **理由**: - cron 表达式所有人都看得懂,schedule 修改是配置而非代码 - 稳定老牌,3.0 之后无破坏性变化 - 和 Fiber 在同一进程内运行,graceful shutdown 容易做 **代价**: - 没有分布式锁——但本服务 v1 是单实例,不需要 --- ## 不在本 ADR 范围 - **下单 / 账户 / 私钥**:永久禁止(G1)。要做必须新仓库。 - **多交易所聚合**:v3 才考虑,届时再写 ADR。 - **WS / 实时推送**:见 D4,需要单独写 ADR。 - **观测性(metrics / tracing)**:MVP 阶段只用 slog 结构化日志,Prometheus / OTel 留待生产部署前评估。 --- ## 后续 ADR 的触发条件(来自 AGENTS.md §10) - 换 HTTP 框架 → 新 ADR - 引入消息队列 / WS → 新 ADR - 跨服务通信改 gRPC → 新 ADR - 数据库换型(PG → ClickHouse)→ 新 ADR - 引入新数据源 → `ai/specs/.md`(不一定要 ADR,除非影响整体架构)