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