85a0031a78
- 新增 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 依赖
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
// ============================================================
|
||
// 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;
|