# cryptoHermes — Market Data Gateway 为上游 Hermes 量化分析引擎提供统一的币圈行情与衍生品上下文。Hermes 只需要调用本服务的 `/v1/market/context` 一个接口,就能拿到 BTC/ETH 多周期 K 线、当前价、funding、OI、多空比等用于策略分析的数据。 本服务只做行情采集与聚合,**不下单、不接私钥、不查账户、不托管任何资金**。 --- ## 特性 - Binance USDⓈ-M Futures 公共接口,无需 API Key。 - 多周期 K 线 / 24h ticker / funding / Open Interest / 全局多空比 / 大户持仓多空比 / Taker 主动买卖量。 - 历史数据自动落库(Binance 官方历史 OI / 多空比只保留 30 天,必须自己存)。 - Clean Architecture:controller / usecase / repo 分层,usecase 只依赖接口。 - Fiber v2 REST API + robfig/cron 定时采集 + TimescaleDB hypertable 存时序数据。 - Rate Limit 节流(默认 20 req/s),可重复运行的 upsert,K 线只入库已收线。 --- ## 技术栈 | 类型 | 选型 | |---|---| | 语言 | Go 1.23 | | HTTP | Fiber v2 | | DB | TimescaleDB (PG 16) | | DB Driver | pgx v5 (pgxpool) | | Scheduler | robfig/cron v3 | | Config | cleanenv (yaml + env) | | 限流 | golang.org/x/time/rate | | Migration | golang-migrate CLI | --- ## 快速开始 ### 1. 依赖 ```bash # Go 1.23+ go version # golang-migrate CLI(用于跑 migration) go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest ``` ### 2. 启动数据库 ```bash make docker-up ``` 会用 `timescale/timescaledb:latest-pg16` 启一个 Postgres,监听本地 5432。 ### 3. 执行 migration ```bash export DATABASE_URL="postgres://postgres:postgres@localhost:5432/hermes_market?sslmode=disable" make migrate-up ``` 会建 5 张 hypertable:`market_klines` / `funding_rates` / `open_interest` / `long_short_ratio` / `taker_buy_sell_volume`。 ### 4. (可选)回填历史 K 线 第一次启动时 DB 是空的,直接调 `/v1/market/context` 会触发回源 Binance。建议先回填: ```bash for s in BTCUSDT ETHUSDT SOLUSDT BNBUSDT DOGEUSDT; do for i in 15m 1h 4h 1d 1w; do make backfill ARGS="--symbol $s --interval $i --limit 500" done done ``` 也可以指定起止时间做大段回填: ```bash make backfill ARGS="--symbol BTCUSDT --interval 1h --from 1704067200000 --to 1735689600000" ``` ### 5. 启动服务 ```bash make run ``` 服务会: - 监听 `:8080` - 立刻启动 cron,每 15 分钟拉一次 funding / OI / 多空比 / taker volume / 15m K 线,整点拉 1h,每 4h 拉 4h,每日 00:05 拉 1d,每周一 00:10 拉 1w。 ### 6. 验证 ```bash curl -s localhost:8080/v1/health | jq . curl -s 'localhost:8080/v1/market/context?symbol=BTCUSDT' | jq 'keys' ``` --- ## 一键部署(docker compose) 适合生产或测试环境快速起服务。**3 步上线**: ```bash # 1. 准备环境变量 cp .env.example .env # 编辑 .env,必须填写 POSTGRES_PASSWORD(留空时 compose 直接拒绝启动) # 国内机器还要确认 GO_MODULE_PROXY,并按需填写 HTTP_PROXY / HTTPS_PROXY # 如果 .env 是旧文件,补一行:GO_MODULE_PROXY=https://goproxy.cn,direct # HTTP_PROXY / HTTPS_PROXY 不要填 127.0.0.1;Docker 容器访问宿主机代理用 host.docker.internal # 2. 启动(自动建表) docker compose up -d # 3. 验证 curl -s http://localhost:8080/v1/health | jq . docker compose ps # 应看到 3 行:postgres healthy / migrate exited(0) / app running ``` ### 启动顺序 `docker compose up` 会按如下依赖链推进,**任何一环失败都会阻断**: ``` postgres 起 → pg_isready healthcheck 通过 → migrate 容器跑 migrations/ up → 退出 0 → app 起,连 DB,启 cron ``` migrate 是一次性 init container(image `migrate/migrate:v4.18.1`),跑完立即退出。app 用 `depends_on: condition: service_completed_successfully` 等它结束,保证 app 启动时表一定存在。 ### 容器 healthcheck - **postgres**:`pg_isready`,5s 间隔 - **app**:`wget --spider /v1/health`,15s 间隔,start_period 20s `docker compose ps` 会显示 `(healthy)` 或 `(unhealthy)`。k8s / 反向代理 / LB 可直接根据 healthcheck 状态做流量切换。 ### 优雅停机 `docker compose stop` 默认 10s,本项目设 `stop_grace_period: 30s`: - app 收到 SIGTERM - fiber `ShutdownWithContext(10s)` 等当前 HTTP 请求结束 - cron Scheduler.Stop() 等当前 collector 任务结束 - 30s 超时后 docker 才发 SIGKILL ### 日志 容器 stdout 走 docker json-file driver: - 单文件最大 50 MB - 保留 5 个滚动文件 - 应用业务日志额外挂载到宿主机 `./logs/` 查实时日志: ```bash docker compose logs -f app docker compose logs -f postgres ``` ### 回填历史 启动后 DB 是空的;调用 `/v1/market/context` 会 fallback 到 Binance(带 warnings)。要立刻获得满载历史,进容器跑 backfill: ```bash for s in BTCUSDT ETHUSDT SOLUSDT BNBUSDT DOGEUSDT; do docker compose exec app /app/backfill --symbol $s --interval 1h --limit 500 done # 其它 interval 同理 ``` 或等 cron 自然落库(15m K 线每 15 分钟一次,1h 每整点一次)。 ### 生产 checklist 合并前再过一遍: - [ ] `.env` 中 `POSTGRES_PASSWORD` 已改为强密码 - [ ] 国内机器:`.env` 中 `HTTP_PROXY` / `HTTPS_PROXY` 已配 - [ ] `docker compose config` 渲染后无 `${VAR:?}` 报错 - [ ] `docker compose up -d` 后 `docker compose ps` 全部 healthy / exited(0) - [ ] `curl /v1/health` 200,`curl /v1/market/context?symbol=BTCUSDT` 返回非空 `technical` - [ ] 宿主机 `./logs/` 目录有 app 写入权限(默认 alpine root 用户写没问题) - [ ] `make guard` 在仓库根目录全绿(即使是 deploy 修改,守卫规则也必须不破) --- ## 国内网络 `fapi.binance.com` 国内直连不稳定,通过环境变量挂代理即可(Go 默认尊重 `HTTPS_PROXY`): ```bash HTTPS_PROXY=http://127.0.0.1:7890 make run ``` --- ## 配置 `config/config.yml`(所有字段都可被环境变量覆盖,参见 `config/config.go` 的 `env:` tag): ```yaml app: port: 8080 env: local binance: base_url: https://fapi.binance.com timeout: 10s retry_count: 2 rps: 20 # IP 级限速,超过会自动 sleep burst: 40 postgres: dsn: postgres://postgres:postgres@localhost:5432/hermes_market?sslmode=disable max_conns: 10 min_conns: 2 collector: enabled: true # 设为 false 可只跑只读 API,不开 cron symbols: [BTCUSDT, ETHUSDT, SOLUSDT, BNBUSDT, DOGEUSDT] intervals: [15m, 1h, 4h, 1d, 1w] default_limit: 500 ``` 常用环境变量: | 变量 | 说明 | |---|---| | `CONFIG_PATH` | 配置文件路径,默认 `config/config.yml` | | `POSTGRES_DSN` | 覆盖 yaml 中的 DSN(Docker Compose 里就是这么干的) | | `APP_PORT` | HTTP 端口;compose 部署中容器内固定为 8080 | | `APP_HOST_PORT` | docker compose 暴露到宿主机的 HTTP 端口 | | `GO_MODULE_PROXY` | Docker build 阶段下载 Go module 使用的 Go proxy,不是 HTTP 代理 | | `HTTPS_PROXY` | 走代理访问 Binance | --- ## API ### `GET /v1/health` ```json { "status": "ok", "time": 1717000000000 } ``` ### `GET /v1/market/context?symbol=BTCUSDT` **Hermes 主接口**。聚合返回 K 线 + 衍生品 + 数据质量信息: ```jsonc { "symbol": "BTCUSDT", "generatedAt": 1717000000000, "snapshot": { "lastPrice": "108000.12", "priceChangePercent": "2.14", "highPrice": "109200.00", "lowPrice": "104800.00", "volume": "...", "quoteVolume": "..." }, "klines": { "15m": [ /* 300 根 */ ], "1h": [ /* 300 根 */ ], "4h": [ /* 300 根 */ ], "1d": [ /* 300 根 */ ], "1w": [ /* 300 根 */ ] }, "derivatives": { "funding": { "current": { ... }, "history": [ ... ] }, "openInterest": { "current": { ... }, "history": [ ... ] }, "longShortRatio": { "global": [ ... ], "topTraderPosition": [ ... ] }, "takerBuySellVolume": [] }, "technical": { "support": [], "resistance": [], "rangeHigh": null, "rangeLow": null, "longShortLine": null }, "dataQuality": { "source": "binance", "warnings": [] } } ``` > `technical.support/resistance/rangeHigh/rangeLow/longShortLine` 目前按 primary `1h` 计算;`technical.intervals` 已提供每周期 Bollinger / Vegas。完整 per-interval `TechnicalStructure` 与箱体突破信号仍在 v2.x 待办。 ### `GET /v1/market/klines?symbol=BTCUSDT&interval=1h&limit=300` 直接读 DB 返回 K 线数组。`interval` 取值 `15m/1h/4h/1d/1w`。 ### `GET /v1/market/snapshot?symbol=BTCUSDT` 24h ticker 实时拉取(不走 DB)。 ### `GET /v1/market/derivatives?symbol=BTCUSDT&period=1h` 衍生品聚合:funding / OI / 多空比。`period` 默认 `1h`。 --- ## 目录结构 ``` cryptoHermes/ ├── cmd/ │ ├── app/ # 主服务入口 │ └── backfill/ # 历史 K 线回填 CLI ├── config/ # cleanenv 配置 ├── internal/ │ ├── app/ # 装配 DI、cron scheduler、graceful shutdown │ ├── controller/ # Fiber 路由 + middleware │ ├── entity/ # 业务实体(不依赖 Fiber/DB/Binance) │ ├── usecase/ # 业务接口与编排 │ ├── repo/ │ │ ├── webapi/binance/ # Binance HTTP client │ │ └── persistent/postgres/ # 5 个 repository │ └── worker/ # 实际跑采集逻辑的 collector ├── migrations/ # SQL migration(含 hypertable 转换) ├── pkg/ │ ├── httpclient/ # 通用 HTTP client + retry + rate limit │ ├── logger/ # slog 包装 │ └── postgres/ # pgxpool 初始化 ├── docs/dev.md # 完整设计文档 ├── docker-compose.yml ├── Dockerfile ├── Makefile └── go.mod ``` --- ## 数据库 5 张 hypertable,主键设计保证 upsert 幂等: | 表 | 主键 | 时间列 | |---|---|---| | `market_klines` | `(source, symbol, interval, open_time)` | `open_time` | | `funding_rates` | `(source, symbol, funding_time)` | `funding_time` | | `open_interest` | `(source, symbol, period, timestamp)` | `timestamp` | | `long_short_ratio` | `(source, symbol, period, ratio_type, timestamp)` | `timestamp` | | `taker_buy_sell_volume` | `(source, symbol, period, timestamp)` | `timestamp` | K 线时间列存毫秒 epoch,Timescale chunk 间隔默认 1 周(衍生品表)/ 30 天(funding)。 --- ## 常用命令 ```bash make run # 跑服务 make build # 编译两个二进制到 ./bin/ make test # 跑测试 make test-race # -race 模式跑测试 make test-cover # 跑测试 + 输出总覆盖率 make guard # 跑全部可机械验证的守卫规则(G1/G6/G11/G12) make tidy # go mod tidy make docker-up # 起 Timescale make docker-down # 停 docker make migrate-up # apply migration make migrate-down # 回滚 1 步 make backfill ARGS="--symbol BTCUSDT --interval 1h --limit 500" ``` --- ## 测试与覆盖率 v2 第一次把测试体系立住。当前覆盖: | 包 | 测试范围 | 覆盖率 | |---|---|---| | `internal/usecase` | pivot S/R、percentile、LSR 穿越、Bollinger、Vegas、derivatives signal | 算法路径有表驱动与边界用例覆盖 | | `internal/repo/webapi/binance` (mapper.go) | `parseKlineRow`、`toInt64`、6 个 `map*` 函数 | 9 个函数全部 100% | | `internal/repo/persistent/postgres` | 5 个 repo 的 `UpsertMany` + `FindRecent` 正反例(pgxmock) | 包级 87.6% | 跑全部测试 + 覆盖率: ```bash make test-race make test-cover ``` 测试选型说明: - 算法(`indicator.go` / `derivatives_signal.go`)采用 table-driven + `testify/require`。 - Repo 层用 [`pgxmock/v4`](https://github.com/pashagolub/pgxmock) 锁 SQL 字符串与参数顺序——schema 漂移立刻可见。 - `*pgxpool.Pool` 不能直接 mock,因此 5 个 repo 的字段类型被改为内部 `pgxPool` 接口(`internal/repo/persistent/postgres/pool.go`),生产代码继续传 `*pgxpool.Pool`,调用方零改动。 - 集成测试(testcontainers + 真 Postgres + 真 migration)留 v2.1 做 nightly。 --- ## Clean Architecture 边界(验收用) ```bash # 一次跑全部 G1 / G6 / G11 / G12 守卫 make guard ``` 或单独: ```bash # controller 不应直接依赖 binance client grep -r "repo/webapi/binance" internal/controller # 期望: 无输出 # usecase 不应直接依赖具体 binance/postgres 实现 grep -r "repo/webapi/binance\|repo/persistent" internal/usecase # 期望: 无输出 # 全项目不应出现私钥/账户/签名相关字段 grep -ri "apikey\|x-mbx-apikey\|hmac" . # 期望: 无输出 # G12:float64 仅允许在 indicator.go / derivatives_signal.go 内部 grep -rn --include="*.go" "float64\|float32" internal/usecase | grep -vE "indicator|derivatives_signal" # 期望: 无输出 ``` --- ## 路线图 - **v1**:Binance 行情 + 衍生品 + 落库 + `/v1/market/context` 聚合接口。✅ - **v2.0**:技术结构计算第一波——pivot 支撑/压力 + P95/P5 区间 + LSR 穿越价位中位数(primary interval `1h`)。算法测试 + repo / mapper 关键路径测试到位。✅ - **v2.x(当前)**:进行中。 - ✅ Bollinger / Vegas 已落地到 `technical.intervals`。 - ✅ Derivatives signal 已落地到 `derivatives.signal`。 - ⚠️ 完整 per-interval `TechnicalStructure` 尚未升级,Support / Resistance / Range / LSLine 仍只按 primary `1h` 输出。 - ⚠️ 箱体与 per-interval 突破信号未实现。 - ❌ testcontainers 集成测试 + CI 未起。 - **v3**:CoinGlass 清算数据、ETF Flow、CME gap、CVD、链上稳定币流动性、宏观日历、多交易所聚合。 完整设计参见 [`docs/dev.md`](docs/dev.md)。 --- ## License Internal project. Not for redistribution.