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订单流程,说白了就是一个状态机。我画了一张图,帮你理清逻辑:

FIX订单状态机 FIX客户端 券商系统 NewOrderSingle (35=D) ExecutionReport (35=8) PendingNew PartiallyFilled Filled Rejected Cancelled 部分成交 全部成交 拒绝 撤单

这个流程其实很直观:你发订单,券商回报告。报告告诉你订单现在是什么状态。你根据状态决定下一步操作。

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