Files
trade/data/init-db/001_init.sql
T

229 lines
8.4 KiB
SQL
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.
-- ============================================================
-- 001_init.sql — TimescaleDB 数据初始化
--
-- Docker Compose 首次启动时自动执行
-- 挂载路径:./data/init-db:/docker-entrypoint-initdb.d
-- ============================================================
-- 扩展
CREATE EXTENSION IF NOT EXISTS timescaledb;
CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit;
-- ============================================================
-- 1. K 线主表
-- ============================================================
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. 转换为 hypertable(时序分区)
-- ============================================================
SELECT create_hypertable(
'klines',
'time', -- 时间列
chunk_time_interval => INTERVAL '1 day', -- 每个 chunk = 1 天数据
partitioning_column => 'exchange', -- 空间分区列
number_partitions => 4, -- 4 个空间分区
if_not_exists => TRUE
);
-- ============================================================
-- 3. 索引
-- ============================================================
-- 主力查询索引:按交易对+周期+时间范围查(覆盖 95% 查询)
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. 压缩策略
-- ============================================================
-- 启用列式压缩(按 symbol+interval 分组,按 time 排序)
ALTER TABLE klines SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'exchange, symbol, interval',
timescaledb.compress_orderby = 'time DESC'
);
-- 自动压缩:K 线闭合 7 天后自动压缩(压缩比约 90%)
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;
SELECT add_continuous_aggregate_policy('klines_5m',
start_offset => INTERVAL '1 day',
end_offset => INTERVAL '10 minutes',
schedule_interval => INTERVAL '1 minute',
if_not_exists => TRUE
);
-- ---------- 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;
SELECT add_continuous_aggregate_policy('klines_15m',
start_offset => INTERVAL '2 days',
end_offset => INTERVAL '30 minutes',
schedule_interval => INTERVAL '5 minutes',
if_not_exists => TRUE
);
-- ---------- 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;
SELECT add_continuous_aggregate_policy('klines_1h',
start_offset => INTERVAL '3 days',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '5 minutes',
if_not_exists => TRUE
);
-- ---------- 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;
SELECT add_continuous_aggregate_policy('klines_1d',
start_offset => INTERVAL '7 days',
end_offset => INTERVAL '2 hours',
schedule_interval => INTERVAL '1 hour',
if_not_exists => TRUE
);
-- ============================================================
-- 7. 连续聚合的压缩(减少视图存储)
-- ============================================================
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);
-- ============================================================
-- 初始化完成
-- ============================================================
DO $$
BEGIN
RAISE NOTICE 'TimescaleDB initialization complete.';
RAISE NOTICE 'Hypertable: klines';
RAISE NOTICE 'Continuous aggregates: klines_5m, klines_15m, klines_1h, klines_1d';
RAISE NOTICE 'Compression: 7 days delay, 90 days retention';
END $$;