外汇实时行情接入,真正复杂的地方在哪?
第一次接外汇行情接口的时候,大多数人都会觉得这件事不难。 和策略、风控相比,行情模块本身并不算复杂。 能长期稳定跑,而且结构不变,才算可用 如果只是偶尔获取一次行情,用 REST 接口也不是不行。 资源占用情况 接口文档写得再清楚,也解决不了一个核心问题: 把数据原样交给下游 下面这段代码是我常用的一种结构,逻辑非常简单,也刻意保持了“不过界”。 forward_tick 之后的处理逻辑,并不属于行情模块的职责范围。 当开始同时订阅多个货币对时,很容易在行情层写大量分支逻辑: 含义和业务逻辑交给下游 很多外汇行情 API 在功能层面看起来差别不大。 多品种订阅时是否保持一致行为 写到最后,其实只有一个结论。
连上 WebSocket,订阅一个 EURUSD,看着 tick 数据持续推送,基本都会下意识地认为:行情模块已经搞定了。
但只要系统开始长时间运行,或者订阅多个货币对,问题很快就会暴露出来。
连接稳定性、推送结构、字段一致性,这些一开始看起来不起眼的细节,往往会在后期变成系统复杂度的来源。行情模块的特点:不复杂,但很难返工
但它有一个明显特点:
一旦被多个模块依赖,就很难再动。
策略可以反复调整,参数可以不断优化,但行情数据如果在多个地方被使用,只要结构有变化,就会影响整条链路。
所以后来我在系统设计里,会刻意把行情模块当成一层基础设施来看,而不是一个普通功能。
标准也很简单:为什么外汇行情更适合用 WebSocket
但外汇市场的现实情况是:
数据更新频率非常高。
用 REST 拉行情,本质是在做高频轮询,系统不断地在问:
现在有没有新数据?
WebSocket 的模式正好相反:
有数据你再推送,没有就保持连接。
在系统运行时间拉长之后,这种差异会直接体现在:
尤其是多品种订阅时,这个差别会更加明显。接外汇行情接口,关键不是接口而是边界
行情模块的职责边界在哪里?
在我的实践里,行情接入层只做三件事:
到这里就应该结束。
如果在行情层里开始做策略判断、交易时间判断,或者业务状态判断,后期维护成本会明显上升,而且很难拆干净。一个相对克制的外汇行情接入示例
import websocket
import json
WS_URL = "wss://stream.alltick.co/ws"
def on_open(ws):
ws.send(json.dumps({
"op": "auth",
"args": {
"token": "YOUR_API_KEY"
}
}))
ws.send(json.dumps({
"op": "subscribe",
"args": [
{
"channel": "tick",
"symbol": "EURUSD"
}
]
}))
def on_message(ws, message):
data = json.loads(message)
if data.get("channel") == "tick":
forward_tick(data["data"])
def forward_tick(tick):
# 到这里为止
# 行情模块已经完成任务
pass
ws = websocket.WebSocketApp(
WS_URL,
on_open=on_open,
on_message=on_message
)
ws.run_forever()
行情模块只负责一件事:
稳定地把外汇行情数据交出去。多品种订阅时,少做判断反而更稳
但实践下来会发现,这些判断大多不该出现在行情层。
我更倾向于一个简单原则:
这样新增或删除品种时,行情模块几乎不用改动,系统结构也更清晰。外汇行情 API 的差异,往往体现在结构稳定性上
但系统真正跑起来之后,差异通常体现在这些地方:
有些行情服务在设计时,就把外汇、加密资产、股票等行情统一在同一套推送模型中,比如 AllTick API 这一类实时行情接口,在接入外汇行情时整体适配成本会更低,不太容易被细节反复打断。行情模块越“安静”,系统反而越可靠
一个好的行情模块,不需要有太强的存在感。
它不承担业务决策,也不表达逻辑判断,只是持续、稳定地输出数据。
当行情接口选得合适,边界又控制得住,很多系统层面的问题,往往会自然消失。
如果你正在搭建一个长期运行的外汇交易系统,行情模块这一步,值得多花一点时间想清楚。