refactor(data): 重构交易所适配器,修复 Kline 实体复合主键

- 废弃旧 adapter 体系 (base/binance/types.ts),新增 base_rest/rest.ts
  基于 Binance 官方 SDK 实现 REST K 线拉取
- Kline 实体改为四列复合主键 (exchange/symbol/interval/time),
  修复单列 time PK 导致的跨 symbol 写入冲突
- 新增 filterConsecutive():K 线连续性过滤,首缺口截断策略
- 新增 service/kline.ts:批量 UPSERT K 线入库
- 新增 types/ 共享类型定义、example/ 示例、run/ 运行脚本
This commit is contained in:
Rekey
2026-06-08 18:18:16 +08:00
parent 85a0031a78
commit 5e385547c7
16 changed files with 829 additions and 1043 deletions
+140 -6
View File
@@ -18,7 +18,8 @@
- [4.4 关系数据实体 (`db/entities/`)](#44-关系数据实体-dbentities)
- [4.5 交易所适配器 (`exchanges/`)](#45-交易所适配器-exchanges)
- [4.6 K 线合成管道 (`pipeline/`)](#46-k-线合成管道-pipeline)
- [4.7 数据发布 (`publisher/`)](#47-数据发布-publisher)
- [4.7 数据发布 (`publisher/`)](#47-数据发布-publisher)
- [4.8 数据补全服务 (`run/exchange.ts`)](#48-数据补全服务-runexchangets)
5. [数据流生命周期](#5-数据流生命周期)
6. [TypeORM + TimescaleDB 集成细节](#6-typeorm--timescaledb-集成细节)
7. [配置管理策略](#7-配置管理策略)
@@ -138,6 +139,10 @@ data/
├── tsconfig.json # TypeScript 配置
├── bun.lock # Bun 依赖锁定文件
├── run/ # 启动文件
│ ├── main.ts # 模块入口:配置加载 → DB 连接 → Redis 连接 → 适配器启动 → 优雅关闭
│ └── exchange.ts # 数据补全服务:读取 trading_pairs.last_backfill_time → 拉取缺失 K 线 → 批量写入 → 更新时间戳
├── config/ # 中心化配置模块(目录)
│ ├── index.ts # 配置加载与分组导出(pgsql / redis / logging
│ └── validators.ts # 零依赖运行时校验(env.yaml → EnvConfig
@@ -159,8 +164,6 @@ data/
├── publisher/ # Redis 数据发布(待实现)
├── types/ # 共享类型定义
├── utils/ # 工具函数
├── index.ts # 模块入口
├── logger.ts # Pino 日志实例
└── tests/ # 测试
```
@@ -849,6 +852,112 @@ export class RedisPublisher {
---
### 4.8 数据补全服务 [`run/exchange.ts`](run/exchange.ts)
独立启动的数据补全服务,从 `trading_pairs` 表中读取每个交易对的 `last_backfill_time`,据此确定需要拉取的历史 K 线范围,补全完成后将 `last_backfill_time` 更新为最新时间点。
**核心机制 — 基于 `last_backfill_time` 的增量补全**
```
trading_pairs 表:
┌────┬──────────┬──────────────────────┬──────────────────────┐
│ id │ symbol │ last_backfill_time │ kline_intervals │
├────┼──────────┼──────────────────────┼──────────────────────┤
│ 1 │ BTCUSDT │ 2026-06-07 12:00:00 │ 1m,5m,15m,1h,4h,1d │
│ 2 │ ETHUSDT │ 2026-06-08 08:00:00 │ 1m,5m,1h,1d │
└────┴──────────┴──────────────────────┴──────────────────────┘
补全任务生成:
BTCUSDT → [(1m, 06/07 12:00 → now), (5m, 06/07 12:00 → now), ...]
ETHUSDT → [(1m, 06/08 08:00 → now), (5m, 06/08 08:00 → now), ...]
```
- `last_backfill_time` 初始值为 `1970-01-01T00:00:00Z`(epoch 起点),新交易对自动触发全量拉取
- 每次补全完成后,更新为本次实际拉取到的最后一条 K 线时间
- 下次运行时自动从上次结束位置继续,无重复拉取
**使用场景**
| 场景 | 触发方式 | 说明 |
|------|----------|------|
| **定期增量补全** | cron 定时触发 | 每日/每小时补齐最新数据 |
| **首次上线初始化** | 手动执行 | 新交易对 `last_backfill_time` 为 epoch,自动拉全量历史 |
| **定点修复** | `--start` / `--end` 覆盖 | 修复特定时间段的缺失数据 |
| **补全后验证** | `--dry-run` | 仅展示需拉取的任务范围,不实际请求 |
**命令行参数**
```bash
# 全量模式:为所有 active 交易对执行增量补全
bun run run/exchange.ts --concurrency 2
# 指定交易对:
bun run run/exchange.ts --symbols BTCUSDT,ETHUSDT
# 手动覆盖时间范围(忽略 last_backfill_time):
bun run run/exchange.ts \
--symbols BTCUSDT \
--start "2026-06-01T00:00:00Z" \
--end "2026-06-08T00:00:00Z"
# 仅检测不拉取:
bun run run/exchange.ts --dry-run
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--exchange` | (从 DB 读取) | 限定交易所,不填则为所有启用交易所 |
| `--symbols` | (从 DB 读取所有 active) | 限定交易对列表,逗号分隔 |
| `--intervals` | (从 DB 读取 `kline_intervals`) | K 线周期,逗号分隔 |
| `--start` | `last_backfill_time`(不低于 7 天前) | 补全起始时间 (ISO 格式);不填则使用 DB 中的 `last_backfill_time` |
| `--end` | `Date.now()` | 补全结束时间 (ISO 格式) |
| `--concurrency` | `2` | 并发任务数 |
| `--batch-size` | `500` | 单次 REST 请求最大 K 线条数 |
| `--dry-run` | `false` | 仅列出任务范围,不拉取不写入 |
**执行流程**
```
1. 查询 trading_pairs 表(JOIN exchanges),获取 active=true 且 exchange.enabled=true 的交易对
2. 为每个交易对 × 每个 kline_interval 生成一个 BackfillTask
- startTime = --start ?? last_backfill_time(若 last_backfill_time 为 epoch 则兜底为 now-7d
- endTime = --end ?? now
- 若 startTime >= endTime → 跳过(已是最新)
3. 按 exchange 分组,创建对应适配器实例
4. Semaphore 并发执行(默认 2):
a. 按 batch-size 分段切分时间范围
b. 逐段调用适配器 fetchKlines() → 写入 klines 表(UPSERT
c. 记录本次拉取的最后一条 K 线时间
5. 所有任务完成后,更新每个交易对的 last_backfill_time
```
**并发策略**
- 不同(symbol, interval)任务之间并行执行
- 同一任务内部的多次分页请求串行执行(受 REST API 限频约束)
- 单个任务失败不影响其他任务,失败数记录到最终统计
**last_backfill_time 更新逻辑**
```
任务完成后:
pairLastTimes[pairId] = max(pairLastTimes[pairId], 本次拉取最后一条 K 线的 openTime)
最后统一:
UPDATE trading_pairs SET last_backfill_time = pairLastTimes[id]
```
**注意事项**
- 未指定 `--symbols` 且非 `--dry-run` 时,走"全量增量"模式,覆盖所有 active 交易对
- 指定 `--start` 时不使用 `last_backfill_time`,但仍会更新 `last_backfill_time` 为实际拉取时间
- `--dry-run` 模式下不更新 `last_backfill_time`
- 依赖交易所 REST API 限频,当前硬编码每次分页间隔 200ms(Binance
---
## 5. 数据流生命周期
```
@@ -898,6 +1007,8 @@ export class RedisPublisher {
### 启动时序
入口文件:[`run/main.ts`](run/main.ts)
```
1. 加载配置(config/index.ts → 读取 env.yaml → 零依赖校验)
2. 初始化 Pino 日志
@@ -1124,7 +1235,7 @@ export const logger = pino({
### 9.2 优雅关闭(Graceful Shutdown
```typescript
// data/index.ts
// data/run/main.ts
async function shutdown(signal: string): Promise<void> {
logger.info({ signal }, "Shutting down");
@@ -1226,8 +1337,8 @@ cd data && bun install
# 3. 配置环境
# 编辑项目根目录 env.yaml(如不存在则创建)
# 4. 验证配置加载
bun run db/data-source.ts # 测试 DataSource 初始化
# 4. 启动数据模块
bun run run/main.ts
# 5. 运行测试
bun test
@@ -1269,6 +1380,29 @@ bunx typeorm migration:revert -d db/data-source.ts
| 类型/接口 | `PascalCase` |
| 测试文件 | `*.test.ts`(与源文件同目录或 `tests/` 下镜像结构) |
### 11.5 数据补全
```bash
# 增量模式:为所有 active 交易对补齐最新数据
bun run run/exchange.ts --concurrency 2
# 仅检测需补全的任务范围
bun run run/exchange.ts --dry-run
# 指定交易对增量补全
bun run run/exchange.ts --symbols BTCUSDT,ETHUSDT
# 首次上线:拉取最近 90 天全量历史
bun run run/exchange.ts \
--symbols BTCUSDT \
--start "$(date -u -v-90d '+%Y-%m-%dT%H:%M:%SZ')" \
--intervals 1m,5m,15m,1h,4h,1d \
--concurrency 1
# cron 定时任务(每小时执行)
0 * * * * cd /app && bun run data/run/exchange.ts --concurrency 2 >> /var/log/backfill.log 2>&1
```
---
## 12. 风险提示