Files
trade/data/db/init-db/02-init-tables.sql
T
Rekey 309b11ae30 fix(db): TypeORM 压缩配置对齐 SQL DDL,新增 TimescaleDB 初始化脚本
- kline.entity.ts: compress_segmentby 移除 interval(基表固定 1m 无需分段),schedule_interval 365d→30d 与 init-db SQL 一致
- data-source.ts: 生产环境关闭 synchronize,以 init-db SQL 脚本为建表唯一来源
- 新增 data/db/init-db/ 初始化 SQL 链:
  01-timescaledb.sql — 启用 TimescaleDB 扩展
  02-init-tables.sql — 核心业务表 + klines hypertable(7d chunk / 30d 压缩)
  03-continuous-aggregates.sql — 分层连续聚合视图(5m→15m→30m→1h→4h→1d→1w)
2026-06-10 20:03:00 +08:00

345 lines
14 KiB
PL/PgSQL
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.
-- ============================================================
-- 02-init-tables.sql — 核心业务表建表语句
-- ============================================================
-- 根据 data/db/entities/ 中的实体定义生成对应 PostgreSQL DDL。
--
-- 表结构对应关系:
-- exchanges ← exchange.entity.ts (Exchange extends CommonBaseEntity)
-- trading_pairs ← trading-pair.entity.ts (TradingPair extends CommonBaseEntity)
-- klines ← kline.entity.ts (Kline — TimescaleDB Hypertable)
--
-- 执行前提:
-- 1. 01-timescaledb.sql 已执行(TimescaleDB 扩展已启用)
-- 2. PostgreSQL 版本 >= 13gen_random_uuid() 内建支持)
--
-- 幂等性:全程使用 IF NOT EXISTS / IF EXISTS,可重复执行。
-- ============================================================
-- ============================================================
-- 第一节:交易所配置表(exchanges)
-- ============================================================
-- 存储已接入的交易所元信息(Binance / OKX / Bybit 等)。
-- 由 TypeORM Exchange 实体管理(关系数据域)。
-- ============================================================
CREATE TABLE IF NOT EXISTS exchanges (
-- UUID 主键(非自增整数,便于分布式场景)
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- 交易所唯一标识(如 binance / okx / bybit
name VARCHAR(50) NOT NULL UNIQUE,
-- 交易所显示名称(如 Binance / OKX / Bybit
label VARCHAR(100) NOT NULL,
-- 是否启用该交易所的数据采集
enabled BOOLEAN NOT NULL DEFAULT TRUE,
-- 交易所特定配置(JSON:费率、最小下单量、API 限频等)
config JSONB,
-- 记录创建时间(自动填充)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- 最后更新时间(触发器自动刷新,见文末)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- 交易所名称查询索引
CREATE INDEX IF NOT EXISTS idx_exchanges_name ON exchanges (name);
-- 启用状态筛选索引
CREATE INDEX IF NOT EXISTS idx_exchanges_enabled ON exchanges (enabled);
-- ============================================================
-- 第二节:交易对配置表(trading_pairs
-- ============================================================
-- 存储各交易所的交易对元信息。数据模块启动时从该表读取
-- active=true 的交易对列表,决定 WebSocket 订阅范围和 K 线合成范围。
-- ============================================================
CREATE TABLE IF NOT EXISTS trading_pairs (
-- UUID 主键
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- 所属交易所(逻辑外键 → exchanges.id
exchange_id UUID NOT NULL
REFERENCES exchanges(id) ON DELETE CASCADE,
-- 交易对符号(如 BTCUSDT / ETHUSDT
symbol VARCHAR(20) NOT NULL,
-- 基础币种(如 BTC
base_asset VARCHAR(10) NOT NULL,
-- 计价币种(如 USDT
quote_asset VARCHAR(10) NOT NULL,
-- 价格精度(小数位数)
price_precision INTEGER NOT NULL DEFAULT 10,
-- 数量精度(小数位数)
quantity_precision INTEGER NOT NULL DEFAULT 10,
-- 最小下单量
min_qty NUMERIC(32, 8),
-- 下单步长(数量增量)
step_size NUMERIC(32, 8),
-- 最小名义价值(USDT
min_notional NUMERIC(32, 8),
-- 是否激活数据订阅(false 时不采集该交易对行情)
active BOOLEAN NOT NULL DEFAULT TRUE,
-- 是否启用 K 线合成(false 时仅采集原始行情,不合成)
kline_synthesis_enabled BOOLEAN NOT NULL DEFAULT TRUE,
-- 默认 K 线周期
kline_interval VARCHAR(100) NOT NULL DEFAULT '1m',
-- K 线合成周期列表(逗号分隔,如 "1m,5m,15m,1h,4h,1d"
kline_intervals VARCHAR(100) NOT NULL DEFAULT '1m,5m,15m,1h,4h,1d',
-- 历史 K 线最后补全时间(UTC)。默认 Unix epoch 起始,
-- 新交易对从 epoch 起始时间开始全量补拉。
last_backfill_time TIMESTAMPTZ NOT NULL DEFAULT to_timestamp(0),
-- 备注
notes TEXT,
-- 审计时间戳
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- 同一交易所下 symbol 唯一
CONSTRAINT uq_trading_pairs_exchange_symbol UNIQUE (exchange_id, symbol)
);
-- 按激活状态快速筛选
CREATE INDEX IF NOT EXISTS idx_trading_pairs_active ON trading_pairs (active);
-- 按交易所+交易对查询(最常用模式)
CREATE INDEX IF NOT EXISTS idx_trading_pairs_exchange_symbol ON trading_pairs (exchange_id, symbol);
-- ============================================================
-- 第三节:1 分钟 K 线 Hypertableklines
-- ============================================================
-- TimescaleDB hypertable,存储交易所推送的 OHLCV 数据。
-- 写入使用 UPSERTON CONFLICT DO UPDATE),已存在的 K 线
-- 只更新 high/low/close/volume 增量。
--
-- TimescaleDB 配置:
-- - chunk_time_interval: 7 days(周分区;1 day→7 days 减少 7× chunk 数)
-- - 列式压缩:7 天后自动执行(压缩率 ~92%)
-- - 压缩分段键:exchange, symbol(同交易对聚合压缩;interval 固定 1m 无需分段)
-- - 压缩排序键:time DESC(查询通常按时间降序)
--
-- chunk 大小选择指南(16GB / i3-7300U / 1TB SSD):
-- interval chunk 数估算(1000万行) 单 chunk 行数 适用场景
-- ─────────── ────────────────────── ───────────── ──────────────────
-- 1 day 3200+(过碎 ❌) ~3,000 元数据开销 >> 数据
-- 7 days ~450(推荐 ✅) ~22,000 查询剪枝 & 管理平衡
-- 1 month ~100 ~100,000 历史归档为主、写入密集
--
-- 已有数据库在线修复(仅影响新 chunk,旧 chunk 需 migrate_chunk):
-- SELECT set_chunk_time_interval('klines', INTERVAL '7 days');
-- ============================================================
CREATE TABLE IF NOT EXISTS klines (
-- 交易所标识(binance / okx / bybit
exchange TEXT NOT NULL,
-- 交易对符号(如 BTCUSDT
symbol TEXT NOT NULL,
-- K 线周期(固定 "1m",基表仅存 1 分钟)
interval TEXT NOT NULL,
-- K 线开盘时间(UTC)— 时间分区键
time TIMESTAMPTZ NOT NULL,
-- ============================================================
-- OHLCV 价格数据
--
-- 类型选择:NUMERIC(20,8) vs DOUBLE PRECISION
-- NUMERIC : 精确十进制,无浮点舍入;~10-13 字节/值,CPU 计算慢
-- DOUBLE : IEEE 754 浮点;固定 8 字节/值,CPU 原生指令,快 3-5×
--
-- 对于 K 线价格数据,DOUBLE PRECISION 的 15 位有效数字完全够用
-- BTC @ $100K 量级精度到 $0.01 仅需 ~7 位有效数字)。
-- 9 个价格列 × 1000 万行:NUMERIC → DOUBLE 可节省 ~200-400 MB 存储
-- 并显著加速聚合/窗口函数。新部署强烈建议改为 DOUBLE PRECISION。
-- ============================================================
-- 开盘价
open NUMERIC(20, 8) NOT NULL,
-- 最高价
high NUMERIC(20, 8) NOT NULL,
-- 最低价
low NUMERIC(20, 8) NOT NULL,
-- 收盘价
close NUMERIC(20, 8) NOT NULL,
-- 成交量(base 币种)
volume NUMERIC(20, 8) NOT NULL,
-- ============================================================
-- 扩展字段(连续聚合时使用 SUM 聚合)
-- ============================================================
-- 成交额(quote 币种)
quote_volume NUMERIC(20, 8),
-- 主动买入成交量(base 币种)
taker_buy_base_vol NUMERIC(20, 8),
-- 主动买入成交额(quote 币种)
taker_buy_quote_vol NUMERIC(20, 8),
-- 成交笔数
trade_count INTEGER,
-- K 线是否已关闭(true = 该周期 K 线不再变化)
is_closed BOOLEAN NOT NULL DEFAULT TRUE,
-- 审计时间戳
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- 复合主键:同一交易所、同一交易对、同一周期、同一时间的 K 线唯一
PRIMARY KEY (exchange, symbol, interval, time)
);
-- ============================================================
-- 将 klines 转换为 TimescaleDB Hypertable
-- ============================================================
-- 创建 hypertable,按 time 列做周分区(chunk_time_interval = 7 days
-- 1 day → 7 dayschunk 数从 ~3200 降至 ~450,消除元数据瓶颈
SELECT create_hypertable('klines', 'time',
chunk_time_interval => INTERVAL '7 days',
if_not_exists => TRUE
);
-- ============================================================
-- 配置列式压缩
-- ============================================================
-- 启用列式压缩(先启用压缩,再设置分段/排序键)
-- 注意:interval 在基表固定为 '1m',从 segmentby 中移除以减少压缩分段数
ALTER TABLE klines SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'exchange,symbol',
timescaledb.compress_orderby = 'time DESC'
);
-- 添加压缩策略:30 天前的数据自动压缩
-- chunk_time_interval=7d + compress_after=30d → 数据写入 ~37 天后被压缩
-- 选择 30 天的理由:
-- 量化交易中最常见的回测窗口是最近 30 天,保持未压缩可避免解压 CPU 开销。
-- 30 天后的数据访问频率急剧下降,压缩带来的 IO 减少远超解压开销。
-- 对于 4 币种:30 天未压缩 ≈ 36 MB(微不足道)
-- 对于 100 币种:30 天未压缩 ≈ 907 MB(仍在 2GB shared_buffers 可接受范围)
SELECT add_compression_policy('klines',
compress_after => INTERVAL '30 days',
if_not_exists => TRUE
);
-- ============================================================
-- 第四节:updated_at 自动刷新触发器
-- ============================================================
-- TypeORM 的 @UpdateDateColumn 装饰器在应用层自动更新 updated_at。
-- 但通过 SQL 直接操作表时(如补数据脚本),需通过触发器确保
-- updated_at 在每次 UPDATE 时自动刷新到最新时间。
--
-- 注意:此触发器仅影响直接 SQL 操作,TypeORM 的 save()/update()
-- 仍由其装饰器控制 updated_at 行为,两层互不冲突。
-- ============================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 仅在触发器不存在时创建(幂等)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trg_exchanges_updated_at'
) THEN
CREATE TRIGGER trg_exchanges_updated_at
BEFORE UPDATE ON exchanges
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trg_trading_pairs_updated_at'
) THEN
CREATE TRIGGER trg_trading_pairs_updated_at
BEFORE UPDATE ON trading_pairs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trg_klines_updated_at'
) THEN
CREATE TRIGGER trg_klines_updated_at
BEFORE UPDATE ON klines
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
END IF;
END $$;
-- ============================================================
-- 第五节:种子数据(可选)
-- ============================================================
-- 初始化默认交易所和常用交易对,方便开发环境快速启动。
-- 生产环境请根据实际需求修改或删除此节。
-- ============================================================
-- 默认交易所(幂等:ON CONFLICT DO NOTHING
INSERT INTO exchanges (name, label, enabled, config) VALUES
('binance', 'Binance', TRUE,
'{"rateLimit": 1200, "minOrderSize": 0.001, "feeTaker": 0.001, "feeMaker": 0.001}'::jsonb),
('okx', 'OKX', TRUE,
'{"rateLimit": 400, "minOrderSize": 0.001, "feeTaker": 0.001, "feeMaker": 0.0008}'::jsonb),
('bybit', 'Bybit', FALSE,
'{"rateLimit": 600, "minOrderSize": 0.001, "feeTaker": 0.001, "feeMaker": 0.001}'::jsonb)
ON CONFLICT (name) DO NOTHING;
-- 默认交易对(仅 Binance 主流 USDT 永续合约,幂等)
INSERT INTO trading_pairs (exchange_id, symbol, base_asset, quote_asset,
price_precision, quantity_precision, kline_interval, kline_intervals, active)
SELECT
e.id,
sym.symbol,
sym.base,
sym.quote,
2, -- price_precisionUSDT 计价通常 2 位小数)
5, -- quantity_precision(数量通常 5 位小数)
'1m',
'1m,5m,15m,30m,1h,4h,1d,1w',
TRUE
FROM exchanges e
CROSS JOIN (
VALUES
('BTCUSDT', 'BTC', 'USDT'),
('ETHUSDT', 'ETH', 'USDT'),
('BNBUSDT', 'BNB', 'USDT'),
('SOLUSDT', 'SOL', 'USDT')
) AS sym(symbol, base, quote)
WHERE e.name = 'binance'
ON CONFLICT (exchange_id, symbol) DO NOTHING;