统一五类行情接口之后,我为什么仍然保留三层适配器
摘要:2026年6月6日,通过TickDB MCP
get_ticker单次输入600519.SH、700.HK、AAPL.US、BTCUSDT、EURUSD,返回code=0且五个品种均出现在data中,均包含symbol、type、字符串形式的last_price和13位毫秒timestamp。本轮实测仅证明这五个代表品种的 ticker 快照查询成立,不证明全品种覆盖、REST、WebSocket、K线、盘口或逐笔成交可用。本文基于此证据边界,提出一种三层适配器设计——把适配从“每个源写一套”收敛为查询契约、语义规范化和消费保护,让统一入口的接入收益与市场制度差异各自被妥善管理。关键词:行情API统一接入、抽象泄漏、三层适配器、工程验证、TickDB MCP。
一、一个假设场景:凌晨三点,面板上有三个品种在动,两个一动不动
这是一个基于市场制度差异构造的假设场景,不属于本轮 MCP 实测。
凌晨三点,监控面板上五类行情同框:美股还在跳,加密在更新,外汇也在动。但 A 股和港股的价格线像冻住了一样。
值班同事的第一反应是“API 挂了”。检查日志,code=0。查返回,data 里五个品种一个不少,last_price 都有值。问题不是接口不通——是 A 股和港股正处于休市时段,返回的是休市前的最后一笔快照。面板没有报错,但它显示的“当前价格”里有两个已经不是当前价格。
本轮 MCP 实测未覆盖休市时段的快照行为。上述场景仅用于说明:统一接入不会消除市场制度差异,只会改变差异出现的位置。
这个矛盾是怎么来的?不妨倒回去看一个典型的工程演进路径。
阶段一:五套客户端。 最早只接 A 股。后来加港股、美股、加密、外汇。每加一类资产,就接一个数据源,写一套客户端。五套鉴权、五种字段映射、五种错误码处理。
阶段二:统一入口。 用一个 API 把五类行情收敛到一个端点上。一个 Key、一套请求格式。五套客户端删到只剩一层 HTTP 封装。
阶段三:抽象泄漏。 然后就是上面那个场景。统一 API 给了你一致的请求格式,但没有消除交易时区、资产特有字段和部分成功行为的差异。这些差异不能被删掉,只能被管理。
有一个类比可以贯穿全文:ORM 统一了 SQL 方言,让你不用手写 MySQL 和 PostgreSQL 两套语法,但索引策略、事务隔离级别和锁行为仍然不同。统一行情 API 做了类似的事——统一了接入路径和基础字段映射,但不会替你判断交易时段、校验资产特有字段和处理部分成功。
二、一次五类同期查询:能证明什么,不能证明什么
本轮 MCP 实测验证的事实:
- 2026年6月6日,使用 TickDB MCP
get_ticker,单次输入600519.SH、700.HK、AAPL.US、BTCUSDT、EURUSD。 - 返回
code=0,data数组包含全部五个目标 symbol。 - 五项均包含
symbol、type、last_price、timestamp。last_price为字符串,timestamp为本轮均为 13 位毫秒。 AAPL.US与无效 symbolNOTREAL混合查询时,code=0,data只返回AAPL.US。仅查NOTREAL时,code=0,data=[]。
本轮实测不能证明的内容:
- 不能证明 REST、WebSocket、K 线、盘口深度、逐笔成交等其他端点的行为与 MCP
get_ticker一致。 - 不能证明全量品种、所有资产类别在任何时段均可返回有效数据。
- 不能证明休市时段的快照更新行为。
- 不能证明
timestamp在所有端点和品种上均为毫秒——本轮 ticker 为 13 位毫秒,秒级差异主要出现在其他端点(如 trades),不可套用到 ticker。
核心工程推论: code=0 不等于所有请求的品种都返回了数据。应用层必须独立校验 requested_set 与 returned_set 的差异。
三、PoC 证据卡:本轮实测结果(不展示动态价格)
| 输入 symbol | 返回存在 | symbol 字段 | type 字段 | last_price(字符串) | timestamp(13位毫秒) |
|---|---|---|---|---|---|
| 600519.SH | ✓ | ✓ | ✓ | ✓ | ✓ |
| 700.HK | ✓ | ✓ | ✓ | ✓ | ✓ |
| AAPL.US | ✓ | ✓ | ✓ | ✓ | ✓ |
| BTCUSDT | ✓ | ✓ | ✓ | ✓ | ✓ |
| EURUSD | ✓ | ✓ | ✓ | ✓ | ✓ |
| NOTREAL(无效) | ✗ | — | — | — | — |
上表仅记录本轮 MCP 实测中共享核心字段的存在性,不展示动态价格,不代表各类资产的完整返回结构完全相同。
四、三层适配器:不是删除,是收敛
统一入口之后,适配器的职责从“为每个数据源写一套客户端”收敛为三个明确的层次。
| 层 | 职责 | 把差异放到了哪里 | 不负责什么 |
|---|---|---|---|
| L1 查询契约 | 封装统一 API 的请求、鉴权、重试和基础错误码解析。确保 code==0 且 data 是数组。 |
差异被压缩到请求参数和 HTTP 层,上层看不到数据源的鉴权和端点差异。 | 不检查 data 里缺了哪些 symbol,不处理资产特有字段。 |
| L2 语义规范化 | 校验 requested_set vs returned_set;将 last_price 字符串转为 Decimal;标注时间戳单位。 |
差异被显式标注——missing 集合、类型转换、时间单位注释——而非隐藏在原始 JSON 里。 | 不做交易时段判断,不跨品种对齐时间线。 |
| L3 消费保护 | 基于交易日历判断价格有效性;对缺失 symbol 做降级处理(缓存、告警或跳过)。 | 差异在这一层变成业务决策——休市价格是否展示、缺失品种是否告警。 | 不做定价、不做信号生成、不替代业务逻辑。 |
L1 伪代码:查询契约
# 依赖(精确版本锁定):
# pip install requests==2.32.3 python-dotenv==1.0.1
import requests
API_KEY = "your-api-key" # 实际部署从环境变量读取
BASE_URL = "https://api.tickdb.ai"
def fetch_ticker_batch(symbols: list[str]) -> dict:
"""
L1 查询契约:只保证 HTTP 请求成功、code==0、data 是数组。
不保证所有 symbol 都在 data 中。missing 交给 L2 处理。
本轮未运行此 REST 代码,无 REST 输出。此示例基于当前接口契约构造。
"""
try:
resp = requests.get(
f"{BASE_URL}/v1/market/ticker",
headers={"X-API-Key": API_KEY},
params={"symbols": ",".join(symbols)},
timeout=10
)
resp.raise_for_status()
except requests.exceptions.Timeout:
raise RuntimeError("请求超时")
except requests.exceptions.ConnectionError:
raise RuntimeError("无法连接行情服务")
except requests.exceptions.HTTPError as e:
raise RuntimeError(f"HTTP 错误: {e.response.status_code}")
try:
data = resp.json()
except ValueError:
raise RuntimeError("响应不是有效 JSON")
if not isinstance(data, dict):
raise RuntimeError("响应结构异常:顶层不是字典")
if data.get("code") != 0:
raise RuntimeError(f"API 错误: code={data.get('code')}")
raw_list = data.get("data")
if not isinstance(raw_list, list):
raise RuntimeError("响应结构异常:data 不是数组")
returned_symbols = {item["symbol"] for item in raw_list if "symbol" in item}
return {
"requested": set(symbols),
"returned": returned_symbols,
"data": raw_list
}
L2 伪代码:语义规范化 + 校验
from decimal import Decimal, InvalidOperation
def normalize_batch(requested: set, returned: set, data: list) -> dict:
"""
L2 语义规范化:
1. 显式计算 missing 集合
2. 将 last_price 字符串转为 Decimal(缺失或非法即失败,不默认为零)
3. 对重复 symbol、意外 symbol、必需字段缺失均触发失败
"""
missing = requested - returned
extra = returned - requested
normalized = {}
seen_symbols = set()
for item in data:
symbol = item.get("symbol")
if not symbol:
raise ValueError("data 中存在缺少 symbol 字段的条目")
# 重复 symbol 检测
if symbol in seen_symbols:
raise ValueError(f"symbol 重复: {symbol}")
seen_symbols.add(symbol)
# last_price 校验:必须为非空字符串
price_raw = item.get("last_price")
if not isinstance(price_raw, str) or price_raw.strip() == "":
raise ValueError(f"{symbol}: last_price 缺失或为空,阻断处理")
try:
price = Decimal(price_raw)
except (InvalidOperation, ValueError):
raise ValueError(f"{symbol}: last_price 无法解析为 Decimal: {price_raw}")
# 检查有限性
if not price.is_finite():
raise ValueError(f"{symbol}: last_price 为非有限数值")
entry = {
"symbol": symbol,
"last_price": price,
"timestamp": item.get("timestamp"),
}
normalized[symbol] = entry
return {
"normalized": normalized,
"missing": missing,
"extra": extra
}
L3 伪代码:消费保护(含缓存过期标记)
from datetime import datetime, timezone
def guard_missing(missing: set, cache: dict) -> list:
"""
L3 消费保护:对缺失 symbol 做降级处理。
缓存数据必须显式携带 is_stale、原始数据时间和缓存来源,
不能静默把缓存值当实时值。
"""
results = []
for sym in missing:
if sym in cache:
entry = cache[sym]
entry["is_stale"] = True
entry["cached_at"] = entry.get("cached_at", "unknown")
results.append(entry)
else:
print(f"[阻断] {sym} 缺失且无缓存,已从结果集中排除")
return results
五、三层适配器的场景决策
什么时候值得做三层:
| 场景 | 推荐做法 | 理由 |
|---|---|---|
| 只查一个市场的股票 ticker,做页面展示 | L1 可能足够 | 缺失品种可接受人工发现 |
| 跨两个市场做价差监控 | L1 + L2 | 缺失品种静默丢弃会导致价差异常 |
| 多类资产做组合监控 | L1 + L2 + L3 | 休市时间不同步、字段缺失常态化 |
| 给 AI Agent 提供行情工具 | L1 + L2,prompt 中追加 L3 规则 | 模型不会判断 symbol 是否静默丢失 |
什么时候三层会成为负担:
- 接入的资产类别很少且字段语义高度一致时,L2 的分支逻辑维护成本可能高于收益。
- 统一 API 字段契约频繁变更时,三层适配器的同步修改成本会侵蚀其隔离价值。此时应先推动接口稳定性。
选择三层适配器的深度,取决于消费者对缺失品种的容忍度、对实时性的要求以及字段语义的差异程度,不存在“一定需要三层”的场景。
六、已验证边界与未验证边界
本轮 MCP 实测已验证:
get_ticker一次查询五个代表品种,返回code=0且五个品种均出现在data中。- 部分成功行为:有效与无效 symbol 混查时只返回有效品种;仅查无效 symbol 时
data=[]。 last_price为字符串,timestamp为本轮均为 13 位毫秒。- 统一 API Key 体系为接入层提供单一鉴权入口,但 REST、WebSocket、MCP 的鉴权承载方式不同(REST 用 Header
X-API-Key,WebSocket 用 URL 参数,MCP 按协议配置),不可互相替代。
未验证,仅作为方法推演或风险标注:
- REST 端点行为是否与 MCP
get_ticker一致——本文 L1 伪代码为基于接口契约的教学示例,未运行,无 REST 输出。 - WebSocket、K 线、盘口深度、逐笔成交等端点的部分成功行为。
- 休市时段的快照更新行为。
- 资产特有字段的返回结构——本轮 MCP 输出不包含估值字段,文中不对
pe_ttm_ratio、pb_ratio等做存在性断言。 volume_24h在不同资产类别中的量纲差异——本轮未覆盖该字段的跨资产对比,不做结论。
七、FAQ
Q1:统一 API 之后适配器不是多余的吗?
不多余,职责变了。统一之前适配器在翻译不同数据源的格式。统一之后适配器在规范化同一个接口返回的不同资产数据。删掉的是重复的鉴权和基础字段映射,留下来的是缺失校验和类型转换。
Q2:L2 按 symbol 集合做差集校验,是不是过于谨慎?
code=0 但 data 不包含全部请求品种的行为已在本轮实测中确认。如果不做集合差检查,无效 symbol 会被静默丢弃,下游在不知情的情况下基于不完整数据做决策。
Q3:L3 的交易日历维护成本高吗?
取决于场景。盘中监控且休市时不运行脚本,可不维护日历。盘后分析和跨市场对比则需要日历——这不是统一 API 能替代的,但三层适配器把这块独立在 L3,换日历数据源不影响 L1 和 L2。
Q4:三层一定要全做吗?
按消费者对缺失的容忍度、实时性要求和字段语义差异程度选择。单市场展示可能只需 L1,跨市场监控通常需要 L1+L2,多资产组合监控和 AI Agent 接入需要完整的 L1+L2+L3。
你在统一接入多类行情时,最头疼的差异是什么?是集合差校验、时间戳单位不一致,还是同一个字段名在不同资产下含义不同?评论区聊聊。
标签:行情API / 统一接入 / 多层适配器 / 抽象泄漏 / 工程验证 / TickDB MCP
浙公网安备 33010602011771号