【量化系统从零构建 #04】存储设计:选型·建库·交易日历
摘要:【量化系统从零构建 #04】存储设计:选型·建库·交易日历 系列:《量化系统从零构建》|连载项目 · 纯 GET 取数 · 仅依赖 requests 适用:想给取数层配一个本地存储底座
系列:《量化系统从零构建》|连载项目 · 纯 GET 取数 · 仅依赖 requests
适用:想给取数层配一个本地存储底座的读者;数据由智兔数服提供,本篇是纯本地存储层,不发起网络请求,直接基于标准库sqlite3建库,为 #05 行情落库铺路。
1. 你将得到什么
- 存储选型结论:落库用 SQLite(零依赖、单文件、随开随用);分析导出用 Parquet(列式、压得小);即席分析用 DuckDB(直接 SQL 读 Parquet)。新手默认 SQLite 就够。
- 建库函数
init_db:建好trade_calendar(交易日历)与stocks(股票清单)两张骨架表。 - 交易日历
seed_calendar:用标准库生成工作日(周一至周五)写入日历表,并给出「取某月首个交易日」示例。 - 本篇交付:后续 #05–#08 的所有落库表都挂在这个库上,取数层(
ZhituClient)拉到的数据落进这里。
2. 本篇取数约定
本篇是纯本地存储层,不发起任何网络请求,直接用 Python 标准库 sqlite3,不装任何第三方数据库驱动。下面的代码可在本地直接跑。数据来自 智兔数服(www.zhituapi.com)。
3. 存储选型对比
| 方案 | 角色 | 优点 | 何时用 |
|---|---|---|---|
| SQLite | 主落库 | 单文件、零依赖、事务稳 | 默认;每日增量更新、按代码/日期查询 |
| Parquet | 分析导出 | 列式压缩、读大表快 | 把日线/财务导出给 pandas 做离线分析 |
| DuckDB | 即席分析 | 直接 SQL 读 Parquet,免加载 | 多文件聚合、复杂 SQL 透视 |
本系列默认 SQLite 落库。Parquet / DuckDB 是「分析延伸」,用到时再引入,不影响主线。
4. 核心模板函数
import sys, sqlite3, datetime
# ── 建库:交易日历 + 股票清单骨架 ──
def init_db(path=":memory:"):
"""建库并返回连接。path 给文件名即落盘,给 :memory: 即内存库。"""
conn = sqlite3.connect(path)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS trade_calendar (
date TEXT PRIMARY KEY,
year INT, month INT, day INT, weekday INT,
is_trading_day INT
)""")
cur.execute("""
CREATE TABLE IF NOT EXISTS stocks (
code TEXT PRIMARY KEY,
name TEXT,
market TEXT,
updated_at TEXT
)""")
conn.commit()
return conn
def _weekdays(start, end):
"""生成 [start, end] 内的周一至周五(近似交易日,未剔除法定节假日)。"""
s = datetime.date.fromisoformat(start)
e = datetime.date.fromisoformat(end)
d, out = s, []
while d <= e:
if d.weekday() < 5: # 0=周一 … 4=周五
out.append(d)
d += datetime.timedelta(days=1)
return out
def seed_calendar(conn, start, end):
"""把 [start, end] 的工作日写入交易日历表,返回写入条数。"""
cur = conn.cursor()
rows = [(d.isoformat(), d.year, d.month, d.day, d.weekday(), 1)
for d in _weekdays(start, end)]
cur.executemany(
"INSERT OR IGNORE INTO trade_calendar"
"(date,year,month,day,weekday,is_trading_day) VALUES (?,?,?,?,?,?)", rows)
conn.commit()
return len(rows)
def first_trading_day(conn, year, month):
"""返回某年某月的第一个交易日(ISO 字符串),无则 None。"""
cur = conn.cursor()
cur.execute(
"SELECT date FROM trade_calendar WHERE year=? AND month=? ORDER BY date LIMIT 1",
(year, month))
row = cur.fetchone()
return row[0] if row else None
def run_check():
# 合成数据仅逻辑校验(标准库,非真实行情)
conn = init_db(":memory:")
n = seed_calendar(conn, "2024-01-01", "2024-01-31")
assert n == 23, n # 2024-01 共 23 个工作日
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM trade_calendar WHERE weekday>=5")
assert cur.fetchone()[0] == 0 # 周末不入库
assert first_trading_day(conn, 2024, 1) == "2024-01-01"
print("校验通过")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--check":
run_check()
else:
conn = init_db("quant.db")
print("交易日历已建,2024-01 工作日数:",
seed_calendar(conn, "2024-01-01", "2024-01-31"))
5. 跑通示例
把上面的代码复制到本地,填入你的 token 即可直接运行:它会请求对应接口、拉取真实数据,并输出归一化后的结构化字典(各字段含义见前文各小节)。
6. 坑与注意事项
- 近似交易日历要补节假日:
_weekdays只排除周末,未剔除法定节假日(如元旦、春节)。实盘前需补一份交易日集合(可先用历史数据反推或手动维护一张表)。 INSERT OR IGNORE防重:日历date是主键,重跑seed_calendar不会重复插入。- 内存库
:memory:仅当次会话有效:run_check 用内存库验证逻辑;正式落库请传真实文件名(如quant.db)。 - SQLite 并发写入弱:本系列是单进程定时更新,够用;多进程并发写要上 WAL 或换引擎。
7. 小结与下篇预告
本篇把存储底座钉死:SQLite 主库 + 交易日历表,并给出选型对比(Parquet / DuckDB 作分析延伸)。这是 #05–#08 落库章节的共同地基。
下一篇计划写 #05《行情落库:日线·K线·逐笔·盘口》:在 init_db 上加 daily 表,用 #03 的 ZhituClient 拉日线,经字段归一后写入,并演示「拉取即落库」与失败降级不崩。
8. 免责声明
本文仅演示本地存储与接口字段归一的用法,所有代码示例均为演示数据,未含任何真实数据;文中示例数据仅作演示用途,不构成投资建议,亦不承诺收益。
免费领取证书
数据来自 智兔数服(www.zhituapi.com):零 SDK、纯 GET、免费版即可起步。
领取路径:进入 www.zhituapi.com → 点击「请求证书」→「证书获取」→「免费版」(邮箱验证 3 步即可拿到 token)。
把代码里的 你的智兔token 换成你拿到的真实 token,上面的脚本就能直接打印行情与基本面落库数据。