【Python量化系统工程实战 #01】数据存哪里不崩?CSV/SQLite/MySQL 量化存储方案对比与 SQLite 实战建库

系列开篇:从「会写策略」推进到「会搭系统」。本篇先解决最基础的存储选型------回测和实盘都要把 K 线落到本地,三种方案怎么选?

一、为什么存储选型是工程化的第一道坎

很多量化新手写完策略,跑完回测就把 DataFrame 留在内存里------重启一次,模型全没了。或者更糟:用 Excel 存了几万行,每次打开都卡十几秒,等到发现数据已经过时一星期了。

当你从「研究脚本」走向「自动跑起来」,存储必须先定下来:

  • CSV:最直观,Excel 也能打开,但量大了会卡、类型推断会错
  • SQLite:单文件数据库,零部署,1 千万行也能秒级查询
  • MySQL:生产级,但要装服务、搞账号、做备份

本文给你一份实测对比 + 选型决策表,并带一份 SQLite 实战建库脚本,看完就能选对。

二、本文你将得到什么

  1. 三种方案实测对比:1000 行日线的写入/查询/体积数据
  2. 量化场景选型决策表:按数据规模直接对号入座
  3. SQLite 实战建库代码:建表 + 索引 + 批量 upsert,单文件即用
  4. CSV 真实使用陷阱 :为什么 pandas.read_csv 会让你的股票代码「消失」

三、对比方案概览

方案 适用规模 部署复杂度 典型工具链
CSV < 1 万行 零 pandas / csv 模块
SQLite 1 万 ~ 1 千万行 零(单文件) sqlite3 / pandas
MySQL > 1 千万行 / 多用户 高 pymysql / SQLAlchemy

直观印象:CSV 像记事本,SQLite 像单文件 Excel,MySQL 像团队共享数据库服务器。

四、实测对比:CSV vs SQLite

我用 1000 行模拟日线(股票 600519,跨度 2023-03 到 2025-12),分别走 CSV 和 SQLite 路径,对比四种操作的耗时与体积。完整测试代码如下:

python 复制代码
from mairui import Client
import pandas as pd
import numpy as np
import time, sqlite3

# 演示证书(对外示例用;实际项目建议放环境变量)
client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")

# 取真实日线(演示证可拉到 60 个交易日)
raw = client.stock_history("600519", "d", "f")
df = pd.DataFrame(raw)
df["date"] = pd.to_datetime(df["t"]).dt.strftime("%Y-%m-%d")
df = df[["date", "o", "h", "l", "c", "pc", "v"]].rename(
    columns={"o": "open", "h": "high", "l": "low", "c": "close", "pc": "pre_close", "v": "volume"}
)
df["stock"] = "600519"

# 构造 1000 行测试数据(仅用真实首日数据扩展,避免演示证数据量限制)
np.random.seed(42)
df = pd.concat([df] * 25, ignore_index=True)[:1000]
print(f"[data] {len(df)} 行, {df['date'].min()} -> {df['date'].max()}")

# ========== 方案 A:CSV ==========
csv_path = "kline.csv"
df.to_csv(csv_path, index=False)

t0 = time.time()
df_csv = pd.read_csv(csv_path)
t_csv_full = time.time() - t0

t0 = time.time()
df_csv_filt = pd.read_csv(csv_path)
df_csv_filt = df_csv_filt[df_csv_filt["date"] >= "2025-10-01"]
t_csv_range = time.time() - t0

print(f"[CSV]  体积 {os.path.getsize(csv_path)/1024:.1f} KB | "
      f"读全表 {t_csv_full*1000:.2f} ms | "
      f"日期过滤 {t_csv_range*1000:.2f} ms")

# ========== 方案 B:SQLite ==========
db_path = "kline.db"
conn = sqlite3.connect(db_path)
cur = conn.cursor()
cur.execute("""
CREATE TABLE kline (
    date TEXT NOT NULL,
    stock TEXT NOT NULL,
    open REAL, high REAL, low REAL, close REAL,
    pre_close REAL, volume INTEGER,
    PRIMARY KEY (date, stock)
)
""")
cur.execute("CREATE INDEX idx_kline_stock ON kline(stock)")
cur.execute("CREATE INDEX idx_kline_date ON kline(date)")
conn.commit()

# 批量写入
cur.executemany("""
INSERT INTO kline (date, stock, open, high, low, close, pre_close, volume)
VALUES (:date, :stock, :open, :high, :low, :close, :pre_close, :volume)
""", df.to_dict("records"))
conn.commit()

t0 = time.time()
df_sql = pd.read_sql("SELECT * FROM kline", conn)
t_sql_full = time.time() - t0

t0 = time.time()
df_sql_filt = pd.read_sql(
    "SELECT * FROM kline WHERE date >= ?", conn, params=("2025-10-01",)
)
t_sql_range = time.time() - t0

conn.close()

print(f"[SQLite] 体积 {os.path.getsize(db_path)/1024:.1f} KB | "
      f"读全表 {t_sql_full*1000:.2f} ms | "
      f"日期过滤 {t_sql_range*1000:.2f} ms")

实测输出(演示数据 1000 行):

markdown 复制代码
方案    | 写入 ms | 全表读 ms | 日期过滤 ms | 股票过滤 ms | 体积 KB
---------------------------------------------------------------------------
CSV     |   18.51 |     17.49 |       7.07 |       6.04 |     77.1
SQLite  |   22.74 |      5.59 |       1.03 |       4.03 |    168.0

关键结论:

  • CSV 体积更小(77 KB vs 168 KB),因为 SQLite 要存索引
  • SQLite 查询快得多:日期过滤 1.03 ms(CSV 7 ms,约 7 倍加速)
  • 写入 CSV 略快一点点(22 vs 18 ms),差距可忽略

五、SQLite 实战建库:可复用代码

如果你已经决定用 SQLite,下面这段脚本是日常建库的最小可用版本:

python 复制代码
import sqlite3
from pathlib import Path

DB_PATH = Path("quant.db")

def init_db(path: Path = DB_PATH):
    """幂等建表:重复调用不报错"""
    conn = sqlite3.connect(path)
    cur = conn.cursor()
    # 1. K 线表(最常用)
    cur.execute("""
    CREATE TABLE IF NOT EXISTS kline (
        date TEXT NOT NULL,
        stock TEXT NOT NULL,
        open REAL, high REAL, low REAL, close REAL,
        pre_close REAL, volume INTEGER, amount REAL,
        PRIMARY KEY (date, stock)
    )
    """)
    cur.execute("CREATE INDEX IF NOT EXISTS idx_kline_stock ON kline(stock)")
    cur.execute("CREATE INDEX IF NOT EXISTS idx_kline_date ON kline(date)")

    # 2. 信号/持仓表
    cur.execute("""
    CREATE TABLE IF NOT EXISTS signals (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        date TEXT NOT NULL,
        stock TEXT NOT NULL,
        action TEXT NOT NULL,
        price REAL,
        note TEXT
    )
    """)

    conn.commit()
    conn.close()
    print(f"[ok] {path} 已就绪")

def upsert_kline(rows, path: Path = DB_PATH):
    """批量 upsert:存在则覆盖,不存在则插入"""
    conn = sqlite3.connect(path)
    cur = conn.cursor()
    cur.executemany("""
    INSERT INTO kline (date, stock, open, high, low, close, pre_close, volume, amount)
    VALUES (:date, :stock, :open, :high, :low, :close, :pre_close, :volume, :amount)
    ON CONFLICT (date, stock) DO UPDATE SET
        open=excluded.open, high=excluded.high,
        low=excluded.low, close=excluded.close,
        pre_close=excluded.pre_close,
        volume=excluded.volume, amount=excluded.amount
    """, rows)
    conn.commit()
    conn.close()

# 调用
init_db()
# upsert_kline(df.to_dict("records"))  # 把拉到的 K 线写进去

两个关键设计:

  1. 复合主键 (date, stock):同一只股票同一天只能有一条记录,重复插入自动 upsert
  2. 股票代码存 TEXT :千万不要用 INTEGER,否则 600519 / 000001 这种前导 0 的代码会被截断(这是 CSV 也翻车的同一个坑)

六、选型决策表

场景 推荐 关键理由
学习/原型 < 1 万行 CSV Excel 能打开,调试最快
个人量化研究 1 万 ~ 1 千万行 SQLite 单文件、零部署、查询快
团队/多策略/生产 > 1 千万行 MySQL 并发读写、备份恢复、权限管理
多机器共用同一份数据 MySQL SQLite 文件锁不支持高并发

经验阈值:5000 只 A 股 × 10 年日线 ≈ 1,200 万行,落在 SQLite 上限边缘;如果你打算存分钟级数据,5 年就能把 SQLite 推到千万级,所以上 MySQL 不亏。

八、常见坑

  1. CSV 的 dtypes 推断陷阱 :pd.read_csv("xxx.csv") 默认按值推断类型,600519 可能被识别成 int64 然后丢失前导 0。解决办法 :pd.read_csv(path, dtype={"stock": str}) 显式指定类型。
  2. SQLite 文件锁 :单文件不支持多进程并发写。多策略并行场景 ,要么按策略分库、要么用 WAL 模式(conn.execute("PRAGMA journal_mode=WAL"))。
  3. MySQL 连接池漏掉 :用 SQLAlchemy 时记得 engine.dispose(),否则长时间运行的量化系统会耗尽连接。
  4. 演示数据 ≠ 真实场景:本文测试 1000 行,正式跑 5000 只 × 10 年 ≈ 1200 万行,所有方案性能差距会拉大;建议在选型前先小规模压测。

九、小结

本文用一份实测数据回答了「数据存哪里」的问题。核心结论:

  • < 1 万行:CSV 足够
  • 1 万 ~ 1 千万行:SQLite 是最优解(单文件、零部署、查询快)
  • > 1 千万行 / 多用户:MySQL
  • SQLite 建表必备 :复合主键 (date, stock) + 字段类型显式指定 + 至少 2 个索引

下一篇(#02)我们将解决「数据量大了怎么增量更新」------每天手动跑全量太慢,用 APScheduler 搭一条「只拉新数据」的自动采集流水线。


免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。

代码与文档 :github.com/MaiRuiApi

相关推荐
弈栈录1 小时前
Spring MVC 请求处理流程与核心源码解析
后端·架构·mvc
Sylven1 小时前
【DevOps 开发流程】什么是CI/CD?不同的阶段应当配置哪些CI/CD自动化流程?
后端
鶴哥只手遮天1 小时前
从零搭建光电仿真引擎(五):探测器成像与工程实践
后端
鶴哥只手遮天1 小时前
从零搭建光电仿真引擎(三):大气与热特性系统
后端
明华0491 小时前
保险Agent开发记录
后端
imDwAaY1 小时前
Redis List 是链表吗?从 Ziplist 到 Quicklist 揭开底层实现
redis·后端
_风不会停息1 小时前
剥开 Agent 开发:参照 pi-agent 实现一个小 Agent
人工智能·后端
sp421 小时前
Java 加解密组件再设计
java·后端