三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

A06_聚宽_米筐notebook补外部数据:代码没对齐merge永远是空的

A06_聚宽_米筐notebook补外部数据:代码没对齐merge永远是空的

聚宽/米筐 notebook 补外部数据:代码没对齐,merge 永远是空的

事实摘要:量化平台的 notebook 装不了第三方 SDK,但可以直接用requests发 HTTP 请求补外部数据。本文用/hs/list/all/hs/pool/ztgc/{date}/ht/lhb/mrxq三个纯 GET 接口演示,重点解决一个最容易被忽略的问题:同一个平台的不同接口会返回三种股票代码格式,不做归一化,merge结果永远是空表且不报错。文中代码格式转换器经 9 组用例离线自测全部通过。


1. 平台数据再全,也有拿不到的东西

聚宽、米筐这类平台内置了行情和财务数据,但总有覆盖不到的角落:龙虎榜席位明细、每日涨停股池的连板梯队、交易提示日历、部分算好的因子值。

这些平台的 notebook 有个共同限制:你不能pip install一个新 SDK。所以「装个库调一下」这条路走不通,只剩 HTTP 一条路——而这恰好是纯 GET 接口的主场,requests是平台自带的。

但真正会让人卡住半天的不是怎么发请求,而是数据拿回来之后 join 不上


2. 真正的坑:三种代码格式

先看实测。同一天,三个接口返回的dm字段长这样:

[ OK ] 股票列表 /hs/list/all dm 样例 = '000001.SZ' [ OK ] 涨停股池 /hs/pool/ztgc dm 样例 = 'sz000657' [ OK ] 龙虎榜 /ht/lhb/mrxq dm 样例 = '002202'

三个接口,三种格式:点分后缀、小写前缀、纯 6 位。而聚宽和米筐用的是第四种——000001.XSHE/600519.XSHG

于是下面这行看起来完全正常的代码:

df_merged=df_platform.merge(df_lhb,left_on='code',right_on='dm',how='left')

df_platform['code']000001.XSHEdf_lhb['dm']002202两边一个都对不上,how='left'又不会报错,你得到一张右侧全是NaN的表,然后开始怀疑是不是接口没数据。

这是最难查的一类 bug:不崩溃、不报警、结果看起来"只是今天没有匹配到"。


3. 动手:一个双向代码转换器

思路是先把任意格式解析成(6位代码, 交易所)这个中间态,再往任意目标格式转。

importre _SH_PREFIX=("60","68","58","51","11","50","56","20")defguess_exchange(code6):"""只有裸 6 位时,按号段判断交易所。"""ifcode6.startswith(("00","30","12","15","16","18","39")):return"SZ"ifcode6.startswith(_SH_PREFIX):return"SH"ifcode6.startswith(("43","83","87","92")):return"BJ"return"SZ"defparse_any(code):"""任意格式 -> (6位代码, SH/SZ/BJ)"""c=str(code).strip().upper()if"."inc:# 000001.SZ / 600519.XSHGhead,tail=c.split(".",1)iftail=="XSHE":returnhead,"SZ"iftail=="XSHG":returnhead,"SH"iftailin("SZ","SH","BJ"):returnhead,tail m=re.match(r"^(SH|SZ|BJ)(\d{6})$",c)# sz000657ifm:returnm.group(2),m.group(1)ifre.match(r"^\d{6}$",c):# 002202returnc,guess_exchange(c)raiseValueError(f"无法识别的代码格式:{code!r}")_SUFFIX={"SH":"XSHG","SZ":"XSHE","BJ":"XBEI"}defto_joinquant(code):# -> 600519.XSHG(聚宽/米筐)c6,ex=parse_any(code);returnf"{c6}.{_SUFFIX[ex]}"defto_dot(code):# -> 600519.SH(列表接口风格)c6,ex=parse_any(code);returnf"{c6}.{ex}"defto_prefix(code):# -> sh600519(股池风格)c6,ex=parse_any(code);returnf"{ex.lower()}{c6}"defto_bare(code):# -> 600519(行情/龙虎榜路径参数)returnparse_any(code)[0]

离线自测(不需要 token 就能跑,建议直接贴进 notebook 第一个 cell):

[PASS] 000001.SZ -> 000001.XSHE [PASS] sz000657 -> 000657.XSHE [PASS] sh603333 -> 603333.XSHG [PASS] 002202 -> 002202.XSHE [PASS] 600519 -> 600519.XSHG [PASS] 688717 -> 688717.XSHG [PASS] 600519.XSHG -> 600519.XSHG [PASS] 000001.XSHE -> 000001.XSHE [PASS] 830799 -> 830799.XBEI 代码格式自测: 9/9 PASS

4. 带缓存的取数:notebook 场景的刚需

notebook 的使用方式是反复重跑 cell。如果每次重跑都真发一次请求,一天的额度很快就没了。加一层文件缓存,几行就够:

importos,json,time,hashlib,requests BASE="https://api.zhituapi.com"TOKEN=os.environ.get("ZHITU_TOKEN")or"你的智兔token"CACHE_DIR,SLEEP,TIMEOUT,RETRY="_cache",0.35,20,3def_cache_path(url,params):key=hashlib.md5((url+json.dumps(params,sort_keys=True)).encode()).hexdigest()[:16]returnos.path.join(CACHE_DIR,key+".json")defget_json(path,params=None,use_cache=True):params=dict(paramsor{})url,cp=BASE+path,_cache_path(BASE+path,params)ifuse_cacheandos.path.exists(cp):withopen(cp,encoding="utf-8")asf:returnjson.load(f),Noneparams["token"]=TOKENforattemptinrange(1,RETRY+1):try:r=requests.get(url,params=params,timeout=TIMEOUT)exceptrequests.RequestException:time.sleep(SLEEP*attempt*2)# 指数退避continue# 网关错误是 text/plain:104 缺 token / 102 证书不存在if"json"notinr.headers.get("Content-Type",""):returnNone,f"HTTP{r.status_code}|{r.text.strip()}"data=r.json()# 未开通的模块返回 JSON 包装的错误体ifisinstance(data,dict)anddata.get("code")notin(None,200):returnNone,f"业务错误{data.get('code')}:{data.get('msg')}"ifuse_cache:os.makedirs(CACHE_DIR,exist_ok=True)withopen(cp,"w",encoding="utf-8")asf:json.dump(data,f,ensure_ascii=False)time.sleep(SLEEP)returndata,NonereturnNone,"重试耗尽"

注意缓存 key 里不要包含 token,否则换证书就全部缓存失效。


5. 运行结果:对齐之后 merge 才有意义

取当日涨停股池,转成平台风格的order_book_id

importpandasaspd zt,err=get_json("/hs/pool/ztgc/2026-08-12")df=pd.DataFrame(zt)df["order_book_id"]=df["dm"].map(to_joinquant)ext=df[["order_book_id","mc","p","zf","cje","lbc","fbt","zj"]].rename(columns={"mc":"name","p":"close","zf":"pct_change","cje":"turnover","lbc":"board_count","fbt":"first_seal_time","zj":"seal_amount"})

真实输出(2026-08-12,A 股涨红):

order_book_id name close pct_change turnover board_count first_seal_time seal_amount 000657.XSHE 中钨高新 9.33 10.02 436073568.0 1 09:25:00 98243407 000715.XSHE 中兴商业 10.13 9.99 608770896.0 3 09:25:00 170175926 002403.XSHE 爱仕达 12.93 10.04 362526448.0 4 09:25:00 7605154 000017.XSHE 深中华A 5.41 9.96 83346703.0 2 09:30:06 86225164 603099.XSHG 长白山 29.18 9.99 1462451424.0 7 09:31:27 56022361 603877.XSHG 太平鸟 17.71 10.00 180538320.0 2 09:33:30 50028979

和平台里的自选池 join:

watchlist=pd.DataFrame({"order_book_id":["000657.XSHE","000715.XSHE","600519.XSHG","603333.XSHG"],"my_group":["有色","商业","白酒","电缆"],})merged=watchlist.merge(ext,on="order_book_id",how="left")merged["今日涨停"]=merged["close"].notna().map({True:"是",False:"否"})
order_book_id my_group name close pct_change 今日涨停 000657.XSHE 有色 中钨高新 9.33 10.02 是 000715.XSHE 商业 中兴商业 10.13 9.99 是 600519.XSHG 白酒 NaN NaN NaN 否 603333.XSHG 电缆 尚纬股份 6.67 10.07 是 自选 4 只中有 3 只在当日涨停股池里

这才是我们要的结果。如果不做归一化,这张表的右半边会全是NaN

缓存效果:

命中缓存耗时 0.0011 秒(缓存目录 _cache/)

以上仅为公开数据的取数与合并演示,不构成投资建议。


6. 坑与注意事项

#说明与对策
1代码格式三选一列表接口000001.SZ、股池sz000657、龙虎榜002202。跨接口 join 前先归一化,否则静默产生空表。
2字段表和实测有出入股池的hy(所属行业)文档里有、实测不返回。一律row.get(k),不要row[k]
3错误不是 JSON104:缺少token参数/102:Licence证书(xxx)不存在text/plain返回,先判Content-Type
4模块未开通是 JSON 错误体形如{"code":403,"msg":"...未开通..."},和网关错误不是一套,两种都要判。
5北交所别漏43/83/87/92开头是北交所,聚宽后缀是.XBEI,按沪深两分法会判错。
6缓存 key 别带 token否则换证书缓存全废。
7平台外网策略部分平台对 notebook 出网有限制或需白名单,先用一个最小请求验证连通性再写正式逻辑。
8限频要留余量包量版 300 次/分。批量循环时固定sleep(0.35),失败用指数退避而不是死循环重试。

7. 小结

  • 平台 notebook 装不了 SDK,但requests一直都在——纯 GET 接口正好不需要装任何东西。
  • 补外部数据真正的门槛不是 HTTP,是代码格式对齐。先写parse_any(),再谈 merge。
  • 归一化函数是纯字符串逻辑,可以离线自测,建议贴进 notebook 第一个 cell 并保留断言。
  • 加一层不含 token 的文件缓存,重跑 cell 不再消耗额度。

下一篇预告:《集合竞价数据怎么用:开盘异动的量化描述》——从竞价阶段的量价特征出发,看它和当天走势的关系。


动手试试:本文三个接口用免费证书都能跑通,200 次/日、期限不限。
👉 领取免费证书

本文所有数据来自公开行情接口的真实返回,仅用于技术演示,不构成投资建议。

← 返回列表