在实际量化研究项目中,数据存储方案的选择往往比策略模型本身更早地决定了一个项目的天花板。很多个人研究者在初期会习惯性地使用 CSV 或 Excel 文件,但随着数据量增长、因子维度增加以及回测频率提升,文件读写慢、内存溢出、数据一致性差等问题会迅速成为瓶颈。此时,选择一个合适的数据库,不仅能解决存储问题,更能为后续的数据清洗、因子计算、回测引擎乃至实盘对接提供一个稳定、高效、可扩展的基础设施。
本文面向的是具备一定 Python 编程和量化基础的个人研究者或小型团队。我们将从量化数据的典型特征(如时间序列、高维度、高频读写)出发,系统性地对比几种主流数据库方案,包括关系型数据库(如 PostgreSQL)、时序数据库(如 InfluxDB)、列式存储(如 Apache Parquet + DuckDB)以及内存数据库(如 Redis)。文章不会停留在理论对比,而是会给出具体的技术选型决策树、环境搭建步骤、数据入库与查询的代码示例,并重点分析在回测、因子计算等典型场景下的性能表现和常见陷阱。读完本文,你将能够根据自己当前的数据规模、硬件条件和研究阶段,做出一个清晰、可落地的数据库选型决策。
1. 量化数据特征与数据库选型核心维度
在讨论具体数据库之前,必须明确量化研究数据的特点,这直接决定了数据库需要具备哪些能力。
1.1 量化数据的典型特征
- 强时间序列属性:所有行情数据(tick、分钟线、日线)、因子数据、信号数据都严格依赖时间戳。查询模式高度集中于时间范围查询(如
WHERE date BETWEEN '2023-01-01' AND '2023-12-31')和按时间排序。 - 多维度、宽表结构:单个标的(股票、期货等)在某个时间点上的状态,可能由数百个因子(技术指标、基本面数据、另类数据)共同描述,形成非常“宽”的表结构。
- 高吞吐的写入与读取:在数据预处理和因子计算阶段,需要批量写入海量历史数据;在回测阶段,则需要按照时间顺序高速、顺序或随机读取大量标的的数据。
- 以分析型查询为主:与交易系统不同,研究环境的查询多为复杂的分析型查询(OLAP),例如跨标的多因子回归、截面排名、分组统计等,涉及大量的聚合(SUM, AVG)、连接(JOIN)和窗口函数(Window Function)操作。
- 数据局部更新频繁:因子值可能随着计算逻辑的调整而重新计算,需要更新特定时间段、特定标的的数据,而非全表覆盖。
1.2 数据库选型的四个核心评估维度
基于以上特征,我们可以从四个维度评估一个数据库是否适合量化研究场景:
| 维度 | 说明 | 对量化研究的重要性 |
|---|---|---|
| 时序优化 | 原生支持时间序列数据模型,对时间戳建立高效索引,优化时间范围查询。 | 高。直接决定行情和因子数据查询效率。 |
| 列式存储 | 按列而非按行存储数据。当查询只涉及少数列(如只查收盘价和成交量)时,可以极大减少 I/O。 | 高。因子表通常很宽,但每次计算可能只用到其中几列。 |
| 分析性能 | 对聚合查询、复杂 JOIN、窗口函数等 OLAP 操作有良好支持和高性能引擎。 | 高。因子计算和归因分析依赖此类操作。 |
| 易用性与生态 | 安装部署的复杂度、与 Python 生态(pandas, numpy)的集成度、社区活跃度和学习成本。 | 中高。个人研究者需要快速上手,避免在基础设施上耗费过多精力。 |
2. 主流方案深度对比与适用场景
下面我们将几种常见方案放入上述评估框架中进行对比。
2.1 通用关系型数据库:PostgreSQL / MySQL
这是最容易被首先想到的方案,利用其成熟的 SQL 引擎和事务支持。
PostgreSQL 示例:创建行情数据表
CREATE TABLE market_data ( symbol VARCHAR(20) NOT NULL, trade_date DATE NOT NULL, open_price DECIMAL(12, 4), high_price DECIMAL(12, 4), low_price DECIMAL(12, 4), close_price DECIMAL(12, 4), volume BIGINT, turnover DECIMAL(20, 4), PRIMARY KEY (symbol, trade_date) ); CREATE INDEX idx_market_data_date ON market_data(trade_date);优点:
- 功能全面:完整的 SQL 支持,事务 ACID 特性,适合需要强一致性的场景。
- 生态强大:连接工具(如 DBeaver, Navicat)、ORM 框架(SQLAlchemy)支持完善。
- 扩展性好:PostgreSQL 的 TimescaleDB 插件可使其变身专业的时序数据库;MySQL 也有其适用场景。
缺点:
- 时序查询非原生优化:即使对时间戳建索引,在超大规模单时间点查询(例如查询全市场3000只股票某一天的数据)时,性能可能不如专业时序库。
- 行式存储:对于宽表的列式查询效率较低,I/O 压力大。
- 分析性能一般:对于复杂的多标的多因子聚合分析,性能可能成为瓶颈。
适用场景:
- 数据量不大(例如,仅 A 股日线数据,十年数据量约 700 万行)。
- 查询模式相对简单,不涉及极其复杂的分析。
- 项目初期,追求快速验证想法,且团队对 SQL 非常熟悉。
- 需要与现有系统(如 Web 服务)共享数据库。
2.2 专业时序数据库:InfluxDB / TimescaleDB
专为时间序列数据设计,在写入、压缩和时间窗口查询上具有天然优势。
InfluxDB 示例:写入行情数据(使用 InfluxDB 2.x Python Client)
from influxdb_client import InfluxDBClient, Point, WritePrecision from influxdb_client.client.write_api import SYNCHRONOUS client = InfluxDBClient(url="http://localhost:8086", token="your-token", org="your-org") write_api = client.write_api(write_options=SYNCHRONOUS) point = Point("market_data") \ .tag("symbol", "000001.SZ") \ .field("open", 12.50) \ .field("high", 12.80) \ .field("low", 12.40) \ .field("close", 12.75) \ .field("volume", 1000000) \ .time("2023-10-27T15:00:00Z", WritePrecision.NS) write_api.write(bucket="quant_bucket", record=point)优点:
- 极高的时序写入和查询性能:数据模型和存储引擎为时间戳优化,压缩率高。
- 内置时间窗口函数:轻松进行按日、周、月的聚合统计。
- 生态针对监控和指标:与 Grafana 等看板工具集成极佳。
缺点:
- SQL 支持有限或语法特殊:InfluxDB 使用 Flux 或类 SQL,复杂分析能力不如标准 SQL 强大。TimescaleDB(基于 PostgreSQL)则兼容标准 SQL。
- 非标准关系模型:多表关联查询(JOIN)能力较弱,不适合需要频繁跨表关联的复杂因子计算。
- 学习成本:需要理解其独特的数据模型(Measurement, Tag, Field)。
适用场景:
- 存储高频 tick 数据、分钟线数据,查询模式主要是按标的和时间的单维度查询。
- 对写入速度和存储压缩比有极高要求。
- 分析查询相对简单,不涉及复杂跨表关联。
2.3 列式存储 + 分析引擎:Apache Parquet + DuckDB
这是一种新兴的、备受数据科学社区青睐的架构。将数据以列式格式(Parquet)存储在文件系统(如本地 SSD 或对象存储 S3),使用 DuckDB 进行内存分析。
操作示例:将 pandas DataFrame 保存为 Parquet,并用 DuckDB 查询
import pandas as pd import duckdb # 1. 模拟一个因子宽表 DataFrame df = pd.DataFrame({ 'date': pd.date_range('2023-01-01', periods=100, freq='D'), 'symbol': ['STOCK_A'] * 100, 'factor_ma5': np.random.randn(100), 'factor_ma20': np.random.randn(100), 'factor_rsi': np.random.randn(100), # ... 更多因子列 }) # 2. 保存为 Parquet 文件(列式存储) df.to_parquet('factor_data.parquet') # 3. 使用 DuckDB 直接查询 Parquet 文件,无需导入数据库 conn = duckdb.connect() result = conn.execute(""" SELECT symbol, AVG(factor_ma5) as avg_ma5, STDDEV(factor_rsi) as std_rsi FROM read_parquet('factor_data.parquet') WHERE date >= '2023-02-01' GROUP BY symbol """).fetchdf() print(result)优点:
- 极致分析性能:DuckDB 是为 OLAP 设计的进程内数据库,无需服务端,直接在内存中执行,对复杂 SQL 查询速度极快。
- 完美的 Python 生态集成:与 pandas DataFrame 可以零成本转换,查询结果直接是 DataFrame。
- 存储与计算分离:Parquet 文件是静态的,易于备份、共享和版本管理。计算资源按需使用。
- 成本极低:完全免费,部署简单,适合个人研究者。
缺点:
- 并发写入能力弱:DuckDB 更适合“一次写入,多次读取”的分析场景,高并发写入不是其强项。
- 数据需完全载入内存:虽然 DuckDB 会优化,但处理远超内存大小的数据时仍需技巧(如分区)。
- 无服务端:对于需要多进程/多机器共享同一实时数据库的场景不适用。
适用场景:
- 个人量化研究的首选方案。数据量在单机内存可处理范围内(数十GB)。
- 研究流程以“数据准备 -> 批量因子计算 -> 回测”为主,中间结果可以物化为文件。
- 需要频繁进行探索性数据分析(EDA)和复杂 SQL 查询。
2.4 内存数据库:Redis
严格来说,Redis 并非用于持久化存储和分析,但其在量化系统中扮演着重要角色。
适用场景:
- 缓存中间结果:将计算耗时的因子值、预处理后的数据缓存起来,加速回测迭代。
- 存储实时信号:在实盘系统中,作为高速通道存储最新的交易信号、风控状态。
- 发布/订阅:用于不同模块(数据抓取、因子计算、风控、交易)之间的消息通信。
不适用场景:
- 作为主要的、持久化的历史数据存储和分析引擎。
3. 决策树与混合架构实践
面对众多选择,个人研究者可以遵循以下决策路径:
graph TD A[开始选型] --> B{数据量 & 查询复杂度}; B -- 数据量小<br>查询简单 --> C[使用 PostgreSQL]; B -- 数据量大<br>以时序点查询为主 --> D[使用时序数据库 InfluxDB/TimescaleDB]; B -- 数据量大<br>以复杂分析查询为主 --> E{是否需要多进程/服务共享}; E -- 是 --> F[考虑 PostgreSQL 或 ClickHouse]; E -- 否 --> G[强烈推荐 Parquet + DuckDB]; G --> H[完成]; C --> H; D --> H; F --> H;在实际项目中,混合使用多种存储方案往往是更优解。一个典型的混合架构如下:
- 原始数据层:将清洗后的基础数据(日线、分钟线)以Parquet格式存储在硬盘或对象存储上。这是你的“数据湖”,成本低,易管理。
- 因子计算与中间存储:使用DuckDB从 Parquet 文件中读取数据,执行复杂的因子计算 SQL。将计算结果(因子宽表)再次输出为新的 Parquet 文件集。
- 回测引擎数据源:回测时,回测引擎(如 Backtrader, Qlib)直接读取因子 Parquet 文件,或通过 DuckDB 接口查询,实现高速数据供给。
- 缓存与实时层(可选):对于需要极低延迟访问的中间数据或参数,使用Redis进行缓存。
- 元数据与结果管理:使用轻量级的SQLite或PostgreSQL存储回测结果、策略参数、实验记录等结构化元数据。
4. 实战:基于 Parquet + DuckDB 搭建研究数据栈
下面我们以一个具体的例子,展示如何用 Parquet 和 DuckDB 构建一个可用的量化研究数据环境。
4.1 环境准备与数据准备
首先安装必要的 Python 库:
pip install pandas numpy duckdb pyarrow假设我们已有 CSV 格式的日线数据stock_daily.csv,包含symbol,date,open,high,low,close,volume字段。
4.2 步骤一:将原始数据转换为 Parquet
将不同数据源统一转换为 Parquet,这是构建高效数据栈的第一步。
import pandas as pd import os # 读取 CSV df = pd.read_csv('stock_daily.csv', parse_dates=['date']) # 按标的和日期排序,这对后续查询性能有帮助 df = df.sort_values(['symbol', 'date']).reset_index(drop=True) # 保存为 Parquet。使用 snappy 压缩以平衡速度与体积。 df.to_parquet('stock_daily.parquet', engine='pyarrow', compression='snappy') # 对于超大数据,可以按日期或标的进行分区存储,DuckDB 能高效读取分区数据。 # 例如按年份分区: os.makedirs('stock_daily_partitioned', exist_ok=True) for year, group in df.groupby(df['date'].dt.year): group.to_parquet(f'stock_daily_partitioned/year={year}/data.parquet', engine='pyarrow')4.3 步骤二:使用 DuckDB 进行探索性分析
现在,我们可以不将数据导入任何数据库服务,直接进行查询。
import duckdb conn = duckdb.connect() # 查询某只股票2023年的所有数据 query1 = """ SELECT * FROM read_parquet('stock_daily.parquet') WHERE symbol = '000001.SZ' AND date BETWEEN '2023-01-01' AND '2023-12-31' ORDER BY date """ df_000001 = conn.execute(query1).fetchdf() print(df_000001.head()) # 复杂的分析查询:计算所有股票2023年的年化收益率和波动率 query2 = """ WITH daily_returns AS ( SELECT symbol, date, close, LN(close / LAG(close) OVER (PARTITION BY symbol ORDER BY date)) AS daily_log_return FROM read_parquet('stock_daily.parquet') WHERE date >= '2023-01-01' ) SELECT symbol, COUNT(*) as trading_days, AVG(daily_log_return) * 252 as annualized_return, STDDEV(daily_log_return) * SQRT(252) as annualized_volatility FROM daily_returns WHERE daily_log_return IS NOT NULL GROUP BY symbol HAVING COUNT(*) > 100 -- 过滤掉交易天数过少的股票 ORDER BY annualized_return DESC """ factor_df = conn.execute(query2).fetchdf() print(factor_df.head())4.4 步骤三:将 DuckDB 查询集成到因子计算流程
你可以将复杂的因子计算逻辑编写成 SQL 视图或 CTE(公用表表达式),让 DuckDB 高效执行。
# 定义一个计算移动平均因子的函数 def calculate_ma_factors(parquet_path, ma_windows=[5, 10, 20]): conn = duckdb.connect() # 动态生成 SQL,计算多个移动平均 ma_columns = [] for w in ma_windows: ma_columns.append(f"AVG(close) OVER (PARTITION BY symbol ORDER BY date ROWS BETWEEN {w-1} PRECEDING AND CURRENT ROW) AS ma_{w}") ma_columns_sql = ", ".join(ma_columns) query = f""" SELECT symbol, date, close, {ma_columns_sql} FROM read_parquet('{parquet_path}') """ result_df = conn.execute(query).fetchdf() # 将结果保存为新的因子 Parquet 文件 result_df.to_parquet('factor_ma.parquet', engine='pyarrow') return result_df factor_df = calculate_ma_factors('stock_daily.parquet')4.5 步骤四:在回测中读取因子数据
在回测框架中(此处以伪代码示意),可以直接读取 Parquet 文件或通过 DuckDB 查询。
# 伪代码,以 Backtrader 为例 import backtrader as bt import pandas as pd class MyStrategy(bt.Strategy): params = (('ma_period', 20),) def __init__(self): # 在初始化时,一次性读取该股票的所有因子数据到内存字典中 self.factor_data = {} # 假设 factor_ma.parquet 已经包含所有股票的 MA 因子 all_factor_df = pd.read_parquet('factor_ma.parquet') for symbol, group in all_factor_df.groupby('symbol'): self.factor_data[symbol] = group.set_index('date') def next(self): # 在每一个 bar,获取当前标的当前日期的因子值 current_date = self.datas[0].datetime.date(0) symbol = self.datas[0]._name # 从字典中快速定位因子值 ma20 = self.factor_data[symbol].loc[current_date, 'ma_20'] # ... 基于因子值做交易逻辑5. 性能调优与常见问题排查
即使选择了合适的方案,不当的使用也会导致性能低下。以下是一些关键调优点和排查思路。
5.1 Parquet + DuckDB 性能调优
- 分区:如果数据量很大,按日期(
year=2023/month=10)或标的首字母进行分区,能极大提升查询性能。DuckDB 的read_parquet支持通配符,可以读取整个目录。-- 查询2023年10月所有数据 SELECT * FROM read_parquet('stock_daily_partitioned/year=2023/month=10/*.parquet'); - 使用合适的压缩格式:
snappy压缩速度快,gzip压缩率高。对于需要频繁读取的分析数据,snappy是更好的选择。 - 利用 DuckDB 持久化连接与视图:对于重复使用的复杂查询,可以创建视图或将中间表持久化到 DuckDB 的本地数据库文件(
.db格式)中,避免每次重复解析 SQL 和 Parquet 文件。conn = duckdb.connect('my_research.db') # 连接到持久化数据库文件 conn.execute("CREATE VIEW factor_view AS SELECT * FROM read_parquet('factor_ma.parquet')") # 后续查询直接使用 view,更快 result = conn.execute("SELECT * FROM factor_view WHERE symbol='000001.SZ'").fetchdf()
5.2 常见问题与解决方案
| 问题现象 | 可能原因 | 检查与解决方案 |
|---|---|---|
| 查询速度突然变慢 | 1. 未对常用过滤字段(如date,symbol)进行排序。2. Parquet 文件过大,未分区。 3. 内存不足。 | 1. 在生成 Parquet 前,按['symbol', 'date']排序。2. 将大文件拆分为按日期或标的分区的多个小文件。 3. 监控内存使用,考虑使用 DuckDB 的外部聚合功能或升级硬件。 |
| DuckDB 内存占用过高 | 1. 单次查询数据量过大。 2. 同时打开了多个连接或进行了大量中间计算。 | 1. 使用LIMIT采样或分区查询,避免全表加载。2. 及时关闭连接 ( conn.close()),对于复杂管道,考虑分步将中间结果写入 Parquet 释放内存。 |
| 因子计算逻辑更改后,历史数据需全部重算 | 原始架构设计为全量覆盖,未考虑增量更新。 | 设计数据版本管理。将原始数据与因子计算分离。因子表按计算日期分区。重算时只更新受影响的分区。使用 DVC(Data Version Control)等工具管理 Parquet 文件版本。 |
| 多进程回测时数据读取冲突 | 多个进程同时读取/写入同一个 Parquet 文件。 | Parquet 文件是只读的,天然支持多进程读取。确保写入操作(如保存回测结果)写入不同的文件或数据库。对于需要共享的状态,使用 Redis 或数据库。 |
6. 从研究到生产的考量
个人研究者的项目也可能逐步成长,需要提前考虑生产化要素。
- 数据版本化:使用
dvc或git-lfs管理 Parquet 数据文件,确保每次实验的数据可复现。 - 计算流水线化:使用
pipeline工具(如Prefect或Airflow)将数据下载、清洗、因子计算、回测等步骤组织成可调度、可监控的工作流。 - 元数据管理:使用一个轻量级 SQL 数据库(如 SQLite)记录每次回测的参数、绩效指标、使用的数据版本,便于横向对比。
- 监控与日志:在关键步骤(如数据更新、因子计算)加入日志,记录开始结束时间、处理行数、错误信息。
对于个人研究者而言,技术选型的核心是在简洁性与扩展性之间找到平衡点。初期过度设计会拖慢研究进度,而完全不考虑架构则会在数据量增长后被迫重构。以Parquet + DuckDB为核心,辅以SQLite管理元数据,是一个在相当长时间内都能保持高效和简洁的黄金组合。当数据规模真正超越单机能力时,再考虑迁移到分布式数据库(如 ClickHouse)或专业的数仓方案,届时你积累的数据处理流程和 SQL 经验也将平滑过渡。