# 接入 USDT-M 合约数据 — 改造方案 ## 设计思路 **用 symbol 后缀区分账户类型**,而非新增 `account_type` 列。核心原则是尽量不动已有表结构和聚合视图。 ### Symbol 命名约定 | 账户类型 | symbol 示例 | 说明 | |---------|-------------|------| | 现货 | `BTCUSDT` | 不变 | | USDT-M 永续 | `BTCUSDT.P` | `.P` 后缀标记合约 | | Coin-M 永续 | `BTCUSDT_PERP` | 预留 | **核心机制**:对外(入库、查询、展示)统一用带后缀的 symbol;对内(调用 Binance SDK)自动 strip 后缀。 --- ## 改动清单 ### 1. 配置层(3 文件) **`data/env.yaml`** ```yaml exchange: binance: ← 保留不动(向后兼容) api_key: "..." api_secret: "..." binance_futures: ← 新增 api_key: "..." api_secret: "..." ``` **`data/config/validators.ts`** - `ExchangeConfig` 接口中 `binance: ExchangeApiKeys` 保持不动 - 新增 `binance_futures: ExchangeApiKeys` - `validateConfig()` 中新增解析 `exchange.binance_futures` **`data/config/index.ts`** - 导出 `exchange.binance`(已有)+ 新增 `exchange.binanceFutures` --- ### 2. REST 客户端(1 文件,核心改动) **`data/exchanges/rest.ts`** ```typescript import { USDMClient } from "binance"; // 新增引入 // 工具函数:提取裸 symbol(移除 .P / _PERP 后缀) function stripSuffix(symbol: string): string { return symbol.replace(/\.(P|PERP)$/, ""); } // 判断是否为合约 symbol function isFuturesSymbol(symbol: string): boolean { return symbol.endsWith(".P") || symbol.endsWith("_PERP"); } ``` 现有 `fetchBinanceKlines()` 保持不变(处理现货)。新增 `fetchFuturesKlines()`: ```typescript async function fetchFuturesKlines( symbol: string, // BTCUSDT.P interval: KlineInterval, startTime: number, endTime?: number, limit = 500, ): Promise { const rawSymbol = stripSuffix(symbol); // BTCUSDT const client = new USDMClient({ api_key: exchange.binanceFutures.apiKey, api_secret: exchange.binanceFutures.apiSecret, }, { timeout: 3000 }); const rawKlines = await client.getKlines({ symbol: rawSymbol, interval, startTime, endTime, limit: Math.min(limit, 1000), }); return rawKlines.map(k => convertBinanceKline(k, symbol, interval)); } ``` `Client` 类 `fetchKlines()` 增加分支: ```typescript async fetchKlines(...): Promise { switch (this.exchange) { case "binance": if (isFuturesSymbol(symbol)) { return fetchFuturesKlines(symbol, interval, startTime, endTime, effectiveLimit); } return fetchBinanceKlines(symbol, interval, startTime, endTime, effectiveLimit); } } ``` **关键**:`convertBinanceKline()` 传入原始 `"BTCUSDT.P"`,转换结果中 `kline.symbol` 自然就是 `"BTCUSDT.P"`,入库后与现货 `"BTCUSDT"` 不会 PK 冲突。 --- ### 3. 服务层(1 文件) **`data/service/kline.ts`** - 不需任何改动。`upsertOrUpdateKlines()` 直接把 `Kline.symbol` 写入 `KlineEntity.symbol`,后缀天然带进去。 --- ### 4. 实体层(0 文件) **结论:不需要改。** - `kline.entity.ts` 的 4 列 PK 保持不变 - `trading-pair.entity.ts` 不需要加 `account_type`,用 symbol 本身的 `.P` 后缀标识即可 TradingPair 的 symbol 设成 `"BTCUSDT.P"`,唯一约束 `(exchange_id, symbol)` 自动不冲突。 --- ### 5. SQL 初始化脚本(1 文件) **`data/db/init-db/02-init-tables.sql`** 种子数据新增合约交易对: ```sql INSERT INTO trading_pairs (exchange_id, symbol, base_asset, quote_asset, price_precision, quantity_precision, kline_interval, kline_intervals, active) SELECT e.id, sym.symbol, sym.base, sym.quote, 2, 5, '1m', '1m,5m,15m,30m,1h,4h,1d,1w', TRUE FROM exchanges e CROSS JOIN ( VALUES ('BTCUSDT.P', 'BTC', 'USDT'), ('ETHUSDT.P', 'ETH', 'USDT') ) AS sym(symbol, base, quote) WHERE e.name = 'binance' ON CONFLICT (exchange_id, symbol) DO NOTHING; ``` **klines 表结构和连续聚合视图:完全不动。** symbol 值不同,数据天然隔离。 --- ### 6. 运行脚本(1 文件) **`data/run/exchange.ts`** - `getAllPairs()` 不受影响,返回的 `symbol` 本身就是 `"BTCUSDT.P"` - `new Client("binance")` 的 `fetchKlines()` 内部已按 symbol 后缀分派到 `USDMClient` - 回补循环逻辑不变,`lastBackfillTime` 追踪机制不变 **注意**:Binance 的 `USDMClient.getKlines()` 返回与 `MainClient.getKlines()` 同构的 12 元组,`convertBinanceKline()` 可以直接复用。 --- ### 7. 类型定义(0 文件) `Kline.symbol` 字段类型已是 `string`,直接存 `"BTCUSDT.P"`。无需改动。 --- ## 改动汇总 | 文件 | 改动类型 | 行数估算 | |------|---------|---------| | `data/env.yaml` | 新增 `binance_futures` 段 | +4 | | `data/config/validators.ts` | `ExchangeConfig` + `validateConfig()` 新增解析 | +10 | | `data/config/index.ts` | 新增 `exchange.binanceFutures` 导出 | +5 | | `data/exchanges/rest.ts` | 引入 `USDMClient`,新增 `fetchFuturesKlines()`,`Client.fetchKlines()` 增加分支 | +40 | | `data/db/init-db/02-init-tables.sql` | seed 数据插入合约交易对 | +15 | | `data/run/exchange.ts` | 无实质改动(后缀自动路由) | 0 | **不动**:klines 表结构、复合主键、连续聚合视图、service/kline.ts、types 目录、实体层。 共 **5-6 个文件,~70 行净改动**。 --- ## 工作顺序 1. **env.yaml** — 配好 futures 的 API Key 2. **config/validators.ts + config/index.ts** — 解析 + 导出 futures Key 3. **exchanges/rest.ts** — 核心改动,USDMClient + 后缀分派 4. **02-init-tables.sql** — 种子数据 5. 验证:跑 `bun run data/run/exchange.ts` 看能否拉下 `BTCUSDT.P` 的 K 线 6. Coin-M 同理扩展(只加 suffix 规则 + CoinMClient case)