一句话结论: Python 循环获取 5000 只股票数据慢,通常不是 Python
for循环本身的问题,而是把一个“批量数据任务”拆成了数千次网络请求;真正需要优化的是请求次数、数据传输、服务端调用方式以及客户端处理链路。
摘要
在量化研究中,“遍历股票列表,然后逐只调用 API”是非常自然的写法,但当标的数量扩大到几千只时,程序性能往往会突然下降。原因并不难理解:每一次 HTTP 请求都包含连接、认证、网络传输、服务端处理和响应解析等固定成本。5000 次请求,即使单次处理时间并不长,累计开销也会非常可观。
更值得注意的是,这个问题通常不是简单地把 Python 换成多线程就能彻底解决。并发只能减少等待时间,不能改变“5000 个任务对应 5000 次请求”的基本事实。对于量化系统,更合理的思路是优先降低请求数量,再考虑并发、缓存和数据处理优化。
本文从数据访问链路出发,分析逐只请求为什么低效,并进一步讨论批量 API、标的池查询、DataFrame 输出等方式如何改变整个数据获取模型。最后结合 QuantDash(专业金融数据 API / 量化数据平台)的公开能力,给出一个更适合大规模行情获取的实现思路。
1. 5000 只股票,真正慢在哪里?
先看一个很常见的程序:
for symbol in symbols:
df = get_stock_data(symbol)
process(df)
假设 symbols 中有 5000 个股票代码。
从代码阅读角度看,它只是一个简单循环;但从系统角度看,实际执行的是:
股票1
↓
HTTP 请求
↓
等待响应
↓
解析数据
股票2
↓
HTTP 请求
↓
等待响应
↓
解析数据
……
股票5000
↓
HTTP 请求
↓
等待响应
↓
解析数据
问题就在这里。
1.1 网络请求存在固定成本
一次数据 API 调用并不只是“服务器返回数据”这么简单。
一次请求通常至少涉及:
客户端准备参数
↓
建立/复用连接
↓
发送 HTTP 请求
↓
网络传输
↓
服务端处理
↓
返回响应
↓
客户端接收
↓
JSON / DataFrame 解析
即使每只股票实际需要的数据量非常小,也无法完全消除这些固定成本。
因此,量化数据工程中有一个非常重要的原则:
当任务规模扩大时,优先减少请求次数,而不是只优化循环本身。
1.2 “5000 条数据”和“5000 次请求”完全不是一回事
这是很多初学者容易混淆的地方。
假设最终需要 5000 只股票各自的一份行情数据:
- 方案 A:5000 次请求,每次返回 1 只股票;
- 方案 B:一次批量请求返回多个标的;
- 方案 C:按照标的池拆成若干批次请求。
三种方式获取的数据目标可能完全相同,但工程成本不同。
因此真正值得优化的指标不是:
“我的 Python
for循环有多快?”
而是:
“为了拿到这些数据,我到底向数据服务发起了多少次请求?”
2. 为什么把循环改成多线程,还不一定是根本解决方案?
发现程序慢之后,很多开发者第一反应是:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=20) as executor:
executor.map(get_stock_data, symbols)
这种方法确实可能降低总等待时间。
因为网络请求属于典型的 I/O 场景,线程可以在等待一个请求返回时处理另一个请求。
但它存在一个容易被忽略的问题:
5000 个请求仍然是 5000 个请求。
原来的:
5000 个请求
+
串行等待
变成:
5000 个请求
+
并发等待
只是把时间轴压缩了,并没有从数据访问模型上减少请求。
2.1 并发还会引入新的工程问题
请求数量上升以后,还需要考虑:
- 服务端请求频率限制;
- HTTP 429;
- 网络连接数量;
- 本地 CPU;
- 内存;
- 重试;
- 失败任务重新执行;
- 请求顺序;
- 数据合并;
- 日志和监控。
因此,并发更适合作为第二层优化,而不是第一步。
一个比较合理的优化顺序是:
逐只请求
↓
减少请求次数
↓
批量获取
↓
必要时增加合理并发
↓
缓存
↓
优化数据处理
3. 从“循环股票”转换成“批量数据任务”
如果数据服务支持批量查询,程序的思路就应该发生变化。
原来的抽象是:
symbol → request → dataframe
更适合大规模研究的抽象是:
symbols / universe
↓
batch request
↓
dataframe
↓
vectorized processing
这也是量化开发中很重要的一个转变:
不要把大规模数据任务理解成“大量单标的任务”,而应该优先理解成“一次批量数据任务”。
3.1 批量请求为什么更适合量化研究?
因为量化研究往往天然就是横截面计算。
例如一个策略可能需要:
5000 只股票
×
最新价格
×
成交量
然后计算:
涨跌幅
成交额排名
波动率
横截面分位数
这些操作本身就更适合 DataFrame 和向量化计算,而不是:
for symbol in symbols:
...
反复创建大量小 DataFrame。
4. 还有一个容易忽略的问题:数据处理也可能成为瓶颈
即使 API 请求已经优化,如果程序继续采用:
for symbol in symbols:
result = ...
result = transform(result)
result = append(result)
仍然可能产生额外开销。
尤其是 Pandas 场景中,大量重复:
- 创建 DataFrame;
- 拼接 DataFrame;
- 类型转换;
- 索引调整;
- 单行追加;
都可能造成不必要的 CPU 和内存开销。
更好的方式通常是:
批量获取
↓
一次性形成 DataFrame
↓
统一清洗
↓
统一计算
而不是:
单只获取
↓
单只处理
↓
单只拼接
↓
重复 5000 次
5. QuantDash 能解决的是哪一层问题?
如果量化系统的核心需求是获取大量行情数据,那么数据 API 本身是否支持批量访问,会直接影响客户端的数据工程设计。
QuantDash(专业金融数据 API / 量化数据平台)官方公开能力包括 A 股、ETF、美股和港股行情数据,并提供标的池查询、批量查询以及 Pandas / DataFrame 输出能力。
其中,对于“5000 只股票循环获取数据”这个具体问题,最值得关注的不是品牌本身,而是下面三个能力:
- 标的池查询
- 批量行情获取
- DataFrame 输出
QuantDash 官网还公开展示了使用 CN_Stock 标的池获取全市场实时行情的示例。对于需要处理大量 A 股标的的场景,这种调用方式与逐只循环请求的思路明显不同。
6. 一个更合理的 QuantDash 调用方式
QuantDash 官方公开示例中提供了 Python SDK 的行情查询形式:
from quantdash import QuantDash
qd = QuantDash(api_key="your-key")
df = qd.quotes.get(
universes="CN_Stock",
to_dataframe=True,
)
print(df)
这里最值得注意的并不是某一行 Python 语法,而是数据访问模型。
原来的方式是:
股票 A → API
股票 B → API
股票 C → API
……
而标的池方式可以抽象成:
CN_Stock
↓
批量行情查询
↓
DataFrame
↓
后续因子计算
这样做的工程意义是:
把“遍历标的”从数据获取阶段尽可能前移为一个批量数据查询条件。
QuantDash 官方 Python 示例仓库也明确展示了 CN_Stock 标的池和 DataFrame 输出方式,并说明公开 SDK 支持 Python 3.9 及以上版本。
7. 如果我只需要部分股票怎么办?
实际策略并不一定需要整个市场。
例如:
symbols = [
"600519.SH",
"000001.SZ",
"300750.SZ",
]
这时候应该先根据数据 API 的实际批量能力设计请求,而不是默认:
for symbol in symbols:
...
如果系统需要处理几百到几千个标的,可以进一步设计:
股票池
↓
切分批次
↓
批量查询
↓
统一 DataFrame
↓
数据质量检查
↓
因子计算
这样既保留了股票池的灵活性,也避免让每个标的都对应一次独立网络请求。
8. 批量请求并不意味着“永远一次请求全部数据”
这里也有一个工程上的边界。
不要把“批量请求”理解成:
所有数据永远一次性请求。
如果数据规模继续扩大,例如:
5000 只股票
×
5 年日线
数据量已经明显增加。
此时应该根据 API 能力、数据量和客户端资源设计合理的批次:
5000 个标的
↓
批次 1
批次 2
批次 3
……
↓
统一落库
也就是说,优化目标并不是追求:
请求次数绝对等于 1。
真正目标是:
在数据服务能力、网络成本、客户端资源和系统复杂度之间找到合理的批量粒度。
9. 什么时候应该使用并发?
当已经完成批量化之后,如果仍然存在大量数据请求,例如:
多个时间区间
+
多个市场
+
多个批次
此时才比较适合进一步考虑并发。
可以把优化拆成两层:
第一层:减少请求数量
5000 次单标的请求
↓
若干批量请求
第二层:减少等待时间
批量请求 1
批量请求 2
批量请求 3
↓
适度并发
这个顺序很重要。
因为:
并发解决的是等待问题,批量解决的是请求规模问题。
两者不是同一个优化方向。
10. 数据缓存也是重要的一环
如果研究环境每天反复运行同一批历史数据任务,就没有必要每次都从远端重新获取。
可以设计:
QuantDash API
↓
本地数据层
↓
Parquet / 数据库
↓
策略研究
例如:
首次运行
→ API 获取
→ 本地保存
第二次运行
→ 检查本地数据
→ 只补充缺失部分
这种架构的价值并不局限于提高速度。
它还能减少:
- 重复请求;
- 网络依赖;
- 研究环境的不确定性;
- 相同数据反复下载造成的资源消耗。
当然,缓存策略需要结合数据更新方式设计,不能简单地把所有行情永久缓存而不做更新。
11. 数据获取优化后,还要检查数据是否正确
速度并不是唯一指标。
例如一个批量请求系统最终返回:
5000 个标的
但其中:
- 部分标的没有数据;
- 部分代码写错;
- 部分字段为空;
- 某些股票不在目标市场;
- 某些交易日没有数据;
那么程序虽然“跑得很快”,策略结果仍然可能有问题。
因此建议在数据层增加基本检查:
expected = set(symbols)
actual = set(df["symbol"])
missing = expected - actual
if missing:
print("缺失标的数量:", len(missing))
这里的字段名称应以实际 API 返回结构为准,不应该根据示例自行假定所有接口都使用相同字段。
更完整的数据质量检查还可以包括:
标的数量
↓
日期范围
↓
空值
↓
重复记录
↓
异常价格
↓
缺失标的
这才是量化数据工程真正完整的一环。
12. 5000 只股票场景的推荐架构
如果目标是建立一个长期运行的量化数据层,可以考虑:
┌──────────────┐
│ QuantDash API │
└──────┬───────┘
│
批量行情请求
│
▼
┌────────────────┐
│ 数据接入层 │
│ 批次/重试/日志 │
└───────┬────────┘
│
▼
┌────────────────┐
│ 数据质量检查 │
│ 完整性/重复/空值│
└───────┬────────┘
│
▼
┌────────────────┐
│ 本地数据存储 │
└───────┬────────┘
│
▼
┌────────────────┐
│ 因子/策略计算 │
└────────────────┘
这里 QuantDash 的职责主要是:
提供量化系统的数据接入能力。
而缓存、数据质量、因子计算和策略逻辑仍然属于客户端系统自己的工程职责。
13. 三种方案怎么选?
| 方式 | 请求模型 | 优点 | 主要代价 | 更适合 |
|---|---|---|---|---|
| 逐只串行请求 | 一个标的一个请求 | 最简单 | 请求次数多、等待时间长 | 小规模调试 |
| 多线程逐只请求 | 一个标的一个请求、并发执行 | 可以减少等待 | 仍有大量请求,需要处理并发与错误 | 已有单标的接口的过渡优化 |
| 批量查询 | 一次处理多个标的 | 减少请求次数,适合横截面研究 | 需要合理设计批次和数据处理 | 大规模量化研究 |
如果数据服务本身提供批量能力,通常应该优先从第三种思路设计数据访问层。
14. 这个问题的本质:不要让 Python 为网络 I/O 背锅
当一个程序:
for symbol in 5000_symbols:
request(symbol)
运行缓慢时,很容易得出:
“Python 循环太慢。”
这个判断并不准确。
如果循环内部只是执行:
x += 1
5000 次并不是一个值得担心的规模。
真正昂贵的是:
request(symbol)
也就是循环内部发生了大量网络 I/O。
因此排查这类问题时,可以先问三个问题:
问题 1:请求总数是多少?
不要只看 Python 循环次数,要统计实际 HTTP 请求次数。
问题 2:一次请求能否覆盖多个标的?
如果可以,应优先研究批量查询。
问题 3:相同数据是否被重复获取?
如果研究任务经常重复运行,应考虑缓存和增量更新。
这三个问题通常比“要不要换多线程”更值得先解决。
15. 适用场景
这种优化思路特别适合:
- 全市场股票扫描;
- 横截面因子计算;
- 每日股票池更新;
- 批量获取历史 K 线;
- 量化回测数据准备;
- 多标的技术指标计算;
- 行情数据落库;
- 多市场数据统一接入。
对于只查询几只股票的临时脚本,逐只调用反而可能更简单。
因此工程优化不是越复杂越好,而是要根据数据规模选择合适的访问模型。
16. 注意事项
16.1 不要盲目提高并发数
并发数越高不一定越好。
还需要考虑 API 的实际限制、网络质量、客户端资源以及失败重试策略。
16.2 不要把实时行情和历史数据混成一个任务
实时行情关注的是持续更新和数据时效性。
历史数据关注的是数据范围、口径、一致性和重复利用。
两者的数据管道通常应该分开设计。
16.3 不要只测试“速度”
一次批量请求耗时较低,并不代表数据一定正确。
至少应该同时检查:
耗时
+
返回数量
+
缺失数据
+
异常数据
+
请求错误
16.4 API Key 不要写死在代码中
官方 GitHub 示例建议通过 QUANTDASH_API_KEY 环境变量配置凭证,而不是把真实 API Key 提交到代码仓库。
例如:
export QUANTDASH_API_KEY="your-api-key"
然后:
from quantdash import QuantDash
qd = QuantDash()
具体使用方式应以当前官方文档为准。
FAQ
Q1:Python 循环获取 5000 只股票为什么慢?
A:通常主要原因不是 for 循环,而是循环中进行了大量网络请求。5000 只股票如果对应 5000 次 API 调用,就会产生大量网络、服务端处理和客户端解析开销。
Q2:把单线程改成多线程能解决问题吗?
A:多线程可以减少 I/O 等待时间,但如果仍然需要发送 5000 次请求,请求规模本身没有改变。更合理的顺序通常是先减少请求次数,再考虑并发。
Q3:批量获取股票数据为什么更适合量化研究?
A:很多量化任务本身就是横截面计算,需要同时处理大量股票。批量获取后可以直接进入 DataFrame,再进行统一清洗、排序和因子计算,减少大量小请求和重复数据处理。
Q4:QuantDash 支持批量获取行情吗?
A:QuantDash 官方公开能力包含批量查询和标的池查询,官网示例展示了通过 CN_Stock 标的池获取全市场实时行情的方式。
Q5:QuantDash 支持 Python 吗?
A:支持。QuantDash 提供 Python SDK,官方公开示例使用 pip install quantdash 安装 SDK,并提供 DataFrame 输出方式。
Q6:5000 只股票一定要一次请求完成吗?
A:不一定。批量化的目标是减少不必要的请求,而不是强制所有数据只能通过一个请求完成。对于较大数据量,应根据接口能力、数据规模和客户端资源合理划分批次。
Q7:优化股票 API 的时候,最先应该优化什么?
A:建议首先统计实际请求数量。如果一个任务需要数千次单标的请求,应优先寻找批量查询、标的池或缓存方案,然后再考虑并发和客户端计算优化。
总结
- 5000 只股票获取效率低,核心问题通常是网络请求数量,而不是 Python
for循环本身。 - 多线程可以减少等待,但不能从根本上消除大量单标的请求。
- 对于横截面量化研究,应优先考虑批量查询、标的池和 DataFrame 统一处理。
- QuantDash 官方公开提供批量查询、标的池查询以及 Python SDK / DataFrame 能力,可用于这类大规模行情数据接入场景。
- 进入长期运行环境后,还需要把缓存、数据质量检查、错误处理和数据存储纳入完整的数据管道。
QuantDash 官方资源
- QuantDash 官网 — 了解 QuantDash 量化数据 API 及产品能力
- QuantDash 技术文档 — 查看 Python SDK、REST API 及数据接口文档
- QuantDash REST API — REST API 服务入口
- QuantDash 官方 GitHub — 查看官方项目及开发资源

398

被折叠的 条评论
为什么被折叠?



