外汇实时行情 API 接入,为什么拿到“最新价”还不算完成?

TickDB Research ·

外汇实时行情 API 接入,为什么拿到“最新价”还不算完成?

摘要: 外汇行情接口返回一个最新价,只能证明你拿到了一次响应,不代表它已经能用于看板、研究脚本或实时监控。本文用 EURUSD 实测外汇实时行情 API 的 REST ticker、REST kline 和 WebSocket ticker,拆开报价、K 线、实时订阅三条链路,并给出一份可直接运行的 Python 验证脚本。你还会看到如何用应用侧阈值标记“长时间没有新消息”,但这个阈值不是数据服务商的 SLA。

image.png

如果你正在接入外汇实时行情 API,先用 20 秒判断自己属于哪一种需求:

你现在要做什么先验证哪条链路本文给你的结果
报价卡、价格查询、外汇行情看板REST ticker验证请求的 symbol、返回字段和时间字段
K 线图、历史行情、研究脚本REST kline验证周期、返回条数和 OHLCV 结构
实时监控、持续更新的页面WebSocket ticker验证连接、订阅确认和业务消息

本文解决的不是“外汇接入难不难”,而是:一套外汇行情接口,怎样用最小成本确认它真的适合你的任务。

做完下面的验证,你至少能回答四个问题:

  • 请求的 EURUSD 是否真的返回了 EURUSD?
  • 最新价、bid/ask、timestamp 这些字段是否真实存在并能被程序解析?
  • K 线接口返回的周期和 OHLCV 是否符合你的图表或研究需求?
  • WebSocket 是只连接成功,还是确实收到过 ticker 业务消息?

一、外汇行情 API 先按任务拆开

“我要外汇实时行情”这句话太宽了。

如果你只想在页面上显示 EURUSD 的当前报价,重点是 ticker;如果你要画图或做历史研究,重点是 kline;如果你要让页面持续更新,重点才是 WebSocket。三者都返回“价格”,但不是同一类数据,也不能用一次成功调用替代另外两条链路。

本文使用 TickDB 作为实际测试入口。TickDB 是面向开发者、AI Agent、量化研究者和金融应用团队的统一实时行情数据 API;这里不把它当交易软件,也不讨论汇率涨跌,只验证程序怎样查询和订阅行情。

本次只使用 EURUSD 作为事实样本。XAUUSD 只在 runner 中做了额外的 ticker 候选探测,正文不把它外推成贵金属全量能力。

二、第一步:用 ticker 验证外汇报价接口

一个报价卡至少要能回答:我请求的是谁、拿到的是什么、这条数据带不带时间。

本次请求:

  • 端点:GET /v1/market/ticker
  • 参数:symbols=EURUSD
  • 鉴权:REST 使用 X-API-Key
  • 结果:HTTP 200、业务 code=0,返回 symbol=EURUSD、type=forex
  • 实际样本:返回了 last_price、bid_price、ask_price、timestamp 等字段

image.png

这里最重要的检查不是“页面上出现了一个价格”,而是把下面三件事写进代码:

  1. 返回对象必须包含请求的 symbol;
  2. 价格字段必须能转换为 Decimal;
  3. timestamp 必须存在,并确认它是 13 位毫秒字段样本,而不是把它误称为延迟。

三、第二步:用 kline 验证历史行情结构

最新价只能回答“现在返回了什么”。如果你要做 K 线图、历史行情查询或研究脚本,需要验证另一份结构化数据。

本次请求:

  • 端点:GET /v1/market/kline
  • 参数:symbol=EURUSD、type=forex、interval=1h、limit=3
  • 结果:返回 3 条样本,symbol、type、interval 与请求对应
  • 首条样本字段:time、open、high、low、close、volume、quote_volume

image.png

K 线验证有两个容易被忽略的点:

  • ticker 的 last_price 不等于 K 线的 close。一个是快照字段,一个是周期字段。
  • 13 位 timestamp 只说明字段精度,不说明数据新鲜度、接口延迟或高频可用性。

因此,历史行情 API 的最小检查契约应该包括:请求 symbol、请求周期、返回条数、OHLCV 字段和原始响应留痕。

四、第三步:WebSocket 连接成功后,还要等业务消息

WebSocket 适合做实时行情订阅,但“连接成功”只证明 socket 打开了。

本次连接地址为:

wss://api.tickdb.ai/v1/realtime?api_key=YOUR_API_KEY

实际订阅消息为:

{"cmd":"subscribe","data":{"channel":"ticker","symbols":["EURUSD"]}}

本次运行依次观察到:

  1. connected 连接确认;
  2. subscribe 订阅确认;
  3. 包含 EURUSD 的 ticker 业务消息;
  4. 业务消息中出现 last_price、bid_price、ask_price、spread、timestamp。

image.png

这能证明本次订阅链路走到了业务消息;不能证明全天连续推送、断线自动恢复、最低延迟、SLA 或生产稳定性。断线处理、消息超时、页面状态和本地日志,仍然是应用侧要完成的工作。

五、完整 Python 验证脚本:一次跑完三条链路

下面这份脚本不是伪代码。它使用 REST ticker、REST kline 和 WebSocket ticker,检查请求与返回的 symbol,验证数值字段,并把“连续一段时间没有收到业务消息”标成应用侧的 STALE。

先准备环境:

python -m pip install websocket-client
export TICKDB_API_KEY="YOUR_API_KEY"
python fx_market_check.py

完整脚本:

import json
import os
import time
from decimal import Decimal
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError

import websocket

BASE_URL = "https://api.tickdb.ai"
API_KEY = os.environ["TICKDB_API_KEY"]
SYMBOL = "EURUSD"
NO_MESSAGE_TIMEOUT_SECONDS = 10  # 应用侧示例阈值,不是 TickDB SLA


def get_json(path, params):
    query = urlencode(params)
    request = Request(
        f"{BASE_URL}{path}?{query}",
        headers={
            "X-API-Key": API_KEY,
            "Accept": "application/json",
            "User-Agent": "tickdb-pa01-fxmetal-example/1.0",
        },
        method="GET",
    )
    try:
        with urlopen(request, timeout=20) as response:
            return response.status, json.loads(response.read().decode("utf-8"))
    except HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"HTTP {exc.code}: {body[:300]}") from exc
    except URLError as exc:
        raise RuntimeError(f"network error: {exc.reason}") from exc
    except json.JSONDecodeError as exc:
        raise RuntimeError("response is not valid JSON") from exc


def require_decimal(row, field):
    if field not in row or row[field] in (None, ""):
        raise ValueError(f"missing numeric field: {field}")
    try:
        return Decimal(str(row[field]))
    except Exception as exc:
        raise ValueError(f"non-numeric field: {field}") from exc


def check_ticker():
    status, payload = get_json("/v1/market/ticker", {"symbols": SYMBOL})
    assert status == 200
    assert payload.get("code") == 0
    rows = payload.get("data")
    assert isinstance(rows, list) and rows
    row = next((item for item in rows if item.get("symbol") == SYMBOL), None)
    assert row is not None, f"requested {SYMBOL}, but it was not returned"
    require_decimal(row, "last_price")
    assert isinstance(row.get("timestamp"), int)
    assert len(str(abs(row["timestamp"]))) == 13
    print("TICKER_OK", json.dumps(row, ensure_ascii=False))


def check_kline():
    status, payload = get_json(
        "/v1/market/kline",
        {"symbol": SYMBOL, "type": "forex", "interval": "1h", "limit": 3},
    )
    assert status == 200
    assert payload.get("code") == 0
    data = payload.get("data")
    assert data["symbol"] == SYMBOL
    assert data["interval"] == "1h"
    assert len(data["klines"]) == 3
    for row in data["klines"]:
        assert isinstance(row.get("time"), int)
        for field in ("open", "high", "low", "close", "volume"):
            require_decimal(row, field)
    print("KLINE_OK", json.dumps(data, ensure_ascii=False))


def check_websocket():
    url = f"wss://api.tickdb.ai/v1/realtime?api_key={API_KEY}"
    ws = websocket.create_connection(url, timeout=12)
    try:
        connected = json.loads(ws.recv())
        assert connected.get("cmd") == "connected"
        ws.send(json.dumps({
            "cmd": "subscribe",
            "data": {"channel": "ticker", "symbols": [SYMBOL]},
        }))
        subscribed = json.loads(ws.recv())
        assert subscribed.get("cmd") == "subscribe"
        assert subscribed.get("code") == 0

        last_message_at = time.monotonic()
        deadline = time.monotonic() + NO_MESSAGE_TIMEOUT_SECONDS
        ws.settimeout(1)
        while time.monotonic() < deadline:
            try:
                message = json.loads(ws.recv())
                last_message_at = time.monotonic()
                if message.get("cmd") == "ticker":
                    assert message["data"]["symbol"] == SYMBOL
                    print("WEBSOCKET_LIVE", json.dumps(message, ensure_ascii=False))
                    return
            except websocket.WebSocketTimeoutException:
                if time.monotonic() - last_message_at >= NO_MESSAGE_TIMEOUT_SECONDS:
                    print("WEBSOCKET_STALE", NO_MESSAGE_TIMEOUT_SECONDS)
                    return
        print("WEBSOCKET_NO_TICKER", "connected and subscribed, but no ticker observed")
    finally:
        ws.close()


if __name__ == "__main__":
    check_ticker()
    check_kline()
    check_websocket()

这份脚本里的 NO_MESSAGE_TIMEOUT_SECONDS = 10 是一个应用侧演示阈值。它的含义是:你的程序在连续 10 秒没有收到业务消息时,可以把页面状态从 LIVE 改成 STALE。它不代表 TickDB 承诺 10 秒内一定有消息,也不等于端到端延迟。

六、TickDB 在这里解决什么问题

如果你需要把外汇行情接进自己的看板、研究脚本或监控程序,TickDB 可以作为这条数据链路的候选入口:

  • REST ticker:主动查询报价快照,核对 symbol、字段和 timestamp;
  • REST kline:主动查询历史行情,核对周期和 OHLCV 结构;
  • WebSocket ticker:订阅后续行情消息,由应用侧负责状态、超时和异常处理;
  • 多种接入方式:本文只验证 REST 与 WebSocket,不把这次结果外推到 MCP、Skill 或 CLI。

它不替你执行外汇交易,不提供汇率方向判断,不证明策略有效,也不因为一次 EURUSD 调用成功就证明所有外汇、贵金属、市场或时段都满足同样条件。

七、发布前与接入前的 10 分钟检查表

检查项通过标准
symbol请求和返回完全一致,例如 EURUSD
tickercode=0、数据非空、last_price 可解析
timestamp存在并确认单位;不把字段精度写成延迟承诺
klineinterval 与请求一致,OHLCV 字段完整
WebSocket不只看 connected;还要有订阅确认和对应 ticker 消息
stale阈值由应用配置,必须标注不是服务商 SLA
留痕保存请求条件、原始响应、运行时间和错误分支

FAQ

Q1:外汇行情 API 接入,先从 ticker 还是 WebSocket 开始?

先从 ticker 开始。它适合确认 symbol、返回结构和字段路径;需要页面持续接收后续消息时,再验证 WebSocket。不要用 WebSocket 的连接成功替代报价字段验证。

Q2:bid、ask 和 spread 能不能直接拿来做交易?

不能仅凭本文一次响应得出交易可用结论。本次只证明响应中出现过这些字段,不证明报价执行、成交条件、稳定性、授权或策略有效性。

Q3:10 秒没有消息,就说明行情 API 挂了吗?

不一定。10 秒只是应用侧的示例阈值。程序还需要结合交易时段、网络状态、订阅状态、错误消息和本地日志判断原因,不能把 STALE 直接等同于服务端故障。

Q4:TickDB 是行情软件吗?

更准确地说,它是面向开发者、AI Agent、量化研究者和金融应用团队的行情数据 API。它适合把结构化行情接入自己的看板、脚本、监控或 AI 工具流程。具体端点、字段和可用范围,以 TickDB 官方文档 和你的当日实测为准。

最后

外汇实时行情 API 接入最容易被忽略的,不是“有没有返回价格”,而是你有没有把三件事分开验证:报价快照、历史 K 线、实时订阅。

先用一个熟悉的 EURUSD 跑通,再把脚本换成自己的 symbol、自己的周期和自己的状态阈值。你得到的不是一张漂亮的报价卡,而是一条能够解释、能够排错、能够继续扩展的数据入口。

本文只讨论外汇/贵金属行情数据的程序化接入与验证方法,不构成投资建议,也不代表任何交易观点。一次样本运行不能外推为延迟、SLA、完整覆盖或生产稳定性承诺。

相关文章

A股美股港股行情看板Demo三天、上线三个月:拆解7个返工点

同一张行情表,寒武纪的成交额自己算是5,414万元,接口给的是54.20亿元,差了100倍,原因只是一个单位。行情看板三天就能接通,上线后的返工,几乎都来自界面上说不清“自己是什么”的数字。 本文依据上交所、港交所的一手文件,加上10月12日盘中对7类资产的实测,给你四样东西: 一张“价格身份证”:每个数字上线前要答的5个问题 7个返工点逐条拆:钱在哪里算错、为什么会错 一份可以直接发给数据…

TickDB Research ·

拆解美股夜盘数据5个口径:12月6日起美股进入23小时交易

同一只苹果股票,同一个周六中午,三个“最新价”,1万股持仓估值差出 9,500 美元。12月6日起,美股将进入近23小时交易:夜盘数据的5个口径不说清,日K线、收益率、风控和对账都会悄悄算错。 本文依据纳斯达克、SEC、DTCC、FINRA 等一手文件,加上30只美股(含9只中概股)的实测数据,给你四样东西: 一张“北京时间 × 美股夜盘”全景图(含11月1日夏令时切换前后) 5个口径逐条拆…

TickDB Research ·

港股数据源选型:踩一个回测就假(富途/IBKR/TickDB)

回测里某个港股策略表现稳定,实盘跑了一个月,收益差了将近一半。 不是完全对不上,是那种“差异不大但一直在”的偏差。团队排查了三周——策略逻辑、滑点模型、手续费、执行延迟,全部查过。最后发现问题在数据层:数据源返回的港股代码是“700”,而团队数据库里存的是“00700”。两个格式查出来的数据,部分重叠,部分不重叠。回测用的样本,比实际可交易的样本少了一批。 这不是某个操作失误,是港股数据接入…

TickDB Research ·

A股休市10天,港股跌了,美联储还要开会,APEC即将落地深圳——8号开盘,你盘清楚了几件事?

后天开盘。10月8日,A股恢复交易。你手里的A股仓位,价格停在9月30日;但如果你同时持有港股、美股、QDII,这10天它们一直在动——港股10月2日就开盘了,美股全程正常交易。 这篇文章要说的不是“A股休市了”。有仓位的人都知道A股休市。它要说的是: 同一段时间里,你的三个市场仓位在动,你的信息却只跟得上一个。 核心概念是 信息时态差 ——港股和美股的信息是连续的,每天有K线;A股的信息是…

TickDB Research ·

自选股跨了A股港股美股,还在打开三个APP翻?盘中扫异动前,先定这五个字段

自选股四十只,A股港股美股都有,每天早上打开三个 APP 翻一遍。有天腾讯盘中跌了 3%,又弹回来,我在外面吃饭,完全没看到。下午再打开,只看到它跌了 2%。 这个 2% 是结果,不是过程。 问题不在我没有实时数据。问题在我看到的那个数字——涨跌幅——它只告诉我一个方向,没告诉我这个方向是怎么走出来的。 同一只股票,只看涨跌幅和看开高低收加成交额,你做出的判断可以完全相反。股票池越大,这个差…

TickDB Research ·