AI量化知识树

多市场数据管道避坑指南:沪深北三地代码、字段与交易规则的统一接入

由bqzkrr8d创建,最终由bqzkrr8d 被浏览 4 用户

2025年Q4,我在给vnpy写北交所数据feed。同一个831445品种,三个数据源给出了三种代码格式、两套时区标准、一组完全不兼容的涨跌幅字段名。排bug排到凌晨两点,根因不是逻辑写错——是沪深北三个市场的规则差异,在数据接入层被原封不动地复制了三遍

如果你也维护过多市场数据管道,下面的场景你大概率不陌生。


30秒速查:一个代码,三套规则

A股代码前缀识别决策树
│
├─ 60/00开头 → 主板 → 涨跌±10% → 无门槛 → ✅ 直接接入
├─ 300/301开头 → 创业板 → 涨跌±20% → 需10万+2年 → ⚠️ 权限字段不同
├─ 688开头 → 科创板 → 涨跌±20% → 需50万+2年 → ⚠️ 权限字段不同
├─ 8/920开头 → 北交所 → 涨跌±30% → 需50万+2年 → ⚠️ 权限字段不同
└─ 83/87开头 → 新三板 → 非交易所 → 门槛100万+ → 🚫 流动性陷阱

看上去简单,但当你要在代码里自动识别并匹配对应的交易时段、报价步长、涨跌幅限制时,问题就来了:同一个决策树上的两个“50万门槛”,对应的是流动性结构完全不同的两个市场


核心问题:同样50万门槛,流动性结构差4倍

科创板与北交所的合格投资者门槛完全一致:50万元日均资产 + 2年交易经验。按说投资者群体高度重叠,流动性应趋同。

事实正相反。

┌────────────────────────────────────────┐
│  北交所 vs 科创板:同样的50万,不同的流动性  │
│                                        │
│  基金持仓占比:  1.70%  vs  6.44%        │
│  日均换手率:    7-8%   vs  3.37%        │
│  盘后固定交易:  无     vs  有(使用率0.012%) │
│                                        │
│  结论:高换手≠高流动性质量              │
└────────────────────────────────────────┘

根据Wind持仓数据(2023年报汇总),公募基金在科创板的持股占比约 6.44%,在北交所仅为 1.70%。持仓深度相差近4倍。

换手率却呈现相反图景:2025年,北交所日均换手率已攀升至 7%-8%,同期科创板稳定在 3.37% 左右(数据来源:沪深交易所公开统计月报)。

一句话:北交所换手率反超,但流动性主要由短线资金驱动,机构持仓深度仅科创板的1/4。

为什么?三个工程视角

1. 市值容量限制机构进入

科创板公司平均市值是北交所的5-10倍。一只50亿市值的票,公募配5%就是2.5亿,流动性承受力强。北交所大量公司市值不足10亿,同样的仓位占比,冲击成本翻倍。不是机构不想买,是容量不够

2. 涨跌幅制度放大波动劝退长钱

北交所±30%波幅为短线策略提供了空间,吸引量化热钱;但对换手率低、建仓期长的配置型资金,意味着更高波动和更难控制的滑点。目前尚无学术研究能精确量化30%涨跌幅对日内波动率的净效应——市场数据积累年限不足,这也是管道设计时无法直接引用的已知未知。

3. 研究覆盖度低导致价格中“噪音”占比高

科创板公司平均5-8家卖方覆盖,北交所可能仅0-1家。信息效率不足,意味着价格波动中情绪驱动成分更大。这在数据层表现为:北交所K线序列中,因事件冲击导致的结构性断点更频繁,对回测拼接算法是额外负担。


数据管道里的实际坑:字段、时段、权限

盘后交易:你知道它存在,但它的量可以忽略

板块 盘后固定价格交易 时段
沪/深主板 15:00收盘
科创板 15:00-15:30
创业板 15:00-15:30
北交所 仅大宗交易

近52周,科创板盘后交易额占全部交易额比重仅为 0.0123%(数据来源:上交所市场质量报告)。一项被正式设计的制度,使用率趋近于零。如果你的数据管道为了完整性把盘后时段单独拉取合并,开销远大于收益——可以直接过滤掉该时段的数据而不影响任何回测结论

一句话:盘后时段数据,在量化回测中基本是噪声,建议在生产环境中配置为可选项。

打新首日:无限制涨跌下的数据断点

各板块新股首日规则完全不同:

板块 首日涨跌幅 次日起
主板 44%上限 ±10%
科创板/创业板 无限制(有临停机制) ±20%
北交所 无限制(有临停机制) ±30%

北交所新股首日破发率在三个板块中最高(具体盈亏分布无权威实证数据)。对数据管道的影响:首日K线可能包含极端价格,需在清洗脚本中设置过滤阈值,否则会污染因子计算。

新三板≠北交所:代码段混淆会导致流动性错配

北交所脱胎于新三板精选层,历史上沿用83/87代码段。尽管已启用920号段,存量公司仍使用老代码。

2023年全年,北交所成交额 7,272亿元,新三板全市场仅 612亿元 ——相差近12倍(数据来源:北交所、全国股转公司年度统计)。

一个代码是83开头,到底对应流动性充裕的北交所,还是挂单半小时才成交的新三板?在生产代码中,必须显式区分exchange字段,不能仅凭代码前缀推断流动性


生产级方案:用统一API消除多层解析

纯本地工具:代码归属自动识别

以下脚本仍保留,用于本地离线快速校验。逻辑基于最长前缀匹配,可嵌入到数据管道的初始化阶段。

"""
A股代码归属识别 + 交易权限自检工具(本地版)
无外部依赖,可集成到数据管道初始化脚本中
"""

# 代码前缀 → 板块规则映射(数据来源:沪深北交易所最新业务规则)
CODE_RULES = {
    # 上交所
    "600": {"exchange": "SSE", "board": "主板", "limit": 10, "threshold": 0},
    "601": {"exchange": "SSE", "board": "主板", "limit": 10, "threshold": 0},
    "603": {"exchange": "SSE", "board": "主板", "limit": 10, "threshold": 0},
    "605": {"exchange": "SSE", "board": "主板", "limit": 10, "threshold": 0},
    "688": {"exchange": "SSE", "board": "科创板", "limit": 20, "threshold": 50},
    # 深交所
    "000": {"exchange": "SZSE", "board": "主板", "limit": 10, "threshold": 0},
    "001": {"exchange": "SZSE", "board": "主板", "limit": 10, "threshold": 0},
    "002": {"exchange": "SZSE", "board": "主板", "limit": 10, "threshold": 0},
    "003": {"exchange": "SZSE", "board": "主板", "limit": 10, "threshold": 0},
    "300": {"exchange": "SZSE", "board": "创业板", "limit": 20, "threshold": 10},
    "301": {"exchange": "SZSE", "board": "创业板", "limit": 20, "threshold": 10},
    # 北交所
    "8": {"exchange": "BSE", "board": "北交所", "limit": 30, "threshold": 50},
    "920": {"exchange": "BSE", "board": "北交所", "limit": 30, "threshold": 50},
}

AFTER_HOURS_SUPPORT = {
    "SSE-主板": False, "SSE-科创板": True,
    "SZSE-主板": False, "SZSE-创业板": True,
    "BSE-北交所": False,
}

IPO_RULES = {
    "主板": "首日±44%,次日起±10%",
    "科创板": "首日无限制(临停),次日起±20%",
    "创业板": "首日无限制(临停),次日起±20%",
    "北交所": "首日无限制(临停),次日起±30%",
}

def identify_stock(code: str):
    """最长前缀匹配,返回板块元数据"""
    code = code.strip().split(".")[0]  # 处理如600519.SH的格式,取纯数字部分
    if len(code) < 6:
        return {"error": f"代码长度不足6位: {code}"}
    
    prefixes = sorted(CODE_RULES.keys(), key=len, reverse=True)
    for prefix in prefixes:
        if code.startswith(prefix):
            info = CODE_RULES[prefix].copy()
            info["code"] = code
            info["after_hours"] = AFTER_HOURS_SUPPORT.get(f"{info['exchange']}-{info['board']}", False)
            info["ipo_rule"] = IPO_RULES.get(info["board"], "")
            return info
    return {"error": f"无法识别代码: {code}"}

# 测试:四只代表性品种
test_cases = [
    "688981.SH",  # 中芯国际,科创板
    "300750.SZ",  # 宁德时代,创业板
    "831445.BJ",  # 长虹能源,北交所
    "600519.SH",  # 贵州茅台,主板
]

for tc in test_cases:
    res = identify_stock(tc)
    print(f"{tc}: {res.get('exchange', '?')} {res.get('board', '?')} 涨跌幅±{res.get('limit', '?')}% 门槛{res.get('threshold', '?')}万")

核心是前缀最长匹配,不是静态对照表。 688和300看似相近,门槛差5倍。在生产管道里,这段代码通常放在品种校验层,入仓前自动分类,减少下游策略引擎的硬编码。

API集成版:直接从TickDB拉取带元数据的全量品种

本地脚本能解决代码识别,但当需要最新品种状态、交易所确认的字段名、统一的权限描述时,直接调API更可靠。以下是一个最小集成示例,展示生产环境中如何用统一接口获取沪深北三地元数据,并处理限流。

"""
统一API元数据拉取:一次获取全量品种的交易所、板块、类型
处理限流3001、权限1001,返回归一化结构
"""

import os, time, requests

API_KEY = os.getenv("TICKDB_API_KEY")
BASE_URL = "https://api.tickdb.ai/v1"

def fetch_all_cn_stocks():
    url = f"{BASE_URL}/symbols/available"
    headers = {"X-API-Key": API_KEY}
    params = {"market": "CN", "type": "stock", "limit": 500}
    backoff = 1
    all_symbols = []
    offset = 0

    while True:
        params["offset"] = offset
        resp = requests.get(url, headers=headers, params=params, timeout=10)
        data = resp.json()

        if data["code"] == 3001:              # 限流退避
            wait = int(resp.headers.get("Retry-After", backoff))
            time.sleep(wait)
            backoff = min(backoff * 2, 8)
            continue
        if data["code"] == 1001:              # 权限或参数错误,阻断
            raise RuntimeError(f"API Error 1001: {data.get('message')}")
        if data["code"] != 0:
            raise RuntimeError(f"Unexpected code {data['code']}")

        batch = data["data"]["products"]
        for item in batch:
            all_symbols.append({
                "symbol": item["symbol"],    # 格式如688981.SH
                "name": item["name"],        # 中文简称,如中芯国际
                "exchange": item["exchange"],# SSE / SZSE / BSE
                "type": item["type"],        # stock / etf / futures 等
            })
        if len(batch) < 500:
            break
        offset += 500
        backoff = 1

    return all_symbols

核心是API返回里symbol/name/exchange/type四字段完全统一。 不论主板600519.SH还是北交所831445.BJ,JSON结构一致。这解决了多市场数据管道里最头疼的问题:不同市场代码段、后缀、字段命名——在接入层一次标准化,下游策略零修改。这和ORM统一不同SQL方言是一个逻辑:适配做在底层,上层无感知。


你真正在维护的,是三套不同的规则体系

当你的管道同时接入沪深北三地时,面对的不是“同一个A股”的三个切片,而是三套完全独立的规则:

维度 沪主板 科创板 创业板 北交所
准入门槛 0 50万+2年 10万+2年 50万+2年
涨跌幅 ±10% ±20% ±20% ±30%
盘后时段 有(可忽略) 有(可忽略)
新股首日 44%上限 无限制 无限制 无限制
基金持仓占比 6.44% 1.70%
日均换手率 3.37% 7-8%
机构资金容量

任何一项差异没处理好,都会在回测中形成系统偏差,而不会报错。

TickDB 的方向是把这些差异在数据接入层就统一掉。REST端点 https://api.tickdb.ai/v1/symbols/available?market=CNX-API-Key Header鉴权,返回统一的symbol/name/exchange/type四字段结构。WebSocket实时流走 wss://api.tickdb.ai/v1/realtime,同样一套字段体系。所有接口文档在 https://docs.tickdb.ai 开源可查。需要把行情查询封装进Agent,用MCP工具链 https://mcp.tickdb.ai(注:非浏览器访问地址,需在MCP客户端中配置集成)。

如果你已经在维护多市场数据feed,直接用curl验证上述端点返回的实际字段,比任何描述都更直接。


速查清单(建议加入团队Wiki)

问题 沪主板 科创板 创业板 北交所
代码格式 600xxx.SH 688xxx.SH 300xxx.SZ 8xxxxx.BJ / 920xxx.BJ
涨跌停 ±10% ±20% ±20% ±30%
资金门槛 0 50万 10万 50万
盘后交易 有(建议忽略) 有(建议忽略)
新股首日限制 44%上限
权限互认 科创板权限不可直接用于北交所
API中exchange字段 SSE SSE SZSE BSE

你在数据管道里踩过最坑的跨市场不一致是什么?

我在北交所ticker和kline的成交量字段名上载过跟头——一个叫volume_24h,一个叫volume,同品种同交易日,两个不同的名字。最后在清洗脚本里加了一层显式重命名才对齐。

如果你也在维护多市场数据feed,欢迎在评论区留下你的避坑记录——多一个字段名别名,后来人就少排一天错


📡 数据由 TickDB.ai 提供

{link}