-- ============================================================
-- 03-continuous-aggregates.sql — K 线分层连续聚合视图
-- ============================================================
-- 从 klines（1m）基表创建分层连续聚合物化视图链：
--   1m → 3m → 5m → 15m → 30m → 1h → 2h → 4h → 6h → 8h → 1d → 1w → 1mon
--
-- GROUP BY 包含 type 列，现货与合约数据隔离聚合。
--
-- 执行前提：
--   1. klines hypertable 已创建（由 02-init-tables.sql 创建）
--   2. klines 表中已有数据（至少一条，否则视图创建成功但无数据）
--
-- 执行方式：
--   psql -U trader -d trade -f 03-continuous-aggregates.sql
--
-- 幂等性：使用 IF NOT EXISTS，可重复执行
--
-- ============================================================
-- 聚合刷新模式选择（二选一）
-- ============================================================
-- 本脚本默认注释掉所有 add_continuous_aggregate_policy 调用。
-- 你需要根据部署场景选择一种刷新模式：
--
-- 【模式 A：定时调度刷新】（传统方式，适合简单场景）
--   取消注释各节的 add_continuous_aggregate_policy 调用即可。
--   TimescaleDB Job Scheduler 按 schedule_interval 自动刷新。
--   缺点：回填期间可能与 INSERT 竞争资源；聚合有调度延迟。
--
-- 【模式 B：应用层触发式刷新】（推荐，精细控制）
--   保持 policy 注释状态。在应用层写入每条 1m K 线后，
--   检测时间桶是否关闭，若关闭则调用 refresh_continuous_aggregate。
--   优点：回填零干扰；聚合零延迟（桶关闭立即刷新）；无调度开销。
--   应用层代码模板见 04-backfill-workflow.sql 末尾。
--
-- 回填工作流（两种模式通用）：
--   1. 批量 INSERT 历史 K 线（policy 已注释，不冲突）
--   2. 手动全量刷新所有视图（见文件末尾注释块）
--   3. 接入实时数据（模式 A 启用 policy / 模式 B 应用层触发）
-- ============================================================

-- ============================================================
-- 3m K 线（从 1m 基表聚合）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_3m
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('3 minutes', time) AS time,
    exchange,
    symbol,
    type,
    '3m'::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)::integer AS trade_count
FROM klines
GROUP BY time_bucket('3 minutes', klines.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_3m',
--     start_offset => INTERVAL '1 day',
--     end_offset => INTERVAL '3 minutes',
--     schedule_interval => INTERVAL '3 minutes',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 5m K 线（从 1m 基表聚合）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_5m
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('5 minutes', time) AS time,
    exchange,
    symbol,
    type,
    '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)::integer AS trade_count
FROM klines
GROUP BY time_bucket('5 minutes', klines.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_5m',
--     start_offset => INTERVAL '1 day',
--     end_offset => INTERVAL '5 minutes',
--     schedule_interval => INTERVAL '5 minutes',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 15m K 线（从 5m 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_15m
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('15 minutes', time) AS time,
    exchange,
    symbol,
    type,
    '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)::integer AS trade_count
FROM klines_5m
GROUP BY time_bucket('15 minutes', klines_5m.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_15m',
--     start_offset => INTERVAL '2 days',
--     end_offset => INTERVAL '15 minutes',
--     schedule_interval => INTERVAL '15 minutes',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 30m K 线（从 15m 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_30m
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('30 minutes', time) AS time,
    exchange,
    symbol,
    type,
    '30m'::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)::integer AS trade_count
FROM klines_15m
GROUP BY time_bucket('30 minutes', klines_15m.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_30m',
--     start_offset => INTERVAL '3 days',
--     end_offset => INTERVAL '30 minutes',
--     schedule_interval => INTERVAL '30 minutes',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 1h K 线（从 30m 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1h
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 hour', time) AS time,
    exchange,
    symbol,
    type,
    '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)::integer AS trade_count
FROM klines_30m
GROUP BY time_bucket('1 hour', klines_30m.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_1h',
--     start_offset => INTERVAL '7 days',
--     end_offset => INTERVAL '1 hour',
--     schedule_interval => INTERVAL '1 hour',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 2h K 线（从 1h 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_2h
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('2 hours', time) AS time,
    exchange,
    symbol,
    type,
    '2h'::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)::integer AS trade_count
FROM klines_1h
GROUP BY time_bucket('2 hours', klines_1h.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_2h',
--     start_offset => INTERVAL '10 days',
--     end_offset => INTERVAL '2 hours',
--     schedule_interval => INTERVAL '2 hours',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 4h K 线（从 1h 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_4h
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('4 hours', time) AS time,
    exchange,
    symbol,
    type,
    '4h'::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)::integer AS trade_count
FROM klines_1h
GROUP BY time_bucket('4 hours', klines_1h.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_4h',
--     start_offset => INTERVAL '14 days',
--     end_offset => INTERVAL '4 hours',
--     schedule_interval => INTERVAL '4 hours',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 6h K 线（从 1h 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_6h
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('6 hours', time) AS time,
    exchange,
    symbol,
    type,
    '6h'::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)::integer AS trade_count
FROM klines_1h
GROUP BY time_bucket('6 hours', klines_1h.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_6h',
--     start_offset => INTERVAL '20 days',
--     end_offset => INTERVAL '6 hours',
--     schedule_interval => INTERVAL '6 hours',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 8h K 线（从 4h 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_8h
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('8 hours', time) AS time,
    exchange,
    symbol,
    type,
    '8h'::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)::integer AS trade_count
FROM klines_4h
GROUP BY time_bucket('8 hours', klines_4h.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_8h',
--     start_offset => INTERVAL '30 days',
--     end_offset => INTERVAL '8 hours',
--     schedule_interval => INTERVAL '8 hours',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 1d K 线（从 4h 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1d
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 day', time) AS time,
    exchange,
    symbol,
    type,
    '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)::integer AS trade_count
FROM klines_4h
GROUP BY time_bucket('1 day', klines_4h.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_1d',
--     start_offset => INTERVAL '30 days',
--     end_offset => INTERVAL '1 day',
--     schedule_interval => INTERVAL '1 day',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 1w K 线（从 1d 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1w
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 week', time) AS time,
    exchange,
    symbol,
    type,
    '1w'::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)::integer AS trade_count
FROM klines_1d
GROUP BY time_bucket('1 week', klines_1d.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_1w',
--     start_offset => INTERVAL '90 days',
--     end_offset => INTERVAL '1 day',
--     schedule_interval => INTERVAL '1 day',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 1mon K 线（从 1d 聚合，分层链）
-- ============================================================
CREATE MATERIALIZED VIEW IF NOT EXISTS klines_1mon
WITH (timescaledb.continuous) AS
SELECT
    time_bucket('1 month', time) AS time,
    exchange,
    symbol,
    type,
    '1mon'::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)::integer AS trade_count
FROM klines_1d
GROUP BY time_bucket('1 month', klines_1d.time), exchange, symbol, type
WITH NO DATA;

-- 【模式 A 用户】取消下面注释以启用定时调度刷新
-- SELECT add_continuous_aggregate_policy('klines_1mon',
--     start_offset => INTERVAL '365 days',
--     end_offset => INTERVAL '1 day',
--     schedule_interval => INTERVAL '1 day',
--     if_not_exists => TRUE
-- );

-- ============================================================
-- 推荐索引：加速按 symbol + type + time 的查询
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_klines_3m_symbol_time ON klines_3m (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_5m_symbol_time ON klines_5m (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_15m_symbol_time ON klines_15m (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_30m_symbol_time ON klines_30m (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1h_symbol_time ON klines_1h (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_2h_symbol_time ON klines_2h (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_4h_symbol_time ON klines_4h (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_6h_symbol_time ON klines_6h (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_8h_symbol_time ON klines_8h (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1d_symbol_time ON klines_1d (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1w_symbol_time ON klines_1w (exchange, symbol, type, time DESC);
CREATE INDEX IF NOT EXISTS idx_klines_1mon_symbol_time ON klines_1mon (exchange, symbol, type, time DESC);

-- ============================================================
-- 截面查询索引：加速同一时间点多品种回测查询
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_klines_1d_exchange_time_symbol ON klines_1d (exchange, time DESC, symbol, type);
CREATE INDEX IF NOT EXISTS idx_klines_1w_exchange_time_symbol ON klines_1w (exchange, time DESC, symbol, type);

-- ============================================================
-- 首次创建后手动刷新所有视图（填充历史数据）
-- 取消注释以下行执行：
-- ============================================================
-- CALL refresh_continuous_aggregate('klines_3m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_5m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_15m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_30m', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_2h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_4h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_6h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_8h', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1d', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1w', NULL, NULL);
-- CALL refresh_continuous_aggregate('klines_1mon', NULL, NULL);
