e91cad79e6
- 新增 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 初始化脚本
207 lines
8.2 KiB
SQL
207 lines
8.2 KiB
SQL
-- ============================================================
|
||
-- 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;
|