By default, a recipe is a static object. If you have a recipe (which is a JSON document) and copy its settings to form the basis of a new recipe, the two diverge from that moment on; neither inherits changes made to the other.

Required reading: What is a recipe?

You are always likely to require a library of static recipes. But you can make quick and convenient changes by composing a recipe from a standard (or base) one on-the-fly. A ‘composed recipe’ is dynamic in the sense that it inherits changes made to the base recipe (or recipes). You can use a composed recipe anywhere a standard recipe can be used.

For example, you might want to take:

  • Example 1:  A base recipe for valuing a portfolio of equities and compose a recipe with a market data rule prepended to prefer Bloomberg prices over Refinitiv prices.

  • Example 2:  A base recipe for valuing a portfolio of equities and compose a recipe with a quote interval added to every market data rule to look back further in time for pricing data.

  • Example 3:  A base recipe for valuing a portfolio of FxForwards and compose a recipe with the produceSeparateResultForLinearOtcLegs option enabled in order to value legs separately.

  • Example 4:  Base recipes for valuing separate portfolios of equities, bonds and FX options and compose a recipe able to value a portfolio containing all three.

Understanding how composition works

To compose a recipe you must define a set of rules that specify:

  1. Base recipe(s) to inherit settings from. You can inherit the entire JSON document, or just certain sections (that is, particular settings).

  2. Operation(s) to perform to change or augment these inherited settings.

This composition ruleset has a unique identifier (scope and code) in the same way as a standard recipe. You can specify this scope and code when you value a portfolio or perform any other operation that requires a recipe. When you do, LUSID processes the rule set, composes a recipe on demand, and applies its settings.

Note: If you change the base recipe, the composed recipe automatically inherits changes providing they do not conflict.

To define a composition ruleset and load it into LUSID, call the UpsertRecipeComposer API, specifying:

  • A scope and code that together uniquely identify the composed recipe. It's important these do not clash with those of the base recipe.

  • In the operations collection, at least one operation, each consisting of:

    • A value that is either:

      • fromRecipe to identify a base recipe to inherit some or all settings from.

      • asString or asJson to specify the content of a changed or augmented setting.

    • An op that is either Prepend, Append, Update, Remove or Insert. Note that Prepend and Append can only be used with collections of settings. Insert can only be used to add settings that do not yet exist. Update and Remove can only be used on settings that do exist.

    • A path that identifies a location at which to perform op. Note the $ symbol refers to the entire JSON document when used in conjunction with Insert and fromRecipe, thereby inheriting the entire base recipe.

See below for examples. Note the order of operations in the collection is significant; LUSID performs them in the order specified.

Note: You can call the GetRecipeComposerResolvedInline API to test your composition ruleset passes validation before loading it into LUSID if you wish.

Once loaded, you can call:

  • The GetRecipeComposer API to examine the composition ruleset and, if necessary, the UpsertRecipeComposer API again to modify it.

  • The GetDerivedRecipe API to examine the composed recipe that LUSID will produce on demand.

  • If necessary, the DeleteRecipeComposer API to delete the composition ruleset.

Example 1: Prepending a market data rule

Note: A recipe is a hierarchical JSON document and, in any collection, LUSID uses the first matching object found. Prepending a market data rule to the marketRules collection in the market object is therefore a technique for prioritising that rule.

Consider the following part of a base recipe retrieved from LUSID with a single market data rule designed to locate equity prices from Refinitiv:

{
  "scope": "StandardRecipes",
  "code": "SimpleEquityPricer",
  "market": {
    "marketRules": [
      {
        "key": "Quote.Figi.*",
        "supplier": "DataScope",
        "dataScope": "MyRefinitivPrices",
        "quoteType": "Price",
        "field": "mid",
        "priceSource": "",
        "sourceSystem": "Lusid"
      }
    ]
  },
  "description": "Simple recipe to value equities using price * units",
  ...
}

We can call the UpsertRecipeComposer API to load a composition ruleset that inherits from and augments this recipe:

curl -X POST 'https://<your-domain>.lusid.com/api/api/recipes/composer'
  -H 'Content-Type: application/json-patch+json'
  -H 'Authorization: Bearer <your-API-access-token>'
  -d '{
  "recipeComposer": {
    "scope": "ComposedRecipes",
    "code": "SimpleEquityPricerBBG",
    "operations": [
      {
        "value": {
          "fromRecipe": {
            "scope": "StandardRecipes",
            "code": "SimpleEquityPricer"
          }
        },
        "path": "$",
        "op": "Insert"
      },
      {
        "value": {
          "asJson": "{\"key\": \"Quote.Figi.*\", \"supplier\": \"Bloomberg\", \"dataScope\": \"MyBBGPrices\", \"quoteType\": \"Price\", \"field\": \"mid\"}"
        },
        "path": "Market.MarketRules",
        "op": "Prepend"
      }
    ]
  }
}'

Note the following:

  • The composed recipe has a scope of ComposedRecipes and a code of SimpleEquityPricerBBG. You can specify this scope and code anywhere the scope and code of the base recipe (StandardRecipes and SimpleEquityPricer) can be used.

  • The first operation identifies the base recipe and inserts the entire JSON document; that is, inherits every setting. This operation is performed first.

  • The second operation adds a new JSON object consisting of a market data rule to the start of the marketRules collection in the market object. This operation is performed second.

We can call the GetDerivedRecipe API with the scope and code of the composed recipe to examine it:

curl -X GET 'https://<your-domain>.lusid.com/api/api/recipes/derived/ComposedRecipes/SimpleEquityPricerBBG'
  -H 'Authorization: Bearer <your-API-access-token>'

The response is as follows; the composed recipe has two market data rules, with the Bloomberg rule first and thus preferred:

{
  "value": {
    "scope": "ComposedRecipes",
    "code": "SimpleEquityPricerBBG",
    "market": {
      "marketRules": [
        {
          "key": "Quote.Figi.*",
          "supplier": "Bloomberg",
          "dataScope": "MyBBGPrices",
          "quoteType": "Price",
          "field": "mid",
          "priceSource": "",
          "sourceSystem": "Lusid"
        },
        {
          "key": "Quote.Figi.*",
          "supplier": "DataScope",
          "dataScope": "MyRefinitivPrices",
          "quoteType": "Price",
          "field": "mid",
          "priceSource": "",
          "sourceSystem": "Lusid"
        }
      ],
      "suppliers": {},
      "options": {
        "defaultSupplier": "Lusid",
        "defaultInstrumentCodeType": "LusidInstrumentId",
        "defaultScope": "default",
        "attemptToInferMissingFx": false,
        "calendarScope": "CoppClarkHolidayCalendars",
        "conventionScope": "Conventions"
      },
      "specificRules": [],
      "groupedMarketRules": []
    },
    "pricing": {
      "modelRules": [],
      "modelChoice": {},
      "options": {
        "modelSelection": {
          "library": "Lusid",
          "model": "SimpleStatic"
        },
        "useInstrumentTypeToDeterminePricer": false,
        "allowAnyInstrumentsWithSecUidToPriceOffLookup": false,
        "allowPartiallySuccessfulEvaluation": false,
        "produceSeparateResultForLinearOtcLegs": false,
        "enableUseOfCachedUnitResults": false,
        "windowValuationOnInstrumentStartEnd": false,
        "removeContingentCashflowsInPaymentDiary": false,
        "useChildSubHoldingKeysForPortfolioExpansion": false,
        "validateDomesticAndQuoteCurrenciesAreConsistent": false,
        "conservedQuantityForLookthroughExpansion": "PV"
      },
      "resultDataRules": []
    },
    "aggregation": {
      "options": {
        "useAnsiLikeSyntax": false,
        "allowPartialEntitlementSuccess": false,
        "applyIso4217Rounding": false
      }
    },
    "description": "Simple recipe to value equities using price * units",
    "holding": {
      "taxLotLevelHoldings": true
    }
  }
}

We can now specify the scope and code of the composed recipe in a call to the GetValuation API:

curl -X POST "https://<your-domain>.lusid.com/api/api/aggregation/$valuation"
  -H "Authorization: Bearer <your-API-access-token>"
  -H "Content-Type: application/json-patch+json"
  -d '{
    "portfolioEntityIds": [ {"scope": "Equities", "code": "UK"} ],
    "valuationSchedule": {"effectiveAt": "2024-03-11T00:00:00.0000000+00:00" },
    "recipeId": {"scope": "ComposedRecipes", "code": "SimpleEquityPricerBBG},
    "metrics": [{"key": "Valuation/PvInPortfolioCcy", "op": "Sum"}]
  }'

Example 2: Changing the look back period

In this example:

  • The base recipe has two market data rules. One has a quoteInterval of 3D.0D, to look back three days from the valuation datetime for valid pricing data. The other has no quoteInterval, so LUSID looks back the default interval of one day.

  • The composition ruleset has two operations. The first inherits all the settings in the base recipe. The second changes the quoteInterval field of every market data rule to one week. Note the * wildcard character must be encapsulated in square brackets.

  • In the composed recipe, both market data rules have a quoteInterval set to 1W.0D, including the rule without the field explicitly set before.

Base recipe (part of response from LUSID)

Composition ruleset (ready to upsert to LUSID)

Composed recipe (part of response from LUSID)

{
  "scope": "StandardRecipes",
  "code": "Lookback",
  "market": {
    "marketRules": [
      {
        "key": "Quote.Figi.*",
        "supplier": "Bloomberg",
        "dataScope": "MyBBGPrices",
        "quoteType": "Price",
        "field": "mid",
        "quoteInterval": "3D.0D"
      },
      {
        "key": "Quote.Figi.*",
        "supplier": "DataScope",
        "dataScope": "MyRefinitivPrices",
        "quoteType": "Price",
        "field": "mid",
      }
    ]
  },
  ...
}
{
  "recipeComposer": {
    "scope": "ComposedRecipes",
    "code": "LookbackHarmonised",
    "operations": [
      {
        "value": {
          "fromRecipe": {
            "scope": "StandardRecipes",
            "code": "Lookback"
          }
        },
        "path": "$",
        "op": "Insert"
      },
      {
        "value": {
          "asString": "1W.0D"
        },
        "path": "Market.MarketRules.[*].QuoteInterval",
        "op": "Update"
      }
    ]
  }
}
{
  "scope": "ComposedRecipes",
  "code": "LookbackHarmonised",
  "market": {
    "marketRules": [
      {
        "key": "Quote.Figi.*",
        "supplier": "Bloomberg",
        "dataScope": "MyBBGPrices",
        "quoteType": "Price",
        "field": "mid",
        "quoteInterval": "1W.0D"
      },
      {
        "key": "Quote.Figi.*",
        "supplier": "DataScope",
        "dataScope": "MyRefinitivPrices",
        "quoteType": "Price",
        "field": "mid",
        "quoteInterval": "1W.0D"
      }
    ]
  },
  ...
}

Example 3: Enabling separate leg valuation

In this example:

  • The base recipe has the default value of false for the produceSeparateResultForLinearOtcLegs option.

  • The composition ruleset has two operations. The first inherits all the settings in the base recipe. The second changes the option to true.

Base recipe (part of response from LUSID)

Composition ruleset (ready to upsert to LUSID)

Composed recipe (part of response from LUSID)

{
  "scope": "StandardRecipes",
  "code": "FxForwards",
  ...
  "pricing": {
    "modelRules": [],
    "modelChoice": {},
    "options": {
      "produceSeparateResultForLinearOtcLegs": false,
      ...
    }
  },
  ...
}
{
  "recipeComposer": {
    "scope": "ComposedRecipes",
    "code": "FxForwardsSeparateLegs",
    "operations": [
      {
        "value": {
          "fromRecipe": {
            "scope": "StandardRecipes",
            "code": "FxForwards"
          }
        },
        "path": "$",
        "op": "Insert"
      },
      {
        "value": {
          "asString": "true"
        },
        "path": "Pricing.Options.ProduceSeparateResultForLinearOtcLegs",
        "op": "Update"
      }
    ]
  }
}
{
  "scope": "ComposedRecipes",
  "code": "FxForwardsSeparateLegs",
  ...
  "pricing": {
    "modelRules": [],
    "modelChoice": {},
    "options": {
      "produceSeparateResultForLinearOtcLegs": true,
      ...
    }
  },
  ...
}

Example 4: Merging three recipes

In this example:

  • Base recipe 1 has a market rule, base recipe 2 has a market rule and a pricing model rule, and base recipe 3 has a pricing model rule.

  • The composition ruleset has three operations. The first inherits all the settings in base recipe 2, which is the most complex recipe. The second appends the market data rule from base recipe 1. The third prepends the pricing model rule from base recipe 3.

Base recipe 1 (part of response from LUSID)

Composition ruleset (ready to upsert to LUSID)

Composed recipe (part of response from LUSID)

{
  "scope": "StandardRecipes",
  "code": "Equities",
  "market": {
    "marketRules": [
      {
        "key": "Quote.Figi.*",
        "supplier": "Bloomberg",
        "dataScope": "MyEquityPrices",
        "quoteType": "Price",
        "field": "mid",
      }
    ]
  },
  ...
}
{
  "recipeComposer": {
    "scope": "ComposedRecipes",
    "code": "EquitiesBondFxOptions",
    "operations": [
      {
        "value": {
          "fromRecipe": {
            "scope": "StandardRecipes",
            "code": "Bonds"
          }
        },
        "path": "$",
        "op": "Insert"
      },
      {
        "value": {
          "fromRecipe": {
            "scope": "StandardRecipes",
            "code": "Equities"
          }
        },
        "path": "Market.MarketRules",
        "op": "Append"
      },
      {
        "value": {
          "fromRecipe": {
            "scope": "StandardRecipes",
            "code": "FxOptions"
          }
        },
        "path": "Pricing.ModelRules",
        "op": "Prepend"
      }
    ]
  }
}
{
  "scope": "ComposedRecipes",
  "code": "EquitiesBondFxOptions",
  "market": {
    "marketRules": [
      {
        "key": "Quote.Isin.*",
        "supplier": "DataScope",
        "dataScope": "MyBondPrices",
        "quoteType": "Price",
        "field": "mid",
      },
      {
        "key": "Quote.Figi.*",
        "supplier": "Bloomberg",
        "dataScope": "MyEquityPrices",
        "quoteType": "Price",
        "field": "mid",
      }
    ]
  },
  "pricing": {
    "modelRules": [
      {
        "supplier": "Lusid",
        "modelName": "BlackScholes",
        "instrumentType": "FxOption"
      },
      {
        "supplier": "Lusid",
        "modelName": "BondLookupPricer",
        "instrumentType": "Bond"
      }
    ]
  },
  ...
}

Base recipe 2 (part of response from LUSID)

{
  "scope": "StandardRecipes",
  "code": "Bonds",
  "market": {
    "marketRules": [
      {
        "key": "Quote.Isin.*",
        "supplier": "DataScope",
        "dataScope": "MyBondPrices",
        "quoteType": "Price",
        "field": "mid",
      }
    ]
  },
  "pricing": {
    "modelRules": [
      {
        "supplier": "Lusid",
        "modelName": "BondLookupPricer",
        "instrumentType": "Bond"
      }
    ]
  },
  ...
}

Base recipe 3 (part of response from LUSID)

{
  "scope": "StandardRecipes",
  "code": "FxOptions",
   ...
  "pricing": {
    "modelRules": [
      {
        "supplier": "Lusid",
        "modelName": "BlackScholes",
        "instrumentType": "FxOption"
      }
    ]
  },
  ...
}