-- ============================================================
-- 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;
