系列开篇:从「会写策略」推进到「会搭系统」。本篇先解决最基础的存储选型——回测和实盘都要把 K 线落到本地,三种方案怎么选?
一、为什么存储选型是工程化的第一道坎
很多量化新手写完策略,跑完回测就把 DataFrame 留在内存里——重启一次,模型全没了。或者更糟:用 Excel 存了几万行,每次打开都卡十几秒,等到发现数据已经过时一星期了。
当你从「研究脚本」走向「自动跑起来」,存储必须先定下来:
- CSV:最直观,Excel 也能打开,但量大了会卡、类型推断会错
- SQLite:单文件数据库,零部署,1 千万行也能秒级查询
- MySQL:生产级,但要装服务、搞账号、做备份
本文给你一份实测对比 + 选型决策表,并带一份 SQLite 实战建库脚本,看完就能选对。
二、本文你将得到什么
三、对比方案概览
| 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 线写进去
两个关键设计:
六、选型决策表
| 学习/原型 < 1 万行 | CSV | Excel 能打开,调试最快 |
| 个人量化研究 1 万 ~ 1 千万行 | SQLite | 单文件、零部署、查询快 |
| 团队/多策略/生产 > 1 千万行 | MySQL | 并发读写、备份恢复、权限管理 |
| 多机器共用同一份数据 | MySQL | SQLite 文件锁不支持高并发 |
经验阈值:5000 只 A 股 × 10 年日线 ≈ 1,200 万行,落在 SQLite 上限边缘;如果你打算存分钟级数据,5 年就能把 SQLite 推到千万级,所以上 MySQL 不亏。
八、常见坑
九、小结
本文用一份实测数据回答了「数据存哪里」的问题。核心结论:
- < 1 万行:CSV 足够
- 1 万 ~ 1 千万行:SQLite 是最优解(单文件、零部署、查询快)
- > 1 千万行 / 多用户:MySQL
- SQLite 建表必备:复合主键 (date, stock) + 字段类型显式指定 + 至少 2 个索引
下一篇(#02)我们将解决「数据量大了怎么增量更新」——每天手动跑全量太慢,用 APScheduler 搭一条「只拉新数据」的自动采集流水线。
免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。
代码与文档:https://github.com/MaiRuiApi
网硕互联帮助中心


评论前必须登录!
注册