4. FIX协议实战:使用Python构建FIX客户端、发送NewOrderSingle、接收ExecutionReport
FIX协议,说白了就是金融圈里的“通用语言”。
我刚开始接触量化交易那会儿,觉得FIX协议特别神秘。后来做港股接口对接,才发现这东西其实没那么玄乎。它就是一个消息格式标准,让交易系统之间能互相理解。
今天咱们就动手,用Python写一个FIX客户端。目标很明确:发一个NewOrderSingle(新订单),收一个ExecutionReport(执行报告)。
4.1 FIX协议的核心概念
先聊几个关键点。FIX消息由一个个“标签=值”对组成,用分隔符隔开。每个标签都有特定含义。
| 标签 | 字段名 | 说明 |
|---|---|---|
| 35 | MsgType | 消息类型,D表示NewOrderSingle,8表示ExecutionReport |
| 11 | ClOrdID | 客户端订单ID,唯一标识 |
| 54 | Side | 买卖方向,1=买,2=卖 |
| 38 | OrderQty | 订单数量 |
| 44 | Price | 价格 |
| 40 | OrdType | 订单类型,1=市价,2=限价 |
| 17 | ExecID | 执行ID,唯一标识一笔成交 |
| 150 | ExecType | 执行类型,0=新建,F=部分成交,2=全部成交 |
嗯,这里要注意:FIX消息的结尾必须有一个校验和字段(标签10)。这个校验和是前面所有字符的ASCII码之和模256。
核心要点:FIX协议本质上是键值对的有序集合。你只要按顺序拼好标签和值,加上校验和,就能发出去。接收方也是按顺序解析。
4.2 构建FIX客户端
我个人习惯用Python的socket库直接做TCP连接。为什么不用现成的FIX引擎?因为自己写一遍,才能真正理解协议细节。
先看一个简单的FIX客户端框架:
import socket
import time
class FIXClient:
def __init__(self, host, port, sender_comp_id, target_comp_id):
self.host = host
self.port = port
self.sender_comp_id = sender_comp_id
self.target_comp_id = target_comp_id
self.sock = None
self.msg_seq_num = 1
def connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
print(f"已连接到 {self.host}:{self.port}")
def disconnect(self):
if self.sock:
self.sock.close()
self.sock = None
def send_message(self, msg_type, fields):
"""发送FIX消息"""
# 构建消息体
body = f"35={msg_type}\x01"
body += f"49={self.sender_comp_id}\x01"
body += f"56={self.target_comp_id}\x01"
body += f"34={self.msg_seq_num}\x01"
body += f"52={time.strftime('%Y%m%d-%H:%M:%S')}\x01"
for tag, value in fields:
body += f"{tag}={value}\x01"
# 计算消息长度
body_length = len(body.encode())
header = f"8=FIX.4.4\x019={body_length}\x01"
# 计算校验和
raw_msg = header + body
checksum = sum(ord(c) for c in raw_msg) % 256
trailer = f"10={checksum:03d}\x01"
full_msg = raw_msg + trailer
self.sock.send(full_msg.encode())
self.msg_seq_num += 1
print(f"发送: {full_msg}")
return full_msg
def receive_message(self):
"""接收FIX消息"""
buffer = b""
while True:
data = self.sock.recv(4096)
if not data:
break
buffer += data
# 检查是否收到完整消息(以校验和结尾)
if b"\x01" in buffer:
# 简单处理:按分隔符拆分
messages = buffer.split(b"\x01")
for msg in messages[:-1]:
if msg:
print(f"接收: {msg.decode()}")
buffer = messages[-1]
return buffer
小技巧:FIX消息的分隔符是\x01(SOH字符)。在调试时,可以用|代替\x01来显示,这样更直观。我曾经在调试时没注意这个细节,结果一直解析失败。
4.3 发送NewOrderSingle
NewOrderSingle是FIX协议里最常用的消息类型。它的MsgType是D。
来看一个港股限价买单的例子:
def send_new_order_single(client, symbol, side, quantity, price, cl_ord_id):
"""发送新订单"""
fields = [
("11", cl_ord_id), # ClOrdID
("55", symbol), # Symbol
("54", str(side)), # Side: 1=买, 2=卖
("38", str(quantity)), # OrderQty
("40", "2"), # OrdType: 2=限价
("44", str(price)), # Price
("60", time.strftime('%Y%m%d-%H:%M:%S')), # TransactTime
("207", "HK"), # 交易市场
("1", "123456"), # 账户号
]
client.send_message("D", fields)
print(f"已发送新订单: {cl_ord_id}")
# 使用示例
client = FIXClient("192.168.1.100", 8888, "MYTRADER", "BROKER")
client.connect()
# 发送一个港股限价买单
send_new_order_single(
client=client,
symbol="00700", # 腾讯控股
side=1, # 买入
quantity=1000,
price=380.50,
cl_ord_id="ORD20240101001"
)
这里有个坑:港股代码在FIX消息里通常不带前缀。比如腾讯是00700,不是HK.00700。我刚开始对接时,习惯性加了HK.前缀,结果被券商退单了。
4.4 接收ExecutionReport
订单发出去后,券商会返回ExecutionReport(MsgType=8)。这个报告会告诉你订单的状态变化。
解析ExecutionReport的代码:
def parse_execution_report(raw_msg):
"""解析执行报告"""
fields = {}
# 按分隔符拆分
pairs = raw_msg.split("\x01")
for pair in pairs:
if "=" in pair:
tag, value = pair.split("=", 1)
fields[tag] = value
# 提取关键字段
exec_report = {
"cl_ord_id": fields.get("11", ""), # 客户端订单ID
"exec_id": fields.get("17", ""), # 执行ID
"exec_type": fields.get("150", ""), # 执行类型
"ord_status": fields.get("39", ""), # 订单状态
"symbol": fields.get("55", ""), # 股票代码
"side": fields.get("54", ""), # 买卖方向
"order_qty": fields.get("38", ""), # 订单数量
"cum_qty": fields.get("14", ""), # 累计成交数量
"leaves_qty": fields.get("151", ""), # 剩余数量
"price": fields.get("44", ""), # 价格
"avg_px": fields.get("6", ""), # 成交均价
"last_shares": fields.get("32", ""), # 本次成交数量
"last_px": fields.get("31", ""), # 本次成交价格
}
return exec_report
# 在接收循环中使用
def handle_messages(client):
"""处理接收到的消息"""
while True:
raw = client.receive_message()
if raw:
msg_type = extract_msg_type(raw)
if msg_type == "8": # ExecutionReport
report = parse_execution_report(raw)
print(f"收到执行报告:")
print(f" 订单ID: {report['cl_ord_id']}")
print(f" 执行类型: {report['exec_type']}")
print(f" 状态: {report['ord_status']}")
print(f" 累计成交: {report['cum_qty']}")
print(f" 剩余: {report['leaves_qty']}")
# 根据执行类型做不同处理
exec_type = report['exec_type']
if exec_type == "0": # 新建
print(" 订单已提交")
elif exec_type == "F": # 部分成交
print(f" 部分成交: {report['last_shares']}股 @ {report['last_px']}")
elif exec_type == "2": # 全部成交
print(f" 全部成交: 均价 {report['avg_px']}")
elif exec_type == "8": # 拒绝
print(f" 订单被拒绝")
elif msg_type == "3": # Reject
print("收到拒绝消息")
# 处理拒绝逻辑
注意:ExecutionReport可能会多次到达。同一个订单可能先收到“新建”,再收到“部分成交”,最后收到“全部成交”。你的系统必须能处理这种多次报告的情况。我曾经在早期版本里只处理了第一次报告,结果漏掉了后续的成交信息。
4.5 完整流程与状态机
整个FIX订单流程,说白了就是一个状态机。我画了一张图,帮你理清逻辑:
这个流程其实很直观:你发订单,券商回报告。报告告诉你订单现在是什么状态。你根据状态决定下一步操作。
4.6 完整示例:下单并等待成交
最后,我把所有东西串起来,写一个完整的示例:
def place_order_and_wait():
"""下单并等待成交"""
client = FIXClient("192.168.1.100", 8888, "MYTRADER", "BROKER")
client.connect()
# 发送订单
cl_ord_id = f"ORD{int(time.time())}"
send_new_order_single(
client=client,
symbol="00700",
side=1,
quantity=1000,
price=380.50,
cl_ord_id=cl_ord_id
)
# 等待执行报告
print("等待执行报告...")
while True:
raw = client.receive_message()
if raw:
msg_type = extract_msg_type(raw)
if msg_type == "8":
report = parse_execution_report(raw)
if report["cl_ord_id"] == cl_ord_id:
print(f"收到订单 {cl_ord_id} 的执行报告")
print(f" 状态: {report['ord_status']}")
print(f" 累计成交: {report['cum_qty']}")
# 如果全部成交或拒绝,退出循环
if report["exec_type"] in ("2", "8"):
print("订单处理完成")
break
client.disconnect()
if __name__ == "__main__":
place_order_and_wait()
经验之谈:生产环境里,你不能这样同步等待。应该用异步方式,把收到的报告放到队列里,由另一个线程处理。我最初做的时候就是同步等待,结果UI卡死了。后来改成异步,体验好很多。
好了,FIX协议实战就聊到这儿。你动手试试,把代码跑起来,感受一下FIX消息的收发过程。遇到问题很正常,多看看日志,多抓包分析,慢慢就熟练了。
公众号:蓝海资料掘金营,微信deep3321