Advanced Features
We now have a full model which receives messages and actions, including custom fields, enum cases, and custom repeating group fields. In this last section we discuss more advanced functionality available.
Ambiguous optionality
It is possible to import a field in the assignable
section of an internal state declaration which is declared both optionally and non-optionally in the model.
Mismatched optionality imported message fields
For example let us import the field ExecInst
in NewOrderSingle
:
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
opt ExecInst
}
and let us declare the field in the ExecutionReport
as required, as we want to it to always be present in any outgoing message:
outbound message ExecutionReport {
req OrderID
req ExecID
req ExecType
req OrdStatus
req Side
req OrderQtyData.OrderQty
req LeavesQty
req CumQty
opt Text
req Parties
req ExecInst
}
Adding ambiguous optionality fields to the state
Now if we add the ExecInst
field to the state we need to use ambiguous notation for the type to denote that the field is both optional and non-optional. We do this using :*
notation - such fields require stated initial values to be non-optional:
internal state {
assignable{
Side : Side
Price :? Price
OrderQtyData.OrderQty : Qty
OrdStatus : OrdStatus = OrdStatus.PendingNew;
OrdType : OrdType
LeavesQty : Qty
CumQty : Qty
SpreadProportion :? float
Parties : Parties
ExecInst :* ExecInst
OrderID : string
ExecType : ExecType = ExecType.PendingNew;
}
live_order : bool = false;
AvgPx : float
bestBid: Price
bestAsk: Price
}
Declaring default values
In the situation above we must provide a default value for an optional field in an incoming message which corresponds to an assignable field - this is so that a value can be provided for a required value in an outgoing message. Failure to do see will result in the error
This message require needs a default value as it is optional but used to assign an ambiguous state value
For NewOrderSingle
we thus write:
opt ExecInst default = Some Held
to provide such a value.
Verification Packs
A verification pack is something you might want to prove about your model. An example would be that any transition of the OrdStatus
fields of the state is valid - for example that once an order is cancelled, it does not become reactivated. There are a set of such verification goals pre-written which users can activate for analysis by imandra. The existing set of such goals is
EmptyString
- checks that no outbound string values are the empty string - which is not allowed in the FIX specs;Venue
- checks that any transition ofOrdStatus
field of the state is valid. The way to specify a verfication pack is
VerificationPacks[EmptyString,Venue]
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 'Advanced Tutorial Model'
//
//
// For further info see https://docs.imandra.ai
//
//
import FIX_4_4
@title: "Advanced Tutorial Model"
messageFlows {
NewOrderFill {
name "NewOrderFill"
description "Initialise Book State, send NewOrderSingle Message then receive a fill action"
template [
bookState,
NewOrderSingle,
fill
]
}
}
VerificationPacks[EmptyString,Venue]
precision(Price,2)
internal state {
assignable{
Side : Side
Price :? Price
OrderQtyData.OrderQty : Qty
OrdStatus : OrdStatus =
OrdStatus.PendingNew;
OrdType : OrdType
LeavesQty : Qty
CumQty : Qty
SpreadProportion :? float
Parties : Parties
ExecInst : ExecInst
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 {
opt ClOrdID valid when it != (Some "N/A")
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
opt Account
opt ExecInst default = {|ExecInst.Held, ExecInst.AllOrNone|}
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
req ExecInst
}
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()
state.ExecType = ExecType.New
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.