为什么:原 controller handler 在 HTTP 层同时承担了 4 项 usecase 职责 (多源拼装、错误降级策略、limit 硬编码、响应结构组装),违反了 "controller 只做参数解析 + 调用 + 序列化" 的边界。等后续给衍生品加 缓存 / taker volume / 数据质量字段时,HTTP 层会持续变厚。 改动: - 新增 internal/usecase/market_query.go: MarketQueryUsecase.GetDerivatives() 并发拉 current funding/OI + 串行查历史 + 错误聚合为 warnings 返回 - /derivatives handler 退化到 4 行;MarketDeps / restapi.Deps 同步收窄 - /klines 保持直查 KlineRepo(thin query,G11 例外) - 新增 G11 守卫:grep "FundingRepo|OIRepo|LSRepo|TakerRepo" internal/controller 破坏性 API 变更(/derivatives 响应): - 删除 funding.error / openInterest.error - 新增顶层 warnings: []string 顶层 symbol / funding / openInterest / longShortRatio / takerBuySellVolume 保持。 harness 同步: - AGENTS.md §6 加 G11 grep - ai/project-map.md 加 market_query.go + /derivatives 数据流分支 - ai/task-templates.md T6 加"破坏性变体"分支(本次重构暴露了原假设过严) - ai/harness-health.md W1 记一次轻量验证 + 守卫数 10→11 验收: go build / go vet / G6 grep / G11 grep 全绿。
8.7 KiB
Risk Guardrails — cryptoHermes
可机械验证 / 不可逾越的规则。每条规则给出为什么、怎么验证。
任何 PR 都应该满足这里全部规则。
G1. 绝不接入交易能力(Hard Stop)
规则:本服务只读公共行情。禁止任何下单、撤单、查询账户/持仓/资金、签名请求、托管、转账。
为什么:项目定位是"上游 Hermes 量化分析引擎的行情网关"。一旦引入交易能力,安全边界、合规风险、密钥管理责任全部上升一个数量级。docs/dev.md 第 1 章原文:"本服务只提供市场数据与分析上下文,不做交易、不下单、不托管资金"。
怎么验证:
# Go 代码不可出现私钥/签名相关字段
grep -ri --include="*.go" "apikey\|x-mbx-apikey\|hmac\|secret_key\|api_secret" .
# 期望:无输出(注释/字符串/文档中说明"不接入"不算)
# Binance 私有接口前缀不可出现
grep -r --include="*.go" "fapi/v1/order\|fapi/v1/account\|fapi/v2/account\|fapi/v2/positionRisk" .
# 期望:无输出
例外:无。如果上游需求变化要做交易,必须先开新仓库或新模块(独立鉴权 / 独立部署),不能在本仓库内直接放开。
G2. 价格 / 成交量绝不用 float64(Hard Stop)
规则:所有金额、价格、成交量字段全链路用 string。DTO、entity、JSON 响应都是 string。落库时由 PG 转 NUMERIC(36,18)。
为什么:108000.12 这种价格 float64 表示就会丢精度。Binance 原样返回字符串,本服务保留字符串到 PG。这一点在所有 5 个 entity 文件里都已经做到了,新代码必须遵守。
怎么验证:
# 任何新增 entity 字段如果叫 *Price/*Volume/*Quantity/*Amount,类型必须是 string
grep -rn --include="*.go" "Price\s*float\|Volume\s*float\|Quantity\s*float\|Amount\s*float" internal/entity
# 期望:无输出
例外:纯统计/分析(如指标计算的中间过程)可以用 float64,但结果落库或返回 API 时必须转回 string。
G3. 未收线 K 线(IsClosed=false)不入库
规则:从 Binance 拉到的最新一根 K 线如果 close_time > now(),不能写入 market_klines。
为什么:未收线 K 线的 close/high/low/volume 还会变。如果入库,后续不更新就是脏数据,更新又会触发 upsert 风暴。internal/repo/persistent/postgres/kline_repo.go 的 UpsertMany 已经过滤 IsClosed=false,internal/repo/webapi/binance/mapper.go 已经自动设置 IsClosed = closeTime < now()。新代码(含 backfill)必须遵守。
怎么验证:手动跑一次 collector,立刻 SELECT count(*) FROM market_klines WHERE close_time > extract(epoch from now()) * 1000 应为 0。
G4. UpsertMany 必须幂等
规则:所有 repo 的 UpsertMany 用 INSERT ... ON CONFLICT (<PK>) DO UPDATE SET ... updated_at = now()。禁止 INSERT 不带 ON CONFLICT 或用 INSERT ... ON CONFLICT DO NOTHING(后者错过了更新场景)。
为什么:cron 会重复跑、backfill 会和 cron 重叠、重启会重拉。重复执行必须不能产生重复行也不能丢更新(例如 OI 后修正、close 值微调)。
怎么验证:
make backfill ARGS="--symbol BTCUSDT --interval 1h --limit 100"
psql $DATABASE_URL -c "SELECT count(*) FROM market_klines WHERE symbol='BTCUSDT' AND interval='1h'"
make backfill ARGS="--symbol BTCUSDT --interval 1h --limit 100"
psql $DATABASE_URL -c "SELECT count(*) FROM market_klines WHERE symbol='BTCUSDT' AND interval='1h'"
# 期望:两次 count 一致
G5. Rate Limit 不可绕开
规则:所有外部 HTTP 调用走 pkg/httpclient,该 client 已挂 golang.org/x/time/rate(默认 20 req/s,burst 40)。禁止 在业务代码里直接用 net/http 调 Binance。
为什么:Binance USDⓈ-M Futures IP 级限额 2400 weight/min。一旦超限会被封 IP 几分钟到几小时。本地代理在国内是稀缺资源,封了影响整个开发环境。
预算:新增 collector 任务前估算 weight:
GET /fapi/v1/klines:1(limit ≤ 100)→ 10(limit ≤ 1000)GET /fapi/v1/ticker/24hr:1(单 symbol)/ 40(全量)GET /fapi/v1/openInterest:1GET /futures/data/*:1
怎么验证:
grep -rn --include="*.go" "net/http\"" internal/repo/webapi
# 期望:只在 pkg/httpclient 里,repo/webapi 不应直接 import net/http
G6. Clean Architecture 边界
规则:见 ai/project-map.md "不可逾越的边界"段。3 个 grep 必须无输出。
为什么:一旦 controller / usecase 直接 import 具体实现,测试就需要起 DB 起 Binance;后续替换数据源(CoinGlass)也变成大手术。
怎么验证:
grep -r "repo/webapi/binance\|repo/persistent" internal/controller # 必须无输出
grep -r "repo/webapi/binance\|repo/persistent" internal/usecase # 必须无输出
G7. Migration 必须 down/up 互逆
规则:任何 SQL migration 改动后,本地必须跑:
make migrate-down && make migrate-up
两步都成功(不报错、不丢表)才能提交。
为什么:生产回滚是兜底手段,down 脚本错了就回不去。
额外规则:
- 新 migration 不可改已发布 migration 的内容——只能加新文件(编号递增)。
- 改表结构如果会丢数据(drop column / change type),必须在 PR 里显式说明。
- 改 hypertable chunk 间隔需要先看
docs/dev.md第 10 章。
G8. K 线数组下标解析不可破
规则:internal/repo/webapi/binance/mapper.go 中 mapKline 的下标解析对应 Binance 官方约定:
[0] openTime ms
[1] open string
[2] high string
[3] low string
[4] close string
[5] volume string
[6] closeTime ms
[7] quoteAssetVolume string
[8] numberOfTrades int
[9] takerBuyBaseVolume string
[10] takerBuyQuoteVolume string
[11] ignore
禁止:调整顺序、跳过位置、改用对象解析(Binance 这个接口就是数组)。
为什么:错一位整张表数据就废了,回滚成本极高。docs/dev.md 第 9.2 节有完整说明。
怎么验证:动了 mapper.go 之后跑一次回填,对比 Binance 网页 K 线图同一根的 OHLCV 是否一致。
G9. 配置项不引入秘钥
规则:config/config.yml 和环境变量列表里不允许出现 api_key、secret、token、password(DB 密码除外,且必须从环境变量读、不能 hardcode)。
为什么:本服务不需要任何 Binance 私有鉴权,也不向外部传秘密。一旦引入会破坏 G1。
怎么验证:
grep -i "api_key\|api_secret\|access_token" config/
# 期望:无输出
G10. WebSocket / 实时推送不在 v1 范围
规则:v1 只用 REST 拉取。不引入 gorilla/websocket、不订阅 wss://fstream.binance.com。
为什么:WS 长连接的可观测性、重连、消息丢失处理是独立工程量。v1 用 REST + cron 已经能满足 Hermes 的"分钟级上下文"需求。要做 WS 必须先写 ADR。
怎么验证:
grep -rn --include="*.go" "websocket\|fstream.binance" .
# 期望:无输出
G11. Controller 不编排 derivatives 多源查询
规则:internal/controller/** 不得直接持有或调用 FundingRepository / OpenInterestRepository / LongShortRatioRepository / TakerVolumeRepository。这类衍生品历史数据的"查哪些表 + 多源拼装 + 错误降级 → warnings"必须在 internal/usecase/ 完成,controller 只做参数解析、调用 usecase、返回 JSON。
为什么:曾经 /derivatives handler 一次性编排了 6 处查询、4 个静默吞错的 _、硬编码的 limit 和响应结构——HTTP 层在替业务层做决定。要给衍生品加 taker volume、缓存、降级策略、数据质量 warnings 时,应该只动 usecase,不该让 HTTP handler 跟着变厚。
例外:KlineRepository 暂时允许 controller 直查,因为 /klines 现在就是 thin query。一旦它要加缓存 / live fallback / interval 聚合,就同样要搬进 usecase。
怎么验证:
grep -rn "FundingRepo\|OIRepo\|LSRepo\|TakerRepo" internal/controller
# 期望:无输出
升级守护规则的时机
同一类错误第二次出现 → 写一条新规则进来。修改本文件 = 改动了项目契约,需要在 PR 描述里说明原因并 @ 维护者。
新增 Hard Stop 规则时,必须给出可机械验证的命令——只能靠 review 来执行的规则会被绕过。