5e385547c7
- 废弃旧 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/ 运行脚本
254 lines
8.4 KiB
TypeScript
254 lines
8.4 KiB
TypeScript
import { MainClient, type Kline as BinanceRestKline } from "binance";
|
||
|
||
import { logger } from "../utils/logger";
|
||
import { BaseRestClient } from './base_rest';
|
||
import type { KlineInterval, Kline, MarketInfo } from '../types';
|
||
|
||
/** K 线周期 → 毫秒数映射(用于时间桶计算) */
|
||
export const KLINE_INTERVAL_MS: Record<KlineInterval, number> = {
|
||
"1m": 60_000,
|
||
"5m": 300_000,
|
||
"15m": 900_000,
|
||
"30m": 1_800_000,
|
||
"1h": 3_600_000,
|
||
"4h": 14_400_000,
|
||
"1d": 86_400_000,
|
||
"1w": 604_800_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,
|
||
interval: KlineInterval,
|
||
): Kline {
|
||
const [
|
||
openTime,
|
||
open,
|
||
high,
|
||
low,
|
||
close,
|
||
volume,
|
||
closeTime,
|
||
quoteVolume,
|
||
tradeCount,
|
||
takerBuyBaseVol,
|
||
takerBuyQuoteVol,
|
||
// [11] ignore — 丢弃
|
||
] = raw;
|
||
|
||
return {
|
||
exchange: "binance",
|
||
symbol,
|
||
interval,
|
||
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 拉取 UI K 线并转换为本系统 Kline。
|
||
*
|
||
* getUIKlines 与 getKlines 返回同构的 Kline[] 元组,
|
||
* getUIKlines 额外支持 timeZone 参数,适合按交易所时区对齐。
|
||
*
|
||
* @param symbol - 交易对(如 BTCUSDT)
|
||
* @param interval - K 线周期
|
||
* @param startTime - 起始时间(Unix ms)
|
||
* @param endTime - 结束时间(Unix ms),可选
|
||
* @param limit - 单次拉取条数,默认 500(最大 1000)
|
||
*/
|
||
async function fetchBinanceKlines(
|
||
symbol: string,
|
||
interval: KlineInterval,
|
||
startTime: number,
|
||
endTime?: number,
|
||
limit = 500,
|
||
): Promise<Kline[]> {
|
||
const client = new MainClient({
|
||
api_key: 'ONSJKIGRpDYLn6FdV17aAKfjclZ4I2LzamflhuMpsoRQA427lLKeyJlGtg2RZ7DH',
|
||
api_secret: '5Mfv4TgvDlRzCHbtl2nJL4mVHUvMm8pyjKiRjMoosBMxrhlqMw6CuQbg2qbS2Npd',
|
||
});
|
||
|
||
// Binance 硬限制:单次最多 1000 条
|
||
const safeLimit = Math.min(limit, 1000);
|
||
|
||
const rawKlines = await client.getKlines({
|
||
symbol,
|
||
interval,
|
||
startTime,
|
||
endTime,
|
||
limit: safeLimit,
|
||
});
|
||
|
||
logger.info({
|
||
symbol,
|
||
interval,
|
||
startTime,
|
||
endTime,
|
||
limit: safeLimit,
|
||
}, 'fetchBinanceKlines arguments');
|
||
|
||
if (!rawKlines || rawKlines.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
return filterConsecutive(
|
||
rawKlines.map((k, index) => {
|
||
// if (index === rawKlines.length - 1) {
|
||
// console.log(k);
|
||
// }
|
||
return convertBinanceKline(k, symbol, interval);
|
||
}),
|
||
interval,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 过滤出严格连续(无时间缺口)的 K 线序列。
|
||
*
|
||
* 处理流程:
|
||
* 1. 按 openTime 升序排序(防御性,确保时间单调递增)
|
||
* 2. 从首条 K 线开始遍历,仅保留相邻间隔恰好等于 intervalMs 的条目
|
||
* 3. 一旦检测到缺口(间隔 ≠ intervalMs),立即终止并丢弃后续所有数据
|
||
*
|
||
* 设计意图:
|
||
* - 时间序列分析(回测、指标计算)依赖连续数据,缺口会引入偏误
|
||
* - 缺口之后的数据可能来自另一段不连续的拉取结果,混入后风险更高
|
||
* - "截断"策略优于"填充/跳过",避免伪造数据或隐藏数据质量问题
|
||
*
|
||
* @param klines - 待过滤的 K 线数组(可能乱序、可能含缺口)
|
||
* @param interval - K 线周期,用于查表获取 intervalMs
|
||
* @returns 从首条开始严格连续的最大前缀子序列;空数组无缺口时返回完整排序结果
|
||
*/
|
||
function filterConsecutive(klines: Kline[], interval: KlineInterval) {
|
||
// 查表获取当前 K 线周期对应的毫秒数
|
||
const intervalMs = KLINE_INTERVAL_MS[interval];
|
||
|
||
// 防御性排序: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;
|
||
}
|
||
|
||
/**
|
||
* 拉取历史 K 线数据,返回标准化 Kline 数组。
|
||
*
|
||
* 根据交易所 ID 分发到各自的 SDK 拉取函数。
|
||
*
|
||
* @param symbol - 交易对符号(如 BTCUSDT)
|
||
* @param interval - K 线周期
|
||
* @param startTime - 起始时间(Unix ms)
|
||
* @param endTime - 结束时间(Unix ms),可选
|
||
* @param limit - 最大返回条数,默认取自 config.defaultLimit
|
||
*/
|
||
async fetchKlines(
|
||
symbol: string,
|
||
interval: KlineInterval,
|
||
startTime: number,
|
||
limit?: number,
|
||
endTime?: number,
|
||
): Promise<Kline[]> {
|
||
const effectiveLimit = limit ?? this.config.defaultLimit;
|
||
switch (this.exchange) {
|
||
case "binance":
|
||
return fetchBinanceKlines(symbol, interval, 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 [];
|
||
}
|
||
}
|