Files
cryptoHermes/ai/risk-guardrails.md
2026-05-24 21:50:22 +08:00

257 lines
12 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.
# Risk Guardrails — cryptoHermes
可机械验证 / 不可逾越的规则。每条规则给出**为什么**、**怎么验证**。
任何 PR 都应该满足这里全部规则。
---
## G1. 绝不接入交易能力Hard Stop
**规则**:本服务只读公共行情。**禁止**任何下单、撤单、查询账户/持仓/资金、签名请求、托管、转账。
**为什么**:项目定位是"上游 Hermes 量化分析引擎的行情网关"。一旦引入交易能力,安全边界、合规风险、密钥管理责任全部上升一个数量级。`docs/dev.md` 第 1 章原文:"本服务只提供市场数据与分析上下文,不做交易、不下单、不托管资金"。
**怎么验证**
```bash
# 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. 价格 / 成交量绝不用 float64Hard Stop
**规则**:所有金额、价格、成交量字段**全链路用 `string`**。DTO、entity、JSON 响应都是 `string`。落库时由 PG 转 `NUMERIC(36,18)`
**为什么**`108000.12` 这种价格 float64 表示就会丢精度。Binance 原样返回字符串,本服务保留字符串到 PG。这一点在所有 5 个 entity 文件里都已经做到了,新代码必须遵守。
**怎么验证**
```bash
# 任何新增 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 值微调)。
**怎么验证**
```bash
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/sburst 40。**禁止** 在业务代码里直接用 `net/http` 调 Binance。
**为什么**Binance USDⓈ-M Futures IP 级限额 2400 weight/min。一旦超限会被封 IP 几分钟到几小时。本地代理在国内是稀缺资源,封了影响整个开发环境。
**预算**:新增 collector 任务前估算 weight
- `GET /fapi/v1/klines`1limit ≤ 100→ 10limit ≤ 1000
- `GET /fapi/v1/ticker/24hr`1单 symbol/ 40全量
- `GET /fapi/v1/openInterest`1
- `GET /futures/data/*`1
**怎么验证**
```bash
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也变成大手术。
**怎么验证**
```bash
grep -r "repo/webapi/binance\|repo/persistent" internal/controller # 必须无输出
grep -r "repo/webapi/binance\|repo/persistent" internal/usecase # 必须无输出
```
---
## G7. Migration 必须 down/up 互逆
**规则**:任何 SQL migration 改动后,本地必须跑:
```bash
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。
**怎么验证**
```bash
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。
**怎么验证**
```bash
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。
**怎么验证**
```bash
grep -rn "FundingRepo\|OIRepo\|LSRepo\|TakerRepo" internal/controller
# 期望:无输出
```
---
## G12. 指标计算的 float64 边界
**规则**`float64``internal/usecase/` 下**只允许出现在 `indicator.go` 内部**作为 transient 计算变量;进入 `entity` / 返回上层之前必须 `strconv.FormatFloat(_, 'f', -1, 64)` 转回 string。`internal/usecase/indicator.go` 同时**禁止** import `internal/repo/...` 的任何子包。
**为什么**G2 已经预留"指标计算中间过程可用 float64"的例外但没有可机械验证的边界。Milestone 6 之后会有更多指标加入,必须把例外明文化、可 grep 化,避免某天 float64 悄悄出现在 entity 字段或被 repo 持久化。决策细节见 `ai/adr/0002-indicator-numeric-boundary.md`
**怎么验证**
```bash
# usecase 下除 indicator.go 之外不得出现 float64
grep -rn --include="*.go" "float64\|float32" internal/usecase | grep -v indicator
# 期望:无输出
# indicator.go 不得 import 任何 repo 子包
grep -n "internal/repo" internal/usecase/indicator.go
# 期望:无输出
# indicator.go 不得 import binance / postgres / pgx
grep -nE "binance|postgres|pgx" internal/usecase/indicator.go
# 期望:无输出(注释中的 ADR 引用不算)
# entity 字段任何位置不得 float
grep -rn --include="*.go" "float64\|float32" internal/entity
# 期望:无输出
```
**例外**:无。如果未来某个指标输入/输出确实需要 decimal 精度(累加型大数据集),先升级 ADR-0002 再改实现。
---
## G13. docker compose 部署路径不可裸奔
**规则**`docker compose up -d` 的生产/测试路径必须满足:
- Postgres 密码从 `.env` 注入,`.env.example` 只能留空占位,不能给 `postgres` 这类弱默认值。
- `.dockerignore` 必须排除 `.env``.env.*`,防止 `Dockerfile``COPY . .` 把 secrets 送进 build context / builder layer。
- app 容器内端口固定为 8080宿主机暴露端口只通过 `APP_HOST_PORT` 调整。不要在 compose 中用 `${APP_PORT}` 同时控制容器内监听和宿主机映射。
- app 必须等待 Postgres healthcheck 通过、migration init container 成功退出后再启动。
- Docker build 阶段必须显式传入 `GO_MODULE_PROXY`(映射为 Dockerfile 内的 `GOPROXY`/ HTTP(S) proxy build args`go mod download` 失败必须立刻失败,禁止 `go mod download || true``GO_MODULE_PROXY` 只能填 Go module proxy`https://goproxy.cn,direct`),不能填本机 HTTP 代理地址。
**为什么**:这个服务虽然不接 Binance 私钥,但 DB 密码和代理凭据仍可能出现在 `.env`。同时 app 启动前若表不存在,会导致 collector/API 失败;端口变量混用会让健康检查和端口映射分叉。
**怎么验证**
```bash
make guard
# 部署机额外跑(本地无 docker 时可跳过,但发布前必须跑)
docker compose config
docker compose up -d
docker compose ps
curl -s http://localhost:${APP_HOST_PORT:-8080}/v1/health
```
**例外**:无。若改为 Kubernetes / Helm / systemd 部署也必须保留同等约束secret 不进镜像、migration 先于 app、readiness 可观测、容器内端口稳定。
---
## 升级守护规则的时机
同一类错误第二次出现 → 写一条新规则进来。修改本文件 = 改动了项目契约,需要在 PR 描述里说明原因并 @ 维护者。
新增 Hard Stop 规则时,**必须给出可机械验证的命令**——只能靠 review 来执行的规则会被绕过。