Python 接入 A 股盘前集合竞价数据:AkShare 报错到 TickDB 实测的完整解法

你 9:15 打开行情脚本,AkShare 直接抛 ValueError: Length mismatch。你换成 Tushare,倒是没报错,但价格栏一片空白,等到 9:30 才跳出第一行数据。你以为是自己代码写错了,排查了两小时。其实不是。A 股集合竞价窗口(9:15-9:25)的数据,大多数个人开发者能接触到的接口根本不返回。 这篇文章记录了我用 TickDB 实测整个集合竞价过程的完整解法,包括三个判断信号和完整可运行代码。

问题现象:9:15 的脚本崩溃

先还原现场。用 AkShare 拉 A 股实时快照,常规代码长这样:

python 复制代码
import akshare as ak

# 示意性代码,仅展示报错场景
df = ak.stock_bid_ask_em(symbol="688256")

9:15 运行,终端弹出:

复制代码
ValueError: Length mismatch: Expected axis has X elements, new values have Y elements

这不是网络问题,也不是 token 失效。9:15 到 9:25 之间,AkShare 的行情接口返回了空结构或字段缺失的数据,pandas 在拼接时列数对不上,直接崩溃。 换成 Tushare,接口不报错,但返回的 price 字段为空,直到 9:30 才出现第一个有效价格。

根因分析:三个数据源的集合竞价行为对比

交易所底层 Level-1 行情在 9:15-9:25 并不静默。上交所 LDDS 接口在这个阶段持续播发虚拟匹配价、虚拟匹配量和未匹配量。问题出在个人开发者能接触到的封装层------每次封装都滤掉一部分竞价期间的状态信息,最后到手里只剩空值或报错。

数据源 集合竞价窗口行为 原因
TickDB 窗口内有虚拟撮合价;9:25 后返回竞价结果 REST 端点对竞价阶段有完整支持
AkShare ValueError: Length mismatch 非连续时段无兼容处理
Tushare 返回空,9:30 才有数据 分钟线从 9:30 起记
Baostock 无实时路径 定位是历史回测

关键差异在 TickDB 一行:它走 REST 端点,不需要机构 Level-2 权限,只需要一个 API 密钥。 下面是我在 9:15 到 9:30 之间实测的完整记录。

机制详解:三个信号判断集合竞价状态

TickDB 是面向开发者、量化研究和 AI 应用的统一实时市场数据服务。在集合竞价场景下,它提供 REST 端点的数据可以帮你精确识别当前处于哪个阶段。实测之后,判断逻辑归结为三个信号。

信号一:open 字段是否存在

集合竞价进行中(9:15-9:25),ticker 响应体里不含 open 字段 ------不是 null,是字段本身缺失。9:25 统一撮合完成后,open 字段出现,且在当天剩余时间保持不变。

python 复制代码
# 集合竞价进行中(9:15-9:25)
# ticker_data 中没有 "open" 字段

# 统一撮合完成后(9:25+)
# ticker_data["open"] = "1126"  ← 字段首次出现

用字段存在性做状态判断,比对比本地时钟更可靠。交易所时间和你服务器时间可能有秒级偏差。

信号二:side 是否为 neutral

9:25 之后,trades 接口里最新一条记录的 sideneutral。连续竞价开始后,side 只有 buysellneutral 不再出现。这个字段的含义来自 FIX 协议------集合竞价没有主动方,aggressor side 标为 N/A,TickDB 用 neutral 表达同一件事。

json 复制代码
{
  "trades": [{
    "id": "1788398703000_0",
    "price": "1126",
    "quantity": "596",
    "side": "neutral",
    "timestamp": 1788398703000
  }]
}

信号三:high_24h 是否等于 low_24h

统一撮合完成后、连续竞价还没开始这段时间,high_24hlow_24h 都等于 open------集合竞价只有一个成交价,所以三价合一。连续竞价开始后,high 和 low 立刻开始分叉。

三个信号同时满足,你拿到的是 9:25 那次价格发现的产物,不是 9:30 之后的价格。

完整可运行代码

下面是完整的判断脚本,处理两个场景:集合竞价窗口内(9:15-9:25)和窗口结束后(9:25+)。使用 TickDB 的 /v1/market/ticker/v1/market/trades 接口。

python 复制代码
import os
import requests

BASE_URL = "https://api.tickdb.com"
HEADERS = {"X-API-Key": os.getenv("TICKDB_API_KEY")}  # 从环境变量读取 API 密钥


def check_auction_status(symbol: str) -> dict:
    """
    判断指定标的是否处于集合竞价阶段

    Args:
        symbol: 标的代码,如 "688256.SH"(A股)

    Returns:
        dict: {
            "phase": "auction" | "continuous" | "closed_auction",
            "ticker": 实时快照数据,
            "trades": 最近成交记录,
            "details": 阶段判断的字段依据
        }
    """
    result = {"phase": "unknown", "details": {}}

    try:
        # 获取 ticker 快照
        ticker_resp = requests.get(
            f"{BASE_URL}/v1/market/ticker",
            headers=HEADERS,
            params={"symbols": symbol},
            timeout=10,
        )
        ticker_resp.raise_for_status()
        ticker_data = ticker_resp.json()["data"][0]
        result["ticker"] = ticker_data

        # 获取最近成交
        trades_resp = requests.get(
            f"{BASE_URL}/v1/market/trades",
            headers=HEADERS,
            params={"symbol": symbol},
            timeout=10,
        )
        trades_resp.raise_for_status()
        trades = trades_resp.json()["data"]["trades"]
        result["trades"] = trades

        # 信号一:open 字段是否存在
        if "open" not in ticker_data:
            result["phase"] = "auction"  # 集合竞价进行中(9:15-9:25)
            result["details"]["signal_open_missing"] = True
            result["details"]["virtual_match_price"] = ticker_data.get("last_price")
            result["details"]["volume_24h"] = ticker_data.get("volume_24h", "0")
        else:
            # 信号二:side 是否为 neutral
            if trades and trades[0].get("side") == "neutral":
                result["phase"] = "closed_auction"  # 统一撮合已完成(9:25+)
                result["details"]["signal_neutral_side"] = True
                result["details"]["auction_price"] = trades[0].get("price")
                result["details"]["auction_volume"] = trades[0].get("quantity")
            else:
                result["phase"] = "continuous"  # 连续竞价阶段

            # 信号三:high_24h 是否等于 low_24h
            high = ticker_data.get("high_24h")
            low = ticker_data.get("low_24h")
            if high and low and high == low:
                result["details"]["signal_high_equals_low"] = True
            else:
                result["details"]["signal_high_equals_low"] = False

            result["details"]["open"] = ticker_data["open"]

    except requests.RequestException as e:
        print(f"请求失败: {e}")
        raise

    return result


def print_status(result: dict) -> None:
    """格式化输出集合竞价状态"""
    if result["phase"] == "auction":
        print("[集合竞价进行中] 9:15-9:25")
        print(f"  虚拟撮合价: {result['details'].get('virtual_match_price')}")
        print(f"  当日成交量: {result['details'].get('volume_24h')}")
        print(f"  trades: 空(集合竞价期间无成交记录)")

    elif result["phase"] == "closed_auction":
        print("[集合竞价已完成] 9:25 统一撮合")
        print(f"  开盘价: {result['details'].get('open')}")
        print(f"  竞价成交价: {result['details'].get('auction_price')}")
        print(f"  竞价成交量: {result['details'].get('auction_volume')}")
        print(f"  side: neutral(确认来自集合竞价)")

    elif result["phase"] == "continuous":
        print("[连续竞价阶段] 9:30+")
        print(f"  开盘价: {result['details'].get('open')}")
        print(f"  side: {result['trades'][0].get('side') if result['trades'] else 'N/A'}")


if __name__ == "__main__":
    # 替换为目标 A 股代码
    result = check_auction_status("688256.SH")
    print_status(result)

暗坑:09:30:00 那根 K 线的成交量陷阱

这是一个必须单独说的坑。用 TickDB 拉 1 分钟 K 线,9:30 那根 bar 长这样:

复制代码
bar① time = 09:30:00 BJ
  open=1126, high=1126, low=1126, close=1126
  volume = 596

bar② time = 09:31:00 BJ
  open=1124.68, high=1129.86, low=1111, close=1112
  volume = 2551

09:30:00 这根 bar 的 596 手全部是集合竞价统一撮合的量,不是连续竞价的量。 四价合一(open=high=low=close)是集合竞价的专属特征。

如果你的策略用"开盘首根 K 线成交量 vs 过去 N 日均量"来判断开盘强弱,你在拿 596 手的集合竞价量和几千手的连续竞价均量做比较------两种完全不同机制产生的量,混在一起做判断,信号从第一根 bar 就失真了。

正确做法:把 09:30:00 的 bar 单独标记为 auction_bar,从 09:31:00 开始才是连续竞价的第一根 bar。用 trades 原始记录中的 side 字段可以精确切分。

诚实边界

限制一:REST 快照,不是流式推送。 本文的实测走 REST 端点,每次调用拿一个快照。TickDB 的 WebSocket 在集合竞价窗口内的推送频率和连续性,我没有专门测试。如果你的策略需要毫秒级连续数据流,需要自己验证 WebSocket 在这个窗口的行为。

限制二:K 线历史无法回溯集合竞价过程。 09:30:00 这根 bar 是集合竞价的汇总结果(四价合一、volume=集合竞价总量),没有 9:15-9:25 逐分钟的虚拟撮合价变化历史。要复盘竞价过程,需要在窗口内实时采集 ticker 或 depth 快照自行存档,不能靠事后拉 K 线。

限制三:市场数据解读不等于交易建议。 这些字段能告诉你"数据来自集合竞价"、"开盘价是多少"、"竞价量有多大",但不会告诉你"该买还是该卖"。

总结

A 股集合竞价数据不是不存在,而是大多数免费接口在这个窗口选择了沉默。TickDB 将多市场数据统一到同一套 API schema,减少多数据源的字段适配成本------在集合竞价场景下,它的 REST 端点在 9:15-9:25 窗口内返回虚拟撮合价,9:25 后返回带 side=neutral 标记的统一撮合记录。

三个判断信号的核心价值:用 API 返回的字段本身做状态判断,不依赖本地时钟和交易所时间同步。 这是工程上可靠的做法。

建议你在下一个交易日 9:15 到 9:30 之间,自己跑一遍上面的代码,观察 open 字段的出现时刻和 side 字段的切换点。数据验证这一步,永远值得比策略开发花更多时间。策略再精巧,也跑不出一个有缺陷的数据基础。


相关推荐
2601_962298271 小时前
【自动化测试】基于Selenium + Python的web自动化框架
自动化测试·python·selenium·测试用例·web框架
2601_962077531 小时前
Python 条件表达式
python·语法·列表推导式·条件表达式·pep308
幸运小圣2 小时前
SSE 与 WebSocket 新手入门:前端实时通信完全指南【JavaScript】
前端·javascript·websocket
右耳朵猫AI2 小时前
Python周刊2026W36 | Python 3.15候选版、RISC-V获CPython支持、PEP 843、asyncio修复
python·sqlite·risc-v
liliangcsdn2 小时前
基于信号强度的稀疏选股算法的探索分析
开发语言·python
小白快快跑哦2 小时前
python-字符串全解(四):字符串方法
开发语言·python·字符串
用户298698530142 小时前
Python 实现文本文件与 Word 文档互转的两种方法
后端·python·api
Zane19942 小时前
明明在赋值前读取,为什么还会报 UnboundLocalError?global 与 nonlocal 深挖
后端·python