FIX 协议开发(2):QuickFIX/J 入门

本系列导航

FIX 协议开发(1):协议介绍及开发方案
FIX 协议开发(2):QuickFIX/J 入门 (本文)
FIX 协议开发(3):QuickFIX/J 实战经验小结

安装

QuickFIX/J 的官网是 https://www.quickfixj.org,其 GitHub 是 https://github.com/quickfix-j/quickfixj。撰写本文时,GitHub 上的版本是 2.2,官网上的文档给到了 2.1 的版本。所以这里选择安装 2.1,避免与文档不匹配。

我们通过 Maven 来引入 quickfixj 库,见此页的“Maven Integration”部分,在 pom.xml 中添加对应 xml 片段(该网页中的xml引入的是2.0.0版本的库,或许是笔误,我将其改为2.1.0)即可:

<!-- QuickFIX/J dependencies -->
<dependency>
    <groupId>org.quickfixj</groupId>
    <artifactId>quickfixj-core</artifactId>
    <version>2.1.0</version>
</dependency>
<!-- 选择所需版本的messages -->
<dependency>
    <groupId>org.quickfixj</groupId>
    <artifactId>quickfixj-messages-fix42</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.22</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.22</version>
</dependency>

创建 FIX Application

使用 QuickFIX 开发 FIX 应用的核心在于实现 Application 接口:

public interface Application {
    void onCreate(SessionID sessionId);
    void onLogon(SessionID sessionId);
    void onLogout(SessionID sessionId);
    void toAdmin(Message message, SessionID sessionId);
    void toApp(Message message, SessionID sessionId) throws DoNotSend;
    void fromAdmin(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon;
    void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType;
}

根据业务需求,实现这些方法即可。框架会以类似事件驱动的方式,调用对应方法。这些方法的定义如下:

  • onCreate
    • This method is called when quickfix creates a new session. A session comes into and remains in existence for the life of the application. Sessions exist whether or not a counter party is connected to it. As soon as a session is created, you can begin sending messages to it. If no one is logged on, the messages will be sent at the time a connection is established with the counterparty.
  • onLogon
    • This callback notifies you when a valid logon has been established with a counter party. This is called when a connection has been established and the FIX logon process has completed with both parties exchanging valid logon messages.
  • onLogout
    • This callback notifies you when an FIX session is no longer online. This could happen during a normal logout exchange or because of a forced termination or a loss of network connection.
  • toAdmin
    • This callback provides you with a peak at the administrative messages that are being sent from your FIX engine to the counter party. This is normally not useful for an application however it is provided for any logging you may wish to do. Notice that the FIX::Message is not const. This allows you to add fields before an adminstrative message before it is sent out.
  • toApp
    • This is a callback for application messages that you are being sent to a counterparty. If you throw a DoNotSend exception in this function, the application will not send the message. This is mostly useful if the application has been asked to resend a message such as an order that is no longer relevant for the current market. Messages that are being resent are marked with the PossDupFlag in the header set to true; If a DoNotSend exception is thrown and the flag is set to true, a sequence reset will be sent in place of the message. If it is set to false, the message will simply not be sent. Notice that the FIX::Message is not const. This allows you to add fields before an application message before it is sent out.
  • fromAdmin
    • This callback notifies you when an administrative message is sent from a counterparty to your FIX engine. This can be usefull for doing extra validation on logon messages such as for checking passwords. Throwing a RejectLogon exception will disconnect the counterparty.
  • fromApp
    • This is one of the core entry points for your FIX application. Every application level request will come through here. If, for example, your application is a sell-side OMS, this is where you will get your new order requests. If you were a buy side, you would get your execution reports here. If a FieldNotFound exception is thrown, the counterparty will receive a reject indicating a conditionally required field is missing. The Message class will throw this exception when trying to retrieve a missing field, so you will rarely need the throw this explicitly. You can also throw an UnsupportedMessageType exception. This will result in the counterparty getting a reject informing them your application cannot process those types of messages. An IncorrectTagValue can also be thrown if a field contains a value that is out of range or you do not support.

官方文档提供如下示例代码,来运行一个 acceptor 模式(接受请求)的 Application:

import quickfix.*;
import java.io.FileInputStream;
 
public class MyClass {
 
    public static void main(String args[]) throws Exception {
        if (args.length != 1) return;
        String fileName = args[0];
 
        // FooApplication is your class that implements the Application interface
        Application application = new FooApplication();
 
        SessionSettings settings = new SessionSettings(new FileInputStream(fileName));
        MessageStoreFactory storeFactory = new FileStoreFactory(settings);
        LogFactory logFactory = new FileLogFactory(settings);
        MessageFactory messageFactory = new DefaultMessageFactory();
        Acceptor acceptor = new SocketAcceptor(application, storeFactory, settings, logFactory, messageFactory);
        acceptor.start();
        // while(condition == true) { do something; }
        acceptor.stop();
    }
}

如果需要一个 initiator 模式(发出请求)的 Application,应把 acceptor 换成 SocketInitiator 实例。

可以留意到,MessageStoreFactory、LogFactory 和 MessageFactory 都是接口,QFJ 库分别自带多种实现,可根据实际情况选择,或自行实现这些接口。

  • MessageStoreFactory 主要用来持久化 Fix Session,即保存状态、创建时间、消息,及其自己维护的发送序列号和接收序列号等。库自带保存到 文件/数据库/内存/不保存 等实现。
  • LogFactory 负责创建不同类型的日志。自带输出日志到 文件/数据库/屏幕/slf4j 等实现。
  • MessageFactory 负责创建 Message。由于 FIX 有多个版本,应选择业务对应版本的 factory。

还可以留意到,SessionSettings 会读取本地的配置文件——上面的代码在缺少配置文件时,是不能跑起来的。如何写配置文件见下一节。

配置文件

配置项很多,分为 [DEFAULT] 和 [SESSION] 部分,详情查阅文档。可以有多个 [SESSION] 块,也就是说,库可以同时连多个账户。注意必填的字段都要填,否则启动时会报错。

接收信息

当接收到对方发送的信息,该如何把需要的字段提取出来?也就是说,在 fromApp()、fromAdmin() 方法中应如何解析传入的 Message 实例。一般来说,可以这样做:

import quickfix.*;
import quickfix.field.*;
 
public void fromApp(Message message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue {
    // 不够类型安全,不建议。只有做底层操作时,才用指定 key 号码的方法
    // StringField field = new StringField(44);
    // message.getField(field);
 
    Price price = new Price();
    message.getField(price); // 返回一个 DoubleField 实例
 
    ClOrdID = new ClOrdID();
    message.getField(clOrdID);
}

更推荐的做法,是使用 MessageCracker。可以使 Application 继承 MessageCracker 类以获得 crack 方法,然后分别为不同类型的 message 重载 onMessage() 方法。在下面的例子中,OrderCancelRequest 类没有 ClearingAccount,编译就不能通过。这是最类型安全的处理方式。

public class MyApplication extends MessageCracker implements quickfix.Application {
    public void fromApp(Message message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue {
        crack(message, sessionID);
    }
 
    public void onMessage(quickfix.fix42.NewOrderSingle message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue {
        ClearingAccount clearingAccount = new ClearingAccount();
        message.get(clearingAccount);
    }
 
    public void onMessage(quickfix.fix42.OrderCancelRequest message, SessionID sessionID) throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue {
        // OrderCancelRequest 没有 ClearingAccount,编译不能通过
        ClearingAccount clearingAccount = new ClearingAccount();
        message.get(clearingAccount);
    }
}

发送信息

和 Python 实现类似,也是自己构造各个字段。注意发送时调用的是 Session 类的静态方法 sendToTarget()。

void sendOrderCancelRequest() {
    Message message = new Message();
    Header header = message.getHeader();
 
    // header.setField(new StringField(8, "FIX.4.2")); // 不推荐
    header.setField(new BeginString("FIX.4.2"));
    header.setField(new SenderCompID("TW"));
    header.setField(new TargetCompID("TARGET"));
    header.setField(new MsgType("D"));
    message.setField(new OrigClOrdID("123"));
    message.setField(new ClOrdID("321"));
    message.setField(new Symbol("LNUX"));
    message.setField(new Side(Side.BUY));
    message.setField(new Text("Cancel My Order!"));
 
    Session.sendToTarget(message);
}

和接收类似,推荐使用经过进一步抽象的写法:

void sendOrderCancelRequest() throws SessionNotFound {
    quickfix.fix41.OrderCancelRequest message = new quickfix.fix41.OrderCancelRequest(
            new OrigClOrdID("123"),
            new ClOrdID("321"),
            new Symbol("LNUX"),
            new Side(Side.BUY)
    );
 
    message.set(new Text("Cancel My Order!"));
 
    Session.sendToTarget(message, "TW", "TARGET");
}

包含多个组的信息

发送

先创建 group,然后调用 message 父类的 addGroup() 方法,逐个添加 group。

接收

先创建 group,调用 message.getGroup(n, group) 把第 n 个组读入到 group 中,最后调用 group.get(xxxField) 提取对应的字段

详见

自定义字段

假设对方有一个 6123 字段,是一个字符串。当然,可以用

message.setField(new StringField(6123, "value"));
StringField field = message.getField(new StringField(6123));

进行读写,但是这毕竟不够安全。

稍微好点的办法是为这个 6123 字段创建一个新的 class:

// MyStringField.java
import quickfix.StringField;

public class MyStringField extends StringField {
    public MyStringField() { super(6123); }
    public MyStringField(String data) { super(6123, data); }
}
 

// 使用
MyStringField stringField = new MyStringField("abcdefg");
 
message.setField(stringField);
message.getField(stringField);

消息校验

默认情况下,框架在收到消息时,对消息进行校验。如果校验失败,例如该填的项没填,又例如出现了未知的字段,就会在底层拒绝这个消息,不会把这个消息传到上面的应用层。

框架还可以动态地为每个 session 读取一个 xml 文件用作消息校验。

自定义消息库

上面提到,构造数据包推荐使用高度抽象过的方式,例如例子中直接创建 quickfix.fix41.OrderCancelRequest 的实例。那么,如果是自定义的消息,应该如何构造,是不是只能用“自定义字段”中的办法来手动构造呢?

答案是可以直接从 Message 库的层面进行自定义。文档中提到最方便的做法:首先下载 QFJ 的源代码,然后是修改 quickfixj-messages 文件夹下对应 FIX 版本字典(例如quickfixj-messages-fix42中)的 xml 文件后,再编译一次 QFJ 库,就可以在 quickfixj-messages-fix42 的 target 文件夹内找到编译好的 Message 库了。它会编译出来两个 jar,不带 source 的是编译后的二进制 class 用于运行的;另一个是 source,方便在 IDE 中查看其定义的(比反编译出来的好看),两个 jar 都有其作用。

官方例程

在其 GitHub 仓库中,有几个实现供参考:

  • Executor 是一个简单的 broker,收到了订单就成交
  • Banzai 是一个简单的客户端,可发送买卖订单

以上便是官方文档的主要内容。开发过程中遇到问题,应先查阅文档。——当然,实际问题不会这么简单、顺利,后面文章的踩坑与填坑之路才是重点。


发表评论