Files
trade/data/db/validators.ts
T
Rekey e91cad79e6 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 初始化脚本
2026-06-07 20:46:35 +08:00

246 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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>;