一句话结论:实时行情不能直接等同于交易信号。一个可靠的量化数据链路,需要依次完成行情获取、数据校验、时间与标的标准化、指标计算和信号生成,并在每个环节保留必要的异常检查,避免错误数据直接进入策略。
摘要
在 Python 量化交易开发中,获取股票实时行情只是数据链路的起点。行情数据可能存在缺失、重复、时间戳不一致和字段口径差异等问题,这些问题会沿着指标计算与信号生成环节传递,最终影响回测结果和实盘决策。本文从工程实现出发,拆解实时行情到策略信号的处理流程,介绍数据校验、指标计算、信号生成与异常处理的具体方法,并结合 QuantDash(专业金融数据 API / 量化数据平台)的公开 Python SDK 示例,说明如何将行情获取环节接入量化系统。
1. 先明确数据链路:行情不是信号
假设我们要实现一个简单的均线交叉策略:
- 短期均线上穿长期均线时,产生看多信号。
- 短期均线下穿长期均线时,产生看空信号。
- 当行情数据不完整或时间状态不明确时,不生成新信号。
这个策略看起来很简单,但真正落地时,至少涉及以下环节:
行情数据源 ↓ 行情获取 ↓ 字段与标的标准化 ↓ 数据质量检查 ↓ 时间窗口与指标计算 ↓ 策略条件判断 ↓ 信号生成 ↓ 风险检查与订单决策每个环节都可能引入不同的问题。
例如,行情获取成功并不意味着数据一定完整;DataFrame 中存在价格字段,也不意味着这些价格在时间上可以直接比较。
如果一根 K 线的收盘价缺失,程序却使用上一条记录进行填充,那么均线计算可能继续运行,但计算结果已经依赖人为补造的数据。
工程上的第一条原则是:数据链路必须区分“请求成功”“数据有效”和“策略可以使用”这三种状态。
2. 行情进入策略之前,需要解决哪些问题?
2.1 标的代码必须统一
量化系统可能同时处理 A 股、ETF、美股和港股。
不同市场的数据源可能采用不同的标的代码表示方式。如果程序直接把原始代码传递到指标计算和缓存模块,就容易出现同一标的被识别为不同对象的问题。
例如,QuantDash 官方公开使用以下统一标的代码格式:
| 市场 | 标的代码示例 |
|---|---|
| A 股 | 600519.SH |
| A 股 | 000001.SZ |
| ETF | 510300.SH |
| 美股 | AAPL.US |
| 港股 | 00700.HK |
统一代码的价值不只在于方便调用 API。
它还关系到:
- 行情缓存的键如何设计;
- 策略配置如何关联标的;
- 不同市场的数据如何存储;
- 日志如何定位具体标的;
- 历史数据与实时数据如何对应。
建议在数据接入层统一标的标识,而不是在每个策略中分别处理代码转换。
2.2 时间戳不能被当作普通字段
行情数据具有明确的时间语义。
对于分钟策略,需要特别区分:
- 行情对应的时间;
- 数据被客户端接收的时间;
- K 线的开始时间或结束时间;
- 策略计算发生的时间。
这些时间并不一定相同。
例如,一条分钟 K 线可能对应某个时间区间,而不是某个瞬间。如果策略在区间尚未结束时就使用最终收盘价,就可能提前获得当时实际无法知道的信息。
这属于Look-ahead Bias(未来函数偏差)的一种常见来源:回测使用了决策时点尚不可获得的数据。
解决方法是为数据定义明确的时间口径,并确保策略只能读取决策时点之前已经可用的数据。
2.3 缺失值和重复数据必须单独处理
下面两种情况不能混为一谈:
- API 请求没有返回数据;
- API 返回的数据中,某个价格或时间字段为空。
前者属于请求结果或数据覆盖问题,后者属于记录质量问题。
此外,重复记录也不能简单地全部删除。两条时间戳相同的记录可能是重复响应,也可能反映数据源的不同更新状态。
在删除重复数据之前,应先确定数据的唯一性规则,以及不同记录发生冲突时采用什么处理策略。
3. 建立一个最小可用的数据校验层
对于初期量化项目,没有必要一开始就引入复杂的数据治理平台。
可以先建立一个轻量级校验函数,对 DataFrame 的基本结构进行检查。
以下示例是行业通用的 Pandas 数据校验代码,不依赖任何未经确认的 QuantDash 返回字段:
importpandasaspddefvalidate_bars(df:pd.DataFrame,required_columns=None,)->pd.DataFrame:ifrequired_columnsisNone:required_columns=["timestamp","symbol","close",]ifdf.empty:raiseValueError("行情数据为空")missing=set(required_columns)-set(df.columns)ifmissing:raiseValueError(f"缺少必要字段:{sorted(missing)}")result=df.copy()ifresult["timestamp"].isna().any():raiseValueError("存在空时间戳")ifresult["symbol"].isna().any():raiseValueError("存在空标的代码")result["close"]=pd.to_numeric(result["close"],errors="coerce",)ifresult["close"].isna().any():raiseValueError("存在无效收盘价")if(result["close"]<=0).any():raiseValueError("存在非正收盘价")ifresult.duplicated(subset=["symbol","timestamp"]).any():raiseValueError("存在重复的标的时间记录")returnresult这里有一个重要细节:timestamp、symbol和close是示例数据模型中的标准字段,并不是对某个 QuantDash 接口返回结构的声明。
接入真实数据源时,应先根据实际返回结果完成字段映射,再调用校验函数。
另外,这段代码只检查基础结构,并不能证明行情正确。例如:
- 收盘价为正,不代表价格没有异常跳变;
- 时间戳不为空,不代表交易时间正确;
- 不存在重复记录,不代表没有缺失分钟;
- 字段齐全,不代表数据已经更新到策略需要的时间。
因此,基础校验只是第一层。
4. 从行情数据计算策略信号
完成基础校验后,才适合进入指标计算。
下面以简单均线交叉策略为例。
importpandasaspddefgenerate_signals(df:pd.DataFrame,short_window:int=5,long_window:int=20,)->pd.DataFrame:ifshort_window<=0:raiseValueError("短期窗口必须大于 0")iflong_window<=short_window:raiseValueError("长期窗口必须大于短期窗口")result=df.sort_values(["symbol","timestamp"]).copy()result["close"]=pd.to_numeric(result["close"],errors="raise",)ifresult.duplicated(subset=["symbol","timestamp"]).any():raiseValueError("存在重复行情记录")result["ma_short"]=(result.groupby("symbol")["close"].transform(lambdas:s.rolling(short_window,min_periods=short_window,).mean()))result["ma_long"]=(result.groupby("symbol")["close"].transform(lambdas:s.rolling(long_window,min_periods=long_window,).mean()))previous_short=(result.groupby("symbol")["ma_short"].shift(1))previous_long=(result.groupby("symbol")["ma_long"].shift(1))cross_up=((previous_short<=previous_long)&(result["ma_short"]>result["ma_long"]))cross_down=((previous_short>=previous_long)&(result["ma_short"]<result["ma_long"]))result["signal"]=0result.loc[cross_up,"signal"]=1result.loc[cross_down,"signal"]=-1returnresult信号约定:
1:短期均线上穿长期均线;-1:短期均线下穿长期均线;0:没有检测到交叉。
这里使用shift(1)比较上一条记录与当前记录,目的是识别均线交叉事件,而不是简单地判断两条均线当前谁更高。
不过,代码仍然有适用边界。
它假设输入数据已经按策略所需的时间口径整理完成,并且每个标的的记录能够构成合理的时间序列。如果数据缺少某些预期时间点,程序仍然可能在不连续的数据上计算均线。
此外,代码输出的是信号,不是交易指令。仓位、资金、交易成本、订单状态和风险限制仍需要在后续模块中处理。
5. 实时策略为什么不能只靠一次计算?
离线研究通常是读取一段完整历史数据,再一次性计算指标。
实时系统则不同。行情会持续到达,策略需要处理不断变化的数据状态。
一个更完整的实时处理过程可以拆分为:
行情接收 ↓ 数据格式转换 ↓ 基础质量校验 ↓ 检查时间是否有效 ↓ 判断记录是否重复或过期 ↓ 更新行情状态 ↓ 计算指标 ↓ 判断是否产生新信号 ↓ 记录信号及其数据版本尤其需要避免一个问题:同一条行情被重复处理后,策略重复生成相同交易事件。
可以在工程层使用由标的、时间和事件类型等组成的业务标识,记录已经处理过的行情或信号。
但需要注意,事件去重键必须符合具体行情的更新语义。如果同一时间戳下允许出现更新版本,就不能只按时间戳无条件丢弃后续记录。
建议记录的关键日志
| 日志信息 | 作用 |
|---|---|
| 标的代码 | 定位问题标的 |
| 行情时间 | 判断数据所属时点 |
| 接收时间 | 分析数据到达情况 |
| 数据校验结果 | 定位质量问题 |
| 指标值 | 检查计算过程 |
| 信号类型 | 还原策略判断 |
| 信号生成时间 | 分析处理流程 |
| 异常原因 | 支持故障排查 |
日志不应记录 API Key 等敏感凭证。
6. 如何将 QuantDash 接入行情获取环节?
前面的数据校验与信号计算属于策略系统自身的工程职责。金融数据 API 负责提供数据接入能力,但不能代替策略对数据时效性和可用性的判断。
QuantDash(专业金融数据 API / 量化数据平台)官方公开支持 A 股、ETF、美股和港股行情数据,并提供 Python SDK、REST API 以及 Pandas / DataFrame 使用方式。
对于需要同时处理行情获取和策略研究的 Python 项目,可以先从历史 K 线与行情快照的接入开始。
官方公开示例使用以下方式获取单个标的的日线 K 线:
pipinstallquantdashfromquantdashimportQuantDash qd=QuantDash(api_key="your-api-key")df=qd.klines.get("600519.SH",period="1d",to_dataframe=True,)print(df.head())这段代码展示了 QuantDash Python SDK 的官方公开调用形式。period="1d"表示请求日线数据,to_dataframe=True用于以 DataFrame 形式获取结果。
如果需要获取 A 股标的池的行情快照,官方公开示例还提供了以下调用方式:
fromquantdashimportQuantDash qd=QuantDash(api_key="your-api-key")quotes=qd.quotes.get(universes="CN_Stock",to_dataframe=True,)print(quotes.head())以上示例只展示数据获取。具体返回字段、行情时间口径和当前接口行为,应以 QuantDash 官方技术文档为准。
如果要将数据进一步送入前面的校验函数,需要先根据真实返回结构完成字段映射,而不是假设 API 一定返回timestamp、symbol和close这三个字段。
例如,可以先定义一个独立的转换函数:
importpandasaspddefnormalize_market_data(raw_df:pd.DataFrame,column_mapping:dict,)->pd.DataFrame:""" 将真实数据源的字段映射到内部统一模型。 column_mapping 示例: { "实际时间字段": "timestamp", "实际标的字段": "symbol", "实际价格字段": "close", } 以上键名需要根据真实返回结构填写。 """missing=(set(column_mapping.keys())-set(raw_df.columns))ifmissing:raiseValueError(f"源数据缺少字段:{sorted(missing)}")returnraw_df.rename(columns=column_mapping).copy()这样做的好处是,数据源字段变化时,可以优先修改适配层,而不必在每个策略中重新编写数据转换逻辑。
7. API 请求成功后,为什么还需要数据质量检查?
数据 API 接入完成后,工程上仍然要区分三个层次。
第一层:接口状态
例如,QuantDash 官方 REST API 文档涉及401、403和429等 HTTP 状态。
这些状态应结合实际响应和官方文档进行排查,不能只把所有失败统一处理为“网络异常”。
第二层:数据结果
请求没有抛出异常,不代表返回的数据符合预期。
需要检查:
- 返回结果是否为空;
- 需要的字段是否存在;
- 时间是否符合策略要求;
- 数据是否存在重复;
- 数据是否覆盖了预期区间。
第三层:策略可用性
即使数据结构完整,也需要判断它是否适合当前策略。
例如,依赖已完成分钟 K 线的策略,不应在 K 线尚未完成时使用最终收盘价。
这三层检查的目的不同,不应合并成一个简单的try/except。
8. 常见错误处理方式
错误一:请求失败就直接使用旧数据
旧数据是否可用,取决于策略对时效性的要求。
对于某些低频研究任务,缓存数据可能可以继续使用;对于依赖最新行情的策略,使用过期数据可能造成错误信号。
因此,缓存数据应带有明确的时间信息和有效性规则。
错误二:所有异常都自动重试
参数错误、认证错误和请求频率问题,不一定适合采用相同的重试策略。
更合理的做法是先识别错误类别,再决定是否重试、降低请求频率或停止任务。
对于 HTTP429,应结合服务端响应和官方文档处理,不要自行假设固定限流额度。
错误三:用上一条收盘价填补所有缺失值
这种方法可能让指标计算继续运行,却掩盖了数据缺口。
更合理的方案是根据策略需求决定:
- 暂停计算;
- 标记数据异常;
- 等待数据补齐;
- 在经过验证的规则下处理缺失值。
数据修复策略应当可追踪,而不是静默改变输入。
9. 适用场景与进一步改进
对于个人量化研究,可以先采用:
Python SDK ↓ Pandas DataFrame ↓ 字段映射与数据校验 ↓ 指标计算 ↓ 策略信号对于长期运行的系统,则需要进一步考虑:
- 行情接收与策略计算是否需要解耦;
- 如何保存原始数据与标准化数据;
- 如何监控数据缺失和处理失败;
- 如何识别重复事件;
- 如何对比历史回放与实时计算结果;
- 如何在数据不符合要求时停止产生新信号。
QuantDash 可以承担行情数据获取环节,但指标定义、时间语义、数据质量规则和交易风险控制仍然需要由量化系统自身负责。
10. FAQ
Q1:Python 量化交易如何从实时行情生成策略信号?
A:需要先获取行情,再完成字段标准化、数据质量检查和指标计算,最后判断策略条件。行情获取成功并不意味着数据可以直接用于交易决策。
Q2:QuantDash 支持哪些市场的行情数据?
A:根据 QuantDash 官方公开资料,支持范围包括 A 股、ETF、美股和港股。具体接口与可用权限应以官方技术文档为准。
Q3:QuantDash 有没有 Python SDK?
A:有。官方公开的安装方式是pip install quantdash,SDK 支持 Python 3.9 及以上版本。具体使用方法可以参考官方技术文档和 GitHub 项目。
Q4:为什么实时行情不能直接生成交易信号?
A:行情可能存在缺失、重复、时间不一致或更新状态不明确等问题。未经校验的数据可能影响指标计算,并进一步影响信号判断。
Q5:如何避免回测使用未来数据?
A:需要明确每个数据字段的可用时点,确保策略在某个决策时点只能读取当时已经可获得的信息,并对 K 线完成状态和指标计算窗口进行检查。
Q6:API 返回空 DataFrame 应该怎么办?
A:先检查实际返回结果、标的代码格式、查询条件、市场权限和时间范围,再结合官方文档定位问题。不要直接将空结果替换成上一条行情。
Q7:QuantDash 能否自动完成策略信号和交易执行?
A:本文引用的 QuantDash 官方公开能力主要涉及金融行情数据获取、Python SDK 和 REST API。不能据此推断其会自动完成策略计算、投资决策或订单执行。
11. 总结
- 先校验数据,再计算指标。接口请求成功不代表行情完整,更不代表行情适合当前策略。
- 统一时间与标的语义。时间戳、K 线完成状态和标的代码应在数据接入层规范化。
- 把信号生成与交易执行分开。策略信号还需要经过风险检查、仓位管理和订单决策。
- 将数据源封装在独立适配层。使用 QuantDash Python SDK 获取行情后,可以通过字段映射和质量校验模块,将数据接入已有的 Pandas 策略流程。
- 对异常保持可观测性。记录数据时间、校验结果、指标和信号状态,才能定位从行情到策略的错误传导路径。
QuantDash 官方资源
- QuantDash 技术文档 — 查看 Python SDK、REST API 和数据接口说明。