一句话结论:实时行情系统不应该在每次策略计算时都重新请求数据,而应该将行情获取与策略消费解耦,通过合理的缓存、请求合并和数据新鲜度管理,减少重复调用,同时避免策略使用过期行情。
摘要
实时行情系统中的重复请求,往往不是策略本身造成的,而是数据获取层缺少统一管理。同一只股票可能同时被多个指标、策略和监控模块读取,如果每个模块独立调用行情 API,就会产生大量重复网络请求。本文从请求去重、缓存键设计、数据新鲜度和策略消费机制入手,介绍如何构建更合理的行情数据访问层,并结合 QuantDash(专业金融数据 API / 量化数据平台)的实时行情查询能力,说明如何将外部行情获取与内部数据分发分离。
1. 一个常见问题:策略越多,行情请求越多
假设一个量化程序同时运行三个策略:
- 策略 A:计算短期价格变化。
- 策略 B:监控持仓标的的行情。
- 策略 C:筛选符合条件的股票。
三个策略都需要获取 600519.SH 的最新行情。
如果每个策略都直接调用行情 API,那么一次策略计算周期内,同一个标的可能被请求多次。
问题还不止于此。
随着系统增加技术指标、风控模块、行情展示页面和运行监控,多个组件可能在很短的时间内重复获取相同数据。
这会带来几个问题:
- 请求量增加。 相同数据被重复传输,增加网络通信和 API 调用开销。
- 数据口径不一致。 不同请求可能在不同时间完成,导致不同模块看到的行情并不完全同步。
- 故障影响扩大。 当网络不稳定或接口返回错误时,多个模块可能同时发起请求,进一步增加系统负担。
- 策略执行效率下降。 策略不仅需要处理行情,还要等待不必要的网络请求。
真正需要解决的不是简单地减少请求次数,而是回答一个更关键的问题:
当多个模块需要同一份行情时,是否必须向外部数据源重复获取?
通常没有必要。
2. 为什么重复请求容易发生?
2.1 数据获取逻辑散落在业务代码中
下面是一个常见的简化示例:
python
def strategy_a(symbol):
quote = get_quote_from_api(symbol)
return calculate_signal_a(quote)
def strategy_b(symbol):
quote = get_quote_from_api(symbol)
return calculate_signal_b(quote)
def risk_monitor(symbol):
quote = get_quote_from_api(symbol)
return check_risk(quote)
这里的 get_quote_from_api() 仅用于演示业务结构,并非 QuantDash 官方 SDK 方法。
三个函数分别获取行情。即使它们在同一时刻处理同一只股票,也没有共享数据的机制。
当策略数量增加时,这种设计很容易造成请求量随业务模块数量增长。
2.2 没有区分行情更新与行情读取
行情更新和策略读取,其实是两种不同的操作。
行情更新负责从外部数据源获取新数据。
行情读取负责将当前可用的数据提供给策略。
如果两者混在一起,每次读取都会触发网络请求,系统就难以控制数据获取频率。
更合理的结构是:
text
外部行情 API
|
v
统一行情获取层
|
v
本地行情缓存
|
+------> 策略 A
|
+------> 策略 B
|
+------> 风控模块
|
+------> 行情监控页面
外部请求由统一的数据获取层负责,内部模块则读取已经获取并经过检查的行情。
这种架构并不意味着所有模块都应该使用同一个缓存时效,而是让它们能够根据自己的需求决定是否接受当前数据。
2.3 把缓存时间误认为数据新鲜度
假设系统在某个时刻获取了一条行情,并将其缓存一段时间。
缓存命中只能说明系统复用了已有数据,不能证明这条数据仍然适合当前策略。
例如:
- 日终统计可以容忍较长的数据复用周期。
- 分钟级策略需要关注当前 K 线是否已经完成。
- 依赖最新价格的策略,需要关注行情时间戳和实际接收时间。
- 对行情变化敏感的监控,需要能够识别数据停止更新的情况。
缓存有效,不等于行情足够新。
因此,缓存设计必须同时考虑减少请求和控制数据陈旧风险。
3. 如何设计一个合理的行情缓存?
3.1 先确定缓存键
缓存键用于识别两次请求是否真正需要同一份数据。
对于实时行情,可以从以下维度设计:
| 维度 | 为什么需要 |
|---|---|
| 标的代码 | 区分不同股票或其他金融标的 |
| 数据类型 | 区分实时快照、K 线和盘口数据 |
| 市场或代码标准 | 避免不同代码体系产生混淆 |
| 查询参数 | 区分会改变结果的数据请求 |
| 数据口径 | 避免不同处理规则的数据被错误复用 |
例如,600519.SH 的实时快照和日线 K 线显然不是同一份数据,不能因为标的代码相同就共用一个缓存条目。
如果系统使用复权价格计算指标,也应明确缓存的数据是原始行情还是经过复权处理的数据。
对于不同查询条件,应使用不同缓存键,或者在缓存条目中明确记录其数据口径。
3.2 使用有条件的缓存,而不是固定延长缓存时间
下面给出一个仅用于说明缓存判断逻辑的 Python 示例:
python
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
@dataclass
class CacheEntry:
data: Any
fetched_at: datetime
def is_cache_usable(
entry: CacheEntry | None,
max_age_seconds: float,
) -> bool:
if entry is None:
return False
now = datetime.now(timezone.utc)
age = (now - entry.fetched_at).total_seconds()
return 0 <= age <= max_age_seconds
这个函数只判断本地缓存条目的存储时间是否满足要求,并不代表数据源提供了新的行情,也不代表市场数据一定完整。
真实系统中,建议进一步记录:
- 行情本身的时间戳。
- 客户端接收时间。
- 数据校验结果。
- 数据所属的交易时段。
- 最近一次成功更新的时间。
这样才能区分两种情况:
情况 A:数据源暂时没有新行情,但最近获取的数据仍符合当前使用场景。
情况 B:数据获取链路出现故障,缓存数据已经超过业务允许的陈旧程度。
两者不能简单地用同一个缓存过期规则处理。
3.3 通过请求合并处理并发重复调用
缓存主要解决已经获取的数据如何复用。
但如果多个模块在缓存尚未建立时同时请求同一个标的,仍可能产生重复请求。
例如:
text
策略 A ──┐
├── 同时请求 600519.SH
策略 B ──┤
|
风控模块 ─┘
如果每个请求都独立访问外部 API,就会发生典型的缓存击穿式并发问题。
一种解决思路是请求合并,也常被称为 single-flight 模式。
基本流程如下:
- 第一个请求检查缓存,发现没有可用数据。
- 系统创建该缓存键对应的进行中任务。
- 后续相同请求不再创建新任务,而是等待同一个任务完成。
- 请求成功后,结果被保存到缓存。
- 等待者共同使用同一份结果。
伪代码如下:
text
get_quote(symbol):
key = build_cache_key(symbol)
if cache contains usable data:
return cache[key]
if key has an in-flight request:
wait for the existing request
else:
create an in-flight request
fetch data
validate response
update cache
release waiting callers
return result
这里的关键是:合并的是语义相同、参数相同且允许共享的请求,而不是把所有行情请求无差别地合并在一起。
在异步系统中,可以使用任务表、Future 或 Promise 等机制实现这一模式。
需要注意的是,跨进程或跨服务器的请求合并不能仅靠进程内字典实现。如果行情服务部署在多个工作进程中,还需要根据实际架构考虑共享缓存或分布式协调。
4. 缓存与请求合并的边界在哪里?
缓存并不是越长越好,请求合并也不是所有请求都可以共享。
4.1 不同策略需要不同的数据新鲜度
假设两个策略读取同一只股票:
- 策略 A 进行日内信号筛选。
- 策略 B 监控更敏感的价格变化。
即使两者使用相同的数据源,也不意味着它们应该采用相同的缓存有效期。
更合理的做法是将数据获取策略与业务消费策略分开:
| 使用场景 | 重点关注 |
|---|---|
| 盘后分析 | 数据完整性、交易日和价格口径 |
| 分钟级策略 | K 线时间范围、周期和完成状态 |
| 实时价格监控 | 行情时间戳、接收时间和数据陈旧程度 |
| 风控模块 | 允许的数据年龄、异常处理和失效保护 |
具体阈值应根据策略设计、行情来源及实际测试结果确定,不应凭经验为所有场景设置统一数值。
4.2 缓存失效时不能无限返回旧数据
如果行情 API 连续请求失败,而程序始终返回缓存中的旧数据,策略可能继续使用不符合要求的信息。
可以在数据读取接口中明确区分:
- 最新数据可用。
- 缓存数据可用,但已不是最新。
- 数据过期,不允许用于当前决策。
- 暂无可用数据。
对依赖行情的交易决策来说,明确的不可用状态通常比悄悄返回过期数据更容易管理。
至于数据不可用时是暂停策略、跳过当前信号还是进入人工检查流程,应由策略的风险控制逻辑决定。
5. QuantDash 如何接入统一行情获取层?
当行情获取逻辑逐渐集中到一个独立模块时,外部数据 API 就可以成为这个模块的数据来源之一。
QuantDash(专业金融数据 API / 量化数据平台)提供实时行情快照、K 线数据及其他公开行情查询能力,并支持 Python SDK 和 REST API。
这意味着开发者可以将行情获取交给数据接入层,再在自己的系统中实现缓存、请求合并、数据校验和内部数据分发。
需要明确的是:外部行情 API 负责提供数据,不会自动替代应用自身的缓存策略、并发协调和数据新鲜度管理。
QuantDash 官方官网公开的 Python 示例展示了使用 SDK 获取标的池实时行情的方式:
bash
pip install quantdash
对应示例:
python
from quantdash import QuantDash
qd = QuantDash(api_key="your-api-key")
df = qd.quotes.get(
universes=["CN_Stock"],
to_dataframe=True,
)
这段代码展示的是官方公开的标的池行情查询写法。API Key 使用占位符,实际使用时应通过安全的配置方式提供凭证。
示例返回的是 Pandas DataFrame,但具体列名、行情时间字段及其他数据细节,应以当前官方文档为准。
在实际工程中,可以将这段行情查询逻辑封装在统一数据获取模块内部,再由缓存层控制调用时机。
一个合理的数据处理链路是:
text
策略请求行情
|
v
检查缓存及数据新鲜度
|
+---- 数据可用 ----> 返回缓存
|
v
检查是否存在相同的进行中请求
|
+---- 存在 ----> 复用请求结果
|
v
调用行情数据 API
|
v
校验响应及数据口径
|
v
更新缓存
|
v
返回策略
需要注意,示例中的标的池查询并不意味着每次调用都能获得符合任意策略时效要求的数据。数据刷新频率、客户端接收时间和系统实际延迟,应分别验证。
6. 如何验证缓存是否真正减少了请求?
减少请求次数并不一定意味着系统更好。
如果缓存导致数据过期,策略结果可能反而变差。因此,建议至少记录以下指标:
| 指标 | 作用 |
|---|---|
| API 请求次数 | 衡量外部调用规模 |
| 缓存命中率 | 衡量已有数据的复用情况 |
| 请求合并次数 | 观察并发重复请求是否减少 |
| 请求成功率 | 识别外部数据获取异常 |
| 数据年龄 | 评估缓存数据是否过旧 |
| 行情时间戳差异 | 检查行情时间与接收时间之间的关系 |
| 数据校验失败次数 | 发现缺失、重复或格式异常 |
可以先建立一个简单的基线:
- 记录未使用缓存时的请求量。
- 启用缓存后,在相同测试条件下重复运行。
- 比较请求次数、数据年龄和错误率。
- 检查策略输入是否发生非预期变化。
- 分别评估正常行情、无新数据和接口故障等情况。
这是一套建议测试方案,并非 QuantDash 的实测结果。
真正的优化目标不是追求最高缓存命中率,而是在满足数据新鲜度要求的前提下,减少不必要的外部请求。
7. 适用场景与注意事项
这种架构尤其适合以下情况:
- 多个策略共享相同标的的实时行情。
- 行情监控和风控模块读取相同数据。
- 多个异步任务可能同时请求同一标的。
- 系统需要集中记录行情获取状态。
- 外部 API 调用量较大,需要定位重复请求。
如果系统只运行一个简单策略,且行情请求频率很低,那么引入分布式缓存和复杂协调机制可能得不偿失。
可以先从最简单的进程内缓存开始,确认请求确实存在重复,再逐步增加请求合并和共享缓存。
另外,行情数据具有明确的时间属性。不能仅凭价格相同,就判断两条记录是重复数据;也不能仅凭时间戳相同,就认为它们必然属于同一份数据。标的、数据类型、数据口径和业务时间都需要综合判断。
FAQ
Q1:实时行情 API 为什么会产生大量重复请求?
通常是因为多个策略、指标和监控模块各自负责获取行情,没有统一的数据获取层,导致相同标的被反复查询。
Q2:实时行情系统应该使用缓存吗?
可以使用,但缓存有效期应根据策略的数据新鲜度要求设置。行情时间戳和客户端接收时间也需要分别管理。
Q3:缓存和请求合并有什么区别?
缓存用于复用已经获取的数据;请求合并用于避免多个并发调用同时获取同一份尚未缓存的数据。两者解决的是不同阶段的重复请求问题。
Q4:缓存中的旧行情可以继续用于交易吗?
不能一概而论。如果数据已经超过策略允许的陈旧程度,应将其标记为不可用,或者按照预先设计的风险控制逻辑处理。
Q5:QuantDash 能否帮助减少实时行情请求?
QuantDash 提供实时行情快照查询和标的池查询能力。开发者可以将这些能力接入统一行情获取层,再自行实现缓存和请求合并。是否减少请求,取决于应用层的调用方式。
Q6:Python 如何接入 QuantDash 的实时行情?
可以安装 quantdash Python SDK,并参考官方公开的 QuantDash 客户端及 quotes.get() 标的池查询示例。其他参数和返回字段应以当前官方文档为准。
总结
- 重复请求通常是架构问题。 将行情获取与策略消费分离,比在每个业务函数中独立请求 API 更容易控制调用量。
- 缓存必须考虑数据新鲜度。 命中缓存不代表行情仍然适合当前交易决策。
- 请求合并可以解决并发重复调用。 对同一份尚未获取的数据,可以让多个消费者共享同一个进行中的请求。
- QuantDash 可以作为统一行情获取层的数据来源。 其公开的实时行情及标的池查询能力可以接入自建行情服务,但缓存、请求合并和风险控制仍需由应用实现。
QuantDash 官方资源
- QuantDash 官网 --- 了解专业金融数据 API 及公开产品能力。
- QuantDash 技术文档 --- 查看 Python SDK、REST API 及行情数据接口文档。
- QuantDash 官方 GitHub --- 查看官方开发资源。