量化交易 PostgreSQL 建表指南:K 线数据 + 信号记录 + 性能优化

0 阅读3分钟

量化系统跑到一定阶段,CSV 一定会成为瓶颈。本文给出一个生产级的 PostgreSQL 建表方案。

为什么 CSV 不够用

场景CSV 耗时PostgreSQL 耗时
查茅台近 20 天收盘价~30s<0.1s
对比 50 只股一年涨幅~5min<1s
多策略并行读写文件锁冲突ACID 保障
重复数据去重手动UNIQUE INDEX

1. K 线数据表

CREATE TABLE daily_prices (
    id            SERIAL PRIMARY KEY,
    symbol        VARCHAR(10) NOT NULL,
    trade_date    DATE NOT NULL,
    open          NUMERIC(12,4),
    high          NUMERIC(12,4),
    low           NUMERIC(12,4),
    close         NUMERIC(12,4),
    volume        BIGINT,
    amount        NUMERIC(16,2),
    created_at    TIMESTAMP DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_symbol_date 
  ON daily_prices(symbol, trade_date);

CREATE INDEX idx_date ON daily_prices(trade_date);

类型选择

类型为什么
open/high/low/closeNUMERIC(12,4)定点数,无浮点误差。4 位兼容港股美股
volumeBIGINT日成交可达数亿股,INTEGER 不够
amountNUMERIC(16,2)成交额可达数千亿,预留空间
trade_dateDATE不带时区,A 股统一北京时间

为什么不用 FLOAT?

>>> 0.1 + 0.2 == 0.3
False

金融场景不允许这种误差。NUMERIC 是定点数,牺牲一点速度换绝对精度。


2. 信号表

CREATE TABLE signals (
    id            SERIAL PRIMARY KEY,
    symbol        VARCHAR(10) NOT NULL,
    strategy      VARCHAR(50) NOT NULL,
    signal_type   VARCHAR(10) NOT NULL CHECK (signal_type IN ('BUY','SELL','HOLD')),
    price         NUMERIC(12,4),
    reason        TEXT,
    created_at    TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_signals_time ON signals(created_at DESC);
CREATE INDEX idx_signals_symbol ON signals(symbol);

CHECK 约束防止脏数据(如 signal_type = 'buy' 大小写不一致)。


3. Upsert:避免重复拉取产生脏数据

from sqlalchemy.dialects.postgresql import insert

def upsert_kline(engine, df):
    """插入或更新 K 线数据"""
    rows = df.to_dict('records')
    stmt = insert(DailyPrice).values(rows)
    stmt = stmt.on_conflict_do_update(
        constraint='idx_symbol_date',
        set_={
            'open': stmt.excluded.open,
            'high': stmt.excluded.high,
            'low': stmt.excluded.low,
            'close': stmt.excluded.close,
            'volume': stmt.excluded.volume,
            'amount': stmt.excluded.amount,
        }
    )
    engine.execute(stmt)

定时任务多次拉同一天数据 → ON CONFLICT DO UPDATE 只更新不复写。


4. Python 批量入库

import pandas as pd
from sqlalchemy import create_engine

engine = create_engine(
    'postgresql://user:pass@localhost:5432/quantdb'
)

df = pd.read_csv('daily_data.csv')
df.to_sql('daily_prices', engine,
          if_exists='append',
          index=False,
          method='multi',       # 批量 INSERT
          chunksize=1000)       # 分批提交

method='multi' 把多行合并成一条 INSERT,比单条插入快 10 倍以上。


5. 常用查询

-- 最近 N 天 K 线
SELECT trade_date, open, high, low, close, volume
FROM daily_prices
WHERE symbol = '600519'
ORDER BY trade_date DESC
LIMIT 20;

-- 今日涨幅 TOP 10
SELECT symbol,
       ROUND((close - prev_close) / prev_close * 100, 2) AS pct_change
FROM (
    SELECT symbol, close,
           LAG(close) OVER (PARTITION BY symbol ORDER BY trade_date) AS prev_close
    FROM daily_prices
    WHERE trade_date >= CURRENT_DATE - INTERVAL '2 days'
) t
WHERE trade_date = (
    SELECT MAX(trade_date) FROM daily_prices WHERE symbol = t.symbol
)
ORDER BY pct_change DESC
LIMIT 10;

-- 连续上涨天数
SELECT symbol, COUNT(*) AS up_days
FROM (
    SELECT symbol, trade_date,
           close > LAG(close) OVER (PARTITION BY symbol ORDER BY trade_date) AS is_up
    FROM daily_prices
    WHERE trade_date > CURRENT_DATE - INTERVAL '10 days'
) t
WHERE is_up
GROUP BY symbol
HAVING COUNT(*) >= 3;

6. 数据量大了怎么办

数据量方案效果
<100 万条默认配置无需优化
100 万 ~ 1000 万加索引 + 调 work_mem查询毫秒级
>1000 万 (分钟线)TimescaleDB 插件自动分区 + 10:1 压缩
-- TimescaleDB 一键转时序表
SELECT create_hypertable('daily_prices', 'trade_date');

-- 7 天后自动压缩
SELECT add_compression_policy('daily_prices', 
    INTERVAL '7 days');

选型速查

方案适用
CSV0-1 股回测,快速原型
SQLite10-50 股,本地单机
PostgreSQL全市场 5000+,生产环境
+TimescaleDB分钟级 × 全市场,高频

早点切到 PG,省下的排查时间比写建表语句多得多。