Modelling interest rate swaps in LUSID

Prev Next

You can model certain interest rate swap contracts as instruments of type InterestRateSwap in LUSID. See all supported instruments.

  • Vanilla swaps with a fixed and a floating leg, or two fixed or two floating legs if required.

  • Cross-currency swaps with any combination of fixed/floating legs and notional exchange.

  • Basis swaps with floating legs referencing different indices or tenors.

  • Amortising swaps with any combination of fixed/floating legs and decreasing notionals.

Note there is an accompanying Jupyter notebook that demonstrates many of the operations and concepts in this article.

Note: This article explains how to master an instrument and then load a transaction using the LUSID API. It is possible to simply load a transaction in the LUSID web app and have LUSID master the instrument for you. More information.

Mastering an instrument

There are numerous tools you can use to master an InterestRateSwap in the LUSID Security Master.

Some fields are common to all types of instrument, such as an intuitive name, the requirement to specify a set of identifiers, and the facility to store extra information as properties:


Understanding the economic definition of an interest rate swap

Fields in the economic definition object are specific to an InterestRateSwap.

The composition of this object is dependent on the kind of swap you want to master. Consider the following call to the UpsertInstruments API; note this is actually a no-op request, since the legs array must be populated:

curl -X POST "https://<your-domain>.lusid.com/api/api/instruments?scope=mycustominstrscope"
   -H "Content-Type: application/json-patch+json"
   -H "Authorization: Bearer <your-API-access-token>"
   -d '{
  "upsert_request_1": {
    "name": "IRS-15Jan30",
    "identifiers": {
      "ClientInternal": {"value": "IRS-15Jan30"}
    },
    "definition": {
      "instrumentType": "InterestRateSwap",
      "startDate": "2025-01-15T00:00:00Z",
      "maturityDate": "2030-01-15T00:00:00Z",
      "legs": [],
      "additionalPayments": []
    }
  }
}'

The economic definition of the entire swap in this article is given in the example below, since it is not always clear how API schemas map onto SDK models:

instruments_api = lusid_api_factory.build(lusid.api.InstrumentsApi)
instrument_request = {
    "upsert_request_1": lusid.models.InstrumentDefinition(
        name = "IRS-15Jan30",
        identifiers = {"ClientInternal": lusid.models.InstrumentIdValue(value = "IRS-15Jan30")},
        definition = lusid.models.InterestRateSwap(
            instrumentType = "InterestRateSwap",
            startDate = "2025-01-15T00:00:00Z",
            maturityDate = "2030-01-15T00:00:00Z",
            legs = [
                lusid.models.FixedLeg(
                    instrumentType = "FixedLeg",
                    startDate = "2025-01-15T00:00:00Z",
                    maturityDate = "2030-01-15T00:00:00Z",
                    notional = 1000000,
                    legDefinition = lusid.models.LegDefinition(
                        notionalExchangeType = "None",
                        payReceive = "Receive",
                        rateOrSpread = 0.05,
                        stubType = "None",
                        conventions=lm.FlowConventions(
                            currency = "USD",
                            payment_frequency = "12M",
                            day_count_convention = "Actual365",
                            roll_convention = "15",
                            business_day_convention = "Following",
                            payment_calendars = [],
                            reset_calendars = []
                        )
                    )
                ),
                lusid.models.FloatingLeg(
                    instrumentType = "FloatingLeg",
                    startDate = "2025-01-15T00:00:00Z",
                    maturityDate = "2030-01-15T00:00:00Z",
                    notional = 1000000,
                    legDefinition = lusid.models.LegDefinition(
                        notionalExchangeType = "None",
                        payReceive = "Pay",
                        rateOrSpread = 0,
                        stubType = "None",
                        conventions=lusid.models.FlowConventions(
                            currency = "USD",
                            payment_frequency = "12M",
                            day_count_convention = "Actual365",
                            roll_convention = "15",
                            business_day_convention = "Following",
                            payment_calendars = [],
                            reset_calendars = []
                        ),
                        index_convention = lusid.models.IndexConvention(
                            fixing_reference = "USD-1D-SOFRINDEX",
                            publication_day_lag = 0,
                            payment_tenor = '1D',
                            day_count_convention= 'Actual365',
                            currency = 'USD', 
                            index_name = 'SOFRINDEX'
                        ),
                        resetConvention="InArrears",
                        compounding=lusid.models.Compounding(
                            compoundingMethod="CompoundedIndex",
                            spreadCompoundingMethod="SpreadExclusive",
                            resetFrequency="1D"
                        )
                    )
                )
            ]
        )
    )
}
try:
    instrument_response = instruments_api.upsert_instruments(
        request_body = instrument_request,
        scope = "mycustominstrscope"
    )
except lusid.ApiException as e:
    print(e)

For information on every field, examine the InterestRateSwap schema. Note in particular the following:

  • The instrumentType must be InterestRateSwap.

  • The startDate is typically the transaction date but can be before or after if required.

  • The legs array must contain two legs, one with payReceive set to Pay and the other to Receive. These can both be a fixed leg, both be a floating leg, or one of each. The order does not matter.

  • You can record additionalPayments if you wish but this has no downstream impact at present.

Specifying a fixed leg

Consider the following example of a fixed leg set to receive 5% on a £100,000 notional once a year on the 15th:

{
  "instrumentType": "FixedLeg",
  "startDate": "2025-01-15T00:00:00Z",
  "maturityDate": "2030-01-15T00:00:00Z",
  "notional": 100000,
  "legDefinition": {
    "notionalExchangeType": "None",
    "payReceive": "Receive",
    "rateOrSpread": 0.05,
    "stubType": "None",
    "conventions": {
      "currency": "USD",
      "paymentFrequency": "12M",
      "dayCountConvention": "Actual365",
      "rollConvention": "15",
      "businessDayConvention": "Following",
      "paymentCalendars": [],
      "resetCalendars": []
    }
  }
}

For information on all fields, examine the FixedLeg schema. Note in particular the following:

  • payReceive is set to Receive. One leg must receive payment.

  • For a fixed leg, rateOrSpread refers to the fixed interest rate expressed as a decimal rather than a percentage, so:

    • 10% should be specified as 0.1

    • 2.5% should be specified as 0.02

    • 0.375% should be specified as 0.00375.

  • startDate and maturityDate are typically the same as the instrument itself.

  • notional is set to the full amount and subsequent transactions are for a single unit, but you can reverse this if desired.

  • notionalExchangeType is set to None to not swap notionals, but this could be Initial, Final or Both for a cross-currency swap.

  • conventions specifies all the information necessary to determine payment schedules. In particular, rollConvention specifies the payment day of the month.

Specifying a floating leg

You must first decide which category of reference index the floating leg should observe:

Category

Explanation

Example index

A

The reference rate is observed once per interest rate period. The observation can be made in advance of a period, or towards the end 'in arrears'.

LIBOR

B

The reference rate is observed more than once per period (up to daily for overnight indices), with the implication that the final payment amount can be calculated only at the end of a period

SOFR, SONIA

C

The reference rate is a compounded index. These indices are published daily and they remove the necessity to capture daily fixings needed for a calculation. The compounding indices are built from daily fixings so the final payment amount can only be calculated at the end of a period.

SOFRINDEX

Consider the following example of a floating leg observing the SOFRINDEX compounded index that pays once a year on the 15th:

{
  "instrumentType": "FloatingLeg",
  "startDate": "2025-01-15T00:00:00Z",
  "maturityDate": "2030-01-15T00:00:00Z",
  "notional": 100000,
  "legDefinition": {
    "notionalExchangeType": "None",
    "payReceive": "Pay",
    "rateOrSpread": 0,
    "stubType": "None",
    "conventions": {
      "currency": "USD",
      "paymentFrequency": "12M",
      "dayCountConvention": "Actual365",
      "rollConvention": "15",
      "businessDayConvention": "Following",
      "paymentCalendars": [],
      "resetCalendars": []
    },
    "indexConvention": {
      "fixingReference": "USD-1D-SOFRINDEX",
      "publicationDayLag": 0,
      "paymentTenor": "1D",
      "dayCountConvention": "Actual365",
      "currency": "USD",
      "indexName": "SOFRINDEX"
    },
    "resetConvention": "InArrears",
    "compounding": {
      "compoundingMethod": "CompoundedIndex",
      "spreadCompoundingMethod": "SpreadExclusive",
      "resetFrequency": "1D"
    }
  }
}

For information on all fields, examine the FloatingLeg schema. Some fields are common to all swaps; note in particular the following:

  • payReceive is set to Pay. One leg in a swap must pay out.

  • For a floating leg, rateOrSpread refers to a spread on the index, if any. For example, a spread of 50bps should be specified as 0.005.

  • For a vanilla swap, notional, notionalExchangeType, stubType and conventions are typically the same as the fixed leg, but can differ if required (ie. for a cross-currency swap).

  • indexConventions specifies all the information necessary to calculate accrued interest amounts from observed rates in a reference index.

Other fields in a floating leg definition are dependent on the category of the reference index chosen:

LegDefinition field

Category A

Category B

Category C

resetConvention

Can be InAdvance (the default) or InArrears.

Must be InArrears.

Must be InArrears.

indexConvention.paymentTenor

Can be any tenor.

Must be 1D.

Must be 1D.

compounding.compoundingMethod (more information)

Do not set.

Can be Averaging or Compounding.

Must be CompoundedIndex.

compounding.spreadCompoundingMethod

Can be Straight, Flat or SpreadExclusive for Compounding. Do not set for Averaging.

Must be SpreadExclusive.

compounding.averagingMethod

Can be Weighted (the default) or Unweighted for Averaging. Do not set for Compounding.

Do not set.

compounding.resetFrequency

Can be any tenor.

Must be 1D.

compounding.calculationShiftMethod

Can be Lookback, NoShift, ObservationPeriodShift or Lockout.

Can be NoShiftor ObservationPeriodShift.

Examining the response

Providing the request is successful, the response:

  • Confirms the globally-unique LUID for the instrument;

  • Provides version information;

  • Generates extra fields that are stored as part of the instrument definition and can be filtered on;

  • Supplies default values for fields not explicitly specified in the request:

{
  "values": {
    "upsert_request_1": {
      "scope": "mycustominstrumentscope",
      "lusidInstrumentId": "LUID_00003H0L",
      "version": {
        "effectiveFrom": "0001-01-01T00:00:00.0000000+00:00",
        "asAtDate": "2026-05-13T11:17:30.4381300+00:00",
        "asAtCreated": "2026-05-13T11:17:30.4381300+00:00",
        "userIdCreated": "00u91lo2d7X42sdse2p7",
        "requestIdCreated": "2026051311-1544deac41e442849923be5091f8a00d",
        "reasonCreated": "",
        "asAtModified": "2026-05-13T11:17:30.4381300+00:00",
        "userIdModified": "00u91lo2d7X42sdse2p7",
        "requestIdModified": "2026051311-1544deac41e442849923be5091f8a00d",
        "reasonModified": "",
        "asAtVersionNumber": 1,
        "entityUniqueId": "60a46c85-58cb-40cc-9840-affb724dc72d"
      },
      "name": "IRS-15Jan30",
      "identifiers": {
        "ClientInternal": "IRS-15Jan30",
        "LusidInstrumentId": "LUID_00003H0L"
      },
      "properties": [],
      "instrumentDefinition": {
        "startDate": "2025-01-15T00:00:00.0000000+00:00",
        "maturityDate": "2030-01-15T00:00:00.0000000+00:00",
        "isNonDeliverable": false,
        "legs": [
          {
            "startDate": "2025-01-15T00:00:00.0000000+00:00",
            "maturityDate": "2030-01-15T00:00:00.0000000+00:00",
            "legDefinition": {
              "conventions": {
                "currency": "USD",
                "paymentFrequency": "12M",
                "dayCountConvention": "Actual365",
                "rollConvention": "15",
                "paymentCalendars": [],
                "resetCalendars": [],
                "settleDays": 0,
                "resetDays": 0,
                "leapDaysIncluded": true,
                "accrualDateAdjustment": "Adjusted",
                "businessDayConvention": "F",
                "accrualDayCountConvention": "Actual365"
              },
              "notionalExchangeType": "None",
              "payReceive": "Receive",
              "rateOrSpread": 0.05,
              "resetConvention": "InAdvance",
              "stubType": "None",
              "firstCouponType": "ProRata",
              "lastCouponType": "ProRata",
              "intermediateNotionalExchange": false
            },
            "notional": 100000,
            "overrides": {},
            "instrumentType": "FixedLeg"
          },
          {
            "startDate": "2025-01-15T00:00:00.0000000+00:00",
            "maturityDate": "2030-01-15T00:00:00.0000000+00:00",
            "legDefinition": {
              "conventions": {
                "currency": "USD",
                "paymentFrequency": "12M",
                "dayCountConvention": "Actual365",
                "rollConvention": "15",
                "paymentCalendars": [],
                "resetCalendars": [],
                "settleDays": 0,
                "resetDays": 0,
                "leapDaysIncluded": true,
                "accrualDateAdjustment": "Adjusted",
                "businessDayConvention": "F",
                "accrualDayCountConvention": "Actual365"
              },
              "indexConvention": {
                "fixingReference": "USD-1D-SOFRINDEX",
                "publicationDayLag": 0,
                "paymentTenor": "1D",
                "dayCountConvention": "Actual365",
                "currency": "USD",
                "indexName": "SOFRINDEX"
              },
              "notionalExchangeType": "None",
              "payReceive": "Pay",
              "rateOrSpread": 0,
              "resetConvention": "InArrears",
              "stubType": "None",
              "compounding": {
                "calculationShiftMethod": "NoShift",
                "compoundingMethod": "CompoundedIndex",
                "resetFrequency": "1D",
                "shift": 0,
                "spreadCompoundingMethod": "SpreadExclusive"
              },
              "firstCouponType": "ProRata",
              "lastCouponType": "ProRata",
              "intermediateNotionalExchange": false
            },
            "notional": 100000,
            "overrides": {},
            "instrumentType": "FloatingLeg"
          }
        ],
        "additionalPayments": [],
        "instrumentType": "InterestRateSwap"
      },
      "state": "Active",
      "assetClass": "InterestRates",
      "domCcy": "USD",
      "relationships": [],
      "dataModelMembership": {
        "membership": []
      }
    }
  },
  "staged": {},
  "failed": {},
  ... 
}

Providing interest rate fixings

If an InterestRateSwap has a floating leg, you must load the correct number of fixings into the Quote Store and provide a recipe that enables LUSID to locate them when an accrued interest calculation is required (which is every time you ask LUSID to value a holding or generate cashflows):

Category

Number of fixings to load

A

One per interest rate period. This should be at the start if resetConvention is set to InAdvance, or at the end if InArrears.

B

One per day.

C

Two per calculation period; one at the start of the interest rate period and one at the effective datetime of the valuation or cashflow generation request. Note this means if you make such a request every day you will need to load a fixing every day.

Consider the example of a Category C swap with the following fixingReference:

"indexConvention": {
  "fixingReference": "USD-1D-SOFRINDEX",
  ...
}

The following call to the UpsertQuotes API loads two fixings for this index into a particular quote scope:

curl -X POST "https://<your-domain>.lusid.com/api/api/quotes/MySOFRFixings"
  -H "Authorization: Bearer <your-API-access-token>"
  -H "Content-Type: application/json-patch+json"
  -d '{
    "Quote-0001": {
      "quoteId": {
        "quoteSeriesId": {
          "provider": "Lusid",
          "instrumentIdType": "RIC",
          "instrumentId": "USD-1D-SOFRINDEX",
          "quoteType": "Index",
          "field": "mid"
        },
        "effectiveAt": "2025-01-15"
      },
      "metricValue": {
        "value": 1.17692687, "unit": "none"
      },
      "scaleFactor": 100
    },
    "Quote-0002": {
      "quoteId": {
        "quoteSeriesId": {
          "provider": "Lusid",
          "instrumentIdType": "RIC",
          "instrumentId": "USD-1D-SOFRINDEX",
          "quoteType": "Index",
          "field": "mid"
        },
        "effectiveAt": "2026-01-15"
      },
      "metricValue": {
        "value": 1.22834029, "unit": "none"
      },
      "scaleFactor": 100
    }
  }'

For general information on loading fixings into the Quote Store, see this article. Note the following about this example:

  • Both fixings are loaded into a MySOFRFixings quote scope (specified in the URL) that is only used for fixings, to avoid clashes.

  • The instrumentIdType of each fixing must be  RIC or ClientInternal.

  • The instrumentId must be the fixingReference specified in the index convention, in this case USD-1D-SOFRINDEX.

  • The provider is set to Lusid and the field to mid, to avoid validation errors.

  • The quoteType must be either Index or Rate.

  • The scaleFactor is set to 100 to scale the rate down but you could omit this and specify metricValue.value as a decimal rather than a percentage, for example 0.0122834029.

Creating a suitable recipe

Your recipe must have a market data rule able to locate these fixings, for example:

"market": {
  "marketRules": [
    {
      "key": "Quote.RIC.USD-1D-SOFRINDEX",
      "dataScope": "MySOFRFixings",
      "supplier": "Lusid",
      "quoteType": "Index",
      "field": "mid"
    },
    ...
  ]
},

For general information on recipes, start with this article. Note the following:

  • The key should be constructed as follows: Quote.<instrumentIdType>.<instrumentId>.

  • The dataScope must match the quote scope into which fixings were loaded.

  • The other fields must match their respective quote fields exactly (values are case-sensitive).

Registering the recipe with portfolios

You must register the recipe with every portfolio in which you intend to hold an InterestRateSwap. See how to do this.

You can use the same recipe for valuation operations if you wish.

Booking a transaction to establish a position

Once an InterestRateSwap instrument is mastered, you can book a transaction in a particular portfolio, for example using the BatchUpsertTransactions API:

curl -X POST 'https://mydomain.lusid.com/api/api/transactionportfolios/MyPortfolioScope/MyPortfolioCode/transactions/$batchUpsert?successMode=Partial&preserveProperties=true'
  -H 'Content-Type: application/json-patch+json'
  -H 'Authorization: Bearer myAPIAccessToken'
  -d '{
  "transactionRequest-1": {
    "transactionId": "Txn01",
    "type": "EnterIRS",
    "instrumentIdentifiers": {"Instrument/default/ClientInternal": "IRS-15Jan30"},
    "transactionDate": "2025-01-15T00:00:00.0000000+00:00",
    "settlementDate": "2025-01-15T00:00:00.0000000+00:00",
    "units": 1,
    "transactionPrice": {
      "price": 0,
      "type": "Price"
    },
    "totalConsideration": {
      "amount": 0,
      "currency": "GBP"
    }
  }
}'

Note the following about this example, which demonstrates an OTC contract with no broker or exchange fees:

  • The type field invokes a custom EnterIRS transaction type to enter into a position without a cost (see below).

  • The transactionDate and settlementDate match the startDate in the instrument definition, but can be before or after if required.

  • units is 1 because notional is recorded in the instrument definition. You can reverse this by setting notional to 1 and recording the number of units in the transaction if you wish.

  • The transactionPrice.price and totalConsideration.amount (cost) are set to 0 to infer that this contract has no current market value, but you can specify a clean or dirty price if buying or selling a live swap.

Note: This example assumes the transaction, settlement and portfolio currencies are all the same. If not, you can specify exchange rates.

We might create a custom EnterIRS transaction type as follows:

curl -X PUT 'https://<your-domain>.lusid.com/api/api/transactionconfiguration/types/default/EnterIRS?scope=default'
  -H 'Content-Type: application/json-patch+json'
  -H 'Authorization: Bearer <your-API-access-token>'
  -d '{
  "aliases": [
    {
      "type": "EnterIRS",
      "description": "Transaction type for entering into an IRS",
      "transactionClass": "Basic",
      "transactionRoles": "AllRoles",
      "isDefault": false
    }
  ],
  "movements": [
    {
      "name": "Increase units of security",
      "movementTypes": "StockMovement",
      "side": "Side1",
      "direction": 1
    }
  ]
}'

Confirming positions

We can generate a holdings report on the transaction date to see that we hold one unit at no cost:

Auditing LUSID’s automatically-generated transactions

We can examine the output transaction that generates this holding:

Valuing your position

To value a position in an InterestRateSwap in a portfolio, work through our valuation checklist.

Note: You can override LUSID’s calculation of cashflows or PV by loading pre-determined structured result data. For more information, see this article and this Jupyter Notebook.

Available pricing model (see how to change)

Market data required

Notes

SimpleStatic (default)

  1. Fixings for floating leg in Quote Store

  2. Market price for instrument in Quote Store

Cannot split by legs.

Discounting

  1. Fixings for floating leg in Quote Store

  2. Discount curve in CMD Store

  3. Interest rate projection curve in CMD Store

Can split by legs if produceSeparateResultForLinearOtcLegs=True in recipe.

ConstantTimeValueOfMoney

Fixings for floating leg in Quote Store

Note PV is simply a sum of projected cashflows. Can split by legs if produceSeparateResultForLinearOtcLegs=True in recipe.

For example, to value the portfolio above on 13 May 2026 using SimpleStatic:

  1. Make sure fixings for a floating leg are loaded into the Quote Store.

  2. Load market prices for the valuation date as either CleanPrice or DirtyPrice into a particular quote scope in the Quote Store, for example:

    curl -X POST "https://<your-domain>.lusid.com/api/api/quotes/MyIRSPrices"
      -H "Authorization: Bearer <your-API-access-token>"
      -H "Content-Type: application/json-patch+json"
      -d '{
        "Quote-0001": {
          "quoteId": {
            "quoteSeriesId": {
              "provider": "Lusid",
              "instrumentIdType": "ClientInternal",
              "instrumentId": "IRS-15Jan30",
              "quoteType": "DirtyPrice",
              "field": "mid"
            },
            "effectiveAt": "2026-05-13"
          },
          "metricValue": {
            "value": 0.09787, "unit": "USD"
          }
        },
      }'
  3. Make sure the recipe you use has a market data rule able to locate these market prices, for example:

    "market": {
      "marketRules": [
        {
          "key": "Quote.ClientInternal.*",
          "dataScope": "MyIRSPrices",
          "supplier": "Lusid",
          "quoteType": "DirtyPrice",
          "field": "mid"
        },
        ...
      ]
    },
  4. Generate a valuation report with appropriate metrics, for example:

Handling the lifecycle of the instrument

An InterestRateSwap is tightly integrated into LUSID’s instrument event framework. To enable this for your domain, you must:

  1. Register a recipe with every portfolio holding an InterestRateSwap.

  2. Create transaction types to determine the economic impact of the transactions automatically generated by InterestRateSwap events.

For much more information, see how to handle instrument events for interest rate swaps.

Instrument event

Event emission criteria

If emitted, effect of LUSID default transaction template

SwapCashFlowEvent

This event is automatically emitted by LUSID for each leg on each payment date to exchange cashflows. Note market data is required for a floating leg.

One transaction for a cash amount is automatically generated for each leg.

SwapPrincipalEvent

This event is automatically emitted by LUSID for each leg where notionalExchangeType is not None in the instrument definition, to exchange notionals.

One transaction for a cash amount is automatically generated for each leg.

MaturityEvent

This event is automatically emitted by LUSID on the maturity date to zero the holding.

One transaction for all units at zero cost is automatically generated.

Appendix: Using the LUSID web app to simplify setup

A dashboard is available in the LUSID web app that you can use to both book a trade and simultaneously master an InterestRateSwap instrument, which may be a simpler user experience. To enable this:

  1. Contact Technical Support for the required license.

  2. In the transaction type you use to enter into a swap, add the TransactionConfiguration/default/UIClass system property and set the value to PMS - IRS.

  3. Navigate to Portfolio Management > Transactions

  4. Click the Create transaction button and open the IRS tab.