Rick.Brown

Virtualizing the FIX Trading Protocol

Blog Post created by Rick.Brown Employee on Oct 23, 2015

Introduction

There are various trading messaging protocols used by investment banks, some of which are supported within DevTest out-of-the-box and some that need some work in DevTest to provide virtual services. FIX is one of those protocols that need some work.

We have virtualized FIX a few times, for a number of banks, but we’ve never had a critical mass to add support to the base product. A field-developed FIX add-in was created a number of years ago, but subsequent versions of DevTest have superseded some of the functionality provided, and the protocol support is only partial functional in the latest version of DevTest (this journal entry is written for DevTest v8.4). If the FIX add-in is updated after the release of this document, that will be the preferred reference.

Technicalities

The FIX trading protocol is a data format, so it should fit neatly as a Data Protocol in DevTest. It runs directly over TCP transport, so there is the potential for missing or out-of-order reception of transmitted messages, so the protocol includes mandatory sections for message sequence numbers, body length calculation and body checksum. It also uses its own field delimiter character and a string as the record delimiter, and there are some request messages that expect multiple responses. Multiple date/time strings are used, and the formats do not match standards used elsewhere (and thereby supported out-of-the-box in DevTest). All of these technicalities have the potential to cause issues for virtual services, so I will describe each of them in detail.

We’ll go in order of when each of these become important in the process of virtualization, but before we do so, let’s start with a frightening thing;

A Sample Message

A FIX message contains name-value pairs, with delimiters. Each of the names is a number that refers to a specific piece of data. The FIX protocol is open-source, so there are look-up tables online that explain what each of the numbers refer to. I occasionally visit these websites for reference:

http://fixwiki.org/fixwiki/FIXwiki

http://www.onixs.biz/fix-dictionary/

http://www.fixtradingcommunity.org/

FIX is nearly 15 years old, so there have been many versions of the protocol. The most commonly used versions are 4.x, but the current version is 5.0sp2.

Here is a sample. To find this message in the above websites, we need to look at the 8= and the 35= fields:

8=FIX.4.49=7935=A56=Server49=Client 34=152=20151015-11:04:23.24298=0108=60141=Y10=236

This message has a number of name-value pairs. Let’s look at what this means:

8=FIX.4.4

  • “8=” means “BeginString”. It is always the first field in a FIX message, it is always unencrypted, it is required.
  • “FIX.4.4” means “FIX version 4.4”

9=79

  • “9=” means “BodyLength”. It is always the second field in a FIX message, it is always unencrypted, it is required.
  • “79” means there are 79 characters between the end of this field and the start of the “10=” field. If the number here does not match the actual length of the FIX message, the FIX component receiving the message should throw an error.

35=A

  • “35=” means “MsgType”. It is always the third field in a FIX message, it is always unencrypted, it is required.
  • “A” means “Logon”. This is the first message in a conversation.

56=Server

  • “56=” means “TargetCompID”. It is the destination of this message. It is always unencrypted, it is required.
  • “Server” means the server hostname (or other connection description information)

49=Client

  • “49=” means “SenderCompID”. It is the source of the message. It is always unencrypted, it is required.
  • “Client” means the client hostname (or other connection description information)

34=1

  • “34=” means “MsgSeqNum”. This is required, and is a constantly-incrementing number
  • “1” means the sequence number sent in this message. The destination checks to make sure this number is higher than any previous FIX message sent from “TargetCompID” to “SenderCompID”, unless field 141 is set to “Y”, in which case, the value here should be “1”. TargetCompID->SenderCompID is a different counter to SenderCompID->TargetCompID, and both machines check the message sequence number of messages sent to them.

52=20151015-11:04:23.242

  • “52=” means “SendingTime”, It is a timestamp, and is required.
  • “20151015-11:04:23.242” is the time this message was sent. The format used here is yyyyMMdd-HH:mm:ss.SSS and is represented in UTC timezone

98=0

  • “98=” means “EncryptMethod”. It is always unencrypted, it is required.
  • “0” means “not encrypted”

108=60

  • “108=” means “HeartBtInt”. It is required
  • “60” means “60 seconds between heartbeat messages being sent”. Heartbeat messages should be sent every 60 seconds by both SenderCompID and TargetCompID, unless a transactionam message has been sent the other way in the past 60 seconds.

141=Y

  • “141=” means “ResetSeqNumFlag”.
  • “Y” indicates that both sides of the conversation should reset the value in their 34 field to 1

10=236

  • “10=” means “CheckSum”. It is always the final field in a FIX message. It is always unencrypted, it is required.
  • “236” is the modulo-256 of the message, preceding the 10 field.

FIX Technicalities

Delimiter Characters

The FIX field delimiter is the ASCII character commonly referred-to as SOH. This is Control-A, or 0x01, and when looking at FIX discussions online, people sometimes use the Caret character, sometimes the pipe character, sometimes the newline character, sometimes <SOH> and occasionally whatever makes most sense in the discussion. Where possible in this document, I will use | [to be confirmed, when I review what I have used in this document]

The FIX record delimiter is an ASCII string of 8 characters in length. The FIX protocol states that the checksum field must come last in a record, so the string is <field_delimiter><checksum_marker><field_delimiter>. I will explain the checksum later in this document; for now, it is sufficient to say that an example of a FIX record delimiter might be |10=123|

DateTime Formats

Some fields, such as SendingTime, use the datetime format of yyyyMMdd-HH:mm:ss.SSS. Others, such as SettlDate, use yyyyMMdd. To perform useful mathematics on date values, DevTest mustn’t mis-identify 8-digit field values as dates, as this would invalidate other field values.

DevTest should correctly identify the longer datetime format, and this is not one of the standard formats built-in to DevTest, and there are two ways of supporting this.

  1. Alter the lisa.properties file to add this date format
  2. Convert this date format to a format that is built-in to DevTest

 

Message Sequence Number

As explained in the message sample section, the message sequence number is a counter per connection, starting from 1 whenever ResetSeqNumFlag is set to “Y” and incrementing for every message sent.

DevTest needs to keep count of MsgSeqNum for each connection between itself and the client that requests messages from it.

Body Length

The value of BodyLength is set at runtime to the number of characters between the delimiter after the BodyLength field (after its end delimiter) and the start of the CheckSum field (after its start delimiter).

DevTest needs to calculate BodyLength for each response at the time when that response is selected.

Checksum

The value of CheckSum is set at runtime to the modulo-256 of the byte value of each character in the message, from the beginning of the message until after the start delimiter of the CheckSum field.

DevTest needs to calculate CheckSum for each response at the rime when that response is selected.

Multiple Responses

Some requests made to a server require multiple responses. For example, a TradeCaptureReportRequest expects a TradeCaptureReportRequestAck followed by a number of TradeCaptureReport messages.

DevTest needs to keep the TCP connection open and return multiple responses when a request requires them.

The FIX Add-in

A FIX transport & data protocol handler was written in the field, a few years ago. It pre-dates the TCP transport support in DevTest. As DevTest has changed and matured over the years, the amount of functionality provided by this add-in has either decreased or been made unreliable. Looking at the source code for this add-in, it has its own TCP transport handler, it includes FIX field delimiters, it converts FIX field numbers into FIX field names in the request, it writes MsgSeqNum, it writes BodyLength and it writes CheckSum. Of these, the only ones that reliably work are the request fieldname conversion and the field delimiter support, so those are what I use it for, and I do everything else myself, in scriptable data protocol handlers.

Scriptable Data Protocol Handler

Scriptable support in DevTest is in multiple places. For FIX support, we will create three scripts. The first script is for any additional manipulation required of the request messages, the second is to make sure the response is stored in a nicely-formatted manner in the service image, and the third is where the heavy lifting happens, to perform all the runtime calculations that are needed to ensure successful acceptance of the response.

Request Script

%beanshell%

import com.itko.util.ParameterList;

import com.itko.util.Parameter;

import java.text.SimpleDateFormat;

 

private convertDateFromFIX(dateTime) {

SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'-'hh:mm:ss");

    Date parsedDate = sdf.parse(dateTime);

sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ");

    dateTime = sdf.format(parsedDate);

    return dateTime;

}

private convertDateFromFIXShort(dateTime) {

SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

    Date parsedDate = sdf.parse(dateTime);

sdf.applyPattern("yyyy-MM-dd");

    dateTime = sdf.format(parsedDate);

    return dateTime;

}

 

ParameterList args = lisa_vse_request.getArguments();

 

//Change all the date formats. There are two date formats in use, in four fields

String sendingTime = args.getParameterValue("SendingTime_52");

String OrigSendingTime = args.getParameterValue("OrigSendingTime_122");

String transactTime = args.getParameterValue("TransactTime_60");

String LegSettlDate = args.getParameterValue("LegSettlDate_588");

 

sendingTime = convertDateFromFIX(sendingTime);

  1. args.setParameterValue("SendingTime_52", sendingTime);

 

if(OrigSendingTime != null) {

OrigSendingTime = convertDateFromFIX(OrigSendingTime);

args.setParameterValue("OrigSendingTime_122", OrigSendingTime);

}

if(transactTime != null) {

transactTime = convertDateFromFIX(transactTime);

args.setParameterValue("transactTime_60", transactTime);

}

if(LegSettlDate != null) {

LegSettlDate = convertDateFromFIXShort(LegSettlDate);

args.setParameterValue("LegSettlDate_588", LegSettlDate);

}

// end date formats

 

//Store unwanted arguments as attributes

ParameterList atts = lisa_vse_request.getAttributes();

String[] argsToRemove = {

    "BodyLength_9",

"MsgType_35",

"MsgSeqNum_34",

"CheckSum_10"

};

for(argToRemove : argsToRemove) {

    String thisValue = args.getParameterValue(argToRemove);

args.removeParameter(argToRemove);

atts.addParameter(new Parameter(argToRemove, argToRemove, thisValue));

}

lisa_vse_request.setAttributes(atts);

lisa_vse_request.setArguments(args);

Response Record Script

%beanshell%

 

import com.itko.lisa.ext.util.fix.FIXDictionaryParser;

import java.text.SimpleDateFormat;

 

private convertDateFromFIX(dateTime) {

SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'-'hh:mm:ss");

    Date parsedDate = sdf.parse(dateTime);

sdf.applyPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ");

    dateTime = sdf.format(parsedDate);

    return dateTime;

}

private convertDateFromFIXShort(dateTime) {

SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");

    Date parsedDate = sdf.parse(dateTime);

sdf.applyPattern("yyyy-MM-dd");

    dateTime = sdf.format(parsedDate);

    return dateTime;

}

 

private getFIXFieldName(FIXFieldNumber) {

FIXDictionaryParser fixDict = FIXDictionaryParser.getInstance();

FIXFieldName = fixDict.getFieldName(FIXFieldNumber);

_logger.debug("Field map get value for key {} = {} ", FIXFieldNumber, FIXFieldName);

if(FIXFieldName == null) FIXFieldName = "fix_unknown_tag";

    return FIXFieldName;

}

 

 

boolean runningAsVSE;

String message = testExec.getStateValue("flMessage");

if (message == null) {

    message = lisa_vse_response.getBodyText();

//    args = lisa_vse_request.getArguments();

runningAsVSE = true;

} else {

runningAsVSE = false;

}

 

output = "<fix_response>\n";

messageLine = message.split("\n");

 

for(thisLine : messageLine) {

    linePart = thisLine.split("=", 2);

fieldName = getFIXFieldName(linePart[0]);

fieldValue = linePart[1];

tagStartValue = "<" + fieldName + "_" + linePart[0] + ">";

tagEndValue = "</" + fieldName + "_" + linePart[0] + ">";

_logger.debug("Start tag: {}", tagStartValue);

_logger.debug("End tag: {}", tagEndValue);

 

////manipulate the message sequence number

//if(fieldName.equals("MsgSeqNum"))   fieldValue = "{{=request_MsgSeqNum_34+1;/*" + linePart[1] + "*/}}";

if(fieldName.equals("MsgSeqNum"))       fieldValue = "replace_at_runtime";

////manipulate body length and checksum, as we'll do those at runtime

if(fieldName.equals("BodyLength"))      fieldValue = "replace_at_runtime";

if(fieldName.equals("CheckSum"))        fieldValue = "replace_at_runtime";

//manipulate the date if it's a date field

if(fieldName.equals("SendingTime"))     fieldValue = convertDateFromFIX(fieldValue);

if(fieldName.equals("OrigSendingTime")) fieldValue = convertDateFromFIX(fieldValue);

if(fieldName.equals("TransactTime"))    fieldValue = convertDateFromFIX(fieldValue);

if(fieldName.equals("SettlDate"))       fieldValue = convertDateFromFIXShort(fieldValue);

if(fieldName.equals("LegSettlDate"))    fieldValue = convertDateFromFIXShort(fieldValue);

if(fieldName.equals("TradeDate"))       fieldValue = convertDateFromFIXShort(linePart[1]);

outputLine = tagStartValue + fieldValue + tagEndValue;

_logger.debug("Message line: {}", outputLine);

    output = output + outputLine + "\n";

}

 

output = output + "</fix_response>";

 

 

_logger.debug("Parsed message:\n{}\n\n", output);

if(runningAsVSE) lisa_vse_response.setBodyText(output);

return output;

Response Replay Script

%beanshell%

 

//import com.itko.util.ParameterList;

//import com.itko.util.Parameter;

import java.text.SimpleDateFormat;

import com.itko.lisa.ext.util.fix.FIXDictionaryParser;

 

// Shared functions

private getFIXValue(String key, String message) {

    // FIX delimiter is SOH (AKA 0x01 or Control-A)

    char CtrlA = 0x1;

    String controlA = Character.toString(CtrlA);

 

MsgValueStartPos = message.indexOf(controlA + key + "=") + key.length() + 2;

    MsgValue = message.substring(MsgValueStartPos, message.indexOf(controlA, MsgValueStartPos));

if(MsgValue == null) MsgValue = "";

    return MsgValue;

}

 

private convertDateLong(dateTime) {

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ");

//_logger.debug("Original date: {}", dateTime);

    Date parsedDate = sdf.parse(dateTime);

    sdf.applyPattern("yyyyMMdd'-'HH:mm:ss.SSS");

    dateTime = sdf.format(parsedDate);

//_logger.debug("Replaced date: {}", dateTime);

    return dateTime;

}

 

private convertDateShort(dateTime) {

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

//_logger.debug("Original date: {}", dateTime);

    Date parsedDate = sdf.parse(dateTime);

sdf.applyPattern("yyyyMMdd");

    dateTime = sdf.format(parsedDate);

//_logger.debug("Replaced date: {}", dateTime);

    return dateTime;

}

 

private getFIXkey(FIXvalue) {

FIXDictionaryParser fixDict = FIXDictionaryParser.getInstance();

    fieldMap = fixDict.getFieldMap();

msgTypeMap = fixDict.getMsgTypeMap();

    Iterator iterator = fieldMap.keySet().iterator();

    String key = "";

    String value = "";

while(iterator.hasNext() && !value.equals(FIXvalue)){

key   = iterator.next();

      value = fieldMap.get(key);

    }

//_logger.debug("Field map get key for value {} = {} ", value, key);

    return key;

}

 

//var message = "";

//var args = "";

byte[] rawMessage;

String message;

//

// read the raw message

//

boolean runningAsVSE;

 

String message = testExec.getStateValue("flMessage");

if (message == null) {

rawMessage = lisa_vse_response.getBodyBytes();

    message = new String(rawMessage);

    //args = lisa_vse_request.getArguments();

runningAsVSE = true;

} else {

runningAsVSE = false;

}

 

String myBody = message;

 

_logger.debug("Message to parse is {}", myBody);

myBody = myBody.substring(myBody.indexOf(">") + 2, myBody.lastIndexOf("<") - 1);

 

 

line = myBody.split("\n");

myBody = "";

for(thisLine : line) {

// _logger.debug("Looking at line: {}", thisLine);

endTagPos = thisLine.indexOf(">");

startTagPos = thisLine.substring(0,endTagPos).lastIndexOf("_") + 1;

startValPos = endTagPos + 1;

endValPos = thisLine.lastIndexOf("<");

// _logger.debug("Tag {} to {}, val {} to {}", startTagPos, endTagPos, startValPos, endValPos);

resultLine = thisLine.substring(startTagPos, endTagPos) + "=" + thisLine.substring(startValPos, endValPos);

    myBody = myBody + resultLine + "\n";

}

 

 

//At some point, we need to expand all the properties.

// We need to do this before calculating the body length and checksum

// and it might be a good idea to do it before we replace the ^J with ^A, so let's try it here

//_logger.debug("Before parseInState:\n{}", myBody);

myBody = testExec.parseInState(myBody);

//_logger.debug("After parseInState:\n{}", myBody);

 

//Convert all the date fields to FIX format

String[] longDateFields = {

"SendingTime",

"OrigSendingTime",

"TransactTime"

};

String[] shortDateFields = {

"TradeDate",

"LegSettlDate"

};

 

for(dateField : longDateFields) {

dateFieldKey = getFIXkey(dateField);

    startPos = myBody.indexOf("\n" + dateFieldKey + "=");

    endPos = myBody.indexOf("\n", startPos + 1);

    // This is general for a set of virtual services, but only some FIX services use

    //   the fields that we're looking for. Therefore, let's check to make sure we

    //   aren't trying to replace a non-existent value.

if(startPos > 0) {

//_logger.debug("Found {} {} from position {} to position {} in myBody", dateField, dateFieldKey, startPos, endPos);

dateToConvert = myBody.substring(startPos + dateFieldKey.length() + 2, endPos);

//_logger.debug("Date to convert: {}", dateToConvert);

convertedDate = convertDateLong(dateToConvert);

myBody = myBody.substring(0,startPos) + "\n" + dateFieldKey + "=" + convertedDate + myBody.substring(endPos);

    } else {

//_logger.debug("Date field {} not found in message", dateField);

    }

}

for(dateField : shortDateFields) {

dateFieldKey = getFIXkey(dateField);

    startPos = myBody.indexOf("\n" + dateFieldKey + "=");

    endPos = myBody.indexOf("\n", startPos + 1);

    // This is general for a set of virtual services, but only some FIX services use

    //   the fields that we're looking for. Therefore, let's check to make sure we

    //   aren't trying to replace a non-existent value.

if(startPos > 0) {

//_logger.debug("Found {} {} from position {} to position {} in myBody", dateField, dateFieldKey, startPos, endPos);

dateToConvert = myBody.substring(startPos + dateFieldKey.length() + 2, endPos);

//_logger.debug("Date to convert: {}", dateToConvert);

convertedDate = convertDateShort(dateToConvert);

myBody = myBody.substring(0,startPos) + "\n" + dateFieldKey + "=" + convertedDate + myBody.substring(endPos);

    } else {

//_logger.debug("Date field {} not found in message", dateField);

    }

}

 

 

  1. testExec.setStateValue("Whole message",message);

 

char CtrlA = 0x1;

String controlA = Character.toString(CtrlA);

//

//Perhaps I should leave something for the FIX DPH to do?

//

//controlA = "\n";

 

myBody = myBody.replace("\n", controlA);

  1. testExec.setStateValue("myBody", myBody);

//_logger.debug("body after replacing ^L with ^A:\n{}", myBody);

 

 

//FIX requires certain control fields to have the correct values

//"MsgSeqNum" must be consistent and incrementing with every message transferred from client-> server, and a separate counter for server->client.

//  Client will check that MsgSeqNum is higher than previously used, unless ResetSeqNumFlag__141 is set to Y, in which case, it's set back to 1

//"BodyLength" is the number of characters (including delimiters) between the end delimiting of the "BodyLength" field and the beginning character of the "Checksum" field

//"Checksum" is the modulo of the message, preceding the checksum name

 

//Store response MsgType in case we need to loop responses

  1. testExec.setStateValue("responseMsgType", getFIXValue(getFIXkey("MsgType"), myBody));

 

//Use a SharedModelMap for MsgSeqNum, so we can have lots of counters, each one defined for a specific server and client

SMM_ns = getFIXValue(getFIXkey("SenderCompID"), myBody);

SMM_key = getFIXValue(getFIXkey("TargetCompID"), myBody);

resetMsgSeqNum = getFIXValue(getFIXkey("ResetSeqNumFlag"), myBody);

String newMsgSeqNumStr = "1";

if(resetMsgSeqNum.equals("Y")) {

com.itko.lisa.vse.SharedModelMap.put(SMM_ns, SMM_key, "1");

newMsgSeqNumStr = "1";

} else {

newMsgSeqNumStr = com.itko.lisa.vse.SharedModelMap.get(SMM_ns, SMM_key);

 

if(newMsgSeqNumStr == null) newMsgSeqNumStr = "1";

 

    int newMsgSeqNum = Integer.parseInt(newMsgSeqNumStr);

newMsgSeqNum++;

newMsgSeqNumStr = String.valueOf(newMsgSeqNum);

com.itko.lisa.vse.SharedModelMap.put(SMM_ns, SMM_key, newMsgSeqNumStr);

 

}

MsgSeqNumField = getFIXkey("MsgSeqNum");

MsgSeqNumFieldValue = getFIXValue(MsgSeqNumField, myBody);

myBody = myBody.replace(controlA + MsgSeqNumField + "=" + MsgSeqNumFieldValue, controlA + MsgSeqNumField + "=" + newMsgSeqNumStr);

 

 

//count characters for BodyLength

startPosBodyLength = myBody.indexOf(controlA + getFIXkey("BodyLength") + "=");

startPos = myBody.indexOf(controlA, startPosBodyLength + 1) + 1;

endPos = myBody.indexOf(controlA + getFIXkey("CheckSum") + "=") + 1;

String bodyPart = myBody.substring(startPos,endPos);

int bodyLength = bodyPart.length();

myBody = myBody.substring(0,startPosBodyLength) +

controlA +

getFIXkey("BodyLength") + "=" + Integer.toString(bodyLength) +

controlA +

myBody.substring(startPos);

//calculate correct checksum for this message

 

endPos = myBody.indexOf(controlA + getFIXkey("CheckSum") + "=") + 1;

char[] inputChars = myBody.substring(0,endPos).toCharArray();

int checkSum = 0;

for(char aChar : inputChars) {

    checkSum += aChar;

}

String myChecksum = Integer.toString(checkSum%256);

while(myChecksum.length() < 3) {

myChecksum = "0" + myChecksum;

}

//myBody = myBody.replaceAll(controlA + getFIXkey("CheckSum") + "=" + ".*?" + controlA, controlA + "10=" + myChecksum + controlA);

myBody = myBody.substring(0,endPos) +

getFIXkey("CheckSum") + "=" + myChecksum +

controlA;

 

 

rawMessage = myBody.getBytes();

_logger.debug("Final message in text:\n{}", myBody);

_logger.debug("Final message in bytes\n{}", rawMessage);

if(runningAsVSE) lisa_vse_response.setBody(myBody);

return myBody;

Multiple Responses

There are many ways in which multiple responses can be supported in DevTest. The method I chose was to create a scripted assertion. This assertion is set to loop back to the response selection step when the response MsgType is set to specific values, which I set to TradeCaptureReportRequestAck and TradeCaptureReport. To enable this, the response replay script above sets a property called “responseMsgType”, which is read by this scripted assertion:

// This script should return a boolean result indicating the assertion is true or false

 

/*

 

We need to get the response type from the response.

We also need to have a counter, so we know how many times we've responded

 

This assertion needs to make sure that, when we've responded to a TradeReportRequest with a TradeReportRequestAck,

we will then respond with a number of TradeRequestReport messages.

 

I have created multiple responses to TradeReportRequest, and I hope to be able to simply

loop around the VSM to pick the next response from the list, until my counter is satisfied

 

*/

 

_logger.debug("TradeReportLoopAssertion: Looping to find multiple responses");

 

int responsesToSend = 10;

int responseNumber;

 

myResponse = testExec.getStateString("responseMsgType", "none");

_logger.debug("TradeReportLoopAssertion: Found MsgType {}", myResponse);

 

switch(myResponse) {

    case "AE":

        //we are looping, up to responsesToSend times

responseNumber = testExec.getStateInt("responseNumber", 0);

responseNumber++;

if(responseNumber <= responsesToSend) {

_logger.debug("TradeReportLoopAssertion: Looping turn {} of {}", responseNumber, responsesToSend);

return true;

        } else {

_logger.debug("TradeReportLoopAssertion: Finished looping after turn {} of {}", responseNumber, responsesToSend);

break;

    case "AQ":

        //we want to start looping

testExec.setStateValue("responseNumber", 1);

_logger.debug("TradeReportLoopAssertion: Setting responseNumber to 1");

break;

    default:

        //this isn't a response we need to loop

_logger.debug("TradeReportLoopAssertion: Not looping response MsgType of {}", myResponse);

break;

 

}

 

return false;

One thing to note here is that multiple responses to a single TCP request is not a common use case. It is not supported in the DevTest recording wizard, so I needed to find a different way to add multiple responses. The client application to this FIX server was logging all FIX messages, so I was able to create a test in DevTest to parse the log files and generate request-response messages, which were then able to be included in my service images as multiple responses to a single request.

 

Recording Sequence

Make sure the FIX JAR add-in is copied to the DevTest hotDeploy directory. This will add FIX as an option in Transport Protocols (which I do not use), FIX delimiters (which I use) and FIX Data Protocol (which I use)

Picture1.png

Picture2.png

Picture3.png

Picture4.png

Picture5.png

Picture6.png

Picture7.png

 

After Recording

Add the scripted assertion to the respond step

 

Picture8.png

Final Thoughts

If the FIX add-in was updated to full functionality for DevTest 8.x, would I use it? I probably would, as long as I could rely on it to perform the runtime actions required and it converted the response message to XML. I would probably still need to add a scriptable DPH, for the date conversions and to extract the response MsgType so I could loop for multiple responses when required.

Is my script optimal? Actually no, there are many ways in Java scripts to perform actions, and you can see in my script examples that I use iterators, ArrayLists and for loops with no concept of a style guide. When I see a style guide, I will update my scripts accordingly.

Outcomes