Handling FxForward settlement and maturity instrument lifecycle events

In this tutorial we'll examine instrument lifecycle events by using LUSID to perform the following task:

“I want to automate the process of handling lifecycle events emitted by LUSID for FxForward instruments in order to reduce friction and improve efficiency.”

We'll see how to:

  • Master a FxForward contract as an instrument in the LUSID Security Master.

  • Establish a position in a portfolio by purchasing a single unit of the contract.

  • Examine the events LUSID emits at each milestone in the instrument's lifecycle.

  • Configure LUSID to automatically exchange the two cashflows at maturity and terminate the contract.

A Jupyter Notebook containing code examples is coming soon.

Mastering a FxForward instrument in LUSID and establishing a position

Imagine on 1 January 2024 we want to enter into a contract to pay/sell 900 EUR and buy/receive 1000 USD in 5 months' time. We can model this FxForward in LUSID as an instrument of type FxForward with the following economic definition:

{
  "instrumentType": "FxForward",
  "startDate": "2024-01-01T00:00:00.0000000+00:00",
  "maturityDate": "2024-06-01T00:00:00.0000000+00:00",
  "domCcy": "USD",
  "domAmount": 1000,
  "fgnCcy": "EUR",
  "fgnAmount": -900,
  "isNdf": false
}

Note the following:

  • The startDate is 1 January and maturityDate is 1 June 2024, signifying a five month contract.

  • The domCcy must be the currency in which we want our holding in this instrument to be valued. It could be EUR, but we have chosen USD.

  • The domAmount is the amount of domCcy. It could be negative, but we have chosen positive since we are buying/receiving USD.

  • The fgnCcy is the other currency, in this case EUR. Note our holding will not be valued in this currency.

  • The fgnAmount is the amount of fgnCcy. It must have the opposite sign to domAmount, so in this example negative since domAmount is positive.

  • isNdf is false (the default) to signify this is a deliverable contract. Note instrument events have different behavior for NDFs (not examined in this tutorial).

We can now purchase a single unit of this contract in a portfolio that happens to be denominated in a third currency, GBP:

[
  {
    "transactionId": "Txn-00001",
    "type": "StockIn",
    "instrumentIdentifiers": {"Instrument/default/ClientInternal": "FxForward-EURUSD-5M"},
    "transactionDate": "2024-01-01T00:00:00.0000000+00:00",
    "settlementDate": "2024-01-01T00:00:00.0000000+00:00",
    "units": 1,
    "transactionCurrency": "USD",
    "totalConsideration": {"currency": "USD"},
    "properties": {
      "Transaction/default/TradeToPortfolioRate": {
        "key": "Transaction/default/TradeToPortfolioRate",
        "value": {
          "metricValue": {
            "value": 0.79
          }
        }
      }
    }
  }
]

Note the following:

  • The type is StockIn since there is no cash outlay but it could be Buy or some other long transaction type.

  • The transactionDate and settlementDate both mark the start of the contract, 1 January 2024. The instrument defines the length.

  • The number of units purchased is 1.

  • The transactionPrice field is deliberately omitted, so LUSID sets the price to 0.

  • The transactionCurrency and totalConsideration.currency (settlement currency) is the domCcy of the instrument, so in this case USD.

  • The totalConsideration.amount field is deliberately omitted, so LUSID sets the cost to 0.

  • The Transaction/default/TradeToPortfolioRate system property records the spot rate on 1 January 2024 from transaction (USD) to portfolio (GBP) currency, in this example 0.79. Note we have chosen to hard-code this rate but you can configure LUSID to look it up dynamically in the Quote Store. See how to do this.

If we examine our holdings in this portfolio on 1 January we see we have a settled position in the FxForward instrument for zero cost:

Understanding instrument events emitted by LUSID

When the instrument matures on 1 June 2024, LUSID automatically emits:

  • A FxForwardSettlementEvent. This event is specific to FxForwards. The intention is that you handle this event to exchange the two cashflows.

  • A MaturityEvent. This event is generic to several instrument types. The intention is that you handle this event to set your position in the instrument to zero, so it drops out of holding reports.

LUSID provides a default transaction template for every type of event. In this tutorial we'll choose to use the default transaction templates for both, but you can create your own custom transaction templates to configure the process if you wish.

Examining the default transaction template for settlement events

We can call the GetTransactionTemplate API to examine the default template for FxForwardSettlementEvent, which at the time of writing is as follows:

{
 "instrumentType": "FxForward",
 "instrumentEventType": "FxForwardSettlementEvent",
 "description": "LUSID default template for automatically generated transactions in respect of FX Forward Settlement events.",
 "scope": "default",
 "componentTransactions": [
   {
     "displayName": "Delivery of domestic amount",
     "condition": "{{FxForwardSettlementEvent.isNdf}} eq false",
     "transactionFieldMap": {
       "transactionId": "{{instrumentEventId}}-{{holdingId}}-dom",
       "type": "FxForwardDomPrincipal",
       "source": "default",
       "instrument": "{{instrument}}",
       "transactionDate": "{{FxForwardSettlementEvent.maturityDate}}",
       "settlementDate": "{{FxForwardSettlementEvent.maturityDate}}",
       "units": "{{eligibleBalance}}",
       "transactionCurrency": "{{FxForwardSettlementEvent.domCcy}}",
       "exchangeRate": "1",
       "totalConsideration": {
         "currency": "{{FxForwardSettlementEvent.domCcy}}",
         "amount": "{{FxForwardSettlementEvent.domTotalAmount}}"
       }
     },
     "transactionPropertyMap": []
   },
   {
     "displayName": "Delivery of foreign amount",
     "condition": "{{FxForwardSettlementEvent.isNdf}} eq false",
     "transactionFieldMap": {
       "transactionId": "{{instrumentEventId}}-{{holdingId}}-fgn",
       "type": "FxForwardFgnPrincipal",
       "source": "default",
       "instrument": "{{instrument}}",
       "transactionDate": "{{FxForwardSettlementEvent.maturityDate}}",
       "settlementDate": "{{FxForwardSettlementEvent.maturityDate}}",
       "units": "{{eligibleBalance}}",
       "transactionCurrency": "{{FxForwardSettlementEvent.domCcy}}",
       "totalConsideration": {
         "currency": "{{FxForwardSettlementEvent.fgnCcy}}",
         "amount": "{{FxForwardSettlementEvent.fgnTotalAmount}}"
       }
     },
     "transactionPropertyMap": []
   },
   {
     "displayName": "Non-deliverable forward cash settlement",
     "condition": "{{FxForwardSettlementEvent.isNdf}} eq true",
     "transactionFieldMap": {
       "transactionId": "{{instrumentEventId}}-{{holdingId}}-cash",
       "type": "NdfSettlement",
       "source": "default",
       "instrument": "{{instrument}}",
       "transactionDate": "{{FxForwardSettlementEvent.fixingDate}}",
       "settlementDate": "{{FxForwardSettlementEvent.maturityDate}}",
       "units": "{{eligibleBalance}}",
       "transactionPrice": {
         "price": "{{FxForwardSettlementEvent.cashFlowPerUnit}}",
         "type": "CashFlowPerUnit"
       },
       "transactionCurrency": "{{FxForwardSettlementEvent.settlementCcy}}",
       "exchangeRate": "1",
       "totalConsideration": {
         "currency": "{{FxForwardSettlementEvent.settlementCcy}}",
         "amount": "{{FxForwardSettlementEvent.cashFlowAmount}}"
       }
     },
     "transactionPropertyMap": []
   }
 ],
 ...
}

Note the following:

  • This template handles instrument events of type FxForwardSettlementEvent emitted by instruments of type FxForward.

  • It is domiciled in the default (that is, system) transaction template scope.

  • It contains instructions for generating three output transactions in every portfolio with a holding in the instrument. We can examine the condition field to see that the first two transactions are for a deliverable FxForward and the third one (at the bottom) is for an NDF.

  • The first transaction settles the domestic leg of a deliverable FxForward (USD in our example). It mandates a transaction type of FxForwardDomPrincipal (grouped in a source of default) and sets both the transaction and settlement currency to domCcy, so the exchangeRate between them is 1.

  • The second transaction settles the foreign leg (EUR in our example). It mandates a transaction type of FxForwardFgnPrincipal (also grouped in a source of default) and sets the settlement currency to fgnCcy (EUR) but the transaction currency to domCcy (USD). Note the exchangeRate field is omitted, so LUSID must be configured to look up the USD/EUR spot rate on the maturity date dynamically in the Quote Store. See how to do this.

Creating a suitable transaction type for settlement event output transactions

The default transaction template mandates FxForwardDomPrincipal and FxForwardFgnPrincipal transaction types grouped in the default transaction type source. We must create these transaction types if they do not exist.

Note: Transaction types are grouped in sources and reside in scopes. For more information on the difference, see this article.

The definition of FxForwardDomPrincipal and FxForwardFgnPrincipal can actually be the same, so we can call the SetTransactionType API to create a single transaction type with two aliases, for example:

curl -X PUT 'https://<your-domain>.lusid.com/api/api/transactionconfiguration/types/default/FxForwardDomPrincipal?scope=default'
  -H 'Content-Type: application/json-patch+json'
  -H 'Authorization: Bearer <your-API-access-token>'
  -d '{
 "aliases": [
   {
     "type": "FxForwardDomPrincipal",
     "description": "Settle the domestic leg of a FxForwardSettlementEvent",
     "transactionClass": "CashFlows",
     "transactionRoles": "AllRoles",
     "isDefault": false
   },
   {
     "type": "FxForwardFgnPrincipal",
     "description": "Settle the foreign leg of a FxForwardSettlementEvent",
     "transactionClass": "CashFlows",
     "transactionRoles": "AllRoles",
     "isDefault": false
   }
 ],
 "movements": [
   {
     "name": "Virtually sell the individual legs to calculate realised gain/loss",
     "movementTypes": "StockMovement",
     "side": "SignedSide1",
     "direction": -1,
     "movementOptions": ["Virtual"]
   },
   {
     "name": "Manifest the cash holdings",
     "movementTypes": "CashCommitment",
     "side": "Side2",
     "direction": 1,
   }
 ],
 "calculations": [
   {
     "type": "Txn:TradeToPortfolioRate"
   },
   {
     "type": "Txn:ExchangeRate"
   }
 ]
}'

Note the following:

  • The first movement uses the Virtual option to calculate realised gain/loss for each leg without actually effecting a sale. Note the direction is negative and a custom SignedSide1 is used (see below).

  • The second movement creates currency holdings using the built-in Side2. Note the direction is positive, but the side respects the sign of each leg's total consideration, so the USD holding ends up positive and the EUR holding ends up negative.

  • The calculation types configure LUSID to look up exchange rates from transaction to portfolio and from transaction to settlement currencies respectively in the Quote Store, which is required for the maturity date. See how to set this up.

We can call the SetSideDefinition API to create the custom SignedSide1 (note this step must be performed before creating the transaction type). The only difference to the built-in Side1 is the amount field:

  • In Side1 the value is Txn:TradeAmount, which is a calculated field that converts the sign of the amount to match that of the units.

  • In SignedSide1 the value is Txn:TotalConsiderationInTradeCurrency, which is the same except it respects the sign of the amount; this is important because the total consideration of one leg (EUR in our example) is negative.

For example:

curl -X PUT 'https://<your-domain>.lusid.com/api/api/transactionconfiguration/sides/SignedSide1?scope=default'
  -H 'Content-Type: application/json-patch+json'
  -H 'Authorization: Bearer <your-API-access-token>'
  -d '{
  "security": "Txn:LusidInstrumentId",
  "currency": "Txn:TradeCurrency",
  "rate": "Txn:TradeToPortfolioRate",
  "units": "Txn:Units",
  "amount": "Txn:TotalConsiderationInTradeCurrency"
}'

Examining the default transaction template for maturity events

In the same way, we can call the GetTransactionTemplate API to examine the default transaction template for MaturityEvent, which at the time of writing is as follows. Note the price and cost are set to zero:

{
  "instrumentType": "FxForward",
  "instrumentEventType": "MaturityEvent",
  "description": "LUSID default template for automatically generated transactions in respect of instrument maturity events.",
  "scope": "default",
  "componentTransactions": [
    {
      "displayName": "Maturity",
      "transactionFieldMap": {
        "transactionId": "{{instrumentEventId}}-{{holdingId}}",
        "type": "Maturity",
        "source": "default",
        "instrument": "{{instrument}}",
        "transactionDate": "{{MaturityEvent.maturityDate}}",
        "settlementDate": "{{MaturityEvent.maturityDate}}",
        "units": "{{eligibleBalance}}",
        "transactionPrice": {
          "price": "0",
          "type": "Price"
        },
        "transactionCurrency": "{{holdingCurrency}}",
        "exchangeRate": "0",
        "totalConsideration": {
          "currency": "{{holdingCurrency}}",
          "amount": "0"
        }
      },
      "transactionPropertyMap": []
    }
  ],
  ...
}

Creating a suitable transaction type for maturity event output transactions

The default transaction template mandates a Maturity transaction type grouped in the default transaction type source. We must call the SetTransactionType API to create this transaction type if it does not exist, for example:

curl -X PUT 'https://<your-domain>.lusid.com/api/api/transactionconfiguration/types/default/Maturity?scope=default'
  -H 'Content-Type: application/json-patch+json'
  -H 'Authorization: Bearer <your-API-access-token>'
  -d '{
 "aliases": [
   {
     "type": "Maturity",
     "description": "Generic transaction type for maturity events for many different instrument types",
     "transactionClass": "Basic",
     "transactionRoles": "AllRoles",
     "isDefault": false
   }
 ],
 "movements": [
   {
     "name": "Reduce holding to zero",
     "movementTypes": "StockMovement",
     "side": "Side1",
     "direction": -1
   }
 ]
}'

Loading spot rates for the maturity date into the LUSID Quote Store

In order that LUSID may calculate realised gains/losses for the output transactions generated by FxForwardSettlementEvent, we must load the following exchange rates into the LUSID Quote Store for the maturity date, 1 June 2024:

  • Rate from the domCcy of the instrument (USD in our example) to the fgnCcy (EUR). Let's imagine the USD/EUR rate has moved in our favour from the implied strike rate of 0.90 in the instrument definition to 0.93.

  • Rate from the domCcy (USD) to the portfolio currency (GBP). Let's imagine the USD/GBP rate has slipped from 0.79 (hard-coded in the purchase transaction) to 0.75.

We can call the UpsertQuotes API to load these exchange rates into the Quote Store. See an example.

In order for LUSID to locate and apply these exchange rates, we must also:

  1. Apply the Txn:TradeToPortfolioRate and Txn:ExchangeRate calculation types to the FxForwardDomPrincipal and FxForwardFgnPrincipal transaction types, as demonstrated above.

  2. Create a new (or configure an existing) recipe to have market data rules able to locate the rates loaded into the Quote Store. See an example.

  3. Register this recipe with the portfolio(s) holding the FxForward. See how to patch an existing portfolio.

Examining the impact of instrument lifecycle events on the portfolio at maturity date

Calling the GetHoldings API on 1 June 2024 reveals that the FxForwardSettlementEvent has generated two cash holdings in the portfolio representing the exchange of the two currencies, one for 1000 USD and one for -900 EUR:

Note the following:

  • We no longer have a holding in the FxForward instrument itself since the number of units was set to zero by the MaturityEvent.

  • The portfolio cost of the USD holding reflects the cost of ‘purchasing’ 1000 USD at the USD/GBP (trade to portfolio rate) of 0.75 on 1 June 2024, namely £750.

  • The portfolio cost of the EUR holding reflects the same original cost minus the gain we have made from entering into the forward contract (relative to not having done so): £750 - £24.19 = £725.81.

We can call the BuildTransactions API with a suitable window to see the three output transactions that have contributed to these positions, and examine the gain/loss calculations in more detail. The first two transactions are generated by FxForwardSettlementEvent and the third by MaturityEvent:

To see the realised gain/loss calculations, click the +/- button at the end of each row (highlighted in red above). For the first transaction settling the domestic (USD) leg, we can see we made a loss of -£40 because the USD/GBP exchange rate slipped from 0.79 on 1 January to 0.75 on 1 June:

However, for the foreign (EUR) leg we see we made a gain of £64.19 because the USD/EUR exchange rate improved from 0.9 (implied) on 1 January to 0.93 on 1 June. Our total realised gain for the instrument as a whole is therefore £64.19 - £40 = £24.19, or $32.26 at the USD/GBP exchange rate of 0.75:

We can see this more clearly by creating an A2B report for the period 31 December 2023 11:59:59 to 1 June 2024 (note setting the start date to just before the initial purchase of the FxForward contract prevents LUSID calculating a MarketValue (Start), which is not useful in this situation):