b4c7636731
- 配置层:env.yaml 新增 binance_futures API Key 段,validators + config 同步 - 清理 TradingPair 实体:删除 kline_interval、kline_intervals、kline_synthesis_enabled - 删除 fetchKlines 系列函数的 interval 参数,硬编码为 1m - 更新 SQL seed 数据、example、base_rest 接口、types 接口 - 新增 AGENTS/08-boundaries.md 执行纪律 - 新增 PLAN-add-futures-data.md 方案文档
334 lines
13 KiB
PL/PgSQL
334 lines
13 KiB
PL/PgSQL
-- ============================================================
|
||
-- 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 版本 >= 13(gen_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 线最后补全时间(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 线 Hypertable(klines)
|
||
-- ============================================================
|
||
-- TimescaleDB hypertable,存储交易所推送的 OHLCV 数据。
|
||
-- 写入使用 UPSERT(ON 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 days:chunk 数从 ~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, active)
|
||
SELECT
|
||
e.id,
|
||
sym.symbol,
|
||
sym.base,
|
||
sym.quote,
|
||
2, -- price_precision(USDT 计价通常 2 位小数)
|
||
5, -- quantity_precision(数量通常 5 位小数)
|
||
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;
|