refactor(data): 重构交易所适配器,修复 Kline 实体复合主键

- 废弃旧 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/ 运行脚本
This commit is contained in:
Rekey
2026-06-08 18:18:16 +08:00
parent 85a0031a78
commit 5e385547c7
16 changed files with 829 additions and 1043 deletions
-186
View File
@@ -1,186 +0,0 @@
// ============================================================
// 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;