import { MainClient, USDMClient, type Kline as BinanceRestKline } from "binance"; import { logger } from "../../utils/logger"; import { exchange } from "../../config"; import { BaseRestClient } from "../base"; import type { Kline, MarketInfo, KlineInterval, PairType, FetchKlinesParams } from "../../types"; // ============================================================ // 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,通过 String() 统一转换。 * * 参考:node_modules/binance/lib/types/shared.d.ts:85-98 */ function convertBinanceKline( raw: BinanceRestKline, symbol: string, interval: KlineInterval, type: PairType, ): Kline { const [ openTime, open, high, low, close, volume, closeTime, quoteVolume, tradeCount, takerBuyBaseVol, takerBuyQuoteVol, // [11] ignore — 丢弃 ] = raw; return { exchange: "binance", symbol, type, interval, openTime, 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 线均为已闭合历史数据 }; } // ============================================================ // BinanceRestClient // ============================================================ export class BinanceRestClient extends BaseRestClient { readonly exchange = "binance"; private client: MainClient; constructor() { super(); this.client = new MainClient( { api_key: exchange.binance.apiKey, api_secret: exchange.binance.apiSecret, }, { timeout: 3000 }, ); } /** * 拉取 1m K 线并转换为本系统标准化 Kline。 * * Binance 硬限制单次最多 1000 条,超限自动裁切。 * 高周期 K 线通过 TimescaleDB 连续聚合视图生成。 * * @param params - fetchKlines 统一参数对象 */ async fetchKlines(params: FetchKlinesParams): Promise { const { symbol, startTime, limit, endTime, type } = params; const effectiveLimit = limit ?? this.config.defaultLimit; // Binance 硬限制:单次最多 1000 条 const safeLimit = Math.min(effectiveLimit, 1000); // 限流 await this.throttle(`${symbol}:1m`); const rawKlines = await this.client.getKlines({ symbol, interval: "1m", startTime, endTime, limit: safeLimit, }); logger.debug( { symbol, interval: "1m", startTime, endTime, limit: safeLimit }, "[binance] fetchKlines 请求参数", ); if (!rawKlines || rawKlines.length === 0) { return []; } // 按 openTime 升序排序(防御性),转换为标准化 Kline return rawKlines .map((k) => convertBinanceKline(k, symbol, "1m", type)) .sort((a, b) => a.openTime - b.openTime); } async fetchMarkets(): Promise { // TODO: 调用 Binance /exchangeInfo 接口 return []; } } // ============================================================ // BinanceFuturesRestClient — USDT-M 永续合约 // ============================================================ export class BinanceFuturesRestClient extends BaseRestClient { readonly exchange = "binance_futures"; private client: USDMClient; constructor() { super(); this.client = new USDMClient( { api_key: exchange.binanceFutures.apiKey, api_secret: exchange.binanceFutures.apiSecret, }, { timeout: 3000 }, ); } /** * 拉取 USDT-M 永续合约 1m K 线。 * * USDMClient.getKlines() 返回与 MainClient 同构的 12 元组, * convertBinanceKline 直接复用,type 由调用方通过 params 传入。 */ async fetchKlines(params: FetchKlinesParams): Promise { const { symbol, startTime, limit, endTime, type } = params; const effectiveLimit = limit ?? this.config.defaultLimit; const safeLimit = Math.min(effectiveLimit, 1000); await this.throttle(`${symbol}:1m`); const rawKlines = await this.client.getKlines({ symbol, interval: "1m", startTime, endTime, limit: safeLimit, }); logger.debug( { symbol, interval: "1m", startTime, endTime, limit: safeLimit }, "[binance_futures] fetchKlines 请求参数", ); if (!rawKlines || rawKlines.length === 0) { return []; } return rawKlines .map((k) => convertBinanceKline(k, symbol, "1m", type)) .sort((a, b) => a.openTime - b.openTime); } async fetchMarkets(): Promise { return []; } }