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,32 @@
|
||||
// ============================================================
|
||||
// common.entity.ts — 实体公共基类
|
||||
// ============================================================
|
||||
// 所有关系实体(TypeORM 管理域)继承此类,统一:
|
||||
// - id: UUID 主键
|
||||
// - created_at: 记录创建时间(自动填充)
|
||||
// - updated_at: 最后更新时间(自动填充)
|
||||
//
|
||||
// TimescaleDB K 线实体(@timescaledb/typeorm 管理域)不继承此类,
|
||||
// 因为它们需要 @TimeColumn() 等特定装饰器。
|
||||
// ============================================================
|
||||
|
||||
import {
|
||||
BaseEntity,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
export abstract class CommonBaseEntity extends BaseEntity {
|
||||
/** UUID 主键(非自增整数,便于分布式场景) */
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id!: string;
|
||||
|
||||
/** 记录创建时间 */
|
||||
@CreateDateColumn({ type: "timestamptz", name: "created_at" })
|
||||
createdAt!: Date;
|
||||
|
||||
/** 最后更新时间(每次 UPDATE 自动刷新) */
|
||||
@UpdateDateColumn({ type: "timestamptz", name: "updated_at" })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// ============================================================
|
||||
// exchange.entity.ts — 交易所配置实体
|
||||
// ============================================================
|
||||
// 映射到 PostgreSQL exchanges 表,存储已接入的交易所元信息。
|
||||
// 由 TypeORM 管理(关系数据),不与 TimescaleDB 耦合。
|
||||
//
|
||||
// 继承 CommonBaseEntity:id (UUID) / created_at / updated_at
|
||||
// ============================================================
|
||||
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
OneToMany,
|
||||
} from "typeorm";
|
||||
import { CommonBaseEntity } from "./common.entity";
|
||||
|
||||
@Entity("exchanges")
|
||||
export class Exchange extends CommonBaseEntity {
|
||||
/** 交易所唯一标识(如 binance / okx / bybit) */
|
||||
@Column("varchar", { length: 50, unique: true })
|
||||
name!: string;
|
||||
|
||||
/** 交易所显示名称(如 Binance / OKX / Bybit) */
|
||||
@Column("varchar", { length: 100 })
|
||||
label!: string;
|
||||
|
||||
/** 是否启用该交易所的数据采集 */
|
||||
@Column("boolean", { default: true })
|
||||
enabled!: boolean;
|
||||
|
||||
/** 交易所特定配置(JSON:费率、最小下单量、API 限频等) */
|
||||
@Column("jsonb", { nullable: true })
|
||||
config?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 该交易所下的所有交易对。
|
||||
* 使用字符串引用避免循环依赖(TradingPair 也引用 Exchange)。
|
||||
*/
|
||||
@OneToMany("TradingPair", "exchange")
|
||||
tradingPairs!: unknown[];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// ============================================================
|
||||
// entities/index.ts — 实体统一导出
|
||||
// ============================================================
|
||||
// TypeORM DataSource 通过 import * as entities from "../entities"
|
||||
// 自动注册所有实体,无需手动逐个添加。
|
||||
// ============================================================
|
||||
|
||||
export { CommonBaseEntity } from "./common.entity";
|
||||
export { Exchange } from "./exchange.entity";
|
||||
export { TradingPair } from "./trading-pair.entity";
|
||||
export { Kline } from "./kline.entity";
|
||||
export type { KlineInterval } from "./kline.entity";
|
||||
@@ -0,0 +1,138 @@
|
||||
// ============================================================
|
||||
// kline.entity.ts — TimescaleDB K 线 Hypertable 实体
|
||||
// ============================================================
|
||||
// 映射到 PostgreSQL klines 表(TimescaleDB hypertable)。
|
||||
// 不继承 CommonBaseEntity — 使用 @timescaledb/typeorm 的
|
||||
// @Hypertable / @TimeColumn 装饰器管理 TimescaleDB 特性。
|
||||
//
|
||||
// 关键 TimescaleDB 特性(由 @Hypertable 装饰器自动配置):
|
||||
// - 自动按 time 列做时间分区(by_range)
|
||||
// - 列式压缩(compress),7 天后自动执行
|
||||
// - 通过 ContinuousAggregate 生成高周期 K 线视图
|
||||
//
|
||||
// 注意:@timescaledb/typeorm v0.0.1 为实验版本,
|
||||
// 不支持空间分区(partitioning_column)。
|
||||
// 若需要空间分区,可通过 db/init-db/ 下的 SQL 脚本手动添加。
|
||||
// ============================================================
|
||||
|
||||
import { Hypertable, TimeColumn } from "@timescaledb/typeorm";
|
||||
import {
|
||||
Entity,
|
||||
PrimaryColumn,
|
||||
Column,
|
||||
Index,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from "typeorm";
|
||||
|
||||
/** K 线周期枚举 */
|
||||
export type KlineInterval =
|
||||
| "1m"
|
||||
| "5m"
|
||||
| "15m"
|
||||
| "30m"
|
||||
| "1h"
|
||||
| "4h"
|
||||
| "1d"
|
||||
| "1w";
|
||||
|
||||
/**
|
||||
* 1 分钟 K 线 Hypertable
|
||||
*
|
||||
* 存储交易所推送的 OHLCV 数据。写入使用 UPSERT
|
||||
* (ON CONFLICT DO UPDATE),已存在的 K 线只更新
|
||||
* high/low/close/volume 增量。
|
||||
*
|
||||
* 高周期 K 线(5m+)通过 TimescaleDB 连续聚合视图
|
||||
* 从 1m 表自动生成,无需单独建表。
|
||||
*/
|
||||
@Hypertable({
|
||||
compression: {
|
||||
compress: true,
|
||||
compress_orderby: "time DESC",
|
||||
compress_segmentby: "exchange, symbol, interval",
|
||||
policy: {
|
||||
schedule_interval: "365 days", // 365 天后自动压缩
|
||||
},
|
||||
},
|
||||
})
|
||||
@Index(["exchange", "symbol", "interval", "time"], { unique: true })
|
||||
@Entity("klines")
|
||||
export class Kline {
|
||||
/** K 线开盘时间(UTC)— @timescaledb/typeorm 自动标记为时间分区列 */
|
||||
@TimeColumn()
|
||||
@PrimaryColumn("timestamptz")
|
||||
time!: Date;
|
||||
|
||||
/** 交易所标识(binance / okx / bybit) */
|
||||
@Column("text")
|
||||
exchange!: string;
|
||||
|
||||
/** 交易对符号(如 BTCUSDT) */
|
||||
@Column("text")
|
||||
symbol!: string;
|
||||
|
||||
/** K 线周期(1m) */
|
||||
@Column("text")
|
||||
interval!: KlineInterval;
|
||||
|
||||
// ============================================================
|
||||
// OHLCV 价格数据(NUMERIC(20,8) 精度,与交易所对齐)
|
||||
// ============================================================
|
||||
|
||||
/** 开盘价 */
|
||||
@Column("numeric", { precision: 20, scale: 8 })
|
||||
open!: number;
|
||||
|
||||
/** 最高价 */
|
||||
@Column("numeric", { precision: 20, scale: 8 })
|
||||
high!: number;
|
||||
|
||||
/** 最低价 */
|
||||
@Column("numeric", { precision: 20, scale: 8 })
|
||||
low!: number;
|
||||
|
||||
/** 收盘价 */
|
||||
@Column("numeric", { precision: 20, scale: 8 })
|
||||
close!: number;
|
||||
|
||||
/** 成交量(base 币种) */
|
||||
@Column("numeric", { precision: 20, scale: 8 })
|
||||
volume!: number;
|
||||
|
||||
// ============================================================
|
||||
// 扩展字段(Binance 等交易所提供)
|
||||
// ============================================================
|
||||
|
||||
/** 成交额(quote 币种) */
|
||||
@Column("numeric", { precision: 20, scale: 8, nullable: true })
|
||||
quote_volume?: number;
|
||||
|
||||
/** 主动买入成交量(base 币种) */
|
||||
@Column("numeric", { precision: 20, scale: 8, nullable: true })
|
||||
taker_buy_base_vol?: number;
|
||||
|
||||
/** 主动买入成交额(quote 币种) */
|
||||
@Column("numeric", { precision: 20, scale: 8, nullable: true })
|
||||
taker_buy_quote_vol?: number;
|
||||
|
||||
/** 成交笔数 */
|
||||
@Column("integer", { nullable: true })
|
||||
trade_count?: number;
|
||||
|
||||
/** K 线是否已关闭(true = 该周期 K 线不再变化) */
|
||||
@Column("boolean", { default: true })
|
||||
is_closed!: boolean;
|
||||
|
||||
// ============================================================
|
||||
// 审计字段
|
||||
// ============================================================
|
||||
|
||||
/** 记录首次写入时间 */
|
||||
@CreateDateColumn({ type: "timestamptz", name: "created_at" })
|
||||
createdAt!: Date;
|
||||
|
||||
/** 记录最后更新时间 */
|
||||
@UpdateDateColumn({ type: "timestamptz", name: "updated_at" })
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// ============================================================
|
||||
// trading-pair.entity.ts — 交易对配置实体
|
||||
// ============================================================
|
||||
// 映射到 PostgreSQL trading_pairs 表,存储各交易所的交易对元信息。
|
||||
// 数据模块启动时从该表读取 active=true 的交易对列表,
|
||||
// 决定 WebSocket 订阅范围和 K 线合成范围。
|
||||
//
|
||||
// 继承 CommonBaseEntity:id (UUID) / created_at / updated_at
|
||||
//
|
||||
// 与 TimescaleDB klines 表的关系:
|
||||
// klines.symbol → trading_pairs.symbol(逻辑外键,不做 DB 级约束)
|
||||
// klines.exchange → exchanges.name(逻辑外键)
|
||||
// ============================================================
|
||||
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from "typeorm";
|
||||
import { Exchange } from "./exchange.entity";
|
||||
import { CommonBaseEntity } from "./common.entity";
|
||||
|
||||
@Entity("trading_pairs")
|
||||
@Index(["exchange", "symbol"], { unique: true }) // 同一交易所下 symbol 唯一
|
||||
@Index(["active"]) // 按激活状态快速筛选
|
||||
export class TradingPair extends CommonBaseEntity {
|
||||
/** 所属交易所 */
|
||||
@ManyToOne(() => Exchange, { nullable: false })
|
||||
@JoinColumn({ name: "exchange_id" })
|
||||
exchange!: Exchange;
|
||||
|
||||
/** 交易对符号(如 BTCUSDT / ETHUSDT) */
|
||||
@Column("varchar", { length: 20 })
|
||||
symbol!: string;
|
||||
|
||||
/** 基础币种(如 BTC) */
|
||||
@Column("varchar", { length: 10 })
|
||||
base_asset!: string;
|
||||
|
||||
/** 计价币种(如 USDT) */
|
||||
@Column("varchar", { length: 10 })
|
||||
quote_asset!: string;
|
||||
|
||||
/** 价格精度(小数位数) */
|
||||
@Column("integer", { default: 10 })
|
||||
price_precision!: number;
|
||||
|
||||
/** 数量精度(小数位数) */
|
||||
@Column("integer", { default: 10 })
|
||||
quantity_precision!: number;
|
||||
|
||||
/** 最小下单量 */
|
||||
@Column("numeric", { precision: 32, scale: 8, nullable: true })
|
||||
min_qty?: number;
|
||||
|
||||
/** 下单步长(数量增量) */
|
||||
@Column("numeric", { precision: 32, scale: 8, nullable: true })
|
||||
step_size?: number;
|
||||
|
||||
/** 最小名义价值(USDT) */
|
||||
@Column("numeric", { precision: 32, scale: 8, nullable: true })
|
||||
min_notional?: number;
|
||||
|
||||
/** 是否激活数据订阅(false 时不采集该交易对行情) */
|
||||
@Column("boolean", { default: true })
|
||||
active!: boolean;
|
||||
|
||||
/** 是否启用 K 线合成(false 时仅采集原始行情,不合成) */
|
||||
@Column("boolean", { default: true })
|
||||
kline_synthesis_enabled!: boolean;
|
||||
|
||||
/** K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d") */
|
||||
@Column("varchar", { length: 100, default: "1m,5m,15m,1h,4h,1d" })
|
||||
kline_intervals!: string;
|
||||
|
||||
/**
|
||||
* 历史 K 线最后补全时间(UTC)。
|
||||
* 记录最近一次 REST 补拉 K 线的结束时间戳,
|
||||
* 下次启动时从此时间点继续补全,避免重复拉取。
|
||||
*
|
||||
* 默认值为 Unix 0(1970-01-01T00:00:00.000Z),
|
||||
* 新交易对从 epoch 起始时间开始全量补拉,
|
||||
* 补全后更新为实际拉取到的最后时间。
|
||||
*/
|
||||
@Column("timestamptz", { default: () => "to_timestamp(0)" })
|
||||
last_backfill_time!: Date;
|
||||
|
||||
/** 备注 */
|
||||
@Column("text", { nullable: true })
|
||||
notes?: string;
|
||||
|
||||
// ============================================================
|
||||
// 工具方法
|
||||
// ============================================================
|
||||
|
||||
/** 解析 kline_intervals 为周期数组 */
|
||||
getIntervals(): string[] {
|
||||
return this.kline_intervals
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user