feat(data): 实现 Binance WebSocket 适配器与架构重构
- 新增 exchanges/ 模块:MarketDataFeed 统一接口、BaseExchangeAdapter 抽象基类、 BinanceAdapter 完整实现(WebSocket + REST) - WebSocket 层基于 binance 官方 SDK 的 WebsocketClient,自动多路复用与断线重连 - REST 层使用 MainClient(Spot),实现 fetchKlines 自动分页补拉 + fetchMarkets 元数据解析 - 数据标准化:Ticker/Trade/OrderBook/Kline 类型定义与 Binance 原生格式互转 - 引入 RxJS Subject 作为统一事件流管道,按 eventType 运行时路由分发 - 重构 config/:YAML 驱动配置加载 + 零依赖运行时校验(fail-fast) - 重构 db/:TypeORM DataSource 配置 + TimescaleDB K 线 Hypertable 实体 - 新增 utils/logger.ts:Pino 结构化日志(开发环境 pino-pretty 彩色输出) - 新增 env.yaml 作为 TS/Python 共享的统一环境配置源 - 删除旧版手写 SQL schema 与散落配置文件,收敛到 TypeORM 实体管理 - 安装 rxjs@7.8.2 依赖
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
// ============================================================
|
||||
// base.ts — 交易所适配器抽象基类
|
||||
// ============================================================
|
||||
// 所有交易所适配器(Binance / OKX / Bybit ...)继承此类,
|
||||
// 复用指数退避重连、连接状态管理、限流等通用逻辑。
|
||||
//
|
||||
// 子类只需实现:
|
||||
// - connect() — 建立 WebSocket/REST 连接
|
||||
// - disconnect() — 断开连接并清理资源
|
||||
// - subscribeTicker() / subscribeTrade() / subscribeOrderbook()
|
||||
// - fetchKlines() — REST 历史 K 线补拉
|
||||
// - fetchMarkets() — 交易对元数据拉取
|
||||
// ============================================================
|
||||
|
||||
import { Subject, type Observable } from "rxjs";
|
||||
import { logger } from "../utils/logger";
|
||||
import type {
|
||||
MarketDataFeed,
|
||||
Ticker,
|
||||
Trade,
|
||||
OrderBook,
|
||||
Kline,
|
||||
KlineInterval,
|
||||
MarketInfo,
|
||||
ConnectionState,
|
||||
AdapterConfig,
|
||||
} from "./types";
|
||||
import { DEFAULT_ADAPTER_CONFIG } from "./types";
|
||||
|
||||
// ============================================================
|
||||
// 工具:异步 sleep
|
||||
// ============================================================
|
||||
|
||||
/** 返回一个在 ms 毫秒后 resolve 的 Promise */
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BaseExchangeAdapter
|
||||
// ============================================================
|
||||
|
||||
export abstract class BaseExchangeAdapter implements MarketDataFeed {
|
||||
/** 交易所标识(子类必须覆盖) */
|
||||
abstract readonly exchange: string;
|
||||
|
||||
/** 适配器配置(可在子类构造函数中覆盖默认值) */
|
||||
protected readonly config: AdapterConfig;
|
||||
|
||||
/** 当前连接状态 */
|
||||
protected _connectionState: ConnectionState = "disconnected";
|
||||
|
||||
/** 当前重连尝试次数(成功连接后重置) */
|
||||
protected reconnectAttempt = 0;
|
||||
|
||||
/** Subject 清理注册表 —— disconnect 时统一 complete */
|
||||
protected activeSubjects = new Set<Subject<unknown>>();
|
||||
|
||||
// ============================================================
|
||||
// 构造函数
|
||||
// ============================================================
|
||||
|
||||
constructor(config: Partial<AdapterConfig> = {}) {
|
||||
this.config = { ...DEFAULT_ADAPTER_CONFIG, ...config };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 连接状态(只读暴露)
|
||||
// ============================================================
|
||||
|
||||
get connectionState(): ConnectionState {
|
||||
return this._connectionState;
|
||||
}
|
||||
|
||||
/** 更新连接状态并记录日志 */
|
||||
protected setConnectionState(state: ConnectionState): void {
|
||||
const prev = this._connectionState;
|
||||
this._connectionState = state;
|
||||
if (prev !== state) {
|
||||
logger.info(
|
||||
{ exchange: this.exchange, from: prev, to: state },
|
||||
`[${this.exchange}] connection state: ${prev} → ${state}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 指数退避重连(所有子类复用)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 执行指数退避重连。
|
||||
*
|
||||
* 延迟公式:delay = baseDelay × 2^min(attempt, 5)
|
||||
* - attempt=0: 3s
|
||||
* - attempt=1: 6s
|
||||
* - attempt=2: 12s
|
||||
* - attempt=5: 96s(之后不再翻倍)
|
||||
*
|
||||
* 超过 maxReconnectAttempts 后抛出错误。
|
||||
*
|
||||
* @throws 达到最大重试次数后抛出
|
||||
*/
|
||||
protected async reconnect(): Promise<void> {
|
||||
const { reconnectBaseDelayMs: baseDelay, maxReconnectAttempts } = this.config;
|
||||
|
||||
if (this.reconnectAttempt >= maxReconnectAttempts) {
|
||||
this.setConnectionState("error");
|
||||
throw new Error(
|
||||
`[${this.exchange}] 重连失败:已达最大重试次数 (${maxReconnectAttempts})`,
|
||||
);
|
||||
}
|
||||
|
||||
const cappedAttempt = Math.min(this.reconnectAttempt, 5);
|
||||
const delay = baseDelay * Math.pow(2, cappedAttempt);
|
||||
|
||||
logger.warn(
|
||||
{
|
||||
exchange: this.exchange,
|
||||
attempt: this.reconnectAttempt + 1,
|
||||
maxAttempts: maxReconnectAttempts,
|
||||
delayMs: delay,
|
||||
},
|
||||
`[${this.exchange}] WebSocket 重连中...`,
|
||||
);
|
||||
|
||||
await sleep(delay);
|
||||
|
||||
this.reconnectAttempt++;
|
||||
this.setConnectionState("connecting");
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
/** 成功连接后重置重连计数器 */
|
||||
protected resetReconnectAttempts(): void {
|
||||
if (this.reconnectAttempt > 0) {
|
||||
logger.info(
|
||||
{ exchange: this.exchange, attempts: this.reconnectAttempt },
|
||||
`[${this.exchange}] 重连成功,计数器重置`,
|
||||
);
|
||||
}
|
||||
this.reconnectAttempt = 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Subject 管理工具
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 创建一个受管理的 Subject,disconnect 时自动 complete。
|
||||
* 子类在 subscribe* 方法中使用此工具创建 Subject。
|
||||
*/
|
||||
protected createManagedSubject<T>(): Subject<T> {
|
||||
const subject = new Subject<T>();
|
||||
this.activeSubjects.add(subject as Subject<unknown>);
|
||||
return subject;
|
||||
}
|
||||
|
||||
/** 完成所有受管理的 Subject(disconnect 时调用) */
|
||||
protected completeAllSubjects(): void {
|
||||
for (const subject of this.activeSubjects) {
|
||||
subject.complete();
|
||||
}
|
||||
this.activeSubjects.clear();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 抽象方法 —— 子类必须实现
|
||||
// ============================================================
|
||||
|
||||
abstract connect(): Promise<void>;
|
||||
abstract disconnect(): Promise<void>;
|
||||
abstract subscribeTicker(symbols: string[]): Observable<Ticker>;
|
||||
abstract subscribeTrade(symbols: string[]): Observable<Trade>;
|
||||
abstract subscribeOrderbook(symbol: string, depth?: number): Observable<OrderBook>;
|
||||
abstract fetchKlines(
|
||||
symbol: string,
|
||||
interval: KlineInterval,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
limit?: number,
|
||||
): Promise<Kline[]>;
|
||||
abstract fetchMarkets(): Promise<MarketInfo[]>;
|
||||
}
|
||||
|
||||
export default BaseExchangeAdapter;
|
||||
+725
-338
File diff suppressed because it is too large
Load Diff
+292
-38
@@ -1,45 +1,299 @@
|
||||
// ============================================================
|
||||
// exchanges/types.ts — WebSocket 事件数据类型
|
||||
// types.ts — 统一行情数据类型定义与 MarketDataFeed 接口
|
||||
// ============================================================
|
||||
// 所有交易所适配器共享的数据结构和接口契约。
|
||||
// 适配器负责将交易所原生数据格式转换为以下标准化类型。
|
||||
//
|
||||
// 设计原则:
|
||||
// - 字段语义与 Binance/OKX/Bybit 通用概念对齐
|
||||
// - 时间戳统一使用 Unix 毫秒(number),便于排序和计算
|
||||
// - 价格/数量使用 number 类型(JavaScript 64-bit float),
|
||||
// 对精度敏感场景(如 orderbook 快照)保留原始字符串
|
||||
// ============================================================
|
||||
|
||||
/** 由 WebSocket 推送的单根 K 线数据 */
|
||||
export interface KlineWsData {
|
||||
/** 交易所标识 */
|
||||
exchange: string;
|
||||
/** 交易对,如 BTCUSDT */
|
||||
symbol: string;
|
||||
/** K 线周期,如 1m / 1h / 1d */
|
||||
interval: string;
|
||||
/** K 线开盘时间(Unix 毫秒时间戳) */
|
||||
time: number;
|
||||
/** 开盘价 */
|
||||
open: number;
|
||||
/** 最高价 */
|
||||
high: number;
|
||||
/** 最低价 */
|
||||
low: number;
|
||||
/** 收盘价 */
|
||||
close: number;
|
||||
/** 成交量(基准币种) */
|
||||
volume: number;
|
||||
import type { Observable } from "rxjs";
|
||||
|
||||
// ============================================================
|
||||
// K 线周期
|
||||
// ============================================================
|
||||
|
||||
/** K 线周期枚举(与 kline.entity.ts 中 KlineInterval 保持一致) */
|
||||
export type KlineInterval =
|
||||
| "1m"
|
||||
| "5m"
|
||||
| "15m"
|
||||
| "30m"
|
||||
| "1h"
|
||||
| "4h"
|
||||
| "1d"
|
||||
| "1w";
|
||||
|
||||
/** 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,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 标准化行情数据结构
|
||||
// ============================================================
|
||||
|
||||
/** 24 小时滚动 Ticker 统计 */
|
||||
export interface Ticker {
|
||||
/** 交易所标识 */
|
||||
exchange: string;
|
||||
/** 交易对符号(大写,如 BTCUSDT) */
|
||||
symbol: string;
|
||||
/** 最新成交价 */
|
||||
lastPrice: number;
|
||||
/** 24h 开盘价 */
|
||||
openPrice: number;
|
||||
/** 24h 最高价 */
|
||||
highPrice: number;
|
||||
/** 24h 最低价 */
|
||||
lowPrice: number;
|
||||
/** 24h 成交量(base 币种) */
|
||||
volume: number;
|
||||
/** 24h 成交额(quote 币种) */
|
||||
quoteVolume: number;
|
||||
/** 24h 价格变化 */
|
||||
priceChange: number;
|
||||
/** 24h 价格变化百分比(0.05 = 5%) */
|
||||
priceChangePercent: number;
|
||||
/** 买一价 */
|
||||
bidPrice: number;
|
||||
/** 买一量 */
|
||||
bidQty: number;
|
||||
/** 卖一价 */
|
||||
askPrice: number;
|
||||
/** 卖一量 */
|
||||
askQty: number;
|
||||
/** 事件发生时间(Unix ms) */
|
||||
eventTime: number;
|
||||
/** 交易所收盘时间(Unix ms,用于判断 K 线是否闭合) */
|
||||
closeTime: number;
|
||||
}
|
||||
|
||||
/** ExchangeWs 构造参数 */
|
||||
export interface ExchangeWsConfig {
|
||||
/** 交易所 ID,ccxt 支持的所有交易所标识 */
|
||||
exchangeId: string;
|
||||
/** 要订阅的交易对列表,如 ['BTCUSDT', 'ETHUSDT'] */
|
||||
symbols: string[];
|
||||
/** K 线周期,默认 '1m' */
|
||||
interval?: string;
|
||||
/** 传递给 ccxt.pro 交易所实例的额外选项(如 agent、apiKey 等) */
|
||||
ccxtOptions?: Record<string, unknown>;
|
||||
/** 逐笔成交 */
|
||||
export interface Trade {
|
||||
/** 交易所标识 */
|
||||
exchange: string;
|
||||
/** 交易对符号 */
|
||||
symbol: string;
|
||||
/** 成交价 */
|
||||
price: number;
|
||||
/** 成交数量(base 币种) */
|
||||
amount: number;
|
||||
/** 成交额(quote 币种 = price × amount) */
|
||||
quoteAmount: number;
|
||||
/** 成交时间(Unix ms) */
|
||||
timestamp: number;
|
||||
/** 买方是否为挂单方(true = 主动卖出 / taker sell) */
|
||||
isBuyerMaker: boolean;
|
||||
/** 交易所成交 ID(可能为字符串,如 Binance tradeId 为 bigint) */
|
||||
tradeId: string;
|
||||
}
|
||||
|
||||
/** ExchangeWs 连接状态 */
|
||||
export type WsConnectionState =
|
||||
| "idle" // 尚未启动
|
||||
| "connecting" // 正在连接 WebSocket
|
||||
| "connected" // 已连接,正在接收数据
|
||||
| "disconnected" // 已断开
|
||||
| "error"; // 错误状态
|
||||
/** 订单簿深度快照 */
|
||||
export interface OrderBook {
|
||||
/** 交易所标识 */
|
||||
exchange: string;
|
||||
/** 交易对符号 */
|
||||
symbol: string;
|
||||
/** 买单列表 [[price, qty], ...],按价格降序(买一在前) */
|
||||
bids: [number, number][];
|
||||
/** 卖单列表 [[price, qty], ...],按价格升序(卖一在前) */
|
||||
asks: [number, number][];
|
||||
/** 上次更新 ID */
|
||||
lastUpdateId: number;
|
||||
/** 事件发生时间(Unix ms) */
|
||||
eventTime: number;
|
||||
}
|
||||
|
||||
/** 标准化 K 线(OHLCV) */
|
||||
export interface Kline {
|
||||
/** 交易所标识 */
|
||||
exchange: string;
|
||||
/** 交易对符号 */
|
||||
symbol: string;
|
||||
/** K 线周期 */
|
||||
interval: KlineInterval;
|
||||
/** 开盘时间(Unix ms) */
|
||||
openTime: number;
|
||||
/** 收盘时间(Unix ms) */
|
||||
closeTime: number;
|
||||
/** 开盘价 */
|
||||
open: number;
|
||||
/** 最高价 */
|
||||
high: number;
|
||||
/** 最低价 */
|
||||
low: number;
|
||||
/** 收盘价 */
|
||||
close: number;
|
||||
/** 成交量(base 币种) */
|
||||
volume: number;
|
||||
/** 成交额(quote 币种) */
|
||||
quoteVolume: number;
|
||||
/** 主动买入成交量(base 币种) */
|
||||
takerBuyBaseVol: number;
|
||||
/** 主动买入成交额(quote 币种) */
|
||||
takerBuyQuoteVol: number;
|
||||
/** 成交笔数 */
|
||||
tradeCount: number;
|
||||
/** 该 K 线是否已关闭(不再更新) */
|
||||
isClosed: boolean;
|
||||
}
|
||||
|
||||
/** K 线增量更新(仅推送最新一根 OHLCV 变化) */
|
||||
export interface KlineDelta {
|
||||
exchange: string;
|
||||
symbol: string;
|
||||
interval: KlineInterval;
|
||||
openTime: number;
|
||||
closeTime: number;
|
||||
open: number;
|
||||
high: number;
|
||||
low: number;
|
||||
close: number;
|
||||
volume: number;
|
||||
isClosed: boolean;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WebSocket 连接状态
|
||||
// ============================================================
|
||||
|
||||
/** 连接状态枚举 */
|
||||
export type ConnectionState =
|
||||
| "disconnected"
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "error";
|
||||
|
||||
// ============================================================
|
||||
// 适配器配置
|
||||
// ============================================================
|
||||
|
||||
/** 交易所适配器通用配置 */
|
||||
export interface AdapterConfig {
|
||||
/** 指数退避重连基数(毫秒),默认 3000 */
|
||||
reconnectBaseDelayMs: number;
|
||||
/** 最大重连次数,默认 10 */
|
||||
maxReconnectAttempts: number;
|
||||
/** REST API 请求冷却时间(毫秒),默认 200 */
|
||||
restRateLimitMs: number;
|
||||
}
|
||||
|
||||
/** 默认适配器配置 */
|
||||
export const DEFAULT_ADAPTER_CONFIG: AdapterConfig = {
|
||||
reconnectBaseDelayMs: 3000,
|
||||
maxReconnectAttempts: 10,
|
||||
restRateLimitMs: 200,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// MarketDataFeed 接口 —— 所有交易所适配器必须实现
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 统一行情数据源接口。
|
||||
*
|
||||
* 每个交易所适配器实现此接口,向上层管道暴露标准化数据流。
|
||||
* 使用 RxJS Observable 作为统一推送机制,pipeline 层可自由
|
||||
* 组合、过滤、分流各交易所数据。
|
||||
*/
|
||||
export interface MarketDataFeed {
|
||||
/** 交易所标识(如 "binance") */
|
||||
readonly exchange: string;
|
||||
|
||||
/** 当前连接状态 */
|
||||
readonly connectionState: ConnectionState;
|
||||
|
||||
/** 建立 WebSocket 连接 */
|
||||
connect(): Promise<void>;
|
||||
|
||||
/** 断开连接 */
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 订阅 24h 滚动 Ticker 流。
|
||||
* 每笔成交触发推送(Binance: <symbol>@ticker)。
|
||||
*/
|
||||
subscribeTicker(symbols: string[]): Observable<Ticker>;
|
||||
|
||||
/**
|
||||
* 订阅逐笔成交流。
|
||||
* 实时推送每笔撮合成交(Binance: <symbol>@trade)。
|
||||
*/
|
||||
subscribeTrade(symbols: string[]): Observable<Trade>;
|
||||
|
||||
/**
|
||||
* 订阅订单簿深度。
|
||||
* depth 参数指定档位(如 5/10/20),默认 20。
|
||||
*/
|
||||
subscribeOrderbook(symbol: string, depth?: number): Observable<OrderBook>;
|
||||
|
||||
/**
|
||||
* REST 拉取历史 K 线(用于补齐缺失数据或回测)。
|
||||
*
|
||||
* @param symbol - 交易对符号
|
||||
* @param interval - K 线周期
|
||||
* @param startTime - 起始时间(Unix ms)
|
||||
* @param endTime - 结束时间(Unix ms)
|
||||
* @param limit - 最大返回条数(默认 500)
|
||||
* @returns 标准化 K 线数组,按时间升序
|
||||
*/
|
||||
fetchKlines(
|
||||
symbol: string,
|
||||
interval: KlineInterval,
|
||||
startTime: number,
|
||||
endTime: number,
|
||||
limit?: number,
|
||||
): Promise<Kline[]>;
|
||||
|
||||
/**
|
||||
* 获取交易所交易对信息(用于自动注册到 trading_pairs 表)。
|
||||
* 返回标准化后的交易对元数据。
|
||||
*/
|
||||
fetchMarkets(): Promise<MarketInfo[]>;
|
||||
}
|
||||
|
||||
/** 交易对元信息(从交易所 REST API 获取) */
|
||||
export interface MarketInfo {
|
||||
symbol: string;
|
||||
baseAsset: string;
|
||||
quoteAsset: string;
|
||||
pricePrecision: number;
|
||||
quantityPrecision: number;
|
||||
minQty?: number;
|
||||
stepSize?: number;
|
||||
minNotional?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 工具类型
|
||||
// ============================================================
|
||||
|
||||
/** Binance WebSocket 原始 K 线数据(kline 事件中的 k 字段) */
|
||||
export interface BinanceRawKline {
|
||||
t: number; // K 线开始时间
|
||||
T: number; // K 线结束时间
|
||||
s: string; // 交易对
|
||||
i: string; // 周期
|
||||
o: string; // 开盘价
|
||||
h: string; // 最高价
|
||||
l: string; // 最低价
|
||||
c: string; // 收盘价
|
||||
v: string; // 成交量
|
||||
n: number; // 成交笔数
|
||||
x: boolean; // 是否已关闭
|
||||
q: string; // 成交额
|
||||
V: string; // 主动买入成交量
|
||||
Q: string; // 主动买入成交额
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user