AI Agent 量化课程 · 第 2 课

看懂数据仓库怎么管

从一份文件,到批量下载、检查、查询的数据仓库

课程结束时:你会拿到 4 个期货主力连续合约 × 2 个频率共 8 份真实数据, 并且能说清 Agent 为什么把一句话拆成 8 个任务、怎么用 DuckDB 一次盘点全部文件。
预习内容(上课前想一想):
  1. 什么是股票和期货中的复权和除权?
  2. 什么是 Parquet 数据格式?和文本数据、数据库的数据格式最大的区别是什么?
  3. 什么是 DuckDB?它的应用场景是什么?
这节课结束时

你能做到

  • 说清楚数据一多之后,"能保存"为什么不够用
  • 通过 AI 完成批量的数据读写
  • 看懂 Agent 怎么把一句话拆成多个数据任务、拿出什么证据证明完成
  • 说清楚 Parquet 和 DuckDB 在这个项目里解决的是什么问题
今天不讲任何因子或策略知识——数据仓库是下一课"计算因子"的地基。
组块 A:数据契约与存储规则 组块 B:批量读写与自动拆分 综合应用:真实案例 + DuckDB
开场

没有框架时,AI 会把数据放成这样

没有 QuantSpace:AI 自己现场决定往哪存

600000.csv 600000_new.csv 600000_final.csv etf_data_复权后.xlsx test2.parquet backtest_final_really_final.csv

用了 QuantSpace:AI 照约定存

data/market/ ├── 1d/ │ └── CFFEX.IF99.parquet ├── 1d_adj/ │ └── CFFEX.IF99.parquet └── 5m/ └── CFFEX.IF99.parquet
左边这些文件里,哪份是最新的?哪份复权了?哪份是日线、哪份是分钟线? 如果连你自己都要想一想,AI 看到这个目录,能知道该读哪份数据吗?
两边用的是同一个 AI coding 工具——差别不是 AI 变聪明了,是右边多了一套 QuantSpace 定好的目录规则可以照着做。没有框架,AI 只能自己现场决定往哪存、叫什么名字,用得越久越乱; 有了框架,AI 每次都照同一套约定存,数据自然待在该待的地方。这就是量化框架真正的作用,也是这节课要讲的东西。
组块 A · 数据身份

复权:股票、ETF 和期货,问题不一样

复权不是"美化"价格,而是消除非交易原因造成的机械价格跳变——但"非交易原因"在不同资产上不是一回事。

股票 / ETF:修正公司行为
  • 分红、送转、拆股 → 除权日价格机械跳变
  • 前复权:保持最新价格不变,缩放历史价格,让今昔可比
  • 真实实现:PandaDataClient.get_adj_factor() 取复权因子表 → skills.compute.adjust.forward_adjust() 计算 → 存入 1d_adj/
期货:修正连续合约换月
  • 单个合约本身没有分红拆股,但主力连续合约会在到期换月时切到新合约
  • 新旧合约往往存在升水/贴水价差,直接拼接会出现"非市场"的跳点
  • 本课批量案例用的是主力连续合约的原始价格(不复权);换月拼接的具体 算法本课不展开(见"本课边界")
共同结论:不管哪种资产,复权状态都是数据身份的一部分—— 必须写进目录名,读取时显式声明,不能让程序去猜。
组块 A · 数据身份

频率与复权,必须写进目录名

是否复权会直接改变收益率和回测结果,必须像 1d5m 一样成为数据身份的一部分—— QuantSpace 用目录而不是文件名后缀来承载这个身份。

目录含义
market/1d/原始日线(不复权)
market/1d_adj/复权后的标准日线,不覆盖原始行情
market/5m/ / 5m_adj/5 分钟线的原始 / 复权版本
DataManager().save_symbol(
    symbol, df, frequency="1d")
DataManager().read_symbol(
    symbol, frequency="1d")

路径 = market/<frequency>/<symbol>.parquet

为什么这么设计:frequency 是显式参数,不是 从数据里猜出来的——调用方必须自己说清楚"这是哪个频率",读取时也要显式传 frequency="1d_adj" 这样的口径,不能让程序或 Agent 猜;② 目录 = frequency、文件名 = symbol,所以 list_symbols(frequency=...) 只需要列一个 目录就行——不再是开场那种"每个脚本自己拼路径",统一交给 DataManager 一个入口管。
组块 A · 全流程总览

一句话,是怎么变成结果的?

  1. ① 收到请求——用户一句自然语言说清标的、频率、时间范围
  2. ② 读取规则——渐进式披露:先读 AGENTS.md,再读 skills/ingestskills/storeSKILL.md,确定用哪些接口、目录契约是什么
  3. ③ 拆分任务——symbol 转换成 PandaData 代码,"N 个标的 × M 个频率"变成 N×M 个具体数据任务
  4. ④ 检查 → 下载 → 验证——list_symbols()/read_symbol() 查本地是否已有; 缺失才下载、save_symbol() 写入;逐文件 validate_ohlcv() 质检
  5. ⑤ 组装 panel——全部任务通过后 read_symbols() 合并成 (symbol, eob) 面板
  6. ⑥ 返回证据——不是"完成了",而是文件数、行数、日期覆盖、质量检查结果

下面每一步怎么走的,拖一拖、点一点自己看;组块 B 接下来放大讲第 ③④⑤ 步, 第 ⑥ 步留到综合应用环节对照真实验收表。

组块 B · 放大讲第 ④ 步

检查 → 下载 → 验证:一个任务具体怎么走

CFFEX.IF99 · 1d 这一个任务为例,八个任务都是同一套逻辑:

dm.list_symbols(frequency="1d") 查本地已有哪些 symbol, 判断 "CFFEX.IF99" 在不在里面 缺失才下载:fetch_market_data(..., type="future") dm.save_symbol("CFFEX.IF99", df, frequency="1d") 写入 Parquet validate_ohlcv(df) 检查 NaN/Inf/负值/ high<low/时间是否单调递增
validate_ohlcv() 只负责报告,不会替你 改数据——返回一份 DataQualityReport.passedTrue 才算这个 任务真的完成;不通过就要回头看取数或转换环节,不能假装没看见。
组块 B · 放大讲第 ⑤ 步

组装 panel:8 个文件怎么拼成一张表

dm.read_symbols(
    ["CFFEX.IF99", "SHFE.RB99", "DCE.I99", "CZCE.MA99"],
    frequency="1d")
  • 逐个 symbol 调用 read_symbol(),加一列 symbol,把索引变成 (symbol, eob) 两级,concat 到一起再按索引排序——这就是本课反复 出现的 (symbol, eob) panel 格式
  • 缺文件不是读到哪报到哪:会把这次请求里所有缺失的 symbol 收集齐,一次性抛出一个 FileNotFoundError 列出全部缺的文件,不是修一个报一个
今天的真实案例:4 个标的 × 1d 全部验证通过后, 拼出的日线 panel 形状是 (968, 5)——下一页真实验收表会看到同一个数字。
panel 拼出来了,"完成"还不是 Agent 说了算——必须拿出 四样具体证据:文件数、每个 symbol 的行数、日期覆盖范围、质量检查结果。
不看笔记回答 · 独立完成
如果把"帮我下载 4 个期货主力连续合约在 2023 年全年的日线和 5 分钟线数据"发给 Agent——
1. 这句话会被拆成几个数据任务?
2. Agent 最终应该拿出哪几样具体证据,才能证明这批数据真的可用?
综合应用 · 真实案例

这句话,最后变成了什么?

来自一次真实 sub-agent 测试(在当前 QuantSpace 项目中实际执行,非虚构):

学员:帮我下载 CFFEX.IF99、SHFE.RB99、DCE.I99、CZCE.MA99 在 2023 年全年的日线和 5 分钟线数据,整理到项目数据目录。
下一页看真实验收表:8 个文件到底对不对。

真实验收表

symbol1d 行数5m 行数日期覆盖质量
CFFEX.IF9924211,6162023-01-03 → 12-29PASS
SHFE.RB9924216,5542023-01-03 → 12-29PASS
DCE.I9924216,5542023-01-03 → 12-29PASS
CZCE.MA9924216,5542023-01-03 → 12-29PASS
8 个 Parquet,共 62,246 行;日线 panel 形状 (968, 5), 5 分钟 panel 形状 (61278, 5)。现在拿出你的预测清单:任务数对上了吗?
综合应用 · 动手看看 1/2

为什么用 Parquet(一):读的时候能跳着读

真实数据:CFFEX.IF99 1 分钟线(58,080 行)——用 pd.read_parquet() 读比 pd.read_csv() 快约 16 倍。

综合应用 · 动手看看 2/2

为什么用 Parquet(二):文件为什么更小

整份文件(9 列)合起来:Parquet 1.3MB vs CSV 4.2MB,压缩比约 3.3 倍—— 这是每一列压缩效果天差地别(285B ~ 523KB 不等)之后的平均结果。

动手看看:为什么用 DuckDB

Parquet 与 DuckDB:怎么配合

打个比方:把数据仓库想象成一座图书馆,两种查法差在哪,一眼就能看出来。

CSV:书全堆在一条传送带上
  • 不分类别、不分年份,什么书都混在一起
  • 要找"2015 年的历史书",只能从头到尾一本本翻
  • DuckDB 碰到 CSV 也没办法,照样得老老实实读完整个文件
Parquet:按类别分区,架子上还贴着标签
  • 小说、历史各占一个区——按列存放,不相关的"区"(列)直接不用进
  • 每层架子贴着"这一层是 2015 年"——每段数据自带取值范围,对不上的"层"(段) 扫一眼标签就跳过
  • DuckDB 就是那位只翻标签、不搬书的管理员:SELECT/GROUP BY 用不到的区不进,范围对不上的层不看
"跳段 + 跳列"合起来,才是不用先把全部原始数据搬进内存的真正原因—— 不是 DuckDB 有魔法,是 Parquet 早就把"书"摆成了方便这样查的样子。
DuckDB 不替代 save_symbol() 写入 不替代 validate_ohlcv() 质检 不维护常驻 .duckdb 文件,每次临时连接

选择口诀:读一个 symbol、继续做 Python 计算 → Pandas/DataManager;跨很多 Parquet 做筛选、盘点、聚合 → DuckDB。

四个组件,一条责任链

组件解决什么
store skillAgent 该用哪些入口和契约
Parquet行情怎样高效、稳定地保存
DataManager路径、读写、panel、查询统一管理
DuckDB跨多份 Parquet 做筛选和聚合
用自己的话写一句话:今天我学到的最重要一件事是……(不能照抄大纲文字)
课后作业 · 间隔重复

今天的课后作业

  1. 挑 3 到 5 个品种(期货,A 股/ETF 都可以),用一句自然语言请求让 Agent 批量下载到项目目录。
  2. DuckDB 和 Parquet 在项目中的作用是什么?它们之间有什么关系?
  3. 什么是除权和复权?
下节预告

从数据仓库,到第一个因子

第 3 课:把今天这份 (symbol, eob) panel 里的 OHLCV,转换成因子。

检查-补齐-验证、拿证据而不是听宣称——这套心智模型不会变, 变的只是从"管理数据"到"计算因子"。