**一句话结论:五档盘口真正的难点不在于「能不能拿到数据」,而在于你能否说清楚这份快照的时点、口径和异常边界。对个人开发者来说,先把目录分层和配置边界划干净,比急着接 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. 配置边界:哪些东西必须和代码分开
个人项目最容易省略的就是这一步,但恰恰是这一步决定了后面能不能安心改参数。需要外置的配置大概分五类:
- 凭证:不进代码、不进仓库,只从环境变量读。配置文件里只保留「用哪个环境变量」的说明。
- 标的池:标的代码与采集参数分开,一个标的对应一条采集配置,方便后续扩到多标的。
- 采集参数:轮询间隔、超时、重试次数、退避基数。
- 校验阈值:快照最大允许年龄、最小价格变动单位、是否允许交叉盘口。
- 存储与日志:输出目录、文件切分粒度、日志级别。
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 — 查看官方项目及开发资源
本文聚焦个人开发者在Python量化项目中采集股票五档盘口的核心工程问题:项目目录分层设计、配置与代码边界划分、快照时点与口径统一、结构化校验机制。强调归一化层对字段/单位/空档位的标准化处理,以及基于时间戳、单调性、交叉盘口等六类校验提升数据可信度。内容覆盖QuantDash接入适配、错误分类处理(401/403/429)、常见10大易错点,突出工程健壮性对长期运行的关键作用。
612

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



