Files
trade/data/exchanges/base.ts
T
Rekey 85a0031a78 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 依赖
2026-06-08 01:24:48 +08:00

187 lines
6.4 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.
// ============================================================
// 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 管理工具
// ============================================================
/**
* 创建一个受管理的 Subjectdisconnect 时自动 complete。
* 子类在 subscribe* 方法中使用此工具创建 Subject。
*/
protected createManagedSubject<T>(): Subject<T> {
const subject = new Subject<T>();
this.activeSubjects.add(subject as Subject<unknown>);
return subject;
}
/** 完成所有受管理的 Subjectdisconnect 时调用) */
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;