盘中做量化候选筛选时,你常常同时需要两样东西:当前价格和盘口深度。
价格用来判断涨跌幅、成交量和价格区间;盘口用来观察买卖挂单的厚度和价差,判断流动性是否足够。
一个典型流程是:
看起来很直接。但如果把价格和盘口放在同一个请求里处理,代码写起来简单,维护起来却容易踩坑。
价格(实时行情)按标的池或批量标的查询,一次就能拿到几十到几百只股票的最新价、涨跌幅、成交量。五档盘口虽然也支持批量,但每只标的返回的是买卖各五档的价格和数量,数据量比行情大得多。
合在一起请求时,你会面临两难:
实时行情和盘口通常共用同一个频率配额。把两者放在同一个循环里批量请求,价格查询的失败重试可能会消耗掉盘口的配额,反过来也一样。
一个具体例子:假设你的套餐每分钟允许 300 次请求。你每轮扫描 200 只股票,行情和盘口各一次,那就是 400 次——直接超限。被迫降频后,两类的数据时效性一起下降。
行情接口偶尔超时、盘口接口偶尔空返回,这些异常如果写在一个 try/except 块里,要么因为盘口异常导致行情数据也没拿到,要么为了区分错误类型写出越来越长的分支。
核心思路是:把行情和盘口拆成独立的获取层,各自管理自己的请求节奏、频率和重试。
┌─────────────────────────────────────┐
│ 筛选调度层 │
│ 1. 调用行情层获取全池最新价格 │
│ 2. 用本地条件过滤候选池 │
│ 3. 调用盘口层查询候选池的深度 │
│ 4. 合并结果供决策 │
└──────┬──────────────────────┬───────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ 行情层 │ │ 盘口层 │
│ quotes.get │ │ depth.get │
│ 独立重试 │ │ 独立重试 │
│ 独立频率 │ │ 独立频率 │
└──────────────┘ └─────────────┘每层只做一件事,各自封装请求细节、错误处理和重试策略。
以下示例使用 QuantDash SDK 演示分层获取。行情层 负责拉取全池实时行情,盘口层 只对筛选后的少量标的查询深度。
# 环境:Python 3.10,quantdash==0.1.0
import os
import time
from typing import List
from quantdash import QuantDash
import pandas as pd
qd = QuantDash(api_key=os.getenv("QUANTDASH_API_KEY"))
# ── 行情层 ──────────────────────────────
def fetch_quotes(symbols: List[str]) -> pd.DataFrame:
"""获取一批标的的实时行情,独立重试"""
max_retries = 2
for attempt in range(max_retries):
try:
df = qd.quotes.get(
symbols=symbols,
to_dataframe=True,
)
if df is not None and not df.empty:
return df
except Exception as e:
if "429" in str(e):
# 限流时按服务端指示等待
time.sleep(5)
continue
raise
return pd.DataFrame()
# ── 盘口层 ──────────────────────────────
def fetch_depths(symbols: List[str]) -> dict:
"""获取候选标的的五档盘口,独立重试"""
if not symbols:
return {}
max_retries = 2
for attempt in range(max_retries):
try:
result = qd.depth.batch(symbols)
return result if result else {}
except Exception as e:
if "429" in str(e):
time.sleep(3)
continue
raise
return {}
# ── 筛选调度 ────────────────────────────
def screen_candidates(pool_symbols: List[str]) -> pd.DataFrame:
"""筛选流程:先行情后盘口,分层获取"""
# 第1步:获取全池实时行情
quotes_df = fetch_quotes(pool_symbols)
if quotes_df.empty:
return pd.DataFrame()
# 第2步:本地过滤——涨跌幅 > 2% 且成交量大于均量
candidates = quotes_df[
(quotes_df["ext.change_pct"] > 0.02)
& (quotes_df["volume"] > 1_000_000)
]
if candidates.empty:
return candidates
# 第3步:只对候选标的查盘口
candidate_symbols = candidates["symbol"].tolist()
depths = fetch_depths(candidate_symbols)
# 第4步:将盘口信息合并到结果中
for sym in candidate_symbols:
if sym in depths:
d = depths[sym]
candidates.loc[
candidates["symbol"] == sym, "bid1_price"
] = d["bid_prices"][0] if d.get("bid_prices") else None
candidates.loc[
candidates["symbol"] == sym, "ask1_price"
] = d["ask_prices"][0] if d.get("ask_prices") else None
return candidates
# 使用示例
if __name__ == "__main__":
pool = ["600519.SH", "000001.SZ", "300750.SZ", "002594.SZ"]
result = screen_candidates(pool)
print(result[["symbol", "ext.last_price", "ext.change_pct", "bid1_price", "ask1_price"]])fetch_quotes 和 fetch_depths 可以分别写单元测试,不需要模拟对方的返回值。分层不是银弹。以下情况可以适当放宽:
Q:分层后实时性会不会变差?
A:分两层顺序执行,总耗时 = 行情耗时 + 本地过滤耗时 + 盘口耗时。本地过滤是毫秒级,盘口只查少量标的,总耗时通常比一次查全量盘口还短。关键在于盘口查询的标的数远少于全池。
Q:如果行情层和盘口层各自的频率限制不同怎么办?
A:这正是分层要解决的问题。每层内部可以单独实现指数退避或令牌桶,互不影响。合在一起时你只能用一个最保守的全局限速。
Q:盘口层返回空了还要继续吗?
A:盘口空可能是非交易时段或标的流动性不足。分层设计允许你根据空结果做决策:比如盘口为空时标记该标的为「流动性不足」,不阻塞整体筛选流程。
Q:批量盘口返回的数据怎么和行情 DataFrame 对齐?
A:按 symbol 字段做键值匹配是最直接的方式。上面示例中直接按 symbol 提取并写入新列。也可以统一转 DataFrame 后用 merge 拼接。
盘中筛选同时需要价格和盘口时,按需分层比「一次全拿」更容易维护。三层分离——行情层、盘口层、筛选调度层——各自管理自己的频率、重试和错误处理,长期运行的好处远大于一次编码的便利。
在标的数量增多、频率配额有限或需要独立测试各层时,分层几乎总是更可持续的选择。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。