feat(data): 实现配置表 CRUD 与 Schema 初始化拆分

- 新增 data/db/ 数据库访问层:pool 管理、类型定义、Zod 校验、参数化 SQL 查询
- 新增 data/db/config-crud.ts:MonitoredSymbolsRepo / ExchangeConfigRepo / AppConfigRepo 三个 CRUD 服务类
- 新增 data/config.ts:中心化配置模块,零依赖 .env 解析 + Zod 校验
- 新增 data/schema/:klines.sql + config.sql 参考 DDL
- 新增 data/exchanges/:交易所类型定义与 Binance WebSocket 封装
- 新增 data/run/:交易所连接启动入口
- 重构 data/init-db/:001_init.sql 仅保留 TimescaleDB + klines,配置表拆分至 002_config.sql
- 更新 docker-compose.yml:挂载 init-db 初始化脚本
This commit is contained in:
Rekey
2026-06-07 20:46:35 +08:00
parent 10e13ae8da
commit e91cad79e6
18 changed files with 8560 additions and 5 deletions
+245
View File
@@ -0,0 +1,245 @@
// ============================================================
// schema/validators.ts — Zod 运行时校验 Schema
// ============================================================
// 用途:
// 1. WebSocket 行情数据到达后校验字段完整性再入库
// 2. 配置文件 / 环境变量加载后类型收窄
// 3. API 输入参数校验
//
// 依赖:zod ^4.x(已包含在 data/package.json
// ============================================================
import { z } from "zod";
// ============================================================
// 基础标量 Schema
// ============================================================
/** 交易所枚举 */
export const ExchangeSchema = z.enum(["binance", "okx", "bybit"]);
export type Exchange = z.infer<typeof ExchangeSchema>;
/** K 线周期枚举 */
export const KlineIntervalSchema = z.enum([
"1m",
"5m",
"15m",
"1h",
"4h",
"1d",
]);
export type KlineInterval = z.infer<typeof KlineIntervalSchema>;
/** 日志级别 */
export const LogLevelSchema = z.enum([
"trace",
"debug",
"info",
"warn",
"error",
"fatal",
]);
/** 交易对格式:大写字母 + 大写字母(如 BTCUSDT),3-12 字符 */
export const SymbolSchema = z
.string()
.regex(/^[A-Z0-9]{4,14}$/, "交易对格式无效,示例:BTCUSDT");
/**
* NUMERIC(20,8) 数值字符串
* pg 驱动默认以字符串返回 NUMERIC 以保留精度
*/
export const NumericStringSchema = z
.string()
.regex(/^-?\d+(\.\d+)?$/, "期望 NUMERIC 字符串");
// ============================================================
// 1. Kline 数据校验 — klines 表
// ============================================================
/** WebSocket 原始 OHLCV 消息校验(单条 K 线,入库前) */
export const KlineRawSchema = z.object({
/** K 线开盘时间(UTC),Unix 毫秒时间戳 */
time: z.number().int().positive(),
/** 交易所 */
exchange: ExchangeSchema,
/** 交易对 */
symbol: SymbolSchema,
/** 周期 */
interval: KlineIntervalSchema,
/** 开盘价 */
open: NumericStringSchema,
/** 最高价 */
high: NumericStringSchema,
/** 最低价 */
low: NumericStringSchema,
/** 收盘价 */
close: NumericStringSchema,
/** 成交量 */
volume: NumericStringSchema,
/** 成交额(可选) */
quote_volume: NumericStringSchema.optional().default("0"),
/** 主动买入量(可选) */
taker_buy_base_vol: NumericStringSchema.optional().default("0"),
/** 主动买入额(可选) */
taker_buy_quote_vol: NumericStringSchema.optional().default("0"),
/** 成交笔数(可选) */
trade_count: z.number().int().nonnegative().optional().default(0),
/** K 线是否闭合 */
is_closed: z.boolean().optional().default(true),
});
export type KlineRaw = z.infer<typeof KlineRawSchema>;
/** 批量 K 线消息校验(WebSocket 可能一次推送多根) */
export const KlineBatchSchema = z.array(KlineRawSchema).min(1);
export type KlineBatch = z.infer<typeof KlineBatchSchema>;
// ============================================================
// 2. 监控交易对配置校验 — monitored_symbols
// ============================================================
/** 插入监控标的 */
export const MonitoredSymbolInsertSchema = z.object({
exchange: ExchangeSchema,
symbol: SymbolSchema,
interval: KlineIntervalSchema,
enabled: z.boolean().optional().default(true),
/** 优先级 0-32767SMALLINT 范围) */
priority: z
.number()
.int()
.min(0)
.max(32767)
.optional()
.default(0),
label: z.string().max(200).nullable().optional().default(null),
notes: z.string().max(1000).nullable().optional().default(null),
});
export type MonitoredSymbolInsert = z.infer<
typeof MonitoredSymbolInsertSchema
>;
/** 更新监控标的 */
export const MonitoredSymbolUpdateSchema = z.object({
enabled: z.boolean().optional(),
priority: z.number().int().min(0).max(32767).optional(),
label: z.string().max(200).nullable().optional(),
notes: z.string().max(1000).nullable().optional(),
});
export type MonitoredSymbolUpdate = z.infer<
typeof MonitoredSymbolUpdateSchema
>;
// ============================================================
// 3. 交易所连接配置校验 — exchange_config
// ============================================================
/** 交易所连接配置输入 */
export const ExchangeConfigInsertSchema = z.object({
exchange: ExchangeSchema,
rest_url: z.string().url().nullable().optional().default(null),
ws_url: z.string().url().nullable().optional().default(null),
ws_ping_interval_ms: z
.number()
.int()
.min(5000)
.max(300000)
.optional()
.default(30000),
rate_limit_per_sec: z.number().positive().max(100).optional().default(20),
max_reconnect_attempts: z
.number()
.int()
.min(0)
.max(100)
.optional()
.default(10),
reconnect_delay_ms: z
.number()
.int()
.min(100)
.max(60000)
.optional()
.default(3000),
enabled: z.boolean().optional().default(true),
notes: z.string().max(500).nullable().optional().default(null),
});
export type ExchangeConfigInsert = z.infer<
typeof ExchangeConfigInsertSchema
>;
// ============================================================
// 4. 流标识校验 — StreamKey
// ============================================================
/** (exchange, symbol, interval) 三元组 */
export const StreamKeySchema = z.object({
exchange: ExchangeSchema,
symbol: SymbolSchema,
interval: KlineIntervalSchema,
});
export type StreamKey = z.infer<typeof StreamKeySchema>;
// ============================================================
// 5. 环境变量 / 配置校验
// ============================================================
/** .env 环境变量 schema */
export const EnvConfigSchema = z.object({
/** 逗号分隔的交易对列表 */
SYMBOLS: z
.string()
.optional()
.default("BTCUSDT,ETHUSDT"),
DB_HOST: z.string().optional().default("localhost"),
DB_PORT: z.coerce.number().int().positive().optional().default(5432),
DB_NAME: z.string().optional().default("trade"),
DB_USER: z.string().optional().default("trader"),
DB_PASSWORD: z.string().optional().default("changeme"),
REDIS_URL: z.string().url().optional().default("redis://localhost:6379"),
REDIS_PUBLISH_ENABLED: z
.enum(["true", "false"])
.optional()
.default("true"),
BATCH_SIZE: z.coerce.number().int().positive().optional().default(500),
FLUSH_INTERVAL_MS: z.coerce
.number()
.int()
.positive()
.optional()
.default(1000),
/** WebSocket 断线重连延迟基数(毫秒) */
WS_RECONNECT_DELAY_MS: z.coerce
.number()
.int()
.positive()
.optional()
.default(3000),
/** WebSocket 心跳间隔(毫秒) */
WS_PING_INTERVAL_MS: z.coerce
.number()
.int()
.positive()
.optional()
.default(30000),
/** WebSocket 最大重连次数 */
WS_MAX_RECONNECT_ATTEMPTS: z.coerce
.number()
.int()
.nonnegative()
.optional()
.default(10),
LOG_LEVEL: LogLevelSchema.optional().default("info"),
NODE_ENV: z
.enum(["development", "production", "test"])
.optional()
.default("development"),
});
export type EnvConfig = z.infer<typeof EnvConfigSchema>;