Python 多市场行情数据验收:让一份研究清单贯通当前、历史与实时
当股票池一起波动时最容易出错的不是代码而是把“我看见的股票”当成“我已经覆盖的全球市场”。如果外汇、黄金、指数、期货或加密资产根本不在研究输入里后面无论如何解释行情结论的适用范围都已经被悄悄缩小了。本文不提供买卖建议也不判断任何资产的涨跌。它只完成一个工程任务用一份 Python 清单验收 8 类代表对象能否依次进入当前行情、已结束日线和实时订阅。适合谁用 Python 维护研究清单、做量化研究输入或搭建行情接入的开发者。你将得到一套可替换对象清单的验收脚本、一次真实运行的冻结结果、失败时该在哪一层停下来的判断方法。先看验收目标不是“有价格”而是数据链不断一份多市场行情数据至少要回答四个不同的问题关卡要验证什么本文的判定方式产品目录研究对象是否处于可发现的产品范围读取目录摘要不把摘要总数当永久能力承诺当前行情清单中的对象能否同轮取得快照8 个对象在一个 ticker 请求中返回历史数据对象能否进入回看与研究每个对象取得 5 条已结束的 1d K 线实时数据至少有一个对象能否真正收到订阅消息收到一条 BTCUSDTticker消息关键不在于所有市场共用同一种字段或同一个交易时段它们并不相同。关键在于你自己维护的同一份对象清单不会在“当前—历史—实时”之间丢失 symbol 和 type 的对应关系。本轮真实结果8 类代表对象四道关卡全部通过下面是一次已冻结运行的结果运行区间为2026-07-26 23:38:42—23:38:53UTC8。价格和目录总量只代表该时刻重跑后会变化不能视为实时行情或永久覆盖范围。资产类别symboltype当次 ticker1d 已结束 K 线外汇EURUSDforexPASS1.137095 条贵金属XAUUSDforexPASS4056.395005 条指数SPXindicesPASS7411.980005 条美股AAPL.USstockPASS333.025 条港股700.HKstockPASS434.65 条A 股600519.SHstockPASS1297.415 条中国期货BU2609futuresPASS4260.005 条加密货币BTCUSDTcryptoPASS64811.160000005 条同轮还得到以下结果无过滤产品目录摘要total_products 39774三个股票对象AAPL.US、700.HK、600519.SH的股票信息、市场指标、资金流请求均为3/3WebSocket 实际收到了BTCUSDT的一条cmdticker消息总判定PASS。这里的 “PASS” 仅说明上面的样本、接口、参数和运行时刻通过了检查。它不证明所有标的、所有周期、完整历史、低延迟、稳定性或 SLA。最小可跑代码先把研究对象写成一张表安装依赖pipinstallrequests websockets certifiexportTICKDB_API_KEY你的 API Key将下面代码保存为tickdb_multi_asset_check.py再执行python3 tickdb_multi_asset_check.py --output-dir tickdb_check_output脚本把 API Key 只从环境变量读取不会打印 Key每次运行会保存脱敏后的请求/返回、汇总和终端输出。若任一必要请求失败会直接报错而不是把缺失数据伪装成成功。#!/usr/bin/env python3importargparseimportasyncioimportdatetimeasdtimportjsonimportosfrompathlibimportPathimportsslimportsysimporttimefromurllib.parseimportquoteimportcertifiimportrequestsimportwebsockets APIhttps://api.tickdb.aiWSwss://api.tickdb.ai/v1/realtimeSSL_CONTEXTssl.create_default_context(cafilecertifi.where())# 只改这里即可替换自己的研究清单type 必须与对象类别匹配。OBJECTS[(外汇,EURUSD,forex),(贵金属,XAUUSD,forex),(指数,SPX,indices),(美股,AAPL.US,stock),(港股,700.HK,stock),(A股,600519.SH,stock),(中国期货,BU2609,futures),(加密货币,BTCUSDT,crypto),]STOCKS[AAPL.US,700.HK,600519.SH]deftimestamp():returndt.datetime.now().astimezone().isoformat(timespecseconds)defsave_json(path,value):path.parent.mkdir(parentsTrue,exist_okTrue)path.write_text(json.dumps(value,ensure_asciiFalse,indent2)\n,encodingutf-8)defapi_get(session,raw_dir,name,path,params,api_key):startedtime.perf_counter()responsesession.get(f{API}{path},paramsparams,headers{X-API-Key:api_key,Accept:application/json},timeout40,)try:bodyresponse.json()exceptrequests.JSONDecodeError:body{unparsed_text:response.text}save_json(raw_dir/f{name}.json,{request:{method:GET,path:path,params:params},response:{status:response.status_code,elapsed_ms:round((time.perf_counter()-started)*1000,1),body:body,},captured_at:timestamp(),})ifresponse.status_code!200orbody.get(code)!0:raiseRuntimeError(f{name}failed: HTTP{response.status_code}, body{body})returnbody[data]asyncdefwebsocket_ticker(api_key,raw_dir):symbolBTCUSDTsubscribe{cmd:subscribe,data:{channel:ticker,symbols:[symbol]}}messages,ticker,error[],None,Nonetry:asyncwithwebsockets.connect(f{WS}?api_key{quote(api_key)},sslSSL_CONTEXT,open_timeout20,close_timeout5,ping_intervalNone,)asws:awaitws.send(json.dumps(subscribe))awaitws.send(json.dumps({cmd:ping}))deadlinetime.monotonic()25whiletime.monotonic()deadlineandlen(messages)12:try:rawawaitasyncio.wait_for(ws.recv(),timeoutmax(0.5,deadline-time.monotonic()))exceptasyncio.TimeoutError:breakmessagejson.loads(raw)messages.append(message)ifmessage.get(cmd)tickerandmessage.get(data,{}).get(symbol)symbol:tickermessagebreakexceptExceptionasexc:errorf{type(exc).__name__}:{exc}save_json(raw_dir/websocket_BTCUSDT_ticker.json,{request:{endpoint:f{WS}?api_keyREDACTED,subscription:subscribe},messages:messages,ticker_message:ticker,error:error,captured_at:timestamp(),})iftickerisNone:raiseRuntimeError(fBTCUSDT ticker not received:{errorormessages})returntickerdefrun(output_dir,api_key):raw_diroutput_dir/rawsessionrequests.Session()started_attimestamp()catalogapi_get(session,raw_dir,catalog_all,/v1/symbols/available,{offset:0,limit:5},api_key)ticker_rowsapi_get(session,raw_dir,ticker_multi_asset,/v1/market/ticker,{symbols:,.join(symbolfor_,symbol,_inOBJECTS)},api_key,)tickers{row[symbol]:rowforrowinticker_rows}rows[]forasset,symbol,asset_typeinOBJECTS:klineapi_get(session,raw_dir,fkline_{symbol.replace(.,_)},/v1/market/kline,{symbol:symbol,interval:1d,limit:5,type:asset_type},api_key,)rows.append({asset:asset,symbol:symbol,type:asset_type,last_price:tickers.get(symbol,{}).get(last_price),ticker_result:PASSifsymbolintickerselseFAIL,kline_count:len(kline.get(klines,[])),})stock_infoapi_get(session,raw_dir,stock_info_three_markets,/v1/market/stock-info,{symbols:,.join(STOCKS),type:stock},api_key)metricsapi_get(session,raw_dir,market_metrics_three_markets,/v1/market/calc-index,{symbols:,.join(STOCKS),type:stock},api_key)forsymbolinSTOCKS:api_get(session,raw_dir,fcapital_flow_{symbol.replace(.,_)},/v1/market/capital-flow,{symbol:symbol,type:stock},api_key)liveasyncio.run(websocket_ticker(api_key,raw_dir))ticker_oksum(row[ticker_result]PASSforrowinrows)kline_oksum(row[kline_count]5forrowinrows)overallPASSif(ticker_ok,kline_ok,len(stock_info),len(metrics))(8,8,3,3)elsePARTIALreport{run_started_at:started_at,run_finished_at:timestamp(),catalog_total:catalog[summary][total_products],objects:rows,ticker_passed_count:ticker_ok,kline_passed_count:kline_ok,stock_info_count:len(stock_info),market_metrics_count:len(metrics),capital_flow_passed_count:len(STOCKS),websocket:{symbol:BTCUSDT,last_price:live[data][last_price]},overall:overall,}save_json(output_dir/summary.json,report)(output_dir/run_transcript.txt).write_text(json.dumps(report,ensure_asciiFalse,indent2)\n,encodingutf-8,)returnreportdefmain():parserargparse.ArgumentParser()parser.add_argument(--output-dir,defaulttickdb_check_output)output_dirPath(parser.parse_args().output_dir)api_keyos.environ.get(TICKDB_API_KEY,).strip()ifnotapi_key:print(Missing TICKDB_API_KEY,filesys.stderr)return2reportrun(output_dir,api_key)print(json.dumps(report,ensure_asciiFalse,indent2))return0ifreport[overall]PASSelse1if__name____main__:raiseSystemExit(main())说明代码展示的是验收逻辑。你实际替换标的时应该同步检查symbol、type、市场交易时段、币种和字段口径它们不能因为入口统一就被默认视为可直接横向比较。这段代码到底做了什么它没有“自动判断全球市场”也没有生成交易信号它只是把研究输入变成一个可核对的契约。研究对象清单 ├── 目录摘要确认发现入口可用 ├── ticker确认当前行情快照 ├── 1d K 线确认对象可进入历史研究 └── WebSocket确认至少一个实时订阅实际有消息股票样本在这里还有一条补充分支股票信息、市场指标、资金流。它们可以作为候选对象的补充研究输入但不能直接跨市场排名更不能从单次返回推导出投资结论。对于工程实现而言最重要的是保存三类产物raw/每个接口的脱敏原始响应方便定位参数或字段问题summary.json供后续程序读取的机器可读验收结果终端输出让人快速确认此次通过/失败的位置。这比只在 Notebook 里看到一个价格更有用一旦后续研究结果异常你能回答“问题出在目录、当前行情、历史 K 线还是实时订阅”。四类失败分别应该怎么处理1. HTTP 200但结果为空不要只把 HTTP 状态码当成功。优先检查type是否与对象匹配、产品过滤条件、分页参数以及响应中的业务状态和实际数据字段。HTTP 200 只表示请求成功到达不代表研究对象真的被返回。2. ticker 有了K 线却没有这通常不是“行情源完全不可用”的结论。先核对 symbol/type再明确当前验证的是1d、limit5的已结束 K 线。其他周期、其他日期范围、完整历史和回测可用性需要单独验收。3. REST 正常WebSocket 没有 ticker把实时订阅视为独立关卡确认订阅频道、symbol、鉴权、网络连通性和消息类型。本文只验证了某一轮收到一次 BTCUSDT ticker未测试重连、断线恢复、延迟或持续稳定性。4. 数据都到了却无法解释或比较这恰恰说明应该暂停使用结论。不同资产可能有不同币种、交易时段、价格精度、复权逻辑和字段定义。统一接入解决的是“进入同一条研究流程”不是自动消除业务口径差异。把它放进你的研究流程先验收再解释每次准备扩大研究范围时可以按下面顺序做先写出对象清单对象、资产类别、symbol、type运行四关检查只对通过的对象进入下游分析对失败对象保留原始响应和失败原因在报告里明确本次覆盖了什么、没有覆盖什么。这样做不会让市场变得可预测但能避免一个更基础的错误用股票局部的输入替代一个本应覆盖多资产的研究问题。发布前/使用前检查表API Key 只在环境变量或密钥管理中不进入代码、截图和日志每个对象都显式写了symbol与typeticker、历史和实时检查的对象集合没有悄悄分叉明确 K 线验证的周期、数量和结束状态将目录总量、价格和实时消息标记为运行时快照对空结果检查业务码、过滤条件、分页和字段而不是只看 HTTP 200不把一次样本通过外推为全量覆盖、低延迟、完整性或 SLA不把研究输入、市场指标或资金流写成荐股、收益承诺或自动交易结论。常见问题多市场行情数据怎样避免把对象映射错把symbol和type放在同一份版本化清单中并让 ticker、K 线和订阅都从这份清单派生。不要在三个任务里手写三套 symbol。REST 和 WebSocket 应该如何分工周期性验收、当前快照和历史 K 线适合走 REST持续观察实时变化才需要 WebSocket。两者是任务分工不是自动连续执行的工作流。股票信息、市场指标、资金流可以直接生成选股结论吗不可以。它们只是研究输入。还需要明确字段含义、适用市场、时间口径和你自己的研究方法本文也不提供任何投资建议。结语真正值得先验证的往往不是某个价格是否变化而是你的研究清单是否足以支撑你准备回答的问题。先让对象进入可追溯的当前、历史和实时数据链再讨论如何解释市场。这是多市场研究里成本最低、也最容易被忽略的一道质量门。本文的实测结果来自 2026-07-26 23:38UTC8的一次冻结运行。目录总量、价格与实时消息会随重跑变化本文不构成投资建议。