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;
|
||||
Reference in New Issue
Block a user