Tick-to-Trade Latency Numbers using CoralFIX and CoralReactor

In this article we use wireshark and tcpdump to analyze the latency numbers of a test trading strategy that receives a market data tick through UDP using CoralReactor and places a trade order through FIX using CoralFIX.

Test Details

A pseudo market data generator sends UDP packets containing market data updates (i.e. ticks). Our test trading strategy receives these packets, parses them and places a trade order using the FIX protocol on a pseudo exchange that immediately executes the order. To warm up the test strategy, we initially send 1 million market data updates. Then we proceed to send one packet every 5 seconds. To measure the latency, we use tcpdump to record the UDP packet coming in and the FIX order going out. With the packet capture file from tcpdump we then use wireshark to calculate the difference in the timestamps of the packet arriving and the FIX order leaving.

Results

As you can see from the wireshark screenshot below, the tick-to-trade latencies are around 8-9 microseconds.

tick-to-trade

Source Code

Note that the source code down below produces zero garbage for the GC.

package com.coralblocks.coralfix.bench.trade;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;

import com.coralblocks.coralbits.util.PriceUtils;
import com.coralblocks.coralfix.FixConstants;
import com.coralblocks.coralfix.FixMessage;
import com.coralblocks.coralfix.FixTags;
import com.coralblocks.coralfix.client.FixApplicationClient;
import com.coralblocks.coralreactor.client.Client;
import com.coralblocks.coralreactor.client.ClientAdapter;
import com.coralblocks.coralreactor.client.receiver.ReceiverUdpClient;
import com.coralblocks.coralreactor.nio.NioReactor;
import com.coralblocks.coralreactor.util.MapConfiguration;

public class TickToTrade extends ClientAdapter {
	
	private final NioReactor nio;
	private final FixApplicationClient fixGateway;
	private final byte[] symbol = new byte[8];
	private long orderIDs = 1;
	
	public TickToTrade(NioReactor nio, final Client marketDataFeed, FixApplicationClient fixGateway) {
		this.nio = nio;
		this.fixGateway = fixGateway;
		
		fixGateway.addListener(new ClientAdapter() {

			@Override
			public void onConnectionOpened(Client client) {
				// gateway is connected and ready to trade...
				marketDataFeed.open(); // open the feed...
				marketDataFeed.addListener(TickToTrade.this); // so that onMessage below is called
			}
		});
	}
	
	public void start() {
		fixGateway.open();
	}

	@Override
    public void onMessage(Client client, InetSocketAddress fromAddress, ByteBuffer msg) {
		
		// parse the market data quote
		long quoteId = msg.getLong();
		boolean isBid = msg.get() == 'B';
		msg.get(symbol);
		long size = msg.getInt();
		long price = msg.getLong();
		
		// hit the market data quote
		FixMessage outFixMsg = fixGateway.getOutFixMessage(FixConstants.MsgTypes.NewOrderSingle);
		outFixMsg.add(FixTags.ClOrdID, orderIDs++);
		outFixMsg.add(FixTags.OrigClOrdID, quoteId);
		outFixMsg.addTrimmed(FixTags.Symbol, symbol);
		outFixMsg.add(FixTags.Side, isBid ? "2" : "1"); // sell to the bid or buy from the offer
		outFixMsg.addTimestamp(FixTags.TransactTime, nio.currentTimeMillis());
		outFixMsg.add(FixTags.OrderQty, size);
		outFixMsg.add(FixTags.OrdType, "2"); // limit order
		outFixMsg.add(FixTags.Price, PriceUtils.toDouble(price));
		outFixMsg.add(FixTags.TimeInForce, "3"); // IoC
		fixGateway.send(outFixMsg);
    }

	public static void main(String[] args) {
		
		NioReactor nio = NioReactor.create();
		
		Client marketDataFeed = new ReceiverUdpClient(nio, 44444);
		
		MapConfiguration config = new MapConfiguration();
		config.add("fixVersion", 44);
		config.add("senderComp", "testClient");
		config.add("forceSeqReset", true);
		
		FixApplicationClient fixGateway = new FixApplicationClient(nio, args.length > 0 ? args[0] : "localhost", 55555, config);
		
		TickToTrade tickToTrade = new TickToTrade(nio, marketDataFeed, fixGateway);
		tickToTrade.start();
		
		nio.start();
	}
}