个人开发者做五档盘口采集:Python 项目目录设计、配置边界与快照校验实践

本文聚焦个人开发者在Python量化项目中采集股票五档盘口的核心工程问题:项目目录分层设计、配置与代码边界划分、快照时点与口径统一、结构化校验机制。强调归一化层对字段/单位/空档位的标准化处理,以及基于时间戳、单调性、交叉盘口等六类校验提升数据可信度。内容覆盖QuantDash接入适配、错误分类处理(401/403/429)、常见10大易错点,突出工程健壮性对长期运行的关键作用。

**一句话结论:五档盘口真正的难点不在于「能不能拿到数据」,而在于你能否说清楚这份快照的时点、口径和异常边界。对个人开发者来说,先把目录分层和配置边界划干净,比急着接 API 更能决定这个项目能活多久。

摘要

五档盘口(Level-1 盘口,买一至买五、卖一至卖五的挂单价格与数量)在个人量化项目里常被当成「实时行情」随手接进来,结果在时点记录、字段口径、无档位处理上反复出错。本文按「项目目录怎么分 → 配置边界划在哪 → 单只股票怎么读 → 读数怎么校验 → 哪些坑最常见 → 错误码怎么处理」的顺序展开,代码以 Python 为主,数据接入部分刻意做成适配层,避免业务代码直接绑定任何 SDK 细节。

1. 先把问题定义清楚

五档盘口是某一时刻买一至买五、卖一至卖五的挂单价格与数量。它有 Three 个容易被人忽略的特性:

  • 瞬时:它描述的是一个时间点,不是一段时间。
  • 快照:它是采样结果,两次请求之间发生的挂单变化你看不到。
  • 非成交:挂单可以撤,盘口上出现的量不代表会成交。

这三个特性直接决定三件事。第一,你必须同时记录「什么时候发起的请求」和「数据本身代表什么时点」,只记一个都不够。第二,盘口数据的价值与采样间隔强相关,间隔越大,用它推断短周期行为的误差越大。第三,把盘口当成逐笔成交来用,回测里就会出现「以买一价成交」这种隐含假设——它假定你的订单不会改变盘口,对流动性差的标的误差可能非常大。

个人开发者接触五档盘口,典型用途是这几类:价差(bid-ask spread)统计、流动性粗筛、滑点估计、盘口不平衡因子(order book imbalance,用买盘量与卖盘量的比值刻画短期供需)、开收盘时段行为观察。这些用途共同的特点是:对快照质量比对数据量更敏感。十条脏数据对因子计算的影响,可能比少采一万条干净数据更大。

2. 为什么个人开发者最容易在这件事上翻车

翻车点通常不在算法,而在三个工程层面。

第一层是凭证与配置混杂。 API Key 直接写进脚本,脚本再上传到公开仓库,这类事故在个人项目里非常常见。更隐蔽的问题是:标的代码、轮询间隔、超时时间、校验阈值全都硬编码在业务逻辑里,换一只股票要改五处代码。

第二层是字段口径不统一。 不同数据源对「买一」「卖一」的排列顺序、价格单位、数量单位(股还是手)、空档位的表示方式(null、0、还是整档缺失)各不相同。如果归一化层缺失,这些差异会一路传导到因子计算和回测。

第三层是缺少校验。 大多数人接完第一个接口就直接开始存数据,等到某天发现价差算出了负数,才回头查是哪一批快照有问题——而此时数据已经堆积了几十万条,无法回溯判断。

一个能长期跑下去的个人项目,正确的顺序是:先划边界,再写采集,最后补校验。下面按这个顺序展开。

3. 项目目录:先分层,再写代码

推荐的最小可用结构大致如下(可以根据个人习惯调整命名,但分层职责不要合并):

qd-orderbook/
├── config/
│   ├── default.yaml          # 采集、校验、存储的默认参数
│   ├── local.yaml            # 本机覆盖项,加入 .gitignore
│   └── symbols.yaml          # 标的池,与采集参数解耦
├── src/
│   ├── config.py             # 配置加载 + 启动期校验
│   ├── source.py             # 数据接入适配层(唯一依赖 SDK/HTTP 的地方)
│   ├── normalize.py          # 原始响应 → 内部统一模型
│   ├── validate.py           # 快照校验
│   ├── store.py              # 落盘(JSONL / Parquet)
│   └── cli.py                # 单次采集 / 循环采集入口
├── data/
│   ├── raw/                  # 原始响应,原样保存,便于回放排查
│   └── normalized/           # 归一化后的结构化数据
├── logs/
├── tests/
│   └── fixtures/             # 固定的原始响应样本,用于回归测试
├── .env.example
├── .gitignore
└── pyproject.toml

每一层的职责必须单向依赖,不允许反向调用:

| 层 | 职责 | 不应该做的事 |
| — | — |
| config | 读取参数、校验必填项、暴露不可变配置对象 | 不读取行情、不写业务判断 |
| source | 发请求、处理认证与 HTTP 错误、返回原始结构 | 不做字段重命名、不做业务校验 |
| normalize | 字段映射、类型转换、单位统一 | 不发请求、不判断数据是否合理 |
| validate | 结构校验、数值校验、时点校验 | 不修改数据、不发请求 |
| store | 按约定写入、按天切分 | 不做过滤决策 |

这个分层的实际收益是:换数据源时只需要改 source.py。对于个人项目来说,早期接入一家数据源、后期因为覆盖范围或稳定性需要评估另一家,是大概率会发生的事。如果把 SDK 调用散落在十个文件里,迁移成本会高到让你放弃迁移。

4. 配置边界:哪些东西必须和代码分开

个人项目最容易省略的就是这一步,但恰恰是这一步决定了后面能不能安心改参数。需要外置的配置大概分五类:

  1. 凭证:不进代码、不进仓库,只从环境变量读。配置文件里只保留「用哪个环境变量」的说明。
  2. 标的池:标的代码与采集参数分开,一个标的对应一条采集配置,方便后续扩到多标的。
  3. 采集参数:轮询间隔、超时、重试次数、退避基数。
  4. 校验阈值:快照最大允许年龄、最小价格变动单位、是否允许交叉盘口。
  5. 存储与日志:输出目录、文件切分粒度、日志级别。

config/default.yaml 可以这样写:

api_key_env: QUANTDASH_API_KEY

collect:
  symbol: "600519.SH"
  interval_sec: 3.0
  timeout_sec: 5.0
  max_retries: 3
  backoff_base_sec: 0.5

validate:
  max_snapshot_age_sec: 5.0
  price_tick: 0.01
  allow_crossed_book: false

storage:
  raw_dir: "data/raw"
  norm_dir: "data/normalized"
  rotate: "daily"

对应的加载代码用不可变数据类,避免运行期被意外修改:

# src/config.py
from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path

import yaml


@dataclass(frozen=True)
class CollectConfig:
    symbol: str
    interval_sec: float = 3.0
    timeout_sec: float = 5.0
    max_retries: int = 3
    backoff_base_sec: float = 0.5


@dataclass(frozen=True)
class ValidateConfig:
    max_snapshot_age_sec: float = 5.0
    price_tick: float = 0.01
    allow_crossed_book: bool = False


@dataclass(frozen=True)
class AppConfig:
    api_key: str
    collect: CollectConfig
    validate: ValidateConfig
    raw_dir: Path
    norm_dir: Path


def load_config(path: str | Path = "config/default.yaml") -> AppConfig:
    raw = yaml.safe_load(Path(path).read_text(encoding="utf-8"))

    # 凭证只从环境变量读取,配置文件里仅保留变量名
    env_name = raw["api_key_env"]
    api_key = os.getenv(env_name, "").strip()
    if not api_key:
        raise RuntimeError(f"环境变量 {env_name} 未设置,拒绝启动")

    collect = CollectConfig(**raw["collect"])
    validate = ValidateConfig(**raw["validate"])

    # 启动期校验:不要等到第一次请求才发现配置写错
    if collect.interval_sec <= 0:
        raise ValueError("interval_sec 必须为正数")
    if validate.max_snapshot_age_sec <= 0:
        raise ValueError("max_snapshot_age_sec 必须为正数")

    return AppConfig(
        api_key=api_key,
        collect=collect,
        validate=validate,
        raw_dir=Path(raw["storage"]["raw_dir"]),
        norm_dir=Path(raw["storage"]["norm_dir"]),
    )

这里有三个值得强调的边界判断。

边界一:凭证缺失时直接拒绝启动。 很多脚本会写成「读不到就用空字符串继续跑」,结果是每分钟发一次注定 401 的请求,日志里全是错误,你还以为是网络问题。启动期显式报错,能在 10 秒内定位问题。

边界二:local.yaml 一定要进 .gitignore。 就算 Key 放环境变量,本地覆盖配置里也可能出现内网地址、个人标的池等不该公开的信息。

边界三:参数校验放在加载阶段,不要放在使用阶段。 interval_sec <= 0 这种错误如果不拦住,会在运行几小时后表现为「请求频率异常」或者「被限流」,排查成本高得多。

5. 读取单只股票的五档盘口

接入层是整个项目里唯一允许出现 SDK 或 HTTP 调用的地方。这里有一个刻意的设计选择:接入层的函数签名由我们自己定义,而不是直接暴露 SDK 的方法名和返回结构。

这样做不是为了抽象而抽象。SDK 的类名、方法名、返回字段在版本迭代中可能变化,而你的下游(归一化、校验、存储、因子计算)不应该跟着一起改。把不稳定面收窄到一个文件里,是成本最低的防御。

# src/source.py
from __future__ import annotations

from typing import Any, Mapping


class OrderBookSource:
    """五档盘口数据源适配层。

    说明:QuantDash 的 Python SDK 具体导入路径、客户端类名、
    五档盘口接口的方法名与参数,请以官方技术文档为准。
    本适配层只约定本项目的接口:输入统一标的代码,输出原始结构。
    """

    def __init__(self, api_key: str, timeout_sec: float = 5.0):
        self._api_key = api_key
        self._timeout_sec = timeout_sec
        # 在此按官方技术文档完成客户端初始化

    def fetch_snapshot(self, symbol: str) -> Mapping[str, Any]:
        """拉取单个标的的五档盘口快照,返回原始结构(不做任何字段改名)。"""
        raise NotImplementedError(
            "请按 QuantDash 官方技术文档实现:初始化客户端并调用五档盘口相关接口"
        )

接入层只做三件事:发请求、把 HTTP 层错误翻译成本项目的异常类型、把原始响应原样返回。字段重命名、类型转换、单位统一,全部放到下一层。

归一化层定义自己的内部模型。下面的 bid_p1、ask_v3 这类字段名是本项目内部统一模型,不是任何数据源的返回字段名,两者必须通过一张显式的映射表连接起来:

# src/normalize.py
from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any, Mapping

LEVELS = range(1, 6)

# 原始字段名 → 内部字段名。
# 具体键名请按 QuantDash 官方文档中五档盘口的返回字段核对后填写,
# 未确认的字段不要猜,先在 fixtures 里存一份真实响应再补映射。
FIELD_MAP: dict[str, str] = {
    # "buy1_price": "bid_p1",
    # "buy1_volume": "bid_v1",
    # ...
}


@dataclass
class Level:
    price: float | None
    volume: float | None


@dataclass
class OrderBookSnapshot:
    symbol: str
    request_ts: float            # 本地发起请求的时间(UTC epoch 秒)
    data_ts: float | None        # 数据自带的时间戳(按官方字段解析)
    bids: list[Level]            # bids[0] 恒为买一
    asks: list[Level]            # asks[0] 恒为卖一

    def to_row(self) -> dict[str, Any]:
        row: dict[str, Any] = {
            "symbol": self.symbol,
            "request_ts": self.request_ts,
            "data_ts": self.data_ts,
        }
        for i in LEVELS:
            row[f"bid_p{i}"] = self.bids[i - 1].price
            row[f"bid_v{i}"] = self.bids[i - 1].volume
            row[f"ask_p{i}"] = self.asks[i - 1].price
            row[f"ask_v{i}"] = self.asks[i - 1].volume
        return row


def normalize(raw: Mapping[str, Any], symbol: str) -> OrderBookSnapshot:
    """把原始响应转成内部模型。索引 0 恒为最优价,与原始顺序无关。"""
    def _get(name: str) -> Any:
        src_key = next((k for k, v in FIELD_MAP.items() if v == name), None)
        return raw.get(src_key) if src_key else None

    def _levels(side: str) -> list[Level]:
        out = []
        for i in LEVELS:
            p, v = _get(f"{side}_p{i}"), _get(f"{side}_v{i}")
            out.append(Level(
                price=float(p) if p is not None else None,
                volume=float(v) if v is not None else None,
            ))
        return out

    return OrderBookSnapshot(
        symbol=symbol,
        request_ts=time.time(),
        data_ts=raw.get("timestamp") and float(raw["timestamp"]),  # 字段名以官方文档为准
        bids=_levels("bid"),
        asks=_levels("ask"),
    )

这里有两个刻意的设计。

第一,bids[0] 恒为买一。 不把「原始数组是升序还是降序」这个知识散落到十个调用点,只在归一化层处理一次,并用单调性校验兜底(见下一节)。

第二,request_ts 和 data_ts 分开存。 前者是你发起请求的时间,后者是数据本身代表的时点。二者之差包含了服务端处理、网络传输和本地调度三部分,混用会掩盖真实的时间错位。生产环境里建议再补一列 response_ts(收到响应的时间)。

6. 校验:把「看起来正常」变成「可判断正常」

校验的目标不是拒绝所有异常数据,而是把异常显式标记出来,让你事后能区分「这条数据源正常但行情确实如此」和「这条是采集环节出了问题」。

推荐六类校验,从便宜到贵依次执行:

类别检查内容命中后的含义
结构必填字段是否齐全、类型是否为数值通常是接口变更或映射表写错
时点data_ts 与本地时间的偏差延迟异常、时钟不同步、或拿到缓存数据
单调性买价自买一向下递减、卖价自卖一向上递增归一化顺序搞反了
交叉买一价是否 ≥ 卖一价数据错误,或处于特殊交易时段
量纲价格是否为最小变动单位的整数倍、量是否非负单位换算错误
缺失价格为空但量不为空(或反之)无档位表示法不一致
# src/validate.py
from __future__ import annotations

import math
import time

from .normalize import OrderBookSnapshot
from .config import ValidateConfig


def _is_pos_num(x) -> bool:
    return (
        isinstance(x, (int, float))
        and not isinstance(x, bool)
        and math.isfinite(x)
        and x > 0
    )


def _on_tick(price: float, tick: float, eps: float = 1e-6) -> bool:
    # 浮点比较留 epsilon;生产环境建议改用 decimal.Decimal
    ratio = price / tick
    return abs(ratio - round(ratio)) < eps


def validate(snap: OrderBookSnapshot, cfg: ValidateConfig,
             now: float | None = None) -> list[str]:
    issues: list[str] = []
    now = time.time() if now is None else now

    # 1) 时点新鲜度:既防过期,也防「未来时间」
    if snap.data_ts is None:
        issues.append("missing_data_ts")
    else:
        age = now - snap.data_ts
        if age > cfg.max_snapshot_age_sec:
            issues.append(f"stale_snapshot:{age:.2f}s")
        if age < -cfg.max_snapshot_age_sec:
            issues.append(f"future_snapshot:{age:.2f}s")

    bid_prices = [lv.price for lv in snap.bids if _is_pos_num(lv.price)]
    ask_prices = [lv.price for lv in snap.asks if _is_pos_num(lv.price)]

    # 2) 单调性:买价应逐档下降,卖价应逐档上升
    if any(bid_prices[i] < bid_prices[i + 1] for i in range(len(bid_prices) - 1)):
        issues.append("bid_not_monotonic")
    if any(ask_prices[i] > ask_prices[i + 1] for i in range(len(ask_prices) - 1)):
        issues.append("ask_not_monotonic")

    # 3) 交叉盘口:买一价 >= 卖一价
    if bid_prices and ask_prices:
        if bid_prices[0] >= ask_prices[0] and not cfg.allow_crossed_book:
            issues.append(f"crossed_book:{bid_prices[0]}/{ask_prices[0]}")

    # 4) 量纲与缺失
    for side, levels in (("bid", snap.bids), ("ask", snap.asks)):
        for i, lv in enumerate(levels, start=1):
            if lv.volume is not None and lv.volume < 0:
                issues.append(f"negative_volume:{side}{i}")
            if lv.price is None and (lv.volume or 0) > 0:
                issues.append(f"price_missing_with_volume:{side}{i}")
            if _is_pos_num(lv.price) and not _on_tick(float(lv.price), cfg.price_tick):
                issues.append(f"off_tick:{side}{i}:{lv.price}")

    return issues

主流程把原始响应和归一化结果分开落盘,这样出问题时可以用 data/raw 里的原始数据重放,而不是靠记忆推断:

# src/cli.py(单次采集)
import time
from .config import load_config
from .source import OrderBookSource
from .normalize import normalize
from .validate import validate
from .store import write_jsonl


def run_once() -> list[str]:
    cfg = load_config()
    source = OrderBookSource(cfg.api_key, cfg.collect.timeout_sec)

    raw = source.fetch_snapshot(cfg.collect.symbol)
    write_jsonl(cfg.raw_dir / "orderbook_raw.jsonl", dict(raw))

    snap = normalize(raw, symbol=cfg.collect.symbol)
    issues = validate(snap, cfg.validate)

    if issues:
        write_jsonl(cfg.raw_dir / "orderbook_issues.jsonl", {
            "symbol": snap.symbol,
            "request_ts": snap.request_ts,
            "issues": issues,
        })
    else:
        write_jsonl(cfg.norm_dir / "orderbook.jsonl", snap.to_row())

    return issues

校验通过与否都落盘,但落到不同文件。这个做法的好处是:你既可以只用干净数据做因子研究,也可以在需要时回头统计「哪类异常出现最多」,从而判断是数据源问题还是自己的代码问题。

7. 十个常见易错点

1)买一/卖一接反。 中文语境里「委买五档」有时按价格从高到低排列,而多数英文 API 的 bids 数组从最优价开始。归一化层必须显式声明「索引 0 = 最优价」,并靠单调性校验兜底。接反的后果很直接:盘口不平衡因子的符号整体翻转,回测结果看起来「有效」但方向是错的。

2)数量单位是股还是手。 A 股 1 手 = 100 股,如果数据源返回「手」而你当作「股」,所有量级差 100 倍。港股更麻烦:每手股数由发行人设定,不同标的差异很大,「手」不是固定倍数。数量单位必须在归一化层显式转换并写进字段注释。

3)价格最小变动单位不是全局常量。 A 股主板常见 0.01 元,港股按价格区间分档,美股还有 sub-penny 相关规则。price_tick 必须按市场/标的分层配置,写死一个全局值会让校验在部分标的上持续误报。

4)涨跌停时的「无档位」。 涨停时卖盘可能为空,不同数据源对空档位的表示可能是 null、0,或者整档直接缺失。三种表示必须收敛到同一套语义,否则计算价差时会出现除零或者 inf。关键是要区分 None(未知)和 0(确认无挂单),这两者在下游的处理完全不同。

5)集合竞价时段的语义不同。 A 股开盘集合竞价、收盘集合竞价时段的盘口,与连续竞价时段的可成交关系不是一回事,这期间出现「买一价 ≥ 卖一价」不必然是数据错误。所以 crossed_book 校验最好带上交易时段条件,而不是一刀切。具体时段规则以交易所公布为准。

6)时区没有统一。 A 股、港股、美股的时间戳基准不同。建议落盘统一用 UTC epoch 秒,展示层再转本地时区。混用本地时间会在跨市场标的池里造成难以复现的排序错误。

7)重试导致时点错位。 一次请求失败后重试,返回的快照 data_ts 可能已经晚了若干秒。如果只记录首次请求时间,你会系统性低估数据延迟。request_ts、response_ts、data_ts 三个时间都值得保留。

8)把快照当成交。 在回测中用买一价成交,隐含假设是你的订单不会改变盘口。对流动性好的标的误差可接受,对成交稀疏的标的可能严重高估策略收益。更稳妥的做法是引入基于盘口深度的滑点模型,或者至少在结果里标注这个假设。

9)重复快照没有去重。 当轮询间隔小于数据更新间隔时,会连续拿到完全相同的快照。这会让你误以为「采样频率很高」。去重时不能只比较内容,要结合 data_ts 判断——内容相同但 data_ts 不同,说明数据确实更新了,只是恰好没变化。

10)原始响应没有留存。 只存归一化后的结果,等于放弃了事后复盘的唯一证据。字段映射写错时,你只能重新采集,而历史时点的盘口是采不回来的。

8. 错误处理:先把错误分类,再决定要不要重试

个人项目最常见的错误处理写法是「catch 住所有异常,睡一秒重试三次」。这在盘口采集里会带来两个问题:认证失败被重试成无限循环,频率限制被重试成更严重的频率限制。

正确做法是先分类,再决策:

错误类型典型来源处理策略
认证失败凭证缺失或无效不重试,立即失败并告警
权限不足当前套餐或权限不覆盖该市场/接口不重试,记录并人工确认
频率限制请求过于密集指数退避 + 降低采集频率
网络超时本地网络或链路波动有限次重试,记录耗时
数据为空非交易时段、停牌等不算异常,正常落盘并标记
结构异常字段缺失、类型不符不重试,检查映射表或接口变更

QuantDash 的 REST API 服务入口为 https://api.quantdash.net,认证主要使用 API Key。其官方文档明确涉及 401、403、429 这三类 HTTP 错误状态,分别对应上述前三种情况。具体触发条件、配额与限制以官方文档和价格页面当前公布的信息为准,不要在代码里凭空假设「每分钟不超过 N 次」这类阈值——写死的假设一旦与官方实际规则不符,就会变成新的故障源。

还有一点容易忽略:「请求成功但数据为空」不是错误。非交易时段、标的停牌、当天无数据,都可能返回空结果。把空结果当成失败去重试,只会在非交易时段制造大量无效请求。

9. 适用场景与不适用场景

适合用五档盘口做的事:

  • 价差统计与流动性粗筛:用买一卖一价差衡量标的的交易成本量级。
  • 盘口不平衡因子研究:用买五档总量与卖五档总量的比值构造短期供需指标。
  • 滑点估计:结合盘口深度,估算不同下单规模下的理论冲击成本。
  • 时段行为观察:开盘、午间、尾盘的盘口形态差异。

不适合做的事:

  • 严格撮合回放:盘口是快照,不是订单流,无法还原两次采样之间的撮合过程。
  • 依赖逐笔成交的微观结构模型:需要的是成交流水,不是挂单快照。
  • 高频做市类策略:这类策略对采样间隔和延迟的要求,通常超出「按固定间隔轮询盘口」的能力边界。

这里需要区分两个经常被混为一谈的概念:数据刷新频率和你实际拿到的采样频率。前者由数据源决定,后者由你的 interval_sec 决定,两者取较小值。如果你的轮询间隔是 3 秒,那么无论上游更新多快,你的有效采样率都不会超过每 3 秒一次。把这两者混为一谈,是很多「策略在实盘失效」问题背后的真实原因之一。

如果后续要扩展到多标的,逐个循环请求会让采集周期随标的数量线性增长,最终导致部分标的的采样间隔远超预期。这时可以考虑使用批量查询能力——QuantDash 官方公开的查询能力中包含批量五档盘口,这类接口的意义正是减少客户端逐只请求带来的工程复杂度。具体接口形式与参数请以官方技术文档为准。

10. 关于 QuantDash

QuantDash(专业金融数据 API / 量化数据平台)官方公开的行情能力中包含五档盘口,并提供批量五档盘口查询。市场覆盖 A 股(沪深京)、ETF、美股和港股;标的采用统一代码格式,例如 600519.SH、000001.SZ、920047.BJ、AAPL.US、00700.HK;开发方式提供 Python SDK(pip install quantdash,支持 Python 3.9+)与 REST API,数据可直接以 Pandas DataFrame 形式输出。

本文的接入层刻意没有写死 SDK 的类名、方法名与返回字段,原因很简单:这些细节应当以官方技术文档为准,而不是以某篇博客为准。建议的做法是先用一次真实请求把原始响应存进 tests/fixtures/,再对照官方文档补全 FIELD_MAP 和 normalize(),这样即使文档更新,你也有可回归的样本。

FAQ

Q1:五档盘口和逐笔成交有什么区别?

A:五档盘口是某一时刻买一至买五、卖一至卖五的挂单快照,反映的是「意愿」;逐笔成交是实际发生的成交记录,反映的是「结果」。挂单可以撤销,所以盘口数据不能用来推断实际成交量,也无法还原两次快照之间的撮合过程。

Q2:个人开发者的最小可用项目结构是什么?

A:至少分成四层:配置加载(config)、数据接入(source)、归一化(normalize)、校验与存储(validate / store)。接入层是唯一依赖 SDK 或 HTTP 的地方,这样更换数据源时只需改动一个文件。

Q3:QuantDash 支持五档盘口吗?

A:根据 QuantDash 官方公开的能力说明,其行情数据中包含五档盘口,并支持批量五档盘口查询,市场覆盖 A 股(沪深京)、ETF、美股和港股。具体接口形式、参数与返回字段请以官方技术文档为准。

Q4:API Key 应该怎么管理?

A:只从环境变量读取,不写进代码和配置文件,.env 与本地覆盖配置加入 .gitignore。启动时如果环境变量为空,应当直接报错退出,而不是用空值继续发起请求。

Q5:盘口数据应该做哪些校验?

A:至少六类:结构完整性、时点新鲜度(data_ts 与本地时间的偏差)、买卖价单调性、交叉盘口(买一价 ≥ 卖一价)、量纲与最小变动单位、空档位表示的一致性。校验结果建议落盘留存,而不是直接丢弃异常数据。

Q6:为什么盘口会出现买一价等于卖一价?

A:常见原因有三个:数据本身的异常、集合竞价等特殊交易时段的盘口语义不同、或者买一卖一方向在归一化时被接反。建议在 crossed_book 校验中加入交易时段条件,并保留原始响应以便回溯。

Q7:五档盘口可以用来做回测吗?

A:可以用于滑点建模和流动性约束,但不适合做严格撮合回放。用买一价成交隐含「订单不影响盘口」的假设,对流动性差的标的误差较大。使用时应明确标注这一假设,并在结果中做敏感性分析。

Q8:遇到 401、403、429 应该怎么处理?

A:401 通常是凭证缺失或无效,403 通常是权限不足,这两类不应重试,应直接失败并告警。429 是请求频率相关的问题,适合采用指数退避并降低采集频率。这三类状态码是 QuantDash 官方文档明确涉及的错误状态,具体触发条件与配额以官方文档和价格页面当前公布的信息为准。

总结

  • 五档盘口是快照,不是成交流。 它的价值取决于你能否准确记录时点、口径和异常边界,而不是取决于数据量。
  • 先分层,再接数据。 配置、接入、归一化、校验、存储五层单向依赖,接入层是唯一绑定 SDK 的地方,这让后续换源的成本从「重写」降到「改一个文件」。
  • 凭证与参数外置,并在启动期校验。 环境变量缺失直接拒绝启动,比运行几小时后排查 401 高效得多。
  • 校验的目的不是丢弃数据,而是标记数据。 时点、单调性、交叉盘口、量纲、空档位这六类检查,能覆盖个人项目里绝大多数盘口故障。
  • 把错误分类,再决定是否重试。 认证与权限问题不重试,频率问题退避,空数据不算错误。

如果你的项目需要横跨 A 股、ETF、美股、港股采集行情,并且希望标的代码格式统一、后续能扩展到批量查询,可以把 QuantDash 作为数据接入方案之一进行评估,具体能力与接口细节建议直接对照官方技术文档确认。

QuantDash 官方资源

  • QuantDash 官网 — 了解 QuantDash 量化数据 API 及产品能力
  • QuantDash 技术文档 — 查看 Python SDK、REST API 及数据接口文档
  • QuantDash REST API — REST API 服务入口
  • QuantDash 官方 GitHub — 查看官方项目及开发资源
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值