云计算百科
云计算领域专业知识百科平台

【Python量化系统工程实战 #01】数据存储选型 CSVSQLiteMySQL 对比与 SQLite 实战建库

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

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

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

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

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

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

二、本文你将得到什么

  • 三种方案实测对比:1000 行日线的写入/查询/体积数据
  • 量化场景选型决策表:按数据规模直接对号入座
  • SQLite 实战建库代码:建表 + 索引 + 批量 upsert,单文件即用
  • 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 路径,对比四种操作的耗时与体积。完整测试代码如下:

    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 行):

    方案 | 写入 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,下面这段脚本是日常建库的最小可用版本:

    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 线写进去

    两个关键设计:

  • 复合主键 (date, stock):同一只股票同一天只能有一条记录,重复插入自动 upsert
  • 股票代码存 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 不亏。

    八、常见坑

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

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

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

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


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

    代码与文档:https://github.com/MaiRuiApi

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Python量化系统工程实战 #01】数据存储选型 CSVSQLiteMySQL 对比与 SQLite 实战建库
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!