国内期货有 CTP,加密交易所有 REST/WebSocket,而海外机构市场(CME、盈透、全球银行间)的通用语言是 FIX。它是金融界最老、最广的「文本交换格式」——没有 SDK、没有私有二进制,只有一本字段字典和一行行
Tag=Value。本篇讲清楚 FIX 是什么、消息长什么样、核心消息类型怎么用、Session 怎么保活、状态怎么对齐,最后给出选型建议与衍生规范(FAST 压缩、加密交易所现状)。
1. FIX 是什么
FIX(Financial Information eXchange,金融信息交换协议)是全球金融交易领域的事实标准消息协议,几个关键事实:
| 事实 | 内容 |
|---|---|
| 性质 | 面向交易的消息格式标准(文本协议),规定「消息怎么组织、字段叫什么、状态怎么表达」 |
| 起源 | 1992 年由 Fidelity Investments(富达)与 Salomon Brothers 为股票交易开发,目的是摆脱私有格式的耦合 |
| 维护 | 由 FIX Protocol Ltd(FPL,前身是 FIX Protocol Organization)与各交易对手共同维护 |
| 现状 | 全球股票、期货、固定收益、外汇的机构直连普遍采用,CME、ICE、LSEG、盈透等均为 FIX 会员/支持方 |
| 版本 | 主流为 FIX 4.2 / 4.4(Session 层),FIX 5.0+ 引入消息目录组件化;对接时以对手方实际支持版本为准 |
一个流传很广的说法:FIX 不是软件、不是接口库,而是一本「字典」。它规定的是字节如何编码、消息如何编号、状态如何表示;具体怎么传输(TCP 直连、专线、经认证网络)是各家自己的事情。
FIX 的诞生背景是「每家银行一个私有格式」:90 年代一家机构要对接 N 家对手方就要写 N 套解析器。FIX 的野心是让格式统一,字段字典由行业共同维护——这套思路后来被 XML、JSON 时代继承,但 FIX 在低延迟文本领域至今仍是老大哥。
2. FIX 在交易链路中的位置
策略/订单管理系统(OMS)
│ 内部下单意图
▼
┌───────────────────────────────┐
│ FIX 引擎(客户端侧) │ ← 软件公司/买方机构负责这半边
│ 消息组装 / 解析 / Session 管理 │
│ 序号维护 / 心跳 / 重传 │
└──────────────┬────────────────┘
│ FIX 连接(专线 / 认证网络 / 公网 + 加密)
┌──────────────▼────────────────┐
│ FIX 网关(对手方侧) │ ← 交易所 / 做市商 / 银行 / 清算所
│ 通常由对方提供接入文档与测试环境 │
└──────────────┬────────────────┘
▼
交易所撮合 / 做市商内部系统 / 银行间市场
一句话定位:FIX 是「订单管理系统(OMS)与对手方之间」的最后一公里协议。你的系统内部可以用任何技术栈(消息队列、REST、gRPC),但只要对外走机构通道,出口就是 FIX。
典型使用方:
| 角色 | 用 FIX 做什么 |
|---|---|
| 买方(**对冲**基金、资管) | 通过 FIX 连卖方执行通道(券商/银行),下单+拿回报 |
| 卖方(做市商、银行) | 对外提供 FIX 接口,接受客户订单 |
| 交易所/清算所(CME 等) | 提供 FIX/FAST 直连,供会员与 ISV 接入 |
| 经纪商(盈透等) | 机构客户用 FIX 替代桌面端 |
3. FIX 消息结构
3.1 三段式:Header / Body / Trailer
每条 FIX 消息固定由三部分组成(以 FIX 4.x 为例):
| 段 | 标签 | 作用 |
|---|---|---|
| Header(消息头) | 8=BeginString、35=MsgType、49=SenderCompID、56=TargetCompID、34=MsgSeqNum、52=SendingTime | 标识版本、消息类型、收发双方、序号、时间 |
| Body(消息体) | 各消息类型专属字段(如 55=Symbol、54=Side、44=Price、38=OrderQty) | 业务内容 |
| Trailer(消息尾) | 10=CheckSum(校验和) | 完整性校验,必须放在最后一位 |
约定:Header 里
8、35之外,49/56/34/52也是 Session 层必带字段;10=CheckSum必须是整条消息的最后一个字段,任何正文后追加字段都会让校验失败。
3.2 Tag=Value 格式与最小可读示例
FIX 的编码规则简单到可以用一句话讲完:每对字段用 = 表示,字段间以 SOH(0x01,ASCII 1)分隔。人工阅读时 SOH 常写成 |,解码时按 | 切分即可。
一条最小可读的新订单(NewOrderSingle)示例:
8=FIX.4.4|35=D|49=SENDER01|56=TARGET01|34=7|52=20260816-10:00:00.000|11=ORD10001|55=ESU6|54=1|60=20260816-10:00:00.000|38=2|40=2|44=4100.50|10=092
| 标签 | 含义 | 取值 |
|---|---|---|
| 8 | BeginString 协议版本 | FIX.4.4 |
| 35 | MsgType 消息类型 | D(NewOrderSingle) |
| 49 / 56 | SenderCompID / TargetCompID | 双方 CompID(登录认证的「用户名」) |
| 34 | MsgSeqNum 消息序号 | 7(本方 Session 内递增) |
| 52 | SendingTime 发送时间 | UTC 时间 |
| 11 | ClOrdID 客户端订单号 | ORD10001(幂等的关键) |
| 55 | Symbol 合约 | ESU6 |
| 54 | Side 方向 | 1=买 |
| 60 | TransactTime 交易时间 | UTC |
| 38 | OrderQty 数量 | 2 |
| 40 | OrdType 订单类型 | 2=限价单 |
| 44 | Price 价格 | 4100.50 |
| 10 | CheckSum 校验和 | 092 |
一个非常实际的工程点:
|只是展示用的记号,线上字节流里是\x01(SOH)。字符串里绝不能出现裸 SOH;文本值里也不能塞=(用别的分隔符)。新手最容易踩的就是「把一个带|的文本塞进 Value」。
3.3 校验和(CheckSum)
10=CheckSum 的算法:把整条消息(从 8= 起,到 Trailer 之前的最后一个 SOH 为止)每个字节的 ASCII 码求和,对 256 取模,格式化为 3 位十进制数字(不足补 0):
checksum = sum(所有字节) % 256,输出形如 "092"
- 计算范围不含
10=本身,但含正文里每个字段的等号与 SOH。 - 收到消息先算校验和再解析正文——校验不过的消息必须丢弃(这通常是断包、粘包或双方字段字典不一致的信号)。
3.4 Session 层 vs 业务层
FIX 把问题拆成两层,对接时最容易理解错的就是这里:
| 层 | 管什么 | 关键字段 |
|---|---|---|
| Session 层 | 登录、心跳、序号、重传、断开重连 | 8 / 34 / 35=A / 0 / 1 / 2 / 4 |
| 业务层(App 层) | 下单、撤改单、回报、行情请求 | 35=D / F / G / 8 / W 等 |
两层必须分开实现:Session 层坏了(序号对不上、心跳断)会连业务一起毁掉——所以先让 Session 层健壮(重连、重传、序号持久化),再谈业务逻辑。
💀 Session 层坏了会连业务一起毁掉
Session 层坏了(序号对不上、心跳断)会连业务一起毁掉。 FIX 对接必须先让 Session 层健壮——重连、重传、序号持久化——再谈业务逻辑。业务层的下单撤单如果依赖一个不健壮的 Session 层,一次断线就能让订单状态彻底不可信。
4. 核心消息类型
| MsgType(35=) | 名称 | 方向 | 用途 |
|---|---|---|---|
| A | Logon | 双向 | 登录,同时协商心跳间隔 |
| 0 | Heartbeat | 双向 | 心跳保活 |
| 1 | TestRequest | 双向 | 探测对方是否存活 |
| 2 | ResendRequest | 双向 | 请求重传错过的消息 |
| 4 | SequenceReset | 双向 | 序号重置(Gap Fill) |
| 5 | Logout | 双向 | 登出 |
| D | NewOrderSingle | 客户端→对手方 | 下单 |
| F | OrderCancelRequest | 客户端→对手方 | 撤单 |
| G | OrderCancelReplaceRequest | 客户端→对手方 | 改单(价格/数量) |
| 8 | ExecutionReport | 对手方→客户端 | 订单状态/成交回报(回报的载体) |
| 9 | OrderCancelReject | 对手方→客户端 | 撤单被拒绝 |
| W | MarketDataSnapshot | 对手方→客户端 | 行情快照 |
| X / P | MarketDataIncremental / 更新 | 对手方→客户端 | 行情增量 |
4.1 35=D NewOrderSingle(下单)
下单必备字段:
| 标签 | 字段 | 说明 |
|---|---|---|
| 11 | ClOrdID | 客户端订单号——幂等的唯一凭据,全局唯一、不能复用 |
| 55 | Symbol | 合约/代码,以对手方字典为准 |
| 54 | Side | 1=买 2=卖(做市另有 4/5 等含义) |
| 38 | OrderQty | 数量 |
| 40 | OrdType | 1=市价 2=限价 3=**止损**等 |
| 44 | Price | 限价单必填;**市价单**不填 |
| 59 | TimeInForce | 0=当日有效 1=GTC 3=IOC 4=FOK 等 |
| 60 | TransactTime | 交易时间(UTC) |
4.2 35=8 ExecutionReport(成交回报)
回报是 FIX 世界里最复杂的消息:一个订单的每一次状态变化都会推一条 35=8,且带 39=OrdStatus 与 150=ExecType 双状态。关键字段:
| 标签 | 字段 | 说明 |
|---|---|---|
| 11 | ClOrdID | 对应的客户端订单号(可能被 41=OrigClOrdID 关联) |
| 37 | OrderID | 对手方订单号 |
| 17 | ExecID | 本次执行唯一 ID——幂等去重的依据 |
| 39 | OrdStatus | 订单状态(见第 6 节) |
| 150 | ExecType | 执行类型(见第 6 节) |
| 151 | LeavesQty | 剩余未成交数量 |
| 14 | CumQty | 累计成交数量 |
| 31 | LastPx / 32=LastQty | 本笔成交价格/数量(部分成交时每笔一条) |
| 103 | OrdRejReason | 拒单原因码 |
工程铁律:回报以 17=ExecID 去重,状态以 39=OrdStatus 为准。重连重传后同一条回报可能收到两次,ExecID 相同就必须丢弃重复处理。
⚠️ 在真实市场里你以为是撤掉了比明确知道没撤掉危险得多
ExecType=8(Rejected)与 ExecType=4(Canceled)不可混同——Rejected 意味着「从未成为订单」,Canceled 意味着「成为过订单后被撤销」。撤单请求发出后没有跟踪 150=6→4 的确认链,以为撤掉其实没撤,在真实市场里比明确知道没撤掉危险得多。
4.3 35=F OrderCancelRequest(撤单)与 35=G 改单
| 消息 | 关键字段 | 语义 |
|---|---|---|
| F 撤单 | 11=ClOrdID(新生成)、41=OrigClOrdID(原订单号)、55/54/38 | 请求撤销原订单;结果看 35=8(成功)或 35=9(失败,如已成交) |
| G 改单 | 11=新订单号、41=原订单号、新 44/38 | 交易所按「先撤旧、再挂新」原子处理;部分市场改价 = 撤+重挂,可能丢失队列位置 |
4.4 35=A Logon(登录)
Session 开始的唯一入口,几个必带项:
| 标签 | 字段 | 说明 |
|---|---|---|
| 49 / 56 | SenderCompID / TargetCompID | 双方 CompID,等同「账号」 |
| 98 | EncryptMethod | 加密方式,多数为 0=无加密(靠链路层加密) |
| 108 | HeartBtInt | 心跳间隔(秒),登录时协商 |
| 141 | ResetSeqNumFlag | Y=重置双方序号(首次连接/每日重置用) |
5. Session 生命周期
客户端 对手方
│ │
│──────── 35=A Logon(108=30)────────▶│
│◀─────── 35=A Logon(含对方序号)───────│
│ │
┌─────▼─────┐ 正常时段:消息本身即保活 ┌─▼─────┐
│ 已登录 │◀────────────────────────────▶│ 已登录 │
│ │ 空闲超过 N 秒 → 发 35=0 心跳 │ │
└─────┬─────┘ 对方 35=0 内无响应 → └─┬─────┘
│ 发 35=1 TestRequest 探测 │
│ 仍无响应 → 断连 + 重连 │
│ │
│──────── 35=5 Logout(收尾)──────────▶│
│◀─────── 35=5 Logout ──────────────────│
▼ ▼
三个核心机制:
- 34=MsgSeqNum(消息序号):Session 内每条消息递增,双方各维护「自己发出的序号」与「对方应有的序号」。收到
34比预期大的消息 → 发 35=2 ResendRequest 请求重传;收到比预期小的 → 重复消息直接丢弃或按 Gap Fill 处理。 - 112 心跳间隔:登录时用 108 协商(常见 10s/30s/60s,以对手方要求为准)。空闲超过该间隔就主动发 Heartbeat;双方都必须在超时前收到对方消息(任何业务消息都算「心跳」),否则发 TestRequest 探测,再超时就判定断连。
- Logout:正常收盘或程序退出时先发 35=5 再断开;不辞而别的断连在对手方那里会留下一段「挂死」的 Session,重连时可能被拒或需要重置序号。
序号是 FIX Session 的生命线:序号必须持久化(写盘/写库),进程崩溃重启后要能从上次的序号继续,而不是回到 1——否则对手方按序号判定「丢消息了」,要求全量重传。
💀 序号必须持久化否则重连后丢单不可查
序号是 FIX Session 的生命线:序号必须持久化(写盘/写库),进程崩溃重启后要能从上次的序号继续,而不是回到 1。 否则对手方按序号判定「丢消息了」,要求全量重传;心跳超时判定过松会导致「连接还在、市场已变」。
6. 订单状态与 ExecType
订单的每一次变化都体现为一条 35=8,其中 39=OrdStatus 与 150=ExecType 是两个容易混淆但必须同时读的字段:
| 39=OrdStatus | 含义 | 与 150=ExecType 的关系 |
|---|---|---|
| 0 New | 已受理 | 150 同值为 0(新单确认) |
| 1 PartiallyFilled | 部分成交 | 150=1 |
| 2 Filled | 全部成交 | 150=2 |
| 4 Canceled | 已撤销 | 150=4(或 150=6/9 的取消子类型) |
| 5 Replaced | 已改单 | 150=5 |
| 6 PendingCancel | 撤单已提交未确认 | 150=6 |
| 8 Rejected | 被拒单,订单不存在 | 150=8(带 103=RejReason) |
| A PendingNew | 订单已提交未确认 | 150=A |
| E PendingReplace | 改单已提交未确认 | 150=E |
解读方法(一句话):39 是「订单现在长什么样」,150 是「这条消息在叙述哪一次变化」。例如撤单发出后先收到 150=6 PendingCancel,随后收到 39=4 Canceled 的回报才说明撤单成功;若收到 150=6 后来了 35=9 OrderCancelReject,则撤单失败、订单还在。
- ExecType=8(Rejected)与 ExecType=4(Canceled)不可混同:Rejected 意味着「从未成为订单」,Canceled 意味着「成为过订单后被撤销」。
- 部分成交时 32/31(LastQty/LastPx)逐笔更新、14/151(CumQty/LeavesQty)随之变化——用 14 与 151 判断剩余量,不要自己累加 32(重传会造成重复累加)。
7. FIX 的优缺点
7.1 优点
| 优点 | 说明 |
|---|---|
| 标准化 | 字段字典全球统一,跨市场/跨对手方迁移成本低,「会 FIX 就全会」 |
| 成熟可靠 | 三十年行业验证,Session 层(序号、重传、心跳)设计极其健壮 |
| 双向全功能 | 下单、撤改、回报、行情、做市报价全在一套协议里 |
| 生态完善 | 开源引擎(QuickFIX/QuickFIXJ、OnixS 商业版等)、测试工具、字典齐全 |
7.2 缺点
| 缺点 | 说明 |
|---|---|
| 复杂度 | 文本协议 + 序号 + 重传 + 心跳,Session 层实现远比 REST 麻烦;字段字典版本管理是持续负担 |
| 无内置安全 | 明文文本、无认证加密,安全靠链路层(专线/TLS)解决 |
| 低效 | 文本体积大、解析慢,高吞吐场景必须上 FAST 压缩(见第 8 节) |
| 细节地狱 | 各家对可选字段、子类型、错误码的使用不一致,同一协议不同对手方仍需逐家联调 |
7.3 与交易所专有 API 的对比
| 维度 | FIX | 交易所专有 API(加密 REST/WS、CTP 二进制) |
|---|---|---|
| 通用性 | 跨市场、跨对手方 | 每家有各自的格式与认证 |
| 上手成本 | 高(Session 层 + 字典) | 低(SDK/文档齐全) |
| 延迟 | 文本协议相对高,FAST 可压缩 | 二进制/JSON 按场景设计,普遍更快 |
| 状态语义 | 高度标准化(39/150) | 各家枚举不同,需映射 |
| 适用场景 | 海外机构直连、跨市场统一出口 | 单市场深度接入、加密高频 |
7.4 什么时候需要 FIX 直连
- 对手方只提供 FIX 接口(CME 等交易所、多数海外经纪商、银行间)。
- 需要一套代码接多个市场:OMS 侧统一 FIX 出口,市场差异收在配置里。
- 需要机构级的可靠性语义(序号重传、对账、审计):这是 REST 轮询给不了的。
- 不需要:仅接国内期货柜台(走 CTP)、仅接加密交易所(官方 REST/WS 更简单)——FIX 不是「更高级」,而是「另一种市场的要求」。
8. 衍生规范:FAST 与加密交易所现状
8.1 FAST(FIX Adapted for STreaming)
- 是什么:FIX Protocol 组织针对行情/高吞吐场景推出的压缩编码规范(FIX Adapted for STreaming),用模板(XML 描述字段的编码方式)+ 数据位图大幅压缩体积,行情的体积可压缩到文本 FIX 的十分之一甚至更低。
- 用在哪:CME 等交易所的实时行情直连普遍用 FAST;下单回执仍多用普通 FIX。
- 工程代价:模板管理、增量编码(前后字段变化才带值)、乱序恢复都是新复杂度——FAST 不是「能读就行」,解码器要与对方模板严格对应。
- 概念辨析:FAST 是同一协议家族里的编码层,不是新协议;对接时先确认对方要的是「FIX Session + FAST 行情」还是「纯 FIX」。
8.2 加密交易所对 FIX 的支持现状
| 交易所 | FIX 支持 | 说明(以官方文档为准) |
|---|---|---|
| 币安 | 有 | 提供机构级 FIX 接口(含测试环境),面向做市/机构客户 |
| OKX | 有 | 提供 FIX 接口,机构客户可用 |
| Bybit | 有 | 提供 FIX 接口 |
| 其他中小所 | 多为 REST/WS | FIX 主要在一线大所出现 |
加密 FIX 的几个特点:
- 认证通常是「API Key + 预共享密钥/签名」的变体,而不是传统 CompID 密码对——对接前读清对方文档。
- 报价精度、最小步进、订单类型枚举与国内完全不同,字段映射要逐项做。
- 对大多数个人/中小团队,官方 REST/WebSocket 仍是更优选择;FIX 只是「机构通道」选项之一。
风险提示
⚠️ 风险提示
FIX 对接的事故集中在 Session 层:序号没有持久化导致重连后丢单不可查;心跳超时判定过松导致「连接还在、市场已变」;撤单请求发出后没有跟踪 150=6→4 的确认链,以为撤掉其实没撤。在真实市场里,「你以为是撤掉了」比「明确知道没撤掉」危险得多。 请务必:序号与订单状态持久化;断开后先重连、再重传、再恢复下单;所有字段按对手方字典校验;上线前在对手方测试环境完成断线注入(拔网线、序号回退、重复回报)演练。本文协议细节以 FIX 官方文档与对手方接入规范为准。