一个 Python 量化项目最容易被低估的工作,不是策略代码,而是行情数据进入系统之后应该如何组织。
最初只研究一只股票时,直接把行情放进一个 DataFrame 就可以开始计算指标。但随着研究范围扩大,项目可能逐渐涉及数百只股票、多个市场、不同周期的 K 线,以及多个版本的复权口径。
如果所有数据都由策略脚本直接请求、清洗和计算,后续就很容易出现重复请求、数据口径不一致、难以复现实验等问题。
因此,搭建量化项目时,除了确定需要哪些行情,还应该提前考虑一个问题:
怎样组织数据,才能让策略从一个简单的 Python 脚本逐步扩展为可维护的研究系统?
答案不一定是搭建复杂的数据平台。对于个人开发者和小型研究项目,一个分层清晰、职责明确的数据流程,通常已经足够。
一、把行情获取和策略逻辑分开
先看一种常见的写法:
python
from quantdash import QuantDash
qd = QuantDash(api_key="your_api_key")
df = qd.klines.get(
"600519.SH",
period="1d",
count=100,
to_dataframe=True,
)
df["ma20"] = df["close"].rolling(20).mean()
df["signal"] = (
df["close"] > df["ma20"]
).astype(int)
这段代码适合演示基本操作,但如果项目中的每个策略都直接请求行情,那么数据接入逻辑就会散落在不同文件里。
例如:
- 策略 A 使用前复权数据。
- 策略 B 使用默认价格口径。
- 策略 C 自行删除空值。
- 策略 D 对重复日期采用不同的处理方式。
即使这些策略研究的是同一个标的,也可能因为数据处理方式不同而产生不同的结果。
更合理的方式,是将系统拆成几个逻辑层:
text
金融数据接口
|
v
数据获取层
|
v
标准化与质量检查层
|
v
行情存储层
|
v
研究数据集
|
v
指标计算与策略层
|
v
回测与结果分析
这些层并不意味着必须部署多个服务。对于单人项目,它们可以只是不同的 Python 模块。
关键在于:每一层只负责自己应该负责的事情。
二、第一层:数据获取应该关注什么?
数据获取层负责向数据源请求行情,并处理与请求有关的问题。
它的主要职责包括:
- 根据标的代码和周期请求行情。
- 传递需要的时间范围。
- 记录请求使用的数据参数。
- 处理认证错误和请求失败。
- 将获取结果交给后续处理环节。
这一层不应该负责计算交易信号,也不应该决定策略什么时候买入或卖出。
例如,使用 QuantDash 官方公开的 Python SDK,可以将客户端初始化和行情获取集中在一个模块中:
python
import os
from quantdash import QuantDash
def create_client():
api_key = os.getenv("QUANTDASH_API_KEY")
if not api_key:
raise RuntimeError(
"缺少环境变量 QUANTDASH_API_KEY"
)
return QuantDash(api_key=api_key)
def fetch_daily_bars(client, symbol, count=100):
return client.klines.get(
symbol,
period="1d",
count=count,
to_dataframe=True,
)
这里的函数只是把行情请求封装起来,没有假设 API 一定成功,也没有声称返回数据已经通过质量检查。
实际项目还应根据需要补充日志、错误分类和参数验证。
如果后续更换数据源,策略层就不必跟着修改每一处请求代码,而只需要调整数据接入模块与标准化模块。
三、第二层:标准化数据,不等于把所有数据变成一样
不同市场的数据可能存在字段、时间、价格口径和交易规则上的差异。
标准化的目的,是为研究代码提供一套明确的内部约定,而不是消除所有市场差异。
例如,可以统一内部使用以下字段名称:
text
symbol
trade_date
open
high
low
close
volume
但还应记录或明确其他重要属性:
- 标的所属市场。
- 行情周期。
- 价格复权方式。
- 时间字段的语义。
- 成交量和成交额的单位。
- 数据来自哪个数据源。
- 数据在什么时间被获取或更新。
如果两个 DataFrame 都有 close 列,也不意味着它们的价格可以直接比较。
一份数据可能是不复权价格,另一份可能是前复权价格。另一种情况是,两个数据集分别采用不同的时间边界。
因此,标准化不仅是重命名字段,还包括明确字段背后的含义。
一个实用的 DataFrame 规范
可以为研究层定义最基本的数据约定:
- 一行代表一个标的在一个时间点或时间区间上的行情记录。
- 每条记录必须能够确定对应的标的和时间。
- 时间排序规则必须一致。
- 价格口径必须可追溯。
- 重复记录必须经过检查。
- 数据缺失应有明确处理规则,不能默认全部填零。
这些约定可以写进项目文档,并通过自动化检查执行。
四、第三层:行情存储应该选择什么?
当研究从一只股票扩展到多个标的时,每次运行策略都重新下载全部历史行情,可能会产生不必要的重复工作。
这时可以考虑将行情保存在本地。
对于初期项目,常见选择有三种。
| 存储方式 | 适合场景 | 主要限制 |
|---|---|---|
| CSV | 小规模数据、手动检查、跨工具交换 | 类型管理较弱,反复读取大型文件可能不够方便 |
| Parquet | Pandas 数据处理、重复研究、列式读取 | 需要明确数据分区和版本管理方式 |
| SQLite 等关系型数据库 | 需要结构化查询、主键约束和事务管理 | 需要设计表结构、索引和写入流程 |
没有必要在项目第一天就引入复杂的数据库系统。
如果只是研究少量日线行情,可以先从 CSV 或 Parquet 开始。随着数据规模和查询需求增长,再决定是否需要数据库。
选择时应关注实际工作负载,而不是仅仅追求技术栈复杂度。
缓存与历史数据存储不是一回事
这两个概念经常被混淆。
缓存主要用于减少重复获取或重复计算。它可能随时被重新构建。
历史数据存储则应服务于研究的可重复性,需要明确数据的版本、来源和处理口径。
如果某个历史数据文件在每次请求时都被直接覆盖,即使它被称为缓存,也可能让以前的回测无法复现。
因此,对于重要的研究实验,建议保存数据快照,或者至少记录数据集的版本标识和获取时间。
五、如何设计最小的数据目录?
一个初期 Python 量化项目可以采用下面这样的结构:
text
quant_project/
├── config/
│ └── settings.py
├── data/
│ ├── raw/
│ ├── processed/
│ └── manifests/
├── src/
│ ├── data/
│ │ ├── client.py
│ │ ├── normalize.py
│ │ └── validate.py
│ ├── research/
│ │ └── indicators.py
│ └── strategies/
│ └── moving_average.py
├── tests/
├── notebooks/
└── requirements.txt
这是一个通用项目组织示例,不是任何数据平台的官方目录要求。
目录的含义如下:
raw/:保留获取后、尚未进行研究处理的数据。processed/:保存经过标准化和必要清洗的数据。manifests/:记录数据集的来源、参数、版本和处理规则。data/:集中管理行情相关逻辑。research/:放置指标计算等研究工具。strategies/:放置策略规则。tests/:保存数据校验和策略逻辑测试。notebooks/:用于探索性分析,不承担全部生产逻辑。
对于较大的数据集,还可以按市场、周期和日期进行分区。
但不要过早建立大量目录。如果当前项目只有一个研究脚本,过度拆分反而会增加维护成本。
六、数据集需要记录哪些元信息?
仅仅保存一份 Parquet 文件,通常不足以完整描述一次量化研究所使用的数据。
假设两次回测使用同样的策略代码,却得到了不同结果。问题可能来自策略参数,也可能来自行情更新、复权口径变化或者历史数据修订。
如果没有数据元信息,很难迅速判断原因。
建议至少记录:
| 元信息 | 示例或说明 |
|---|---|
| 数据集名称 | A 股日线研究数据 |
| 标的范围 | 明确的标的列表或标的池 |
| 时间范围 | 数据实际覆盖的起止日期 |
| 行情周期 | 日线、5 分钟线等 |
| 价格口径 | 前复权、不复权等 |
| 数据来源 | 数据平台或原始文件来源 |
| 获取时间 | 数据下载或更新的时间 |
| 处理版本 | 清洗与标准化代码的版本 |
| 完整性检查 | 检查时间及异常记录数量 |
如果研究结果需要长期复现,还可以记录文件哈希、代码提交版本和策略参数。
这些元信息并不会自动保证数据正确,但可以让错误更容易被定位。
七、如何避免策略直接依赖数据接口?
数据层的一个重要设计目标,是让策略使用统一的内部数据结构,而不是依赖某个具体数据源的调用细节。
例如,策略只需要接收一个符合内部约定的 DataFrame:
python
import pandas as pd
def calculate_ma_signal(
df: pd.DataFrame,
window: int = 20,
) -> pd.DataFrame:
result = df.copy()
result["ma"] = (
result["close"]
.rolling(window=window)
.mean()
)
result["signal"] = (
result["close"] > result["ma"]
).astype(int)
return result
这个函数不负责请求行情,也不负责决定订单如何成交。
只要传入的数据符合预期,就可以用于不同的数据源、不同的存储格式以及固定的测试数据集。
不过,示例中的信号只是说明数据层与策略层如何解耦,并不是完整的交易策略。真实回测仍需要考虑信号可用时间、持仓变化、交易成本和成交模拟。
八、QuantDash 应该放在架构的哪一层?
如果项目需要接入 A 股、ETF、港股或美股行情,可以考虑使用金融数据 API 完成数据获取。
QuantDash 提供 Python SDK,并支持通过 DataFrame 获取行情。对于以 Pandas 为核心的数据研究项目,这类接口可以作为数据获取层的一个实现。
在架构中,QuantDash 不需要直接承担全部研究逻辑。更清晰的方式是:
text
QuantDash Python SDK
|
v
行情获取模块
|
v
内部字段与数据质量检查
|
v
本地存储或研究数据集
|
v
Pandas 指标与策略代码
这种设计的好处是,数据接口与策略代码之间存在明确的边界。
如果将来更换数据源,可以保留大部分指标和策略逻辑。
需要注意的是,使用 API 并不会自动解决所有数据工程问题。请求是否成功、数据是否完整、不同市场的时间是否对齐,以及回测是否存在前视偏差,都仍然需要单独处理。
项目选型时,可以通过 QuantDash 官网、技术文档和官方 GitHub 示例核对所需接口、参数和权限。
结论:先建立清晰边界,再考虑扩展规模
从零搭建 Python 量化项目,不必一开始就建设复杂的数据基础设施。
先将数据获取、标准化、质量检查、存储和策略逻辑分开,再根据实际研究需求选择 CSV、Parquet 或数据库。
当项目开始积累多个策略、多个市场和不同版本的数据时,元信息和可复现性会变得越来越重要。
一个可扩展的量化项目,不是因为使用了复杂的架构,而是因为行情从进入系统到产生研究结果的每一步,都有明确的职责和可验证的规则。
QuantDash 官方文档
- QuantDash 技术文档 --- 查看 Python SDK、REST API 和数据接口说明。