Repeating Groups
We now add to our a model functionality for Repeating Groups which allow multiple copies of the same record of information to be passed in to the system. For example the Parties
of FIX version 4.4 is defined as
declare repeating record Parties {
@description: Repeating group below should contain unique combinations of PartyID, PartyIDSource, and PartyRole
NoPartyIDs "453" :? NumInGroup
@description: Used to identify source of PartyID. Required if PartyIDSource is specified. Required if NoPartyIDs > 0.
PartyID "448" :? string
@description: Used to identify class source of PartyID value (e.g. BIC). Required if PartyID is specified. Required if NoPartyIDs > 0.
PartyIDSource "447" :? PartyIDSource
@description: Identifies the type of PartyID (e.g. Executing Broker). Required if NoPartyIDs > 0.
PartyRole "452" :? PartyRole
@description: Repeating group of Party sub-identifiers.
PtysSubGrp : PtysSubGrp
}
The NoPartyIDs
is an indicator of the number of copies of this group that exist. All repeating groups must have a field with type NumInGroup
or type NoSides
which is restricted to a maximum length of 2.
In this repeating group, the field PtysSubGrp}
is also a repeating group, meaning there are multiple copies of this group within each member of the Parties
repeating group.
Customising Repeating Groups
With repeating groups, as with non-repeating records and messages, it is possible to add custom fields. Also it is possible to override a defined optionality of an existing repeating group to ensure certain elements be present within each copy of the group.
Add custom fields
As shown in the section on custom fields and cases it is possible to add a custom field to a repeating group. We do so here, by adding a new field of type int
called PartyIndex
:
extend record Parties {
PartyIndex "10002": int
}
It is important to note that any modification to a repeating group is true for all instances of that repeating group within the model. This means any field which is of type Parties
in the model will have the same internal fields and optionality.
Changing field optionality
It is possible to change the optionality of a field with a repeating group. For example we could impose a condition that the PartyID
field be required in all copies of the Parties
field of message NewOrderSingle
. We do this in this model, as well as explicitly importing the fields we want to include in the repeating groups for this model as follows:
repeatingGroup Parties {
req NoPartyIDs
req PartyID
req PartyIndex
req PtysSubGrp
}
repeatingGroup PtysSubGrp {
opt NoPartySubIDs
opt PartySubID
}
Incorporating Repeating Groups
In order to incorporate repeating groups we first add a field to the NewOrderSingle
message as follows:
message NewOrderSingle {
req ClOrdID
req Side
req TransactTime
req OrdType valid when it in [ OrdType.Limit, OrdType.Market, StopSpread ]
req OrderQtyData.OrderQty
opt SpreadProportion valid when case(it){None:true}{Some x: x>0.0 && x<=1.0}
opt Price
ign Account
req Parties
}
We can add a validation statement to this field - for example:
req Parties valid when it.PartyIndex > 0 && it.PartyIndex < 100
valid when it.PartyID != "N/A"
which ensures that the new field PartyIndex
is within a certain range, and that the PartyID
field is not the empty string.
We can also add the Parties
field to ExecutionReport
:
outbound message ExecutionReport {
req OrderID
req ExecID
req ExecType
req OrdStatus
req Side
req OrderQtyData.OrderQty
req LeavesQty
req CumQty
opt Text
req Parties
}
More complex validation statements
It is possible to write more complex validation statements about repeating group elements. For example we could write the following:
validate {
case(this.Parties.PtysSubGrp.PartySubID)
{None:true}
{Some x : this.Parties.PartyID != x}
}
which states that for each copy of the repeating group Parties
, the PartyID
field should not be the same as any of the PartySubID
fields of the contained sub-repeating group PtysSubGrp
if they are present.
Modification of State
We can modify state and introduce repeating groups in the same as for any other field. For example by adding to the assignable
section:
internal state {
assignable{
Side : Side
Price :? Price
OrderQtyData.OrderQty : Qty
OrdStatus : OrdStatus = OrdStatus.PendingNew;
OrdType : OrdType
LeavesQty : Qty
CumQty : Qty
SpreadProportion :? float
Parties : Parties
OrderID : string
ExecType : ExecType = ExecType.PendingNew;
}
live_order : bool = false;
AvgPx : float
bestBid: Price
bestAsk: Price
}
Now the Parties
element of the state will be automatically assigned to from the incoming message, and can be assigned to the Parties
field of the outgoing ExecutionReport
of a receive or reject block.
Errors and Warnings
Tag consistency errors apply to the fields of repeating groups as they do for any other field. Further, it is only possible to assign entire repeating groups e.g.
state.Parties = msg.Parties
It is not possible to assign to a particular field. Particular fields can only be referenced in boolean expressions such as
if (state.Parties.PartyID == "acct") then return
which implicitly checks that all PartyID
fields are not equal to "acct"
.
If you try to write, for example
state.Parties.PartyID = "acct"
you will receive the error message
Repeating Group fields can only be referenced in a boolean expression.
Full Model – Imandra Analysis
Click on the image below for an interactive analysis of this model performed by imandra.
Full Model - IPL Code
//
// Imandra Inc.
// Copyright (c) 2024
//
// Code for 'Repeating Groups Tutorial'
//
// For further info see https://docs.imandra.ai
//
//
//
import FIX_4_4
@title: "Repeating Groups Tutorial"
messageFlows {
NewOrderFill {
name "NewOrderFill"
description "Initialise Book State, send NewOrderSingle Message then receive a fill action"
template[bookState, NewOrderSingle, fill]
}
}
internal state {
assignable{
Side : Side
Price :? Price
OrderQtyData.OrderQty : Qty
OrdStatus : OrdStatus = OrdStatus.PendingNew;
OrdType : OrdType
LeavesQty : Qty
CumQty : Qty
SpreadProportion :? float
Parties : Parties
OrderID : string
ExecType : ExecType = ExecType.PendingNew;
}
live_order : bool = false;
AvgPx : float
bestBid: Price
bestAsk: Price
}
extend enum OrdType {
StopSpread "s"
}
extend message NewOrderSingle {
SpreadProportion "10001" :? float
}
extend record Parties {
PartyIndex "10002" : int
}
repeatingGroup Parties {
req NoPartyIDs
req PartyID
req PartyIndex
req PtysSubGrp
}
repeatingGroup PtysSubGrp {
opt NoPartySubIDs
req PartySubIDType
opt PartySubID
}
message NewOrderSingle {
req ClOrdID
req Side
req TransactTime
req OrdType valid when it in [ OrdType.Limit, OrdType.Market, StopSpread ]
req OrderQtyData.OrderQty
opt SpreadProportion valid when case(it){None:true}{Some x: x>0.0 && x<=1.0}
opt Price
ign Account
req Parties valid when it.PartyIndex > 0 && it.PartyIndex < 100
valid when it.PartyID != "N/A"
validate {
(this.OrdType == OrdType.Market <==> !present(this.Price)) &&
(this.OrdType == OrdType.Limit ==> present(this.Price)) &&
(this.OrdType == OrdType.StopSpread ==> present(this.Price))
}
validate {
this.OrdType == StopSpread <==>
present(this.SpreadProportion)
}
validate {
this.OrdType != OrdType.Market ==>
(case this.Price
{Some price: price > 0.0}
{None: false}
)
}
validate {
case(this.Parties.PtysSubGrp.PartySubID)
{None:true}
{Some x : this.Parties.PartyID != x}
}
}
outbound message ExecutionReport {
req OrderID
req ExecID
req ExecType
req OrdStatus
req Side
req OrderQtyData.OrderQty
req LeavesQty
req CumQty
opt Text
req Parties
}
action fill {
fill_price : Price
fill_qty : Qty
validate {
state.OrdStatus != OrdStatus.PendingNew
}
validate {
this.fill_qty > 0.0
}
validate {
this.fill_qty <= state.LeavesQty
}
validate {
this.fill_price > 0.0
}
validate {
if ( state.Side == Side.Buy )
then ( this.fill_price >= state.bestAsk )
else ( this.fill_price <= state.bestBid )
}
validate {
(state.OrdType != OrdType.Market) ==>
( case state.Price
{ Some p:
if ( state.Side == Side.Buy ) then
( this.fill_price <= p )
else ( this.fill_price >= p )
}
{ None: true }
)
}
}
action bookState {
bestBid : Price
bestAsk : Price
validate{
this.bestAsk > this.bestBid &&
this.bestBid > 0.0 &&
this.bestAsk > 0.0
}
}
receive (f:fill) {
state.LeavesQty = state.LeavesQty - f.fill_qty
state.AvgPx = ( state.AvgPx * state.CumQty + f.fill_qty * f.fill_price ) / ( f.fill_qty + state.CumQty )
state.CumQty = state.CumQty + f.fill_qty
if state.LeavesQty == 0.0 then
{
state.OrdStatus = OrdStatus.Filled
state.ExecType = ExecType.Trade
}
else
{
state.OrdStatus = OrdStatus.PartiallyFilled
state.ExecType = ExecType.Trade
}
send ExecutionReport {
state with
ExecID = fresh();
}
}
receive (ba:bookState){
state.bestBid = ba.bestBid
state.bestAsk = ba.bestAsk
let spread = (state.bestAsk - state.bestBid)/state.bestAsk
if
(case(state.SpreadProportion){None:false}{Some x: x >= spread}) &&
state.OrdStatus == OrdStatus.PendingNew
then
state.OrdStatus = OrdStatus.New
}
receive (msg:NewOrderSingle) {
state.live_order = true
state.LeavesQty = msg.OrderQtyData.OrderQty
state.OrderID = fresh()
assignFrom(msg,state)
if msg.OrdType == StopSpread
then
case(msg.SpreadProportion)
{Some x:
if state.bestAsk != 0.0 then
if x >= (state.bestAsk - state.bestBid)/state.bestAsk
then
{
state.OrdStatus = OrdStatus.New
state.ExecType = ExecType.New
}
}
send ExecutionReport {
state with
ExecID = fresh();
}
}
reject (msg:NewOrderSingle, text:string)
{
missingfield:{
state.ExecType = ExecType.Rejected
state.OrdStatus = OrdStatus.Rejected
send ExecutionReport {
state with
ExecID = fresh();
Text = Some text;
}
}
invalidfield,invalid:{
state.ExecType = ExecType.Rejected
state.OrdStatus = OrdStatus.Rejected
send ExecutionReport {
state with
ExecID = fresh();
Text = Some text;
}
}
}
Generated Documentation
Click here to view the generated documentation for this model.