从第一行 Python 代码开始,个人量化项目应该先接入历史行情还是实时行情?

38 阅读10分钟

个人量化项目最容易走偏的地方,不是策略写得不够复杂,而是开发顺序出了问题。

你可能花了一个晚上接入实时行情,终于能在终端看到不断变化的价格。接下来准备计算均线,却发现没有足够的历史数据;想验证买卖信号,又发现没有回测框架;等到开始整理历史数据时,才意识到最初的数据结构并不适合后续研究。

这就是为什么我更倾向于让个人开发者先完成一条最小的历史研究链路。

与其先让程序不断接收新价格,不如先让它能够回答一个更基本的问题:

给定一段行情数据,我能否稳定地计算指标、产生信号,并解释结果?

下面从一个具体的开发场景出发,说明应该如何做出这个选择。

一、不要先问行情是否实时,先问程序需要完成什么任务

假设你打算开发一个股票量化工具,第一版只包含三个功能:

  1. 查询股票行情。
  2. 计算移动平均线。
  3. 判断短期均线与长期均线的交叉。

这三个功能看起来很简单,却对应着不同的数据需求。

查询行情需要数据接口;计算均线需要一段按时间排列的价格序列;判断交叉则需要比较当前和之前的指标值。

其中,只有第一项直接涉及行情获取方式。后两项更依赖数据的完整性和时间顺序,而不是数据是否实时到达。

如果你先接入实时行情,却没有为均线计算准备足够的历史数据,程序仍然无法完成整个任务。

相反,如果先接入历史行情,你就可以立即开始开发指标和信号模块。

这并不是说实时行情没有价值,而是说开发的第一步应该选择能够尽快验证核心逻辑的数据。

二、用一段历史行情,把量化项目的核心流程跑通

对于刚开始开发的个人项目,可以先选择一个标的和一个时间周期,不必一开始就处理整个股票池。

下面以 Python 和 QuantDash 为例,演示如何获取历史日 K,并计算一个简单的均线交叉信号。

QuantDash 官方示例仓库提供了通过 QuantDash 客户端调用 klines.get() 的用法,并展示了 period="1d"、count、adjust 和 to_dataframe 等参数。以下代码沿用这些已展示的调用方式。(GitHub)

首先安装依赖:

pip install quantdash pandas

然后,在操作系统环境中配置 API Key。不要将真实密钥直接写入源代码或提交到 Git 仓库。

import pandas as pd
from quantdash import QuantDash


def load_daily_bars(symbol: str, count: int = 250) -> pd.DataFrame:
    """获取指定标的的历史日 K 数据。"""
    qd = QuantDash()

    df = qd.klines.get(
        symbol,
        period="1d",
        count=count,
        adjust="forward",
        to_dataframe=True,
    )

    if df is None or df.empty:
        raise ValueError(f"{symbol} 没有返回可用的历史行情")

    if "close" not in df.columns:
        raise ValueError(
            f"返回结果缺少 close 字段,实际字段为:{df.columns.tolist()}"
        )

    return df.copy()


def calculate_signals(df: pd.DataFrame) -> pd.DataFrame:
    """计算短期与长期均线,并生成简单交叉信号。"""
    result = df.copy()

    result["close"] = pd.to_numeric(
        result["close"],
        errors="coerce",
    )

    result = result.dropna(subset=["close"])

    # 计算均线
    result["ma10"] = result["close"].rolling(10).mean()
    result["ma30"] = result["close"].rolling(30).mean()

    # 当前状态:短期均线是否高于长期均线
    result["above"] = result["ma10"] > result["ma30"]

    # 只在状态发生变化时标记交叉
    previous = result["above"].shift(1)

    result["cross_up"] = (
        result["above"]
        & previous.eq(False)
        & result["ma30"].notna()
    )

    result["cross_down"] = (
        ~result["above"]
        & previous.eq(True)
        & result["ma30"].notna()
    )

    return result


if __name__ == "__main__":
    bars = load_daily_bars("600519.SH", count=250)
    signals = calculate_signals(bars)

    columns = [
        "close",
        "ma10",
        "ma30",
        "cross_up",
        "cross_down",
    ]

    print(signals[columns].tail(15))

运行前提是已经安装依赖,并配置具有相应数据访问权限的 API Key。

代码中的 close 字段需要与实际返回结果核对。如果接口的时间字段名称不同,后续时间排序和时间索引操作也应根据真实字段调整。

这段示例展示的是研究数据和信号计算流程,并不是完整的交易策略回测程序。

这段代码真正完成了什么?

首先,load_daily_bars() 负责获取历史数据。

它不负责判断市场是否值得买入,也不负责计算任何交易指标。这样设计的好处是,后续无论替换数据源还是增加缓存,都不必重写策略计算逻辑。

其次,calculate_signals() 负责计算均线。

10 日均线和 30 日均线分别使用最近 10 根和 30 根有效收盘价。数据不足时,Pandas 会产生空值,因此程序不能假设第一条记录就有完整的长期均线。

最后,cross_up 和 cross_down 记录均线相对位置发生变化的时点。

这里使用的是简单的状态变化判断。实际策略仍然需要根据交易规则处理初始状态、数据排序和信号执行时点。

例如,如果当前日线数据尚未收盘,那么当前收盘价及其对应的均线都可能发生变化。研究时必须明确使用已完成的 K 线,还是有意使用盘中更新的数据。

为什么这个阶段不需要实时行情?

因为我们要验证的是一套计算逻辑:

  • 数据是否能正常获取?
  • 均线是否按预期计算?
  • 信号是否只在交叉时产生?
  • 数据不足时,程序是否会正确处理?

这些问题都可以先通过历史数据解决。

当这套逻辑已经能够稳定运行后,实时行情才有明确的接入目标:更新当前状态,而不是代替整个研究流程。

三、历史行情阶段还要做哪些验证?

很多开发者会把“API 返回了数据”当作接入成功的标准。

对于量化项目,这个标准还不够。

至少应该检查以下几个方面。

1. 时间顺序

均线和其他滚动指标依赖数据顺序。

如果接口返回的数据是倒序排列,而程序直接调用 rolling(),计算结果就可能不符合预期。

在确认实际时间字段后,可以显式排序:

# 假设实际返回字段为 date
df = df.sort_values("date").reset_index(drop=True)

这里的 date 只是示例字段名,必须替换成真实返回的时间字段。

2. 重复记录

重复 K 线可能导致某些价格被重复计算,从而改变指标。

如果实际返回结果包含可靠的时间字段,可以检查重复记录:

# 假设 date 是每根日 K 的唯一时间标识
duplicates = df[df.duplicated(subset=["date"], keep=False)]

if not duplicates.empty:
    print("发现重复记录,需要进一步核查")

如果时间字段不足以唯一标识 K 线,则需要结合标的代码和周期构建合适的唯一键。

3. 数据口径

前复权、后复权和不复权数据可能呈现不同的历史价格序列。

如果研究目标是比较长期价格趋势,复权处理通常需要认真考虑;如果要模拟真实成交,则必须另外明确成交价格、交易时点和公司行为处理规则。

不能因为一条价格序列更平滑,就认为它一定适合所有研究任务。

4. 数据覆盖范围

250 根日 K 可以支持一定长度的指标计算,但不代表覆盖了足够多的市场环境。

对于较长周期的策略,历史数据的起止日期、市场状态和样本选择都会影响研究结论。

因此,数据接入阶段就应记录请求参数和实际返回的数据范围。

四、什么时候把实时行情加入这套程序?

假设历史研究已经完成,现在希望程序在交易时段持续更新最新价格,并观察均线是否出现变化。

这时,才需要进一步设计实时行情模块。

可以将系统拆成三个相对独立的部分:

历史行情 ──→ 指标初始化 ──┐
                          ├──→ 指标更新与信号判断
实时行情 ──→ 最新数据处理 ─┘

这是一种逻辑架构示意,不意味着历史行情和实时行情必须来自同一种接口,也不代表两种数据天然具有一致的字段和时间口径。

在实际开发中,至少要明确以下几点。

历史数据负责什么?

负责初始化滚动指标、提供研究基准,并在必要时补齐实时计算所需的历史窗口。

实时数据负责什么?

负责更新当前行情状态,并根据策略定义决定何时重新计算指标。

两种数据如何衔接?

需要对齐标的、时间戳、价格口径和 K 线周期,避免把同一根 K 线重复加入计算序列。

如果你的策略只在日线收盘后运行,可以在数据更新完成后计算一次信号,不一定需要持续获取盘中数据。

如果你的策略关注分钟级变化,则应进一步核实实时行情接口、分钟 K 线接口和实际更新规则是否满足需求。

QuantDash 官方示例仓库展示了历史 K 线获取和实时行情快照获取的基础调用方式,但具体实时接口的返回字段、更新频率和访问限制仍应以当前官方技术文档为准。(GitHub)

五、别让实时行情抢在研究流程之前

一个常见的开发陷阱是,过早将精力放在实时刷新、图表动画、消息通知和自动运行上。

这些功能能够改善使用体验,却不一定能帮助你验证策略。

如果均线计算存在错误,那么刷新得再快也只是在更频繁地展示错误结果。

如果回测没有正确处理交易成本,那么实时信号看起来再准确,也不能直接证明策略具备可执行的盈利能力。

如果数据时间戳没有正确处理,那么历史指标和实时指标就可能在同一时刻给出不同结果。

因此,更值得优先完成的是:

  1. 获取可用的历史行情。
  2. 验证数据结构和时间顺序。
  3. 建立可复现的指标计算流程。
  4. 根据明确的交易规则产生信号。
  5. 评估信号是否具有研究价值。
  6. 再决定实时模块需要提供哪些能力。

这样做的目的不是减少系统功能,而是让每个新增功能都有清晰的作用。

结语:先让程序能够解释过去,再让它持续观察现在

对于以研究为起点的个人量化项目,历史行情通常是最有价值的第一个模块。

它能够帮助你验证数据处理、指标计算和信号生成逻辑,也能让项目在没有实时服务依赖的情况下继续开发。

当研究流程稳定后,实时行情就有了明确的职责:更新当前市场状态,并将已经定义好的逻辑应用于新数据。

如果你的项目从一开始就是盘中监控工具,可以调整这一顺序,但仍应尽早准备足够的历史数据作为指标计算和状态判断的基础。

最重要的不是选择历史行情或实时行情中的某一个,而是先把项目最核心的那条数据处理链路做正确,再逐步扩展到下一条链路。

QuantDash 官方文档