feat: 初始化 cryptoHermes 行情网关 v1 MVP + harness 工程文档

Binance USDⓈ-M Futures 行情网关,给上游 Hermes 量化分析引擎提供单一聚合
接口 /v1/market/context(K 线 + funding + OI + 多空比 + taker volume)。
只读公共行情,不下单、不接私钥、不查账户。

## v1 实现范围(Milestone 1-5)

- Clean Architecture 4 层(controller/usecase/repo/entity),接口边界在
  internal/usecase/ports.go
- Binance Futures REST client(K 线 / ticker24h / funding / OI / 多空比
  / taker volume 共 9 个接口),全链路 string 价格避免 float64 精度问题
- TimescaleDB 5 张 hypertable(market_klines / funding_rates /
  open_interest / long_short_ratio / taker_buy_sell_volume),主键含
  时间维度,UpsertMany 幂等
- robfig/cron 定时采集(15m/1h/4h/1d/1w 多周期 K 线 + 衍生品 15 分钟
  落库),未收线 K 线 (close_time > now) 由 mapper/repo 双重过滤
- pkg/httpclient 统一限流(默认 20 req/s, burst 40)+ 重试,避免触发
  Binance 2400 weight/min IP 上限
- /v1/market/context 聚合接口:errgroup 并发拉 snapshot/funding/OI,DB
  K 线不足 200 根回源 Binance 异步补
- cmd/backfill CLI 支持指定 from/to 大段回填(Binance 历史 OI / 多空比
  官方只保留 30 天,必须自己存)
- Docker Compose + Makefile + golang-migrate,本地一键启

技术指标(support/resistance/Vegas/箱体)留待 v2,技术段返回空对象 +
warning 占位。

## Harness 工程文档

- AGENTS.md — AI agent 工作速查(10 个章节)
- ai/project-map.md — 仓库结构、扩展点、控制流
- ai/risk-guardrails.md — G1-G10 守卫规则(每条带可机械验证命令)
- ai/adr/0001-architecture-foundations.md — 9 条架构基础决策
- ai/task-templates.md — 6 种任务契约模板
- ai/harness-health.md — 当前 harness 健康度评估

3 个 grep 守卫已验证通过:controller / usecase 无具体实现依赖,全项目
无私钥/签名字段。
This commit is contained in:
dela
2026-05-24 17:20:51 +08:00
commit cc7f5a4f32
58 changed files with 5356 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
# 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
**理由**
- 时序数据天然适合 hypertable5 张表全部按时间分片,查询近期数据走最新 chunk旧数据自动归档
- SQL 接口和普通 PG 完全兼容pgx 直连,无需新驱动
- 没有引入新的运维栈(开发机用 Docker 起 `timescale/timescaledb:latest-pg16` 即可)
- Chunk 间隔可按表调K 线 1 周、funding 30 天)
**代价**
- 必须跑 `SELECT create_hypertable(...)` 才能享受时序优化,新表容易忘
- 升级 PG 大版本需要看 Timescale 兼容矩阵
**何时重新评估**:如果数据量进入 TB 级、需要列存压缩或者 OLAP 复杂聚合——考虑 ClickHouse。
---
### D3. ArchitectureClean Architecture4 层 + ports.go 接口边界)
**4 层**`controller / usecase / repo / entity`
**接口边界**`internal/usecase/ports.go` 是唯一定义对外接口的地方。
**理由**
- 上游 Hermes 只通过 REST 调用本服务,但 Binance 这个数据源大概率会扩展到 CoinGlass / ETF / CME。把数据源抽象成 `MarketDataProvider` / `DerivativesProvider` 接口,新增数据源只需要新建 `internal/repo/webapi/<source>/`,不动业务编排。
- 单测可以 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-onlyv1 不上 gRPC / 消息队列 / WebSocket
**选了 REST**(而不是 gRPC、NATS、WS 推送)。
**理由**
- Hermes 是同语言 Go 服务,但跨服务调用频率是分钟级(拿一次 contextREST + 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/<source>.md`(见 AGENTS.md §10
- 不可使用 Binance 的私有签名接口(见 G1
---
### D7. 配置层cleanenvyaml + 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/min20 req/s × 60s = 1200 req/min留出一半余量给重试和 backfill
**强制约束**:所有外部 HTTP 调用走 `pkg/httpclient`,见 G5。
---
### D9. Schedulerrobfig/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/<source>.md`(不一定要 ADR除非影响整体架构

103
ai/harness-health.md Normal file
View File

@@ -0,0 +1,103 @@
# Harness Health — cryptoHermes
**日期**2026-05-24
**版本**v1 MVP 刚完成Milestone 1-5
按 he-maintainer 的 scoring rubric 评估当前 harness 工程状态。Rating: `strong` / `partial` / `weak`
---
## 总览
| 维度 | Rating | 说明 |
|---|---|---|
| 项目地图清晰度 | strong | `ai/project-map.md` 完整覆盖 Clean Arch 4 层 + 数据流 + 扩展点 |
| 验证命令可跑 | strong | Makefile 全部命令在 README + AGENTS.md 双重列出 |
| 快速反馈环路 | strong | `go build ./...` < 2s`go vet ./...` < 1s单包测试支持 |
| 任务契约质量 | partial | 模板已写(`ai/task-templates.md`),但还没有真实任务用过 |
| 守卫规则清晰度 | strong | 10 条 G1-G10每条都有可机械验证命令 |
| 架构决策耐久度 | strong | ADR 0001 记录 9 条核心决策;其他 ADR 等触发 |
| Spec / test-plan | partial | 还没有 specs/,因为 v1 没有需要 spec 的任务v2 新数据源时必填 |
| 失败反馈环路 | partial | AGENTS.md §10 写了升级时机,但还没有 `ai/risk-guardrails.md` 改动史可参考 |
| 文档 vs 代码对齐 | strong | AGENTS.md / README / project-map 全部基于实际仓库结构 |
| MEMORY / 长期上下文 | n/a | 本仓库无 `MEMORY.md`,按设计——长期知识在 `docs/dev.md``ai/` 文档中 |
**总体****strong**MVP 完成时点适用),但有 2 个 partial
- task-templates 未经过真实任务校验
- specs/ 目录还是空的(等 v2 需要时填)
---
## 已具备的工程基础
- **3 层 AI 文档系统**`AGENTS.md`(速查)→ `ai/project-map.md`(结构)→ `ai/adr/`(决策)。`docs/dev.md` 作为最详细的设计源头单独存在。
- **可机械验证的守卫**G1-G10 全部给了 grep / make 命令,没有靠 review 的软规则。
- **唯一的组合根**`internal/app/app.go`DI 一处可见。
- **接口边界文件**`internal/usecase/ports.go` 是唯一定义对外接口的位置,新 agent 找接口去这里。
- **强约束 grep 在 AGENTS.md §6**:每次 PR 都可以直接跑。
---
## 已识别的薄弱点
### W1. task-templates 未经过实战
**现状**:模板已写完,但没有 PR 用过这些契约。
**影响**:模板可能太严格或太松。
**纠正时机**:完成第一个非平凡 PR比如 Milestone 6 的技术指标)后回看,调整模板。
### W2. 还没有 work-status.md
**现状**:目前是单 agent / 单会话工作,不需要 work-status 协调。
**影响**:暂无。
**升级时机**:开始 Milestone 6技术指标或 v2新数据源如果跨多次会话建立 `ai/work-status.md`
### W3. CI 完全未搭建
**现状**:本地有 `make test` / `go vet`,但仓库无 GitHub Actions / GitLab CI。
**影响**:守卫规则全靠人工跑 grep。
**升级时机**:合并第一个外部 PR 前,搭一个最小 CI`go build` + `go vet` + grep 边界检查 + migration up/down
### W4. 单元测试覆盖率未度量
**现状**MVP 阶段重点是端到端可用,未补单测覆盖率。
**影响**refactor 时风险偏高。
**升级时机**Milestone 6 引入指标计算(计算正确性 critical先补 mapper / repo 的 table-driven 测试。
### W5. 部分 docs/* 未提交到 git
**现状**`docs/dev.md` 是 untrackedgit status 显示 `?? docs/`)。
**纠正方式**:本次 commit 一起加进来。
---
## 后续升级触发器
| 触发条件 | 应采取的动作 |
|---|---|
| 同一类错误第二次出现 | 加进 `ai/risk-guardrails.md`,给可验证命令 |
| 重大架构决策(换 HTTP 框架 / 引入 MQ / 跨服务通信) | 写新 ADR |
| 新增数据源CoinGlass / ETF / CME | 必填 `ai/specs/<source>.md` |
| 跨多次会话的长任务 | 写 `ai/work-status.md` |
| 单测/集成测试出现 flake | 加 test-plan约定 retry / quarantine 策略 |
| 文档与代码出现漂移grep 守卫失败但代码正常) | 立刻同步,不要"以代码为准"绕开守卫 |
---
## 复评建议
- **3 个月后**(约 2026-08复评本文件。若 v2 数据源工作启动,至少需要 1 份 spec + 可能新 ADR。
- **每次新 Milestone 完成时**:跑一次 AGENTS.md §6 的 3 个 grep确认边界未漂移。
- **新成员agent 或人)首次提 PR 后**:根据他们卡住的地方反向更新 AGENTS.md / project-map"如果他们卡过这里,下一个人也会卡")。
---
## Harness 文件清单(截至本次 commit
```
AGENTS.md ← AI agent 速查
README.md ← 用户向使用文档
docs/dev.md ← 完整设计文档(源头)
ai/project-map.md ← 仓库结构与扩展点
ai/risk-guardrails.md ← G1-G10 守卫规则
ai/adr/0001-architecture-foundations.md ← 9 条架构基础决策
ai/task-templates.md ← 6 种任务模板
ai/harness-health.md ← 本文件
```
CLAUDE.md / GEMINI.md / .cursor/*.md 等其他 AI 工具的入口文件——**暂不需要**,因为这些工具都会读 AGENTS.md。如未来要差异化对待再分裂。

224
ai/project-map.md Normal file
View File

@@ -0,0 +1,224 @@
# Project Map — cryptoHermes
代码 agent 工作的地形图。每个目录写明:**职责**、**可以依赖谁**、**不可以依赖谁**、**扩展点在哪**。
完整设计见 `docs/dev.md`
---
## 顶层布局
```
cryptoHermes/
├── cmd/ 二进制入口
├── config/ cleanenv 加载
├── internal/ 业务代码(不可被外部 import
├── migrations/ SQL含 hypertable
├── pkg/ 项目内可复用基础设施
├── docs/ 设计文档dev.md 是源头)
├── ai/ harness 工程文档(本目录)
├── docker-compose.yml Timescale + 服务
├── Dockerfile multi-stage golang:1.23-alpine → alpine:3.20
├── Makefile 所有验证命令
└── go.mod Go 1.23
```
---
## `cmd/` — 二进制入口
| 路径 | 职责 |
|---|---|
| `cmd/app/main.go` | 主服务。读 config → 调 `internal/app.Run(cfg)` |
| `cmd/backfill/main.go` | 历史 K 线回填 CLI。直接装配 binance client + kline repo不走 fiber |
**约束**`cmd/` 里只允许装配 + 调用,不允许写业务逻辑。
---
## `config/` — 配置
- `config.go`cleanenv 加载,`yaml + env` 双源。环境变量优先(见 `env:` tag
- `config.yml`:默认配置。**不要在这里放敏感信息**(没有敏感信息可放,因为本服务无私钥)。
**扩展点**:新增配置项 → `config.go` 加字段(同时加 `yaml:``env:` tag`config.yml` 加默认值 → 在 README "配置" 段补一行。
---
## `internal/` — 业务核心
Clean Architecture 四层。**依赖方向单向:`controller → usecase → entity ← repo`**。
```
┌──────────────┐
│ controller │ HTTP 层:参数校验、调 usecase、序列化
└──────┬───────┘
│ depends on
┌──────────────┐
│ usecase │ 业务编排。只依赖 ports.go 中的接口
└──────┬───────┘
│ depends on (interface only)
┌──────────────┐ ┌──────────────┐
│ entity │ ◄────── │ repo │ 外部依赖实现
└──────────────┘ └──────────────┘
(binance / postgres)
```
### `internal/entity/` — 纯业务实体
- `kline.go`, `ticker.go`, `funding.go`, `open_interest.go`, `long_short_ratio.go`, `taker_volume.go`, `market_context.go`
- **价格/成交量字段全部 `string`**,避免 float64 精度问题。落库时 PG 转 `NUMERIC(36,18)`
- `Kline``IsClosed bool` 字段——未收线 K 线 (`close_time > now()`) 由 mapper 自动判定。
- `LongShortRatio` 有常量 `RatioTypeGlobalAccount` / `RatioTypeTopTraderPosition` / `RatioTypeTopTraderAccount`
**不依赖**fiber、pgx、binance、任何外部库。可以从这里 import 到任何其他层。
### `internal/usecase/` — 业务接口与编排
- `ports.go`**全部对外接口在这里定义**。这是 Clean Arch 的边界文件。
- `MarketDataProvider`K线、ticker24h
- `DerivativesProvider`funding/OI/多空比/taker volume 共 7 个方法)
- `KlineRepository` / `FundingRepository` / `OpenInterestRepository` / `LongShortRatioRepository` / `TakerVolumeRepository`
- `market_context.go`:聚合 `/v1/market/context`。errgroup 并发拉 snapshot/funding/OIDB K 线不足 200 根回源 Binance 补,异步写回。
**约束**grep 可验证):
```bash
grep -r "repo/webapi/binance\|repo/persistent" internal/usecase # 必须无输出
```
usecase 不可直接 import 任何具体实现。新增业务流程 → 在 `ports.go` 加接口 → 在 usecase 编排 → 由 `internal/app/app.go` 注入实现。
### `internal/controller/` — HTTP 层
- `restapi/router.go` + `middleware.go`fiber app、recover、logger、request-id
- `restapi/v1/health_routes.go``/v1/health`
- `restapi/v1/market_routes.go``/v1/market/{context,klines,snapshot,derivatives}`
**约束**
```bash
grep -r "repo/webapi/binance\|repo/persistent" internal/controller # 必须无输出
```
controller 只能依赖 usecase 和 entity不能直接调 binance client 或 postgres repo。
**扩展点**:新增路由 → 加 handler → 通过 `MarketDeps` 注入依赖(不要直接 import repo 包)。
### `internal/repo/` — 外部依赖实现
| 子目录 | 实现哪些接口 |
|---|---|
| `webapi/binance/` | `MarketDataProvider``DerivativesProvider` |
| `persistent/postgres/` | 5 个 `*Repository` |
**Binance client**`webapi/binance/`
- `client.go`:包 `pkg/httpclient`,统一 base URL `https://fapi.binance.com`,错误用 `ExternalAPIError{Source, Path, StatusCode, Message}` 包装。
- `dto.go`:贴近 Binance 原始响应K 线是数组)。
- `mapper.go`DTO → entity。**K 线按下标解析**`row[0]=openTime ... row[10]=takerBuyQuoteVolume`),自动设置 `IsClosed = close_time < now()`
- `market.go``GetKlines` / `GetKlinesRange` / `GetTicker24h`
- `derivatives.go`funding × 2 + OI × 2 + 多空比 × 2 + taker volume
**Postgres repos**`persistent/postgres/`
- 5 个 repo每个有 `UpsertMany``ON CONFLICT DO UPDATE`)和 `FindRecent`(按时间倒序拉,返回前反转成正序)。
- `kline_repo.go``UpsertMany` 会过滤掉 `IsClosed=false` 的 K 线。
- 使用 pgxpool + transaction。
**扩展点**
- 新增数据源CoinGlass / ETF→ 新建 `webapi/coinglass/`,实现新的 Provider 接口(先在 `ports.go` 定义)。
- 新增表 → 新 migration + 新 repo + 在 `ports.go` 加新接口。
### `internal/worker/` — Collector
- `collector.go`:被 cron 调度的采集逻辑。每个 `Collect*` 方法遍历 `symbols × intervals/periods`,拉数据 → upsert。
- **不直接被 controller 调用**,只通过 `internal/app/scheduler.go` 触发。
### `internal/app/` — 装配与生命周期
- `app.go`DI 装配的唯一位置。
- 顺序pool → binanceClient → repos → contextUC → collector → scheduler → fiber app
- graceful shutdown监听 SIGINT/SIGTERM10s 超时,先停 cron、再停 fiber、最后关 pool。
- `scheduler.go`cron spec 在这里。
**这里是 Clean Arch 的"组合根"**——所有具体类型在这里被实例化并注入抽象接口。
---
## `pkg/` — 项目内可复用基础设施
| 包 | 职责 |
|---|---|
| `pkg/httpclient/` | 通用 HTTP client + 重试429/5xx 最多 2 次)+ `x/time/rate` 限流 |
| `pkg/logger/` | slog 包装(按 env 选 JSON / Text |
| `pkg/postgres/` | pgxpool 初始化 + Ping |
**约束**`pkg/` 不依赖 `internal/`,反向依赖。可以被外部项目 import虽然目前没有
---
## `migrations/` — SQL
10 个文件5 张表 × up/down
每个 `.up.sql` 末尾必须有:
```sql
SELECT create_hypertable('<table>', '<time_col>',
chunk_time_interval => <ms>,
if_not_exists => TRUE,
migrate_data => TRUE);
```
chunk 间隔:
- K 线 / OI / LS / taker → 1 周 = 604800000 ms
- funding → 30 天 = 2592000000 ms
**改 migration 前必读 `ai/risk-guardrails.md` 中"DB migration"段**
---
## 数据流(控制流)
### 写路径(采集)
```
cron tick
└→ internal/app/scheduler.go
└→ internal/worker/collector.go (Collect*)
├→ binance.GetXxx (HTTP → DTO → entity)
└→ repo.UpsertMany (entity → SQL upsert)
```
### 读路径API
```
HTTP request
└→ internal/controller/restapi/v1/market_routes.go
└→ usecase.MarketContext.Build
├→ marketData.GetTicker24h ┐
├→ derivatives.GetCurrent… ├ errgroup 并发
├→ derivatives.GetCurrent… ┘
├→ klineRepo.FindRecent × 5 周期
│ └ DB 不足 → 回源 binance.GetKlines异步写回
└→ fundingRepo / oiRepo / lsRepo.FindRecent
```
---
## 不可逾越的边界(自动验证见 AGENTS.md §6
1. **controller ↛ binance/postgres**:必须经 usecase。
2. **usecase ↛ binance/postgres 包**:只依赖 `ports.go` 接口。
3. **entity ↛ 任何外部库**:可被所有层 import。
4. **pkg ↛ internal**:基础设施不知道业务。
5. **任何代码 ↛ 私钥/签名**grep `apikey|hmac|x-mbx-apikey|secret_key|api_secret` 必须无 Go 代码命中。
---
## 关键扩展点速查
| 场景 | 改哪 |
|---|---|
| 新增 REST 路由 | `internal/controller/restapi/v1/`,依赖通过 `MarketDeps` 注入 |
| 新增业务编排 | `internal/usecase/ports.go` 加接口 → 新 usecase 文件 → `internal/app/app.go` 装配 |
| 新增数据表 | 新 migration → 新 entity可选→ 新 repo → `ports.go` 接口 → 装配 |
| 新增数据源 | `ai/specs/<source>.md` → 新 `internal/repo/webapi/<source>/` → 新 Provider 接口 |
| 新增定时任务 | `internal/worker/collector.go` 加方法 → `internal/app/scheduler.go` 注册 cron |
| 改配置项 | `config/config.go` 加字段 → `config/config.yml` 加默认 → README 记录 |

184
ai/risk-guardrails.md Normal file
View File

@@ -0,0 +1,184 @@
# 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" .
# 期望:无输出
```
---
## 升级守护规则的时机
同一类错误第二次出现 → 写一条新规则进来。修改本文件 = 改动了项目契约,需要在 PR 描述里说明原因并 @ 维护者。
新增 Hard Stop 规则时,**必须给出可机械验证的命令**——只能靠 review 来执行的规则会被绕过。

205
ai/task-templates.md Normal file
View File

@@ -0,0 +1,205 @@
# Task Templates — cryptoHermes
任何非平凡修改(>1 个文件、改边界、引入新依赖、改 migration、新增 collector先写一份微型任务契约。短即可——长度像 commit message但目的是让你自己和后续 agent能在 30 秒内对齐范围。
---
## 通用契约模板
```
objective: 一句话说要做的事
scope: 会改的文件 / 目录(具体路径,不要"相关文件"
out-of-scope: 显式不改的范围(防止 scope creep
constraints: 引用 ai/risk-guardrails.md 的哪几条G1-G10
validation: 要跑哪些命令证明通过(必须可机械执行)
acceptance: 完成判据(行为可观测的描述)
```
**示例**
```
objective: 给 /v1/market/context 加一个 ?intervals=1h,4h 参数,允许 Hermes 只拉部分周期,减少响应体积
scope:
- internal/controller/restapi/v1/market_routes.go
- internal/usecase/market_context.go
- README.mdAPI 段补 query 参数说明)
out-of-scope:
- 不改 entity响应结构不变只是 klines map 的 key 集合可能少几个)
- 不改 collector采集策略不变
constraints:
- G6不能在 controller 里直接调 repo必须通过 usecase 接收 intervals 参数
validation:
- go build ./... && go vet ./...
- curl 'localhost:8080/v1/market/context?symbol=BTCUSDT&intervals=1h,4h' | jq '.klines | keys'
- 期望:只返回 ["1h","4h"]
acceptance:
- 不传 intervals 时行为完全不变(默认全部 5 个周期)
- 传未知 interval 返回 400
- 至少一个 happy-path 单测覆盖
```
---
## 任务类型 → 模板
### T1. 新增 API 路由
```
objective: <做什么>
scope:
- internal/controller/restapi/v1/<file>.go新 handler
- internal/usecase/<file>.go新业务方法或扩展现有
- internal/usecase/ports.go如果需要新 repo 方法)
out-of-scope:
- 不改 entity 结构
- 不改 collector
constraints: G6
validation:
- go build ./... && go vet ./...
- curl 测 happy-path + 错误参数
acceptance:
- 路由响应符合 JSON schema
- 参数校验完整(缺失/非法都返回 400
- README 更新 API 段
```
### T2. 新增数据表 / migration
```
objective: <为什么要存这个>
scope:
- migrations/0000N_<name>.up.sql
- migrations/0000N_<name>.down.sql
- internal/entity/<name>.go
- internal/repo/persistent/postgres/<name>_repo.go
- internal/usecase/ports.go新 Repository 接口)
out-of-scope:
- 不改已发布 migration编号≤当前 max
constraints:
- G2金额必须 string / NUMERIC(36,18)
- G4UpsertMany 必须幂等)
- G7down/up 互逆)
validation:
- make migrate-up
- make migrate-down
- make migrate-up # 再次成功
- go build ./...
acceptance:
- 表是 hypertablepsql 跑 SELECT * FROM timescaledb_information.hypertables 看到
- upsert 跑两次行数不变
- 至少 1 个 repo 单测(连本地 Timescale
```
### T3. 新增数据源CoinGlass / ETF / ...
**必须先写 `ai/specs/<source>.md`**(见 AGENTS.md §8
```
objective: 接入 <source> 提供 <什么数据>
spec: ai/specs/<source>.md
scope:
- internal/repo/webapi/<source>/{client,dto,mapper,...}.go
- internal/usecase/ports.go新 Provider 接口)
- internal/app/app.goDI 装配)
- config/config.go + config.yml新数据源的 base_url / rps / timeout
out-of-scope:
- 不改 Binance client
constraints:
- G1绝不接私钥
- G2金额 string
- G5必须走 pkg/httpclient 限流)
validation:
- go build ./... && go vet ./...
- 临时挂一个 debug 路由验证能拉到真数据
acceptance:
- 新 Provider 实现接口usecase 不直接 import <source> 包
- 速率预算文档化(在 spec 里)
```
### T4. 改 collector / scheduler
```
objective: <调整哪个采集,为什么>
scope:
- internal/worker/collector.go
- internal/app/scheduler.go
out-of-scope:
- 不改 binance client如果要新方法先开 T3 / T1
- 不改表结构(如果要,开 T2
constraints:
- G3未收线丢弃
- G4upsert 幂等)
- G5rate limit 不绕开)
validation:
- go build ./... && go vet ./...
- 本地起服务跑 1 个 cron 周期psql 看新行写入
acceptance:
- 新 cron 表达式合理(不要 0 0 * * * 这种容易和别人撞的高峰)
- 重启服务后 scheduler 能正常注册
- weight 预算在 PR 描述里说明Binance 2400/min IP 上限)
```
### T5. Bug fix
```
objective: 修 <bug 简述>issue 链接或复现命令)
scope: <最小改动范围>
constraints: <相关 G 条>
validation:
- 写一个能复现 bug 的最小测试(先红后绿)
- go build ./... && go vet ./...
acceptance:
- 测试通过
- 不引入新的 lint warning
```
### T6. 重构 / Clean Architecture 修正
```
objective: <消除哪个边界违反 / 抽取哪段重复>
scope: <文件列表>
out-of-scope:
- 不改行为API 响应字节级一致 / DB schema 不变)
constraints: G6
validation:
- go build ./... && go vet ./...
- 全部 grep 边界检查AGENTS.md §6
- make test
acceptance:
- diff 中不含行为变化(只是搬代码 / 重命名 / 抽接口)
```
---
## 何时升级到 spec
micro contract 写不下、或者需要跨多个 agent / 多次会话才能完成 → 升级到 `ai/specs/<task>.md`
- 多 milestone 跨越
- 引入新数据源
- 改变响应 schema影响上游 Hermes
- 引入新部署组件(消息队列、新 service
spec 内容objective / non-goals / config 形状 / API 形状 / 错误模式 / 兼容性 / 验收 / 验证计划。
---
## 何时升级到 ADR
contract 涉及"以后类似场景应该都这么做" → 写 ADR`ai/adr/000N-<decision>.md`
- 换核心库
- 改架构层数 / 改 Clean Arch 边界
- 引入新通信协议
- 改部署拓扑
ADR 结构参考 `ai/adr/0001-architecture-foundations.md`
---
## 反模式
- ❌ "顺便把这里也改一下"——顺便的部分单独开一个 contract不要混
- ❌ "测试很难写,先合了"——测试难写说明设计有问题,先调设计
- ❌ "之前 review 过类似的,应该没问题"——每个 PR 独立 validate
- ❌ commit message 写"WIP"——本仓库不允许 WIP commit 进 main