Views:

LUSID has a set of built-in entities that represent real-world objects central to the task of managing investment data, such as portfolios, instruments, quotes and so on.

You can create a custom entity to model a real-world object or concept that LUSID does not natively represent, for example corporate office location. Note the following:

  • A custom entity must have a custom entity type that specifies values for two mandatory attributes, displayName and description, and defines the user-specified data fields. You can subsequently modify this custom entity type with caveats.
  • A custom entity must have at least one identifier. Under the hood, each identifier is defined as a custom property with a constraintStyle of Identifier and a 3-stage property key in the CustomEntity domain.
  • You can create relationships between a custom entity and certain other types of entity, for example between a corporate office location and the person entities allowed to work there.
  • LUSID stores custom entity data bitemporally and you can retrieve historical information by rolling back the as at timeline in the normal way. However, LUSID does not know how to interpret your custom entity data. It cannot be used to inform holding calculations or valuation operations.
  • Custom entities are not feature-complete yet.

Providing you have appropriate access control permissions, you can interact with a custom entity (and any relationships it might have):

Note you cannot interact via the LUSID web app at this time.

Defining a custom entity type

The first task is to create a custom entity type defining core characteristics. You can then create as many custom entities of this type as you need.

To do this, obtain an API token and call the CreateCustomEntityDefinition API for your LUSID domain, specifying a unique name for the type, values for the mandatory displayName and description attributes, and defining the core data fields. Note LUSID automatically prefixes the entity type name with ~ to distinguish it from current or future built-in entity types.

Data fields are not the same as properties. They can be required or not, have a lifeTime of Perpetual or TimeVariant (that is, expected to vary during different time periods), but the data type is restricted to String, Boolean, DateTime and Decimal. Unlike properties, data fields cannot be scoped (that is, entitled) independently of parent custom entities.

For example, to create a custom entity type to represent the concept of corporate office location:

curl -X POST "https://<your-domain>.lusid.com/api/api/customentities/entitytypes"
 -H "Authorization: Bearer <your-API-access-token>"
 -H "Content-Type: application/json-patch+json"
 -d '{
  "entityTypeName": "Office",
  "displayName": "Office location",
  "description": "An office or branch location",
  "fieldSchema": [
    {
      "name": "address",
      "lifetime": "Perpetual",
      "type": "String",
      "required": false
    },
    {
      "name": "seatingCapacity",
      "lifetime": "TimeVariant",
      "type": "Decimal",
      "required": false
    },
    {
      "name": "isHeadOffice",
      "lifetime": "TimeVariant",
      "type": "Boolean",
      "required": true
    }
  ]
}'

Providing the request is successful, the response identifies the entity type as ~Office:

{
    "entityTypeName": "Office",
    "displayName": "Office location",
    "description": "An office or branch location",
    "entityType": "~Office",
    "fieldSchema": [
        {
            "name": "isHeadOffice",
            "lifetime": "TimeVariant",
            "type": "Boolean",
            "required": true
        },
        {
            "name": "seatingCapacity",
            "lifetime": "TimeVariant",
            "type": "Decimal",
            "required": false
        },
        {
            "name": "address",
            "lifetime": "Perpetual",
            "type": "String",
            "required": false
        }
    ]
}

Understanding identifiers

A custom entity must have at least one identifier. Consider the following JSON fragment defining One Carter Lane, a corporate location that is both FINBOURNE Technology's London office and European headquarters, and so has an identifier for each context:

"displayName": "One Carter Lane",
"description": "FINBOURNE office and headquarters",
"identifiers": [
  {
    "identifierScope": "Location",
    "identifierType": "OfficeId",
    "identifierValue": "London"
  },
  {
    "identifierScope": "Location",
    "identifierType": "HeadquartersId",
    "identifierValue": "Europe"
  }
],
...

An identifier consists of three components: an identifierScopeidentifierType and identifierValue. The values you assign to these components combine to uniquely identify One Carter Lane in each of the contexts in which it operates. In this example:

To uniquely identify One Carter Lane as a ...identifierScopeidentifierTypeIdentifierValue
Office locationLocationOfficeIdLondon
HeadquartersLocationHeadquartersIdEurope

For each identifier you intend to give a custom entity, you must first call the CreatePropertyDefinition API to create a property definition with the following mandatory characteristics:

  • domain of CustomEntity.
  • A scope with the identifierScope value, for example Location.
  • A code with the identifierType value, for example OfficeId or HeadquartersId.
  • constraintStyle of Identifier.
  • lifeTime of Perpetual.

Note you can call the SearchProperties API with a suitable filter to find all the property definitions that have been created as identifiers for custom entities, in case a suitable definition already exists:

curl -X GET "https://<your-domain>.lusid.com/api/api/search/propertydefinitions?filter=constraintStyle%20eq%20%27Identifier%27%20and%20domain%20eq%20%27CustomEntity%27"
  -H "Authorization: Bearer <your-API-access-token>" 

Creating a property definition to constitute an 'Office location' identifier

The following call creates a property definition with a 3-stage property key of CustomEntity/Location/OfficeId:

curl -X POST "https://<your-domain>.lusid.com/api/api/propertydefinitions"
 -H "Authorization: Bearer <your-API-access-token>"
 -H "Content-Type: application/json-patch+json"
 -d '{
  "domain": "CustomEntity",
  "scope": "Location",
  "code": "OfficeId",
  "displayName": "Office ID",
  "dataTypeId": {"scope": "system", "code": "string"},
  "lifeTime": "Perpetual",
  "constraintStyle": "Identifier",
  "description": "Identifier property used to identify custom entities that are office locations"
 }'

Creating a property definition to constitute a 'Headquarters' identifier

The following call creates a property definition with a 3-stage property key of CustomEntity/Location/HeadquartersId:

curl -X POST "https://<your-domain>.lusid.com/api/api/propertydefinitions"
 -H "Authorization: Bearer <your-API-access-token>"
 -H "Content-Type: application/json-patch+json"
 -d '{
  "domain": "CustomEntity",
  "scope": "Location",
  "code": "HeadquartersId",
  "displayName": "Headquarters ID",
  "dataTypeId": {"scope": "system", "code": "string"},
  "lifeTime": "Perpetual",
  "constraintStyle": "Identifier",
  "description": "Identifier property used to identify custom entities that are headquarters"
 }'

Creating a custom entity with unique identifier values

You can now call the UpsertCustomEntity API to create a custom entity belonging to a custom entity type. Note the following:

  • The underlying custom entity type is identified by its type name in the URL of the UpsertCustomEntity API, in this case /api/customentities/~Office (note the ~ prefix).
  • The identifierScope of each identifier must be the scope of its underlying property definition, in this case Location.
  • The identifierType of each identifier must be the code of its underlying property definition, in this case OfficeId or HeadquartersId.
  • The identifierValue of each identifier must be unique among all identifiers with the same identifierScope and identifierCode, in this case London or Europe.

Note: You can call the UpsertCustomEntities API to upsert multiple custom entities at a time, and decide whether to fail the entire operation if one fails validation.

For example, to create a custom entity representing One Carter Lane: 

curl -X POST "https://<your-domain>.lusid.com/api/api/customentities/~Office"
 -H "Authorization: Bearer <your-API-access-token>"
 -H "Content-Type: application/json-patch+json"
 -d '{
  "displayName": "One Carter Lane",
  "description": "FINBOURNE office and regional headquarters",
  "identifiers": [
    {
      "identifierScope": "Location",
      "identifierType": "OfficeId",
      "identifierValue": "London"
    },
    {
      "identifierScope": "Location",
      "identifierType": "HeadquartersId",
      "identifierValue": "Europe"
    }
  ],
  "fields": [
    {
      "name": "address",
      "value": "One Carter Lane, London, EC4V 5ER"
    },
    {
      "name": "seatingCapacity",
      "value": 150,
      "effectiveFrom": "2021-08-01T00:00:00.00Z"
    },
    {
      "name": "isHeadOffice",
      "value": true,
      "effectiveFrom": "2021-08-01T00:00:00.00Z"
    }
  ]
}'

Note the use of the EffectiveFrom field to give time-variant data fields a 'valid from' date.

Providing the request is successful, the response confirms the date ranges that custom entity fields are valid between:

{
    "href": "https://<your-domain>.lusid.com/api/api/customentities/~Office",
    "entityType": "~Office",
    "version": {
        "effectiveFrom": "0001-01-01T00:00:00.0000000+00:00",
        "asAtDate": "2022-09-20T14:47:45.0500820+00:00"
    },
    "displayName": "One Carter Lane",
    "description": "FINBOURNE office and regional headquarters",
    "identifiers": [
        {
            "identifierScope": "Location",
            "identifierType": "OfficeId",
            "identifierValue": "London",
            "effectiveFrom": "0001-01-01T00:00:00.0000000+00:00",
            "effectiveUntil": "9999-12-31T23:59:59.9999999+00:00"
        },
        {
            "identifierScope": "Location",
            "identifierType": "HeadquartersId",
            "identifierValue": "Europe",
            "effectiveFrom": "0001-01-01T00:00:00.0000000+00:00",
            "effectiveUntil": "9999-12-31T23:59:59.9999999+00:00"
        }
    ],
    "fields": [
        {
            "name": "address",
            "value": "One Carter Lane, London, EC4V 5ER",
            "effectiveFrom": "0001-01-01T00:00:00.0000000+00:00",
            "effectiveUntil": "9999-12-31T23:59:59.9999999+00:00"
        },
        {
            "name": "isHeadOffice",
            "value": true,
            "effectiveFrom": "2021-08-01T00:00:00.0000000+00:00",
            "effectiveUntil": "9999-12-31T23:59:59.9999999+00:00"
        },
        {
            "name": "seatingCapacity",
            "value": 150.0,
            "effectiveFrom": "2021-08-01T00:00:00.0000000+00:00",
            "effectiveUntil": "9999-12-31T23:59:59.9999999+00:00"
        }
    ],
   ...
}

Retrieving custom entities and their relationships

You can retrieve a particular custom entity using the GetCustomEntity API.

You can retrieve all the custom entities of a particular type using the ListCustomEntities API, or perform a filter operation to only retrieve a matching set. Custom entities support filtering on data field values using the fields keyword, for example:

fields[streetAddress] startswith 'One'

Note if you create relationships between a custom entity and other entities you can retrieve them using the GetCustomEntityRelationships API.

Modifying an existing custom entity type

You can optionally modify a custom entity type using the UpdateCustomEntityDefinition API. Please note, however, this is a risky operation if custom entity instances of this type already exist:

  • If you modify the data type of a field (for example, from String to DateTime), values of custom entities that do not match the new type cannot be retrieved. If you revert the change, original values are returned even if they have been updated in the meantime (so your updates are lost).
  • If you modify the lifeTime of a data field (for example, from TimeVariant to Perpetual), values cannot be retrieved. If you revert the change, original values are returned even if they have been updated in the meantime.
  • If you modify the required status (for example, from True to False), values can be retrieved, but note that the new validation applies to all future updates to values. 

A note on unsupported features

Custom entities are in the early phase of their development and do not yet support:

  • Entitlement checking. 
  • Properties.
  • Deleting a custom entity type definition. 
  • Identifier properties that are specific to the custom entity type (identifier properties in the CustomEntity domain can currently be applied to custom entity instances of any type).