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:
- Base recipe(s) to inherit settings from. You can inherit the entire JSON document, or just certain sections (that is, particular settings).
- 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
andcode
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
orasJson
to specify the content of a changed or augmented setting.
- An
op
that is eitherPrepend
,Append
,Update
,Remove
orInsert
. Note thatPrepend
andAppend
can only be used with collections of settings.Insert
can only be used to add settings that do not yet exist.Update
andRemove
can only be used on settings that do exist. - A
path
that identifies a location at which to performop
. Note the$
symbol refers to the entire JSON document when used in conjunction withInsert
andfromRecipe
, thereby inheriting the entire base recipe.
- A
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 ofSimpleEquityPricerBBG
. You can specify this scope and code anywhere the scope and code of the base recipe (StandardRecipes
andSimpleEquityPricer
) can be used. - The first operation (in red above) identifies the base recipe and inserts the entire JSON document; that is, inherits every setting. This operation is performed first.
- The second operation (in green) adds a new JSON object consisting of a market data rule to the start of the
marketRules
collection in themarket
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 (highlighted in red below) 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 (highlighted in red):
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
of3D.0D
, to look back three days from the valuation datetime for valid pricing data. The other has noquoteInterval
, 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 to1W.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 theproduceSeparateResultForLinearOtcLegs
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" } ] }, ... } |