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

    -- 交易对类型（spot / um / cm）
    type                    TEXT         NOT NULL DEFAULT 'spot',

    -- 基础币种（如 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 + type 唯一
    CONSTRAINT uq_trading_pairs_exchange_symbol_type UNIQUE (exchange_id, symbol, type)
);

-- 按激活状态快速筛选
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, type);


-- ============================================================
-- 第三节：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, type（同交易对+类型聚合压缩；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,

    -- 交易对类型（spot / um / cm）
    type            TEXT                     NOT NULL DEFAULT 'spot',

    -- 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, type, 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
);

-- ============================================================
-- 配置列式压缩
-- ============================================================

-- 启用列式压缩（先启用压缩，再设置分段/排序键）
ALTER TABLE klines SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'exchange,symbol,type',
    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;

-- 默认交易对（幂等）
INSERT INTO trading_pairs (exchange_id, symbol, type, base_asset, quote_asset,
    price_precision, quantity_precision, active)
SELECT
    e.id,
    sym.symbol,
    sym.type,
    sym.base,
    sym.quote,
    2,          -- price_precision（USDT 计价通常 2 位小数）
    5,          -- quantity_precision（数量通常 5 位小数）
    TRUE
FROM exchanges e
CROSS JOIN (
    VALUES
        ('BTCUSDT', 'spot', 'BTC', 'USDT'),
        ('ETHUSDT', 'spot', 'ETH', 'USDT'),
        ('BNBUSDT', 'spot', 'BNB', 'USDT'),
        ('SOLUSDT', 'spot', 'SOL', 'USDT'),
        ('BTCUSDT', 'um',   'BTC', 'USDT'),
        ('ETHUSDT', 'um',   'ETH', 'USDT'),
        ('BNBUSDT', 'um', 'BNB', 'USDT'),
        ('SOLUSDT', 'um', 'SOL', 'USDT')
) AS sym(symbol, type, base, quote)
WHERE e.name = 'binance'
ON CONFLICT (exchange_id, symbol, type) DO NOTHING;
