※ QuickFIX/Jを実装してもオンラインブローカーにはなれませんご注意ください
Java Advent Calendarの第11日目です
この記事はIIJ 2020 TECHアドベントカレンダー 10日目の超個人的な続編です。
概要
FXをはじめとする金融取引の中で、電子的に取引を行う場合にはよくFIXプロトコルという専用のプロトコルが利用されます。FIXプロトコル自体は結構歴史のあるプロトコルで様々な場所で利用されている実績のあるプロトコルです。
詳細はこちら > IIJ 2020 TECHアドベントカレンダー 10日目
QuickFIX/JはOSSで開発がされているFIXプロトコルのJavaのクライアント/サーバライブラリです。この記事ではこのQuickFIX/Jの使い方について簡単に解説します。
QuickFIX/J
今回は単一のSpringBoot上でクライアント(Initiator)とサーバ(Acceptor)を構築して、クライアントとサーバの間で注文の送信と結果の返却を実装してみます。
環境構成は下記の通りです。
- SpringBoot 2.4.0
- Java11
- QuickFIX/J 2.2.0
実装済みのサンプルはこちらに用意しておきました。RyuSA/quickfix-example
Application
インタフェース
QuickFIX/Jを実装する際にはWebsocketに近いイベントドリブンな実装方式になります。接続先からFIXの電文を受信し、その電文の種別に応じて処理を書いていくことになります。
QuickFIX/JにはFIXのイベントを表現するためのインタフェースとしてApplication
インタフェースが用意されており、7種類のイベントが定義されています。
- onCreate
- セッションが構築されたときに呼ばれるイベント
- onLogon
- ログオンが完了した時に呼ばれるイベント
- onLogout
- ログアウト時に呼ばれるイベント
- toAdmin
- Adminイベントを送信する際に呼ばれるイベント
- 例:ハートビート送信
- fromAdmin
- Adminイベントを受信した際に呼ばれるイベント
- 例:ハートビート受信
- toApp
- Appイベントを送信する際に呼ばれるイベント
- 例:注文発注
- fromApp
- Appイベントを受信する際に呼ばれるイベント
- 例:レート受信
Initiator/Acceptor共に、このインタフェースを実装したクラスを作成する必要があります。
MessageCracker
クラス
しかし、Application
インタフェースを実装すると下記のような実装をしがちです。
class NoisyApplication implements Application {
// snip
@Override
public void fromApp(Message message, SessionID sessionId)
throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
if (message.getClass().isAssignableFrom(quickfix.fix44.NewOrderSingle.class)) {
// 注文の受信処理
} else if (message.getClass().isAssignableFrom(quickfix.fix44.MarketDataRequest.class)) {
// 市場データの配信要求処理
} else if ...
}
}
Appイベントを受信後、受信したメッセージタイプによってif-else
やswitch
の嵐で処理を分岐させる……といったことを防ぐために、QuickFIX/JにはヘルパークラスとしてMessageCracker
クラスが用意されています。
MessageCracker
クラスはonMessage
メソッドを適切に実装するか、MessageCracker.Handler
アノテーションを適切なメソッドに付与することで下記のようにシンプルな実装に置き換えることができるようになります。
class BetterApplication extends MessageCracker implements Application {
// snip
@Override
public void fromApp(Message message, SessionID sessionId)
throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
crack(message, sessionId);
}
// メッセージタイプが`NewOrderSingle`の場合だけこのメソッドがコールされる
// onMessage以外のメソッド名を指定する場合、`MessageCracker.Handler`アノテーションを付与する
public void onMessage(quickfix.fix44.NewOrderSingle order, SessionID sessionID) {
// DO Something
}
}
このMessageCracker
クラスを継承した上でApplication
インタフェースを実装するとクラスがすっきりとします。
Applicationの設定
サーバ側は上記のApplication
インタフェースの実装を待ち受けるためにSocketAcceptorを起動してポートをListenしておく必要があります。同様にクライアント側はApplication
インタフェースの実装と、カウンターパーティとの接続を管理するSocketInitiatorを定義する必要があります。
また営業時間や営業曜日などの情報、IPアドレスやポート番号などの接続に必要な情報などは設定ファイルの中に定義しておきます。src/main/resource/
配下にquickfix/acceptor.cfg
とquickfix/initiator.cfg
を作成して、下記の詳細を元に設定情報を記載しておきます。
設定: QuickFIX/J User Manual | Configuring QuickFIX/J
今回はクライアントとサーバを単一のSpringBoot上に展開することにします。SocketAcceptorとSocketInitiatorをSpringBootのBeanコンポーネントとして登録して、Beanのライフサイクルに管理を任せてしまいます。
※ ここは各ワークロードによって起動や接続のタイミングはずらしておくべきです。
@Configuration
public class Config {
@Bean
public AcceptorApplication acceptorApplication() {
// Applicationインタフェースを実装したクラス
return new AcceptorApplication();
}
@Bean
public ThreadedSocketAcceptor acceptor(AcceptorApplication acceptorApplication) throws ConfigError {
// 設定ファイル読み込み
SessionSettings setting = new SessionSettings("quickfix/acceptor.cfg");
FileStoreFactory fileStoreFactory = new FileStoreFactory(setting);
MessageFactory messageFactory = new DefaultMessageFactory();
FileLogFactory fileLogFactory = new FileLogFactory(setting);
ThreadedSocketAcceptor acceptor = new ThreadedSocketAcceptor(acceptorApplication, fileStoreFactory, setting, fileLogFactory, messageFactory);
acceptor.start();
return acceptor;
}
@Bean
public InitiatorApplication initiatorApplication() {
// Applicationインタフェースを実装したクラス
return new InitiatorApplication();
}
@Bean
public ThreadedSocketInitiator initiator(InitiatorApplication initiatorApplication) throws ConfigError {
// 設定ファイル読み込み
SessionSettings settings = new SessionSettings("quickfix/initiator.cfg");
MessageStoreFactory storeFactory = new FileStoreFactory(settings);
LogFactory logFactory = new FileLogFactory(settings);
MessageFactory messageFactory = new DefaultMessageFactory();
ThreadedSocketInitiator socketInitiator = new ThreadedSocketInitiator(initiatorApplication, storeFactory, settings, logFactory, messageFactory);
socketInitiator.start();
return socketInitiator;
}
}
クライアントからサーバへ - 注文発注
さっそくクライアントからサーバに注文を発注する部分を実装しましょう。クライアントからサーバに電文を送信する場合はSession#sendToTarget
に対してSession情報と電文を渡すことで電文を送信することができます。
今回はサンプル実装ですので、適当な固定の注文を発注するようにします。
@Component
@RequiredArgsConstructor
class SendNewOrderSingle {
private final ThreadedSocketInitiator initiator;
public void execute() {
quickfix.fix44.NewOrderSingle newOrderSingle = new quickfix.fix44.NewOrderSingle(
new ClOrdID(UUID.randomUUID().toString()),
new Side("1"),
new TransactTime(LocalDateTime.now()),
new OrdType(OrdType.MARKET)
);
newOrderSingle.set(new OrderQty(100));
newOrderSingle.set(new Symbol("USDJPY"));
SessionID session = this.initiator.getSessions().get(0);
try {
Session.sendToTarget(newOrderSingle, session);
} catch (SessionNotFound e) {
log.error("failed to send a newOrderSingle", e);
throw e;
}
}
}
また、あとでサーバからクライアント宛に返ってくる注文の結果を楽しみに待てるようにイベントを登録しておきましょう。約定情報の電文はfromApp
イベントで受信するので、ログとして吐くようにしておきましょう。
public class InitiatorApplication extends quickfix.MessageCracker implements quickfix.Application {
@Override
public void fromApp(Message message, SessionID sessionId)
throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
crack(message, sessionId);
}
public void onMessage(quickfix.fix44.ExecutionReport message, SessionID sessionID) {
log.info("Awesome! Your order has been executed!");
log.info("Execution report; {}", message.toRawString());
}
}
注文受信と約定情報配信 - サーバからクライアント
サーバ側はクライアントからの注文を受け取るイベントを登録して待ち構えておきましょう。今回はサンプルですので、InitiatorからのNewOrderSingleを受信したら100%そのまま常に約定するように実装します。
public class AcceptorApplication extends quickfix.MessageCracker implements quickfix.Application {
@Override
public void fromApp(Message message, SessionID sessionId)
throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
crack(message, sessionId);
}
@quickfix.MessageCracker.Handler
public void onNewOrderSingle(quickfix.fix44.NewOrderSingle order, SessionID sessionID) {
log.info("Congratulations! I recieve a new order!!");
log.info("Order Detail; {}", order.toRawString());
replyNewOrderSingle(order, sessionID);
}
private void replyNewOrderSingle(quickfix.fix44.NewOrderSingle order, SessionID sessionID) {
// 注文を受けて約定情報の元ネタとなる情報を作成
// サンプルなので適当に数値を埋めていく実装
quickfix.field.OrderID orderID;
try {
orderID = new quickfix.field.OrderID(order.getClOrdID().getValue());
} catch (FieldNotFound e) {
throw new InternalError(e);
}
quickfix.field.CumQty cumQty;
quickfix.field.Side side;
quickfix.field.LeavesQty leavesQty;
Instrument instrument;
try {
cumQty = new quickfix.field.CumQty(order.getOrderQty().getValue());
side = order.getSide();
leavesQty = new quickfix.field.LeavesQty(order.getOrderQty().getValue() - cumQty.getValue());
instrument = new Instrument(order.getSymbol());
} catch (FieldNotFound e) {
throw new InternalError(e);
}
quickfix.field.ExecID execID = new quickfix.field.ExecID(UUID.randomUUID().toString());
quickfix.field.ExecType execType = new quickfix.field.ExecType(ExecType.NEW);
quickfix.field.OrdStatus ordStatus = new quickfix.field.OrdStatus(OrdStatus.FILLED);
quickfix.field.AvgPx avgPx = new quickfix.field.AvgPx(0);
// 約定情報を作成
quickfix.fix44.ExecutionReport report = new quickfix.fix44.ExecutionReport(orderID, execID, execType, ordStatus, side, leavesQty, cumQty, avgPx);
report.set(instrument);
try {
// クライアントへ約定情報を送信
Session.sendToTarget(report, sessionID);
} catch (SessionNotFound e) {
throw new InternalError(e);
}
}
}
動作確認
ここまで実装を終えたらアプリケーションを起動しましょう。起動後にクライアント注文を発注するとサーバが受信して約定情報を配信、クライアントが約定情報を受け取るところま観察することができると思います。
まとめ
QuickFIX/Jは見通し良くクラス設計されており、かつヘルパークラス等も充実しているためQuickFIX/Jとしてのエコシステムがよくできていると個人的に感じます。
今回はたまたま手元にあった(?)SpringBootの上に載せてみましたがSpring FrameworkのエコシステムとQuickFIX/Jのエコシステムの親和性が高く、ドメイン領域により集中できる組み合わせだったのではないかなと思いました。