diff --git a/examples/use-cases/audit-trail/Get History of an Entity.ipynb b/examples/use-cases/audit-trail/Get History of an Entity.ipynb new file mode 100644 index 00000000..6b6358d3 --- /dev/null +++ b/examples/use-cases/audit-trail/Get History of an Entity.ipynb @@ -0,0 +1,586 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 17, + "id": "c74f906c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + " \n", + "\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from lusidtools.jupyter_tools import toggle_code\n", + "\n", + "\"\"\"Get History of an Entity\n", + "\n", + "This notebook shows how to get the full history of an entity using the asAt history of an entity.\n", + "\n", + "Attributes\n", + "----------\n", + "entities\n", + "history\n", + "instruments\n", + "\"\"\"\n", + "\n", + "toggle_code(\"Hide docstring\")" + ] + }, + { + "cell_type": "markdown", + "id": "586b08a8", + "metadata": {}, + "source": [ + "# Get History of an Entity\n", + "\n", + "In this notebook, we cover how to get the full history of an entity.\n", + "\n", + "In this case, our entity is an `Instrument`, but you can apply this technique to the following entities:\n", + "- `Abor`\n", + "- `Abor Config`\n", + "- `Allocation`\n", + "- `Block`\n", + "- `Corporate Action Source`\n", + "- `Custom Entity`\n", + "- `Execution`\n", + "- `Instrument`\n", + "- `Legal Entity`\n", + "- `Order`\n", + "- `Order Graph`\n", + "- `Order Instruction`\n", + "- `Package`\n", + "- `Participation`\n", + "- `Person`\n", + "- `Placement`\n", + "- `Portfolio`\n", + "- `Portfolio Group`\n", + "- `Relationship Definition`\n", + "- `Transaction Fee`\n", + "- `Transaction Portfolio`\n", + "\n", + "\n", + "To get the history of an entity, the steps are as follows:\n", + "1. [Create Entity](#1.-Create-Entity)\n", + "2. [Modify Entity](#2.-Modify-Entity)\n", + "3. [Get History of Entity](#3.-Get-History-of-Entity)" + ] + }, + { + "cell_type": "markdown", + "id": "0d12bab6", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Firstly, let's import LUSID packages" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b57bbce5", + "metadata": {}, + "outputs": [], + "source": [ + "# Import generic python modules\n", + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "import pytz\n", + "import os\n", + "\n", + "# Import key modules from the LUSID package\n", + "import lusid as lu\n", + "import lusid.models as lm\n", + "from lusidtools.cocoon.cocoon import load_from_data_frame\n", + "from lusidtools.cocoon.cocoon_printer import format_quotes_response\n", + "from lusidjam import RefreshingToken\n", + "\n", + "# Set the secrets path\n", + "secrets_path = os.getenv(\"FBN_SECRETS_PATH\")\n", + "\n", + "# For running the notebook locally\n", + "if secrets_path is None:\n", + " secrets_path = os.path.join(os.path.dirname(os.getcwd()), \"secrets.json\")\n", + "\n", + "# Authenticate our user and create our API client\n", + "api_factory = lu.utilities.ApiClientFactory(\n", + " token=RefreshingToken(),\n", + " api_secrets_filename=secrets_path\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "5dcc2b9e", + "metadata": {}, + "source": [ + "Next, let's create our API." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "abece5b4", + "metadata": {}, + "outputs": [], + "source": [ + "# LUSID Variable Definitions\n", + "instruments_api = api_factory.build(lu.api.InstrumentsApi)" + ] + }, + { + "cell_type": "markdown", + "id": "15814f45", + "metadata": {}, + "source": [ + "Finally, we declare some helper functions." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cb736e90", + "metadata": {}, + "outputs": [], + "source": [ + "def create_instrument(name, client_internal, figi):\n", + " instr_result = instruments_api.upsert_instruments(\n", + " request_body={\n", + " name: lm.InstrumentDefinition(\n", + " name=name,\n", + " identifiers={\n", + " \"ClientInternal\": lm.InstrumentIdValue(value=client_internal),\n", + " \"Figi\": lm.InstrumentIdValue(value=figi),\n", + " }\n", + " )\n", + " }\n", + " )\n", + " # luid\n", + " return instr_result.values[name].lusid_instrument_id\n", + "\n", + "def update_instrument(luid, figi):\n", + " update_result = instruments_api.update_instrument_identifier(\n", + " identifier_type=\"LusidInstrumentId\",\n", + " identifier=luid,\n", + " update_instrument_identifier_request=lm.UpdateInstrumentIdentifierRequest(\n", + " type=\"Figi\",\n", + " value=figi\n", + " )\n", + " )\n", + " # luid\n", + " return update_result.lusid_instrument_id\n", + "\n", + "def get_asat(luid, as_at=None):\n", + " response = instruments_api.get_instrument(\n", + " identifier_type=\"LusidInstrumentId\",\n", + " identifier=luid,\n", + " as_at=as_at\n", + " )\n", + " return response" + ] + }, + { + "cell_type": "markdown", + "id": "1a4daa3b", + "metadata": {}, + "source": [ + "## 1. Create Entity\n", + "\n", + "First create the entity. In this case we are creating an instrument." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "37ac4898", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LUID_00003IZ7'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "luid = create_instrument(name=\"AsAtInstrument\", client_internal=f\"AS_AT_INSTRUMENT\", figi=\"Original\")\n", + "luid" + ] + }, + { + "cell_type": "markdown", + "id": "64c78168", + "metadata": {}, + "source": [ + "## 2. Modify Entity\n", + "\n", + "Let's modify the entity. For our example, we will be changing the FIGI of the instrument." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "904d95e9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LUID_00003IZ7'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "update_instrument(luid=luid, figi=\"Edit1\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "899c0a46", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LUID_00003IZ7'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "update_instrument(luid=luid, figi=\"Edit2\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "e09c24bd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'LUID_00003IZ7'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "update_instrument(luid=luid, figi=\"Original\")" + ] + }, + { + "cell_type": "markdown", + "id": "deb949c6", + "metadata": {}, + "source": [ + "## 3. Get History of Entity\n", + "\n", + "In order to get the history of an entity, we need to use the version `asAt` data that is returned.\n", + "\n", + "The data looks like the following:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "588475d9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'as_at_created': datetime.datetime(2023, 5, 3, 13, 39, 16, 512886, tzinfo=tzlocal()),\n", + " 'as_at_date': datetime.datetime(2023, 5, 3, 13, 51, 14, 991585, tzinfo=tzlocal()),\n", + " 'as_at_modified': datetime.datetime(2023, 5, 3, 13, 40, 8, 387528, tzinfo=tzlocal()),\n", + " 'as_at_version_number': 4,\n", + " 'effective_from': datetime.datetime(1, 1, 1, 0, 0, tzinfo=tzlocal()),\n", + " 'user_id_created': '00uieujafoYdmkDSx2p7',\n", + " 'user_id_modified': '00uieujafoYdmkDSx2p7'}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "resp = get_asat(luid).version\n", + "resp" + ] + }, + { + "cell_type": "markdown", + "id": "686c5c0d", + "metadata": {}, + "source": [ + "The key aspect is the `as_at_modified` value. If we fetch our entity from a date and time that is slightly before this value, we will see we have a lower `as_at_version_number`. See the following example and how the version number changes from `4` to `3`." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "cb7b0940", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'as_at_created': datetime.datetime(2023, 5, 3, 13, 39, 16, 512886, tzinfo=tzlocal()),\n", + " 'as_at_date': datetime.datetime(2023, 5, 3, 13, 40, 8, 386528, tzinfo=tzlocal()),\n", + " 'as_at_modified': datetime.datetime(2023, 5, 3, 13, 40, 7, 246434, tzinfo=tzlocal()),\n", + " 'as_at_version_number': 3,\n", + " 'effective_from': datetime.datetime(1, 1, 1, 0, 0, tzinfo=tzlocal()),\n", + " 'user_id_created': '00uieujafoYdmkDSx2p7',\n", + " 'user_id_modified': '00uieujafoYdmkDSx2p7'}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "current_as_at = resp.as_at_modified\n", + "previous_as_at = current_as_at - timedelta(milliseconds=1)\n", + "\n", + "get_asat(luid, previous_as_at).version" + ] + }, + { + "cell_type": "markdown", + "id": "b7aa8421", + "metadata": {}, + "source": [ + "We can do this recursively to get the **full history** of an entity." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7ca9e8c8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
dateversion_numbermodified_idclient_internalfigiluid
32023-05-03 13:39:16.512886+00:00100uieujafoYdmkDSx2p7AS_AT_INSTRUMENTOriginalLUID_00003IZ7
22023-05-03 13:39:16.701563+00:00200uieujafoYdmkDSx2p7AS_AT_INSTRUMENTEdit1LUID_00003IZ7
12023-05-03 13:40:07.246434+00:00300uieujafoYdmkDSx2p7AS_AT_INSTRUMENTEdit2LUID_00003IZ7
02023-05-03 13:40:08.387528+00:00400uieujafoYdmkDSx2p7AS_AT_INSTRUMENTOriginalLUID_00003IZ7
\n", + "
" + ], + "text/plain": [ + " date version_number modified_id \\\n", + "3 2023-05-03 13:39:16.512886+00:00 1 00uieujafoYdmkDSx2p7 \n", + "2 2023-05-03 13:39:16.701563+00:00 2 00uieujafoYdmkDSx2p7 \n", + "1 2023-05-03 13:40:07.246434+00:00 3 00uieujafoYdmkDSx2p7 \n", + "0 2023-05-03 13:40:08.387528+00:00 4 00uieujafoYdmkDSx2p7 \n", + "\n", + " client_internal figi luid \n", + "3 AS_AT_INSTRUMENT Original LUID_00003IZ7 \n", + "2 AS_AT_INSTRUMENT Edit1 LUID_00003IZ7 \n", + "1 AS_AT_INSTRUMENT Edit2 LUID_00003IZ7 \n", + "0 AS_AT_INSTRUMENT Original LUID_00003IZ7 " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "as_at = pytz.utc.localize(datetime.now())\n", + "instr_data = {\n", + " \"date\": [],\n", + " \"version_number\": [],\n", + " \"modified_id\": [],\n", + " \"client_internal\": [],\n", + " \"figi\": [],\n", + " \"luid\": []\n", + "}\n", + "\n", + "while True:\n", + " resp = get_asat(luid, as_at)\n", + " ids = resp.identifiers\n", + " version = resp.version\n", + " # add instr data to df\n", + " instr_data['date'].append(version.as_at_modified)\n", + " instr_data['version_number'].append(version.as_at_version_number)\n", + " instr_data['modified_id'].append(version.user_id_modified)\n", + " instr_data['client_internal'].append(ids['ClientInternal'])\n", + " instr_data['figi'].append(ids['Figi'] if 'Figi' in ids else None)\n", + " instr_data['luid'].append(ids['LusidInstrumentId'])\n", + " # final version, end\n", + " if version.as_at_version_number == 1:\n", + " break\n", + " # get previous version by going slightly before modified date\n", + " as_at = version.as_at_modified - timedelta(milliseconds=1)\n", + "\n", + "pd.DataFrame(data=instr_data).sort_values(by='date')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": false, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}