- PairType 定义移至 types/kline.ts (spot/um/cm) - Kline 接口新增 type 字段,全链路透传 - klines 5列复合主键 (exchange, symbol, type, interval, time) - 拆出 BinanceFuturesRestClient (USDMClient) - exchanges/index.ts 注册 binance_futures - trading_pairs 唯一约束加 type,种子数据加合约对 - 12个连续聚合视图 SELECT/GROUP BY/INDEX 加 type - 清理 bnkline.ts 废弃代码和 pair.ts 空函数
9.1 KiB
接入 USDT-M 合约数据 — 改造方案(type 字段方案)
设计思路
用 type 列区分账户类型,而非 symbol 后缀。PairType 枚举已在代码中定义('spot' | 'um' | 'cm'),本次改造将 type 从实体层的死代码变成全链路透传的一等字段。
为什么选 type 而非 .P 后缀
.P 后缀 |
type 字段 |
|
|---|---|---|
| symbol 语义 | 污染标识符,BTCUSDT.P 不是交易对名称 | 干净,symbol 与 Binance 官方一致 |
| 查询 | WHERE symbol LIKE '%.P' 不走索引 |
WHERE type = 'um' 索引友好 |
| 扩展 | Coin-M 需要 _PERP 新后缀规则 |
加 'cm' 枚举值即可 |
| API 调用 | 每次 strip 后缀,容易遗漏 | symbol 原样传入,零变换 |
| 与已有代码 | 与 TradingPair.type 字段矛盾 | 与已有 PairType、实体定义完全对齐 |
Type 枚举
| type | 说明 |
|---|---|
spot |
现货(默认值) |
um |
USDT-M 永续合约 |
cm |
Coin-M 永续合约(预留) |
K 线复合主键
5 列:(exchange, symbol, type, interval, time)。type 在主键中保证相同 symbol 的现货与合约 K 线不会 PK 冲突。
改动清单
1. 配置层(已完成,无需再改)
data/env.yaml、data/config/validators.ts、data/config/index.ts 已有 binance_futures 段,无需改动。
2. 类型定义(1 文件)
data/types/base.ts — Kline 接口加 type 字段
export interface Kline {
exchange: string;
symbol: string;
/** 交易对类型(spot / um / cm) */
type: PairType; // ← 新增
interval: KlineInterval;
openTime: number;
closeTime: number;
// ... 其余字段不变
}
3. 实体层(1 文件)
data/db/entities/kline.entity.ts — type 从 @PrimaryColumn 保持不动(已存在,无需改)。
trading-pair.entity.ts — type 列已存在,无需改。
4. REST 客户端(2 文件,核心改动)
data/exchanges/binance/rest.ts
import { MainClient, USDMClient } from "binance";
// convertBinanceKline 加 type 参数
function convertBinanceKline(
raw: BinanceRestKline,
symbol: string,
interval: KlineInterval,
type: PairType, // ← 新增
): Kline {
return {
exchange: "binance",
symbol,
type, // ← 新增
interval,
// ... 其余字段不变
};
}
// 现货客户端(已有,加 type = 'spot')
export class BinanceRestClient extends BaseRestClient {
readonly exchange = "binance";
private client = new MainClient({...}, { timeout: 3000 });
async fetchKlines(symbol, startTime, limit, endTime): Promise<Kline[]> {
// ...
return rawKlines.map(k => convertBinanceKline(k, symbol, "1m", "spot"));
}
}
// 合约客户端(新增)
export class BinanceFuturesRestClient extends BaseRestClient {
readonly exchange = "binance";
private client = new USDMClient(
{ api_key: exchange.binanceFutures.apiKey, api_secret: exchange.binanceFutures.apiSecret },
{ timeout: 3000 }
);
async fetchKlines(symbol, startTime, limit, endTime): Promise<Kline[]> {
// USDMClient.getKlines() 与 MainClient 同构 12 元组,convertBinanceKline 直接复用
const rawKlines = await this.client.getKlines({ symbol, interval: "1m", ... });
return rawKlines.map(k => convertBinanceKline(k, symbol, "1m", "um"));
}
}
data/exchanges/index.ts — 注册合约客户端
const registry: Record<string, () => BaseRestClient> = {
binance: () => new BinanceRestClient(),
binance_futures: () => new BinanceFuturesRestClient(), // ← 新增
};
5. 服务层(1 文件)
data/service/kline.ts
const entities = KlineItems.map((item) => {
const entity = new Kline();
entity.type = item.type; // ← 新增
// ... 其余字段映射不变
});
await repo.upsert(entities, {
conflictPaths: ["exchange", "symbol", "type", "interval", "time"], // ← +type
skipUpdateIfNoValuesChanged: true,
});
6. 运行脚本(1 文件)
data/run/exchange.ts — 按 pair.type 选择客户端
for (const pair of allPairs) {
const exchangeId = pair.type === 'um' ? 'binance_futures' : 'binance';
const client = createRestClient(exchangeId);
// ... 其余逻辑不变
}
7. SQL 初始化脚本(2 文件)
data/db/init-db/02-init-tables.sql
klines 表:
CREATE TABLE IF NOT EXISTS klines (
exchange TEXT NOT NULL,
symbol TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'spot', -- 新增
interval TEXT NOT NULL,
time TIMESTAMPTZ NOT NULL,
-- OHLCV ...
PRIMARY KEY (exchange, symbol, type, interval, time) -- 5 列
);
ALTER TABLE klines SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'exchange,symbol,type', -- +type
timescaledb.compress_orderby = 'time DESC'
);
trading_pairs 表:
CREATE TABLE IF NOT EXISTS trading_pairs (
-- ...
symbol VARCHAR(20) NOT NULL,
type TEXT NOT NULL DEFAULT 'spot', -- 新增
-- ...
CONSTRAINT uq_trading_pairs_exchange_symbol_type UNIQUE (exchange_id, symbol, type) -- +type
);
种子数据:
INSERT INTO trading_pairs (exchange_id, symbol, type, base_asset, quote_asset,
price_precision, quantity_precision, active)
SELECT e.id, sym.symbol, sym.type, sym.base, sym.quote, 2, 5, TRUE
FROM exchanges e
CROSS JOIN (
VALUES
('BTCUSDT', 'spot', 'BTC', 'USDT'),
('ETHUSDT', 'spot', 'ETH', 'USDT'),
('BNBUSDT', 'spot', 'BNB', 'USDT'),
('SOLUSDT', 'spot', 'SOL', 'USDT'),
('BTCUSDT', 'um', 'BTC', 'USDT'),
('ETHUSDT', 'um', 'ETH', 'USDT')
) AS sym(symbol, type, base, quote)
WHERE e.name = 'binance'
ON CONFLICT (exchange_id, symbol, type) DO NOTHING;
data/db/init-db/03-continuous-aggregates.sql
12 个连续聚合视图,每个改 3 处(SELECT / GROUP BY / INDEX 各加 type)。以 klines_3m 为例:
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_3m
WITH (timescaledb.continuous) AS
SELECT
time_bucket('3 minutes', time) AS time,
exchange,
symbol,
type, -- 新增
'3m'::text AS interval,
FIRST(open, time) AS open,
-- ...
FROM klines
GROUP BY time_bucket('3 minutes', klines.time), exchange, symbol, type -- +type
WITH NO DATA;
CREATE INDEX IF NOT EXISTS idx_klines_3m_symbol_time
ON klines_3m (exchange, symbol, type, time DESC); -- +type
8. service/pair.ts(1 文件,小改)
getPairLastBackfillTime / updatePairLastBackfillTime 按 symbol 查找时需要限定 type,避免现货/合约二义性:
// 推荐:type 参数可选,默认 'spot' 向后兼容
export async function getPairLastBackfillTime(symbol: string, type: PairType = 'spot') {
const pair = await repo.findOneBy({ symbol, type });
return pair?.last_backfill_time;
}
实际调用处(run/exchange.ts 中)传 pair.type。
改动汇总
| 文件 | 改动 | 行数 |
|---|---|---|
data/types/base.ts |
Kline 接口 +type | +1 |
data/db/entities/kline.entity.ts |
不动(type 已存在) | 0 |
data/exchanges/binance/rest.ts |
引入 USDMClient,拆现货/合约客户端,convertBinanceKline +type | +50 |
data/exchanges/index.ts |
注册 binance_futures | +2 |
data/service/kline.ts |
entity.type 赋值,conflictPaths +1 | +3 |
data/run/exchange.ts |
按 pair.type 选择客户端 | +3 |
data/service/pair.ts |
findOneBy 加 type 参数 | +5 |
data/db/init-db/02-init-tables.sql |
klines +type列+PK+压缩键,trading_pairs +唯一约束+种子数据 | +10 |
data/db/init-db/03-continuous-aggregates.sql |
12 视图 × 3 处(SELECT/GROUP BY/INDEX) | +36 |
共 9 个文件,~110 行净改动。 不动:配置层、env.yaml。
迁移计划(重建数据库)
第1步: 修改 SQL DDL
├── 02-init-tables.sql: klines PK 加 type,trading_pairs 唯一约束加 type
└── 03-continuous-aggregates.sql: 12 视图 SELECT/GROUP BY/INDEX 加 type
第2步: 修改代码
├── types/base.ts: Kline 接口 +type
├── exchanges/binance/rest.ts: 拆现货/合约客户端
├── exchanges/index.ts: 注册 binance_futures
├── service/kline.ts: entity.type + conflictPaths
├── service/pair.ts: findOneBy 加 type
└── run/exchange.ts: 按 pair.type 选客户端
第3步: 重建数据库
├── docker compose down -v && docker compose up -d (清空数据)
├── 执行 01 → 02 → 03 SQL 初始化脚本
└── bun run data/run/exchange.ts 全量回补
注意事项
binance_futures的 API Key 需要在 Binance 开通合约交易权限,否则USDMClient调用会报权限错误。- 合约 K 线数量通常多于现货(7×24 交易),回补时间更长。
- Coin-M 接入只需:
PairType已有'cm',新增CoinMClient类,种子数据加('BTCUSDT', 'cm', ...)即可。