Files
trade/data/exchanges/rest.ts
T
Rekey b4c7636731 添加 USDT-M 合约数据支持(配置层 + 清理多余字段)
- 配置层:env.yaml 新增 binance_futures API Key 段,validators + config 同步
- 清理 TradingPair 实体:删除 kline_interval、kline_intervals、kline_synthesis_enabled
- 删除 fetchKlines 系列函数的 interval 参数,硬编码为 1m
- 更新 SQL seed 数据、example、base_rest 接口、types 接口
- 新增 AGENTS/08-boundaries.md 执行纪律
- 新增 PLAN-add-futures-data.md 方案文档
2026-06-15 23:24:21 +08:00

249 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { MainClient, type Kline as BinanceRestKline, CoinMClient } from "binance";
import { logger } from "../utils/logger";
import { exchange } from "../config";
import { BaseRestClient } from './base_rest';
import type { KlineInterval, Kline, MarketInfo } from '../types';
/** K 线周期 → 毫秒数映射(用于时间桶计算) */
export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
"1m": 60_000,
"3m": 180_000,
"5m": 300_000,
"15m": 900_000,
"30m": 1_800_000,
"1h": 3_600_000,
"2h": 7_200_000,
"4h": 14_400_000,
"6h": 21_600_000,
"8h": 28_800_000,
"1d": 86_400_000,
"1w": 604_800_000,
"1mon": 2_592_000_000,
};
// ============================================================
// Binance REST K 线 → 本系统标准化 Kline 转换
// ============================================================
/**
* Binance SDK Kline 元组格式(getKlines / getUIKlines 返回):
* [0] openTime: number — 开盘时间(Unix ms)
* [1] open: numberInString — 开盘价
* [2] high: numberInString — 最高价
* [3] low: numberInString — 最低价
* [4] close: numberInString — 收盘价
* [5] volume: numberInString — 成交量(base 币种)
* [6] closeTime: number — 收盘时间(Unix ms)
* [7] quoteVolume: numberInString — 成交额(quote 币种)
* [8] tradeCount: number — 成交笔数
* [9] takerBuyBaseVol: numberInString — 主动买入成交量
* [10] takerBuyQuoteVol: numberInString — 主动买入成交额
* [11] ignore: numberInString — 忽略字段
*
* numberInString = string | number,通过 Number() 统一转换。
*
* 参考:node_modules/binance/lib/types/shared.d.ts:85-98
*/
function convertBinanceKline(
raw: BinanceRestKline,
symbol: string,
): Kline {
const [
openTime,
open,
high,
low,
close,
volume,
closeTime,
quoteVolume,
tradeCount,
takerBuyBaseVol,
takerBuyQuoteVol,
// [11] ignore — 丢弃
] = raw;
return {
exchange: "binance",
symbol,
interval: "1m",
openTime: openTime,
closeTime: closeTime,
open: String(open),
high: String(high),
low: String(low),
close: String(close),
volume: String(volume),
quoteVolume: String(quoteVolume),
takerBuyBaseVol: String(takerBuyBaseVol),
takerBuyQuoteVol: String(takerBuyQuoteVol),
tradeCount: String(tradeCount),
isClosed: true, // REST 返回的 K 线均为已闭合历史数据
};
}
// ============================================================
// Binance REST 拉取函数
// ============================================================
/**
* 通过 Binance 原生 SDK 拉取 1m K 线并转换为本系统 Kline。
*
* getUIKlines 与 getKlines 返回同构的 Kline[] 元组,
* getUIKlines 额外支持 timeZone 参数,适合按交易所时区对齐。
*
* @param symbol - 交易对(如 BTCUSDT
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms),可选
* @param limit - 单次拉取条数,默认 500(最大 1000)
*/
async function fetchBinanceKlines(
symbol: string,
startTime: number,
endTime?: number,
limit = 500,
): Promise<Kline[]> {
const client = new MainClient({
api_key: exchange.binance.apiKey,
api_secret: exchange.binance.apiSecret,
}, {
timeout: 3000,
});
// Binance 硬限制:单次最多 1000 条
const safeLimit = Math.min(limit, 1000);
const rawKlines = await client.getKlines({
symbol,
interval: "1m",
startTime,
endTime,
limit: safeLimit,
});
logger.debug({
symbol,
interval: "1m",
startTime,
endTime,
limit: safeLimit,
}, 'fetchBinanceKlines arguments');
if (!rawKlines || rawKlines.length === 0) {
return [];
}
return filterConsecutive(
rawKlines.map((k) => convertBinanceKline(k, symbol)),
);
}
/**
* 过滤出严格连续(无时间缺口)的 K 线序列。
*
* 处理流程:
* 1. 按 openTime 升序排序(防御性,确保时间单调递增)
* 2. 从首条 K 线开始遍历,仅保留相邻间隔恰好等于 intervalMs 的条目
* 3. 一旦检测到缺口(间隔 ≠ intervalMs),立即终止并丢弃后续所有数据
*
* 设计意图:
* - 时间序列分析(回测、指标计算)依赖连续数据,缺口会引入偏误
* - 缺口之后的数据可能来自另一段不连续的拉取结果,混入后风险更高
* - "截断"策略优于"填充/跳过",避免伪造数据或隐藏数据质量问题
*
* @param klines - 待过滤的 K 线数组(可能乱序、可能含缺口)
* @param interval - K 线周期,用于查表获取 intervalMs
* @returns 从首条开始严格连续的最大前缀子序列;空数组无缺口时返回完整排序结果
*/
function filterConsecutive(klines: Kline[]) {
// 防御性排序:Binance API 不保证返回顺序,升序排列确保时间单调
const results = klines.sort((a: Kline, b: Kline) => {
return a.openTime - b.openTime;
});
return results;
// console.log(results);
// let _openTime = 0; // 哨兵:0 表示尚未初始化,非 0 表示上一条已收录 K 线的 openTime
// const rets: Kline[] = []; // 累积连续 K 线结果
// for (let item of results) {
// console.log(item.openTime);
// // 分支 1 —— 首条 K 线:无条件收录,并初始化哨兵
// if (_openTime === 0) {
// _openTime = item.openTime;
// rets.push(item);
// continue;
// }
// // 分支 2 —— 严格连续:当前 openTime 与上一条恰好相差一个周期
// if (item.openTime - _openTime === intervalMs) {
// _openTime = item.openTime;
// rets.push(item);
// continue;
// }
// // 分支 3 —— 检测到缺口:截断,丢弃当前及之后所有 K 线
// break;
// }
// return rets;
}
// ============================================================
// Client —— 多交易所 REST 客户端
// ============================================================
export class Client extends BaseRestClient {
exchange: string;
/**
* @param exchange - 交易所 ID(如 "binance"、"okx"、"bybit"
* 内部根据 ID 分发到对应的 SDK 实现
*/
constructor(exchange: string) {
super();
this.exchange = exchange;
}
/**
* 拉取 1m 历史 K 线数据,返回标准化 Kline 数组。
*
* 根据交易所 ID 分发到各自的 SDK 拉取函数。
* K 线周期固定为 1m,高周期通过 TimescaleDB 连续聚合视图生成。
*
* @param symbol - 交易对符号(如 BTCUSDT
* @param startTime - 起始时间(Unix ms
* @param endTime - 结束时间(Unix ms),可选
* @param limit - 最大返回条数,默认取自 config.defaultLimit
*/
async fetchKlines(
symbol: string,
startTime: number,
limit?: number,
endTime?: number,
): Promise<Kline[]> {
const effectiveLimit = limit ?? this.config.defaultLimit;
switch (this.exchange) {
case "binance":
return fetchBinanceKlines(symbol, startTime, endTime, effectiveLimit);
// TODO: 新增交易所在此添加 case
// case "okx":
// return fetchOkxKlines(symbol, interval, startTime, endTime, effectiveLimit);
default:
throw new Error(
`[Client] 不支持的交易所: "${this.exchange}"` +
`当前仅支持: binance`,
);
}
}
async fetchMarkets(): Promise<MarketInfo[]> {
// TODO: 各交易所实现
return [];
}
}