6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JavaAdvent Calendar 2020

Day 11

SpringBootとQuickFIX/Jで爆速でFIXサーバを実装する話 - 今日からあなたもオンラインブローカー?

Last updated at Posted at 2020-12-10

※ 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インタフェースを実装すると下記のような実装をしがちです。

NoisyApplication.java
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-elseswitchの嵐で処理を分岐させる……といったことを防ぐために、QuickFIX/JにはヘルパークラスとしてMessageCrackerクラスが用意されています。

MessageCrackerクラスはonMessageメソッドを適切に実装するか、MessageCracker.Handlerアノテーションを適切なメソッドに付与することで下記のようにシンプルな実装に置き換えることができるようになります。

BetterApplication.java
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.cfgquickfix/initiator.cfgを作成して、下記の詳細を元に設定情報を記載しておきます。
設定: QuickFIX/J User Manual | Configuring QuickFIX/J

今回はクライアントとサーバを単一のSpringBoot上に展開することにします。SocketAcceptorとSocketInitiatorをSpringBootのBeanコンポーネントとして登録して、Beanのライフサイクルに管理を任せてしまいます。
※ ここは各ワークロードによって起動や接続のタイミングはずらしておくべきです。

Config.java
@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情報と電文を渡すことで電文を送信することができます。

今回はサンプル実装ですので、適当な固定の注文を発注するようにします。

SendNewOrderSingle.java
@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イベントで受信するので、ログとして吐くようにしておきましょう。

InitiatorApplication.java
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%そのまま常に約定するように実装します。

AcceptorApplication.java
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);
        }
    }
}

動作確認

ここまで実装を終えたらアプリケーションを起動しましょう。起動後にクライアント注文を発注するとサーバが受信して約定情報を配信、クライアントが約定情報を受け取るところま観察することができると思います。
image.png

まとめ

QuickFIX/Jは見通し良くクラス設計されており、かつヘルパークラス等も充実しているためQuickFIX/Jとしてのエコシステムがよくできていると個人的に感じます。
今回はたまたま手元にあった(?)SpringBootの上に載せてみましたがSpring FrameworkのエコシステムとQuickFIX/Jのエコシステムの親和性が高く、ドメイン領域により集中できる組み合わせだったのではないかなと思いました。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?