这个问题我问过自己很多次。9:15 到 9:25 ,A 股在统一撮合,开盘价 9:25 就定了。但大多数行情接口在这个窗口返回空值或者直接报错。你的策略如果依赖开盘价做决策,你拿到的"开盘价"到底是 9:25 的竞价结果,还是 9:30 连续竞价的第一笔成交?这两个东西的含义完全不同。
为了搞清楚这个问题,我把个人开发者能接触到的数据源基本试了一遍。AkShare 在 9:15 直接抛 ValueError: Length mismatch,Tushare 不报错但价格字段空到 9:30 ,Baostock 没有实时路径,聚宽和米筐的分钟线从 9:30 才开始,券商 Level-1 封装把竞价虚拟字段过滤得干干净净,机构 Level-2 有完整数据但个人拿不到权限。
最后找到一家在这个窗口有数据的,REST 端点,不需要机构资质。下面是我在 9:15 到 9:30 之间做的完整实测,标的是寒武纪( 688256.SH )。
这 10 分钟的静默,代价是什么
大多数人知道"接口在 9:15 没数据",但没把代价算清楚。
信号迟到 5 分钟。 9:25 交易所主机完成统一撮合,开盘价确定。你的接口 9:30 才给出第一个价格。高波动日,5 分钟可以差 3-5 个点。
K 线量从第一根 bar 就是混的。 实测 1 分钟 K 线:09:30:00 那根的 volume 是 596 手,全是集合竞价统一撮合的量; 09:31:00 起才是连续竞价,第一根就有 2551 手。如果你用"首根 K 线量 vs N 日均量"判断开盘强弱,你在拿两种完全不同机制产生的量做比较。
你不知道那个 "open" 是哪一刻产生的。 是 9:25 统一撮合的价格,还是 9:30:01 连续竞价的第一笔?前者是全市场博弈的均衡点,后者是一笔主动单推动的瞬时成交。算止损、算缺口、算回测基准,含义完全不同。
9:25 那一下,才是真正的开盘
券商 App 显示的开盘价是 9:25 产生的,不是你接口里拿到的 9:30 第一笔。9:15 到 9:20 可报可撤,9:20 到 9:25 只许报不许撤,9:25 交易所主机按最大成交量原则算出一个价格——这才是开盘价。9:30 开始的连续竞价是另一套逻辑:一笔订单冲进来,找到对手方就成交。
一个细节说透区别:上交所数据显示,9:20 到 9:25 不可撤单阶段,每天仍有约 1838 笔撤单尝试被系统作废。在撮合前的最后一刻,还有人试图撤回申报,改变供需结构。这 10 分钟的博弈强度,远超你从 9:30 的 K 线上看到的。
找到的那家数据源,窗口内全程实测
2026 年 9 月 3 日,我从 9:15 到 9:30 ,每隔约 2 分钟调一次接口,共 7 个采集点。
9:15 — 竞价开窗
GET https://api.tickdb.ai/v1/market/ticker?symbols=688256.SH
{
"last_price": "1120", # 虚拟撮合价,非昨收
"prev_close": "1108",
"volume_24h": "0",
"high_24h": "0",
"low_24h": "0"
# open 字段:不存在
}
GET https://api.tickdb.ai/v1/market/depth?symbol=688256.SH
{
"bids": [["1126","35"], ...],
"asks": [["1126","35"], ...] # bid = ask ,虚拟撮合特征
}
GET https://api.tickdb.ai/v1/market/trades?symbol=688256.SH
{
"trades": [] # 无成交记录
}
三个信号同时出现:open 字段缺失,volume_24h 归零,depth 里买一等于卖一,trades 为空。这是集合竞价进行中的 API 状态,和 9:30 连续竞价是两套完全不同的字段结构。
9:20 — 撤单截止
GET https://api.tickdb.ai/v1/market/depth?symbol=688256.SH
{
"bids": [["1126","133"], ...] # 量从 44 跳到 133
}
撤单通道关闭,只有新单能进,没有旧单能出。深度量开始单调递增。
9:24 — 最后 1 分钟
GET https://api.tickdb.ai/v1/market/depth?symbol=688256.SH
{
"bids": [["1126","324"], ...] # 量从 179 急涌到 324
}
量的积累轨迹:
| 时间 | bid[0]量 | 增量 |
|---|---|---|
| 9:15 | 35 | — |
| 9:20 | 133 | +89 |
| 9:24 | 324 | +145 |
这条曲线在 9:30 之前 5 分钟就成型了。9:20 后无法撤单,这个阶段的行为比开盘价本身更真实。
9:25 — 统一撮合完成
GET https://api.tickdb.ai/v1/market/ticker?symbols=688256.SH
{
"open": "1126", # 字段首次出现
"volume_24h": "596", # 竞价总成交量
"high_24h": "1126",
"low_24h": "1126" # 三价合一
}
GET https://api.tickdb.ai/v1/market/trades?symbol=688256.SH
{
"trades": [{
"price": "1126",
"quantity": "596",
"side": "neutral", # 竞价统一撮合专属标记
"timestamp": 1788398703000
}]
}
时间戳精确到秒:09:25:03 。open 字段出现、side=neutral 出现、depth 恢复双边不对称——三个事件同时发生。
9:30 — 连续竞价开始,K 线真相
bar① 09:30:00 open=1126 high=1126 low=1126 close=1126 volume=596
bar② 09:31:00 open=1124.68 high=1129.86 low=1111 close=1112 volume=2551
09:30:00 这根 bar 四价合一,volume 全是集合竞价量。 连续竞价从 09:31:00 才开始。把这根 bar 和 09:31:00 之后的 bar 放在一起做量价分析,信号从第一根 K 线就是失真的。
三个字段信号,判断数据来自哪个阶段
open 字段是否存在。 竞价进行中,open 不在响应体里——不是 null ,是字段本身缺失。9:25 后字段首次出现。用字段存在性判断阶段,比同步本地时钟可靠。
side 是否为 neutral。 9:25 后 trades 里最新一条的 side 是 neutral。连续竞价开始后只有 buy/sell。这个语义来自 FIX 协议——集合竞价没有主动方。大多数数据源的成交记录不区分这两种成交,你拿到一条记录不知道它来自 9:25 还是 9:30:01 。
high_24h == low_24h。 竞价刚结束时,high 和 low 都等于 open 。三价合一是竞价 bar 的专属特征。连续竞价开始后立刻分叉。
代码
import os
import requests
BASE_URL = "https://api.tickdb.ai"
HEADERS = {"X-API-Key": os.getenv("TICKDB_API_KEY")}
def check_auction_phase(symbol: str) -> str:
try:
ticker_resp = requests.get(
f"{BASE_URL}/v1/market/ticker",
headers=HEADERS,
params={"symbols": symbol},
timeout=10,
)
ticker_data = ticker_resp.json()["data"][0]
trades_resp = requests.get(
f"{BASE_URL}/v1/market/trades",
headers=HEADERS,
params={"symbol": symbol},
timeout=10,
)
trades = trades_resp.json()["data"]["trades"]
if "open" not in ticker_data:
return "auction"
if trades and trades[0].get("side") == "neutral":
return "closed_auction"
return "continuous"
except requests.RequestException as e:
print(f"请求失败: {e}")
raise
核心逻辑就是 "open" not in ticker_data 这一行。字段存在性就是状态机。
集合竞价数据到底值多少
回到开头的问题。实测之后,我的判断是:
把信号提前 5 分钟。 9:25 竞价完成,你可以立刻知道高开还是低开、竞价量是大是小,不用等到 9:30 。
读懂量的积累过程。 9:20 撤单截止前后的量跳变幅度,反映不可撤单阶段有多少坚定买入;最后 1 分钟的急涌幅度,反映临近清算时市场情绪是否在加速集中。这个曲线 9:30 之前就能读。
识别重大事件后的市场态度。 财报、监管公告、隔夜美股大跌之后,集合竞价是市场第一次集中表态的窗口。价格飘移速度、撮合价稳定性、不可撤单阶段量的急增幅度——这些过程数据能区分"信息完全消化"和"分歧极大时的脆弱均衡",开盘价本身区分不了。
让风控有可靠的价格基准。 止损单挂在"开盘价基础上",这个开盘价必须是 9:25 确定的那个。如果你的风控系统在 9:25 需要计算持仓市值,用 9:30 的连续竞价价格会有几分钟延迟。
回测数据质量的基础保障。 如果你用含集合竞价量的 09:30:00 bar 做"开盘强度"指标,回测曲线存在系统性偏差。用 trades 原始记录区分竞价和连续竞价成交,是修正这个偏差的唯一办法。
说几个我自己的判断,不一定对:
-
免费行情接口在集合竞价窗口的沉默,本质上是一种数据平权失败。机构用 Level-2 能看到竞价过程,个人开发者看到的是空值。这 10 分钟的信息差,对开盘策略的影响被严重低估了。
-
大多数个人开发者的开盘策略,从来没有见过真正的开盘价。他们分析的是 9:30:01 的第一笔连续竞价成交,然后把它标记为 "open"。
-
用含集合竞价量的 09:30:00 bar 做"开盘强度"因子,回测曲线存在系统性偏差。这个偏差的方向和幅度,很多人可能从来没验证过。
如果你也在做 A 股盘前策略,建议下一个交易日 9:15 到 9:30 自己跑一遍。数据验证这一步,永远值得比策略开发花更多时间。
参考资料:上交所 LDDS Level-1 接口规范、深交所 STEP 接口规范、FIX 协议 Aggressor-Side 字段处理规范