-- ============================================================ -- schema/klines.sql — K 线表 DDL(参考副本) -- ============================================================ -- 数据库:TimescaleDB (PostgreSQL 17 + timescaledb 2.x) -- 说明:存储全交易所 OHLCV 数据,按时间自动分区压缩 -- -- ⚠️ 权威初始化脚本:data/init-db/001_init.sql -- 本文件保留作为 pg.initSchema() 非 Docker 部署的回退方案和文档参考。 -- 修改表结构时请同步更新 001_init.sql。 -- ============================================================ -- ============================================================ -- 1. klines — K 线主表(hypertable) -- ============================================================ CREATE TABLE IF NOT EXISTS klines ( -- ---- 时间维度 ---- time TIMESTAMPTZ NOT NULL, -- K 线开盘时间(UTC) -- ---- 标识维度 ---- exchange TEXT NOT NULL, -- 交易所:binance / okx / bybit symbol TEXT NOT NULL, -- 交易对:BTCUSDT / ETHUSDT interval TEXT NOT NULL, -- 周期:1m / 5m / 15m / 1h / 4h / 1d -- ---- OHLCV 核心数据 ---- open NUMERIC(20,8) NOT NULL, high NUMERIC(20,8) NOT NULL, low NUMERIC(20,8) NOT NULL, close NUMERIC(20,8) NOT NULL, volume NUMERIC(20,8) NOT NULL DEFAULT 0, -- 成交量(基准币种) -- ---- 扩展字段 ---- quote_volume NUMERIC(20,8) DEFAULT 0, -- 成交额(计价币种) taker_buy_base_vol NUMERIC(20,8) DEFAULT 0, -- 主动买入量(基准币种) taker_buy_quote_vol NUMERIC(20,8) DEFAULT 0, -- 主动买入额(计价币种) trade_count INTEGER DEFAULT 0, -- 成交笔数 is_closed BOOLEAN DEFAULT TRUE, -- K 线是否已闭合 -- ---- 元数据 ---- created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), -- 唯一约束:同一根 K 线不可重复 UNIQUE (time, exchange, symbol, interval) ); -- ============================================================ -- 2. 转换为 TimescaleDB hypertable -- ============================================================ -- 按 time 列做 1 天分区,按 exchange 做 4 空间分区 SELECT create_hypertable( 'klines', 'time', chunk_time_interval => INTERVAL '1 day', partitioning_column => 'exchange', number_partitions => 4, if_not_exists => TRUE ); -- ============================================================ -- 3. 索引设计 -- ============================================================ -- 主力查询索引(覆盖 95% 查询场景) -- 用途:按交易对+周期+时间范围查询最新 K 线 CREATE INDEX IF NOT EXISTS idx_klines_lookup ON klines (exchange, symbol, interval, time DESC); -- 回测专用索引 -- 用途:按交易对+周期+时间正序遍历(策略回测) CREATE INDEX IF NOT EXISTS idx_klines_backtest ON klines (symbol, interval, time ASC); -- 最新已闭合 K 线索引(部分索引,减小体积) -- 用途:获取已完成的最新 K 线(避免扫描未闭合数据) CREATE INDEX IF NOT EXISTS idx_klines_latest ON klines (exchange, symbol, interval, time DESC) WHERE is_closed = TRUE; -- ============================================================ -- 4. 压缩策略(列式压缩,压缩比约 90%) -- ============================================================ ALTER TABLE klines SET ( timescaledb.compress, timescaledb.compress_segmentby = 'exchange, symbol, interval', timescaledb.compress_orderby = 'time DESC' ); -- K 线闭合 7 天后自动触发压缩 SELECT add_compression_policy('klines', INTERVAL '7 days', if_not_exists => TRUE); -- ============================================================ -- 5. 数据保留策略 -- ============================================================ -- 1m 粒度 K 线保留 90 天(更粗粒度由连续聚合视图覆盖) SELECT add_retention_policy('klines', INTERVAL '90 days', if_not_exists => TRUE); -- ============================================================ -- 6. 连续聚合视图(从 1m 自动派生高周期 K 线) -- ============================================================ -- ---- 5m K 线 ---- CREATE MATERIALIZED VIEW IF NOT EXISTS klines_5m WITH (timescaledb.continuous) AS SELECT time_bucket('5 minutes', time) AS time, exchange, symbol, '5m'::TEXT AS interval, FIRST(open, time) AS open, MAX(high) AS high, MIN(low) AS low, LAST(close, time) AS close, SUM(volume) AS volume, SUM(quote_volume) AS quote_volume, SUM(taker_buy_base_vol) AS taker_buy_base_vol, SUM(taker_buy_quote_vol) AS taker_buy_quote_vol, SUM(trade_count) AS trade_count FROM klines WHERE interval = '1m' GROUP BY time_bucket('5 minutes', time), exchange, symbol; -- ---- 15m K 线 ---- CREATE MATERIALIZED VIEW IF NOT EXISTS klines_15m WITH (timescaledb.continuous) AS SELECT time_bucket('15 minutes', time) AS time, exchange, symbol, '15m'::TEXT AS interval, FIRST(open, time) AS open, MAX(high) AS high, MIN(low) AS low, LAST(close, time) AS close, SUM(volume) AS volume, SUM(quote_volume) AS quote_volume, SUM(taker_buy_base_vol) AS taker_buy_base_vol, SUM(taker_buy_quote_vol) AS taker_buy_quote_vol, SUM(trade_count) AS trade_count FROM klines WHERE interval = '1m' GROUP BY time_bucket('15 minutes', time), exchange, symbol; -- ---- 1h K 线 ---- CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1h WITH (timescaledb.continuous) AS SELECT time_bucket('1 hour', time) AS time, exchange, symbol, '1h'::TEXT AS interval, FIRST(open, time) AS open, MAX(high) AS high, MIN(low) AS low, LAST(close, time) AS close, SUM(volume) AS volume, SUM(quote_volume) AS quote_volume, SUM(taker_buy_base_vol) AS taker_buy_base_vol, SUM(taker_buy_quote_vol) AS taker_buy_quote_vol, SUM(trade_count) AS trade_count FROM klines WHERE interval = '1m' GROUP BY time_bucket('1 hour', time), exchange, symbol; -- ---- 1d K 线 ---- CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1d WITH (timescaledb.continuous) AS SELECT time_bucket('1 day', time) AS time, exchange, symbol, '1d'::TEXT AS interval, FIRST(open, time) AS open, MAX(high) AS high, MIN(low) AS low, LAST(close, time) AS close, SUM(volume) AS volume, SUM(quote_volume) AS quote_volume, SUM(taker_buy_base_vol) AS taker_buy_base_vol, SUM(taker_buy_quote_vol) AS taker_buy_quote_vol, SUM(trade_count) AS trade_count FROM klines WHERE interval = '1m' GROUP BY time_bucket('1 day', time), exchange, symbol; -- 连续聚合视图也启用压缩 ALTER MATERIALIZED VIEW klines_5m SET (timescaledb.compress = true); ALTER MATERIALIZED VIEW klines_15m SET (timescaledb.compress = true); ALTER MATERIALIZED VIEW klines_1h SET (timescaledb.compress = true); ALTER MATERIALIZED VIEW klines_1d SET (timescaledb.compress = true); -- ============================================================ -- 7. 常用查询示例 -- ============================================================ -- 查询最新 N 根 1h K 线 -- SELECT time, open, high, low, close, volume -- FROM klines_1h -- WHERE exchange = 'binance' AND symbol = 'BTCUSDT' -- ORDER BY time DESC LIMIT 100; -- 查询某个时间范围内的原始 1m K 线 -- SELECT time, open, high, low, close, volume -- FROM klines -- WHERE exchange = 'binance' AND symbol = 'ETHUSDT' AND interval = '1m' -- AND time BETWEEN '2026-06-01' AND '2026-06-06' -- ORDER BY time ASC;