Types
Types are the fundamental way in which data in the model is categorised. In this section we explain the types of data which can be defined and show how the declare types and define new ones. There is a link between an encoding and a type which corresponds to the correlation of the string accepted in a FIX message, for example, and the data representing that string in the model. We will explain this correlation in the sections that follow.
Basic Types
Literals are basic values that one can encode in IPL. There are four types of such values: strings, integers, floating point numbers and boolean constants.
- Strings are character sequences enclosed in double quotes:
"This is a string"
"USD"
- Integer literals are basic integer numbers. No special suffixes/prefixes are allowed – the negative numbers are application of unary negation operator to an integer literal.
42
2017
-1
- Float number literal must be formatted as (whole part).(fractional part).
3.14159
12.344
-123.44
- Boolean literals are just two standard boolean constants
true
andfalse
true
false
Compound Datatypes
Within IPL there are several given compound datatypes - these are concrete types with arguments. At the time of writing, those that are supported are:
LocalMktDate(int,int,int) // year, month, day
MonthYear(int,int,?week) // year, month, week number (1-5)
UTCDateOnly(int,int,int) // year, month, day
UTCTimeOnly(int,int,int,?int) // hour, min, sec, ms
UTCTimestamp(int,int,int,int,int,int,?int) // year, month, day, hour, min, sec, ms
Duration(int,int,int,int,int,int) // year, month, day, hour, min, sec
The way one can declare a field of this type is simply by declaring an object such a field as
a : UTCDateOnly
for example. To attribute values and exploit the types of the arguments we can create values, as described in the Values section.
Finally, note that there are library functions (always in scope) - toUTCDateOnly
, toMonthYear
, and toUTCTimeOnly
- for projecting a UTCTimestamp
to just a UTCDateOnly
, a MonthYear
, or a UTCTimeOnly
, respectively.
Type Declaration
In the IPL model it is possible when defining fields to give types to objects. Fields can exist in records and messages, but their declaration is generally of the form
NAME : TYPE
where TYPE
is the name of the type. For example
year : int
time : UTCTimeStamp
title : string
price : float
Float Precision
Specifications may indicate precision of float types it uses. This requires additional care when performing arithmetic operations. In order to allow this control we introduce two notions:
- Type aliases
- Float precision
In order to declare an alias one can write for example
alias Price : float
With float aliases, a user may wish to explicitly state the precision:
precision (Price, 2)
Note that precision may be set for any alias of a floating point types Qty, Amount, etc. It cannot be set for the float type itself. When analysing the code, this precision is taken into account and the correct multiplcation and addition precision is used for the necessary types.
TimeStamp Precision
Similarly to float precision it is possible to specify the precision of fields which have timestamp related types. The two types to which this applies are
- UTCTimeStamp
- UTCTimeOnly
It is possible to specify for these whether the type should have millisecond or microsecond precision. By default all time related fields have millisecond precision. The way to define specific precision per field is to use the following syntax, for example:
TimeStampPrecisions {
default : milli
TransactTime : micro
}
This states the default time precision for all fields is millisecond but that the field TransactTime
is univerally given microsecond time precision.
Enumeration Types
A simple enum declaration contains a type name and a list of allowed cases:
enum Cardsuit {
Clubs
Diamonds
Hearts
Spades
}
Optionally, it is possible to add a FIX tag value, used to represent the given case on the wire – the value should be in the double quotes, following the case declaration.
Descriptions for each case (and for the type itself) can be given using the @description:
annotation before the declaration. Also, an @encoding:
annotation can be used to state which encoding type is used on the wire for that enum. If this encoding is not present, the encoding is assumed to be of type string
. If the provided FIX value does not match the encoding, IPL will throw an error. Here is an example of the enum declaration, used for for the AccountType field (tag 581) in the FIX data dictionary:
@encoding: int
enum AccountType {
@description: Account is carried on customer Side of Books
CarriedCustomerSide "1"
@description: Account is carried on non-Customer Side of books
CarriedNonCustomerSide "2"
@description: House Trader
HouseTrader "3"
@description: Floor Trader
FloorTrader "4"
@description: Account is carried on non-customer side of books and is cross margined
CarriedNonCustomerSideCrossMargined "6"
@description: Account is house trader and is cross margined
HouseTraderCrossMargined "7"
@description: Joint Backoffice Account (JBO)
JointBackOfficeAccount "8"
}
Here in the first entry, the FIX value corresponding to the CarriedCustomSide case is “1”, the encoding type for the AccountType enum is int. Note that the ‘@description’ annotation used in the example above is entirely optional.
The encoding type char can be given, which denotes that the FIX tag value for the case is a string of fixed length 1. It is possible to encode generally fixed length strings and multiple value chars and strings. Both functionalities for multiple value strings and fixed length strings are forthcoming.
Optional Types
A standard practice is to make certain aspects of the exchanged data optional. The logic may still refer to them, but we need to be careful as to not depend on a calculation that refers to a field that may not exist. For example, consider the following example:
- An incoming message is declared with an optional type ‘Price’
- Within our processing logic, we assign this value to our internal state so that all further analytics are done with it
The problem is that this value may not be assigned after all, so then if we were to merely execute the model, we would get something similar to a NullPointerException that Java developers are well familiar with.
Supporting exceptions would lead to a much more complex syntax and user experience - not to mention the consequences on the mathematical analysis that this would entail. As a result we introduce the concept of optional types. We give examples of how fields in IPL can be declared as optional in the Records section, and discuss how we can reason using optional types in the Optional Types section of the Expressions section.
One important feature of IPL to note is that optional fields with a default value on a message are considered required and not optional, as a value can always be extracted. See Messages section for more information on this feature.
Records
A record data type is useful for representing internal structures and provide a way of grouping information together. A simple record declaration contains a type name and a list of the record’s fields with their corresponding types:
record date {
year : int
month : int
day : int
}
Optionally, it is possible to add:
- a FIX tag, used to represent the field on the wire – it should be in double quotes, following the field name
- descriptions for each field (and for the type itself), using
@description:
annotations before the declaration - a ‘?’ question mark before the type name: that declares the filed as optional – it will have an option type and will be treated differently in the message processing logic
It is necessary to include a fix tag to a field if it will be imported via a message. For more on importing see the Messages section which describes this process. We can for example include a new record
record date {
year "y" : int
month "m" : int
day "d" : int
week "w" :? int
}
This record now adds an optional week field. This means that within the record it may not be present or it may have a value. All of the fields now have FIX tab values which means elements from this record can be imported via messages.
Repeating Groups
Here is an example of a record declaration, used for the
@description: The Parties component block is used to identify and convey information on the entities
@description: both central and peripheral to the financial transaction represented by the FIX message containing
@description: the Parties Block. The Parties block allows many different types of entites to be expressed through the
@description: use of the PartyRole field and identifies the source of the PartyID through the PartyIDSource.
repeating record Parties {
@description: Number of PartyID (448), PartyIDSource (447), and PartyRole (452) entries
@description: Repeating group below should contain unique combinations of PartyID, PartyIDSource, and PartyRole
NoPartyIDs "453" : ? NumInGroup
@description: Party identifier/code. See PartyIDSource (447) and PartyRole (452).
@description: See "Appendix 6-G – Use of <Parties> Component Block"
@description: Used to identify source of PartyID. Required if PartyIDSource is specified. Required if NoPartyIDs > 0.
PartyID "448" : ? String
@description: Identifies class or source of the PartyID (448) value. Required if PartyID is specified.
@description: Note: applicable values depend upon PartyRole (452) specified.
@description: See "Appendix 6-G – Use of <Parties> Component Block"
@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 or role of the PartyID (448) specified.
@description: See "Appendix 6-G – Use of <Parties> Component Block"
@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
}
This is an example of a Repeating Group. This means that many copies of the information held within the group may be present in an incoming message. The field of type NumInGroup
indicates the number of copies of this record that are present. Any repeating group must have an element with type either called NumInGroup
or of type NoSides
. Within the group above there is a field of type PtysSubGrp
. This is a repeating group within a repeating group. If there were 3 copies of the information within the Parties
group and 3 copies of each PtysSubGrp
within each of those, there would in total be 9 copies of the PtysSubGrp
repeating group. We explain how we can reason about the structure of Reasoning Groups in the General Validation section. The structure and typing of repeating groups is assumed to be the same for every copy that appears in the model - that is to say that should a repeating group be imported by two messages, its optionality and type structure is the same in both copies.
In order for a repeating group to be incorporated into the model, it must be explicitly imported in a similar way to Messages. For every copy of a repeating group in the file, the optionality of each field can be determined:
repeatingGroup Parties {
req NoPartyIDs
req PartyID
req PtysSubGrp
}
Custom Entries
IPL allows the addition of custom tags for standard messages, records or enums. For example in the FIX protocol, as described in the secion on enumeration types, an enumeration type receives a tag value in the FIX message which may have an encoding. This defines all the possible string values that can be received in a FIX message. We can extend the values that can be received using a custom extension to the enumeration type. This is also the case for field entries in records and messages as we will demonstrate in this section.
Custom Enum Entries
FIX data dictionaries are large collections of message, record and enum declaration that reflect the FIX protocol specification. Sometimes users of the FIX protocol are adding their own custom fields or enum cases. They have their own tag/value codes and exhibit some venue-specific meanings and behaviors. To allow an ability to encode in IPL such customizations to the basic FIX protocol, we introduce the “extend” statements.
Consider a TimeInForce enum declaration from the FIX data dictionary:
@encoding: char
enum TimeInForce {
@description: Day (or session)
Day "0"
@description: Good Till Cancel (GTC)
GoodTillCancel "1"
@description: At the Opening (OPG)
AtTheOpening "2"
@description: Immediate or Cancel (IOC)
ImmediateOrCancel "3"
@description: Fill or Kill (FOK)
FillOrKill "4"
@description: Good Till Crossing (GTX)
GoodTillCrossing "5"
@description: Good Till Date
GoodTillDate "6"
@description: At the Close
AtTheClose "7"
}
Using “extend enum” one can add their own entries to existing enums. For example, in the code section below, the extra element “BookOrCancel” is added to the enum “TimeInForce” - note that the given tag or name cannot already exist in the enum.
extend enum TimeInForce {
@description: Book Or Cancel
BookOrCancel "C"
}
Within a custom enum extension it is also possible to override the name of an existing element with a given tag, should a user wish to attribute a different descriptive name or behaviour to that tag. For this to work, the original tag name must be brought into scope using the using
keyword. This can be done for example by writing
using TimeInForce { ImmediateOrCancel }
extend enum TimeInForce {
@OverrideName "3" ExecOrCancel
}
Custom Record Field Entries
Some venues may deviate from the standard protocol specification by requiring or making optional new fields for a given message. To allow users to extend the baseline definition, we’ve incorporated syntax to ‘customise’ an already declared message.
/* Original declaration that may have derived */
message CreateOrderSingle "D" = {
@description
...
}
/* Extending the original message declaration above with a custom field */
extend message CreateOrderSingle {
@description: Routing Instruction
RoutingInst "9303" : RoutingInst
...
}
This is a custom entry in the BATS definition of a FIX message field. It has tag 9303, a user-defined description for documentation “Routing Instruction (Bats Only orders)”, and has values defined by the enum RoutingInst (see next section).
Analogously, users may wish to extend fields in existing declared records. This is done using the following code:
record Instrument = {
@description: Specifies when the contract (i.e. MBS/TBA) will settle.
@description: Must be present for MBS/TBA
ContractSettlMonth "667" : ? MonthYear
}
extend record Instrument = {
@description: New type contract settle time
ContractSettlWeek "10103" : ? week
}
Custom Entries in Repeating Group Definitions
It is possible to change the optionality of fields globally for a repeating group. Recall from the Repeating Groups that in the FIX dictionaries the Parties
repeating group is given as (with the documenation comments removed for clarity)
repeating record Parties {
NoPartyIDs "453" : ? NumInGroup
PartyID "448" : ? String
PartyIDSource "447" : ? PartyIDSource
PartyRole "452" : ? PartyRole
PtysSubGrp : PtysSubGrp
}
A user may wish to ensure that the PartyID
always be present. This can be achieved also within a custom extension by writing
extend record Parties {
req PartyID
}
In addition if the special element which determines the number of repeating groups in the message is non-optional, then this means that the repeating group must be present in the FIX message. The order of elements is important in repeating groups - the list of fields must be consistent with the order in which the field entries can appear in a FIX message.
Sets
IPL contains support for sets and associated operations.
Given a base type X
, the type of sets of elements of X
is X set
.
Some fields can be associated with an enumeration type equipped with an encoding, as it’s the case with MultipleValueChar
for example.
The type of this field is then implicitly a set type. For example, if a message requires the field ExecInst
to be present we can write
message OrderSingle {
req ExecInst valid when subset(it,{|Held,NoCross,AllOrNone|})
}
this means that the values acceptable for the ExecInst
field in the incoming message must be a strict subset of the stated values.
Set types can either be introduced via set encodings or by declaring an alias type which is a set, for example
alias mySetType: int set
action myAction {
a : mySetType
}
Set literals can be expressed via the {||}
notation:
let x: int set = {|1,2,3|}
We can test for set membership with the infix operator in
, and for subset inclusion with the subset
function:
let x: int set = {|1,2,3|}
let y: int = 5
let b: bool = y in x // false
let b2: bool = subset({|1|}, x) // true
We can map elements of a set using the builtin map
operator, which takes a set
{|x_1, x_2, ..., x_n|}
and a mapping function f
and returns a new set
{|f(x_1), f(x_2), ..., f(x_n)|}
. As the resulting structure is a set, elements
from the original set mapping to the same value get collapsed together in the
returned set:
let s1: int set = {|1,2,3|}
let s2: bool set = map(s1, {x|x < 2}) // s2 == {|true, false|}
For sets ranging over enum types, we can use the forall
and exists
operators, to test whether a given boolean predicate holds for all members of
the set, or for at least one, respectively:
enum E { A B }
let s: E set = {|A,B|}
let b1: bool = forall(s, {x|x == A}) // false
let b2: bool = exists(s, {x|x == A}) // true
Here we are using the anonymous function notation {x|e}
to express a predicate
on x
given a boolean expression e
which may contain x
.
Lists
IPL contains support for sets and associated operations.
Given a base type X
, the type of lists of elements of X
is X list
.
List types can be aliased as well:
alias my_string_list: string list
List literals can be expressed via the []
notation:
let x: int list = [1,2,3]
We can test for list membership with the infix operator in
, as with sets:
let x: int list = [1,2,3]
let b: bool = 1 in x // true
We can also construct new lists by adding an element to the head of another list:
let x: int list = [2,3]
let y: int list = add(1, x) // y == [1,2,3]
Lists can be deconstructed with the builtin functions hd
and tl
, which
extract the head and the tails of a list, respectively. The function delete
can be used to remove elements from a given list, by providing the exact element
to remove. Note that all occurrences (if any) of the specified element are
removed.
let l: int list = [1,2,2,3]
let x = hd(l) // x == 1
let l2 = tl(l) // l2 == [2,2,3]
let l3 = delete(2, l) // l3 == [1,3]
Calls to hd
and tl
should be guarded by checks that ensure the input lists
are non-empty. Using hd
and tl
on an empty list will generate a runtime
exception when performing region decomposition on the IPL model.
The builtin function length
returns the length of a list:
let l: int list = [1,2,3]
let x = length(l) // x == 3
We can map elements of a list using the builtin map
operator:
let l1: int list = [1,2,3]
let l2: int list = map(l1, {x|x + 1}) // l2 == [2,3,4]
As with sets, we can use the builtin operators forall
and exists
to test
whether a given boolean predicate holds for all members of the list, or for at
least one, respectively. However, unlike sets, these operators can be applied to
lists ranging over any type.
let l: string list = ["x", "y", "z"]
let b1: bool = forall(l, {x|x == "y"}) // false
let b2: bool = exists(l, {x|x == "y"}) // true
In addition, there are operators forall2
and map2
that work similar to
forall
and map
, but act on two lists simultaneously. In these cases, the
predicate or mapping function is applied pair-wise to elements from both lists.
let l1 = [1,2,3]
let l2 = [4,5,6]
forall2(l1, l2, {x y|x == y}) <==> 1 == 4 && 2 == 5 && 3 == 6
map2(l1, l2, {x y|x + y}) <==> [1 + 4, 2 + 5, 3 + 6]
If the input lists l1
and l2
have different lengths, then calling map2(l1,
l2, f)
produces the same result as map2(l1', l2', f)
where l1'
and l2'
are the longest prefixes of l1, l2
with the same length. However, in the case
of forall2
the returned value on lists of different length is false
regardless of their contents.
Maps
IPL supports maps (AKA dictionaries) to store data in key-value pairs. Given
types K
and V
, then <K, V> map
is the type of maps with keys of type K
and values of type V
. It is often convenient to alias map types:
alias my_map: <int, string> map
Map literals can be expressed as follows:
<0 = "a", 1 = "b" | default: "x">
The expression above creates an int
-to-string
map of type <int, string>
map
, with a default value "x"
that is returned whenever one attempts to index
the map with a key that does not exist. We can access the value stored for a
given key with the builtin get
function, or with the subscript syntax [_]
.
Moreover, for any map, we can retrieve its default value with getDefault
.
let m = <0 = "a", 1 = "b" | default: "x">
let x = get(m, 0) // x == "a"
let y = m[1] // y == "b"
let z = getDefault(m) // z == "x"
let b = get(m, 2) == getDefault(m) // true
We can insert new entries in a map with the insert
function, which takes as
input a map and a key-value pair written as k = v
. Conversely, we can remove
an entry from a map given its key with the remove
function.
let m1 = <| default: "x"> // empty map
let m2 = insert(m1, 0 = "a") // m2 == <0 = "a"| default: "x">
let m3 = insert(m2, 1 = "b") // m3 == <0 = "a", 1 = "b" | default: "x">
let m4 = remove(m3, 0) // m4 == <1 = "b" | default: "x">
Maps also support the higher-order operators forall
, exists
, and map
,
similar to sets and lists. However, in this case they are restricted to maps
with enumeration types for keys. The anonymous functions passed to these
operators bind two variables, for the key and value respectively. In particular,
the mapping function passed as input to map
takes a key-value pair as input
but produces only values as output.
enum E { A B }
let m: <E, string> map = <A = "a", B = "b" | default: "x">
let b1: bool = forall(m, {k v|k == A && v == "a"}) // false
let b2: bool = exists(m, {k v|k == A && v == "a"}) // true
let m2: <E, string> map = map(m, {k v|if x == A then "c" else v}) // m2 == <A = "c", B = "b" | default: "x">
Action and Message Types
It is possible in actions and internal declarations to define elements with type referring to actions or messages. For example if it was needed to keep a track of all of the fills for a particular order one could write:
alias fillIndexMap : <int, Fill option> map
action Fill {
fill_qty : Qty
fill_price : Price
}
internal state {
fills : fillIndexMap = <|default:None>;
fill_index : int = 0;
}
receive(f:Fill){
insert(state.fills,state.fill_index=Some f)
state.fill_index = state.fill_index + 1
}
Equally if an outbound message is to be sent and initiated by an action it is possible to write for example:
declare message m "m" {
a "a" : int
b "b" : int
}
internal state {
assignable {
a:int
}
}
outbound message m {
req a
req b valid when it >10
validate{this.a>this.b}
}
action trigger_m {
msg:m
validate{
valid(m)
}
}
receive(a:action_trigger_m){
assignFrom(a.msg,state)
send a.msg
}