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.