多市场数据管道避坑指南:沪深北三地代码、字段与交易规则的统一接入
由bqzkrr8d创建,最终由bqzkrr8d 被浏览 3 用户
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=CN,X-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 提供