diff --git a/examples/use-cases/amortisation-ruleset/AmortisationRules.ipynb b/examples/use-cases/amortisation-ruleset/AmortisationRules.ipynb new file mode 100644 index 00000000..34d01c53 --- /dev/null +++ b/examples/use-cases/amortisation-ruleset/AmortisationRules.ipynb @@ -0,0 +1,1172 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "61f113e0-ba28-400b-9ed8-216883e8df87", + "metadata": {}, + "outputs": [], + "source": [ + "from lusidtools.jupyter_tools import toggle_code\n", + "\n", + "\"\"\"Amortisation RuleSet\n", + "\n", + "Attributes\n", + "----------\n", + "Instruments\n", + "Transactions\n", + "Amortisation RuleSets\n", + "AmortisationRules\n", + "Recipe\n", + "Valuation\n", + "\"\"\"\n", + "\n", + "toggle_code(\"Toggle Docstring\")" + ] + }, + { + "cell_type": "markdown", + "id": "0d686878-b96e-4112-9528-974ce3ff08b3", + "metadata": {}, + "source": [ + "# Amortisation Rule Sets\n", + "\n", + "This example demonstrates how to create, update, and delete Amortisation Rule Sets and Rules, and how changes to the amortisation rules affect the amortisation valuation.\n", + "\n", + "We will create bond instruments, add properties to a specific instrument, create an amortisation ruleset, set up a portfolio, add properties to the portfolio, and configure the amortisation rules.\n", + "\n", + "Further we will upsert similar transactions to the respective bond instruments to demonstrate the effects of the amortisation rules through comparison. \n", + "\n", + "We will then create a simple recipe for valuation, including the valuation function.\n", + "\n", + "Once Done, we will then demostrate how updating the amortisation rules impacts the valuation." + ] + }, + { + "cell_type": "markdown", + "id": "3581f291-8ca2-4ef9-9cd6-03f44385abd2", + "metadata": {}, + "source": [ + "## Table of Contents:\n", + "- 1. [Imports](#1.-Imports)\n", + "- 2. [LUSID APIs](#2.-LUSID-APIs)\n", + "- 3. [Instrument Creation](#3.-Instrument-Creation)\n", + "- 4. [Amortisation RuleSet Creation](#4.-Amortisation-RuleSet-Creation)\n", + "- 5. [Portfolio Creation](#5.-Portfolio-Creation)\n", + "- 6. [Setting Amortisation Rule](#6.-Setting-Amortisation-Rule)\n", + "- 7. [Upsert Transactions](#7.-Upsert-Transactions)\n", + "- 8. [Recipe Creation](#8.-Recipe-Creation)\n", + "- 9. [Valuation](#9.-Valuation)\n", + "- 10. [Data Cleaning](#9.-Data-Cleaning)" + ] + }, + { + "cell_type": "markdown", + "id": "1ace0ed9-9c99-4db4-937e-2cfc6a80aabc", + "metadata": {}, + "source": [ + "# 1. Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60ee9804-9071-46f9-907d-3d9c19855b4b", + "metadata": {}, + "outputs": [], + "source": [ + "# Import necessary libraries\n", + "import lusid\n", + "import finbourne_access\n", + "import lusid.models as models\n", + "import openpyxl\n", + "import subprocess\n", + "from lusidtools.cocoon.cocoon import load_from_data_frame\n", + "from lusidtools.cocoon.cocoon_printer import (\n", + " format_instruments_response,\n", + " format_portfolios_response,\n", + " format_transactions_response,\n", + " format_quotes_response,\n", + ")\n", + "from lusidtools.pandas_utils.lusid_pandas import lusid_response_to_data_frame\n", + "from lusidjam import RefreshingToken\n", + "from fbnsdkutilities import ApiClientFactory\n", + "from finbourne_access.utilities import ApiClientFactory as AccessApiClientFactory\n", + "from finbourne_access import models as AccessModels\n", + "import os\n", + "import json\n", + "from IPython.display import Image\n", + "import pandas as pd\n", + "import pytz\n", + "from datetime import datetime\n", + "from lusidtools.lpt.lpt import to_date\n", + "pd.set_option(\"display.max_columns\", None)\n", + "\n", + "# Authenticate our user and create our API client\n", + "secrets_path = os.getenv(\"FBN_SECRETS_PATH\")\n", + "\n", + "lusid_api_factory = lusid.utilities.ApiClientFactory(\n", + " token=RefreshingToken(),\n", + " api_secrets_filename=secrets_path,\n", + " app_name=\"LusidJupyterNotebook\",\n", + ")\n", + "\n", + "api_client = lusid_api_factory.api_client\n", + "\n", + "lusid_api_url = api_client.configuration.host\n", + "access_api_url = lusid_api_url[: lusid_api_url.rfind(\"/\") + 1] + \"access\"\n", + "identity_api_url = lusid_api_url[: lusid_api_url.rfind(\"/\") + 1] + \"identity\"\n", + "\n", + "access_api_factory = finbourne_access.utilities.ApiClientFactory(\n", + " token=api_client.configuration.access_token,\n", + " access_url=access_api_url,\n", + " app_name=\"LusidJupyterNotebook\",\n", + ")\n", + "api_factory = ApiClientFactory(lusid, token=RefreshingToken())" + ] + }, + { + "cell_type": "markdown", + "id": "2b1c49bb-a0b7-413e-8153-2c495e00aa4a", + "metadata": {}, + "source": [ + "# 2. LUSID APIs\n", + "Firstly, we initialize the LUSID APIs required for the notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25c79a41-31bc-403a-a69b-d33672a8726f", + "metadata": {}, + "outputs": [], + "source": [ + "# Initiate the LUSID APIs required for the notebook\n", + "\n", + "instruments_api = api_factory.build(lusid.api.InstrumentsApi)\n", + "amortisation_api = api_factory.build(lusid.api.AmortisationRuleSetsApi)\n", + "transaction_portfolio_api = api_factory.build(lusid.api.TransactionPortfoliosApi)\n", + "portfolio_api = api_factory.build(lusid.api.PortfoliosApi)\n", + "configuration_recipe_api = api_factory.build(lusid.api.ConfigurationRecipeApi)\n", + "aggregation_api = api_factory.build(lusid.AggregationApi)\n", + "property_api = api_factory.build(lusid.PropertyDefinitionsApi)" + ] + }, + { + "cell_type": "markdown", + "id": "1d68849e-cee3-483d-8c9b-c68a44df2888", + "metadata": {}, + "source": [ + "# 3. Instrument Creation\n", + "We will create the relevant bond instruments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74300dd0-961b-434a-be5b-6e8f88f04b0a", + "metadata": {}, + "outputs": [], + "source": [ + "#Setting Scope and Code to be used throughout the notebook\n", + "\n", + "scope = \"valuation-sample\"\n", + "portfolio_code = \"EQUITY_UK_1\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02dcb40b-9f46-48e7-b97e-7d876d1ce836", + "metadata": {}, + "outputs": [], + "source": [ + "#Defining function for bond instrument creation\n", + "\n", + "def create_bond(currency,payment_frequency,roll_convention,day_count_convention,payment_calendars,reset_calendars,settle_days,reset_days,start_date,maturity_date,dom_ccy,\n", + " principal,coupon_rate,bond_identifier,bond_name):\n", + "\n", + " flow_conventions = lusid.FlowConventions(\n", + " currency=currency,\n", + " payment_frequency=payment_frequency,\n", + " roll_convention=roll_convention,\n", + " day_count_convention=day_count_convention,\n", + " payment_calendars=payment_calendars,\n", + " reset_calendars=reset_calendars,\n", + " settle_days=settle_days,\n", + " reset_days=reset_days,\n", + " )\n", + "\n", + " bond = lusid.Bond(\n", + " start_date=start_date,\n", + " maturity_date=maturity_date,\n", + " dom_ccy=dom_ccy,\n", + " principal=principal,\n", + " coupon_rate=coupon_rate,\n", + " flow_conventions=flow_conventions,\n", + " identifiers={},\n", + " instrument_type=\"Bond\",\n", + " calculation_type=\"Standard\",\n", + " )\n", + "\n", + " # define the instrument to be upserted\n", + " bond_definition = lusid.InstrumentDefinition(\n", + " name=bond_name,\n", + " identifiers={\"ClientInternal\": lusid.InstrumentIdValue(bond_identifier)},\n", + " definition=bond,\n", + " )\n", + "\n", + " # upsert the instrument\n", + " upsert_request = {bond_identifier: bond_definition}\n", + " upsert_response = instruments_api.upsert_instruments(request_body=upsert_request)\n", + " bond_luid = upsert_response.values[bond_identifier].lusid_instrument_id\n", + " return(bond_luid)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd48d82d-ce0f-490f-b0ac-143e9712f8b5", + "metadata": {}, + "outputs": [], + "source": [ + "#Creating first Bond Instrument\n", + "\n", + "currency = \"GBP\"\n", + "payment_frequency = \"1M\"\n", + "roll_convention = \"none\"\n", + "day_count_convention = \"Actual365\"\n", + "payment_calendars = []\n", + "reset_calendars = []\n", + "settle_days = 0\n", + "reset_days = 0\n", + "start_date = datetime(2024, 1, 15, 0, 0, tzinfo=pytz.utc).isoformat()\n", + "maturity_date = datetime(2024, 8, 15, 0, 0, tzinfo=pytz.utc).isoformat()\n", + "dom_ccy = \"GBP\"\n", + "principal = 10487\n", + "coupon_rate = 0.07\n", + "bond_identifier = \"Example_Bond_A\"\n", + "bond_name = \"ExampleBond_A\"\n", + "\n", + "luid = create_bond(currency, payment_frequency, roll_convention, day_count_convention, payment_calendars, reset_calendars, settle_days, reset_days, start_date, maturity_date, dom_ccy, \n", + " principal, coupon_rate, bond_identifier, bond_name)\n", + "\n", + "luid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0267add-3535-4eb6-a7c2-a57976952b5d", + "metadata": {}, + "outputs": [], + "source": [ + "#Creating second Bond Instrument\n", + "\n", + "currency = \"GBP\"\n", + "payment_frequency = \"1M\"\n", + "roll_convention = \"none\"\n", + "day_count_convention = \"Actual365\"\n", + "payment_calendars = []\n", + "reset_calendars = []\n", + "settle_days = 0\n", + "reset_days = 0\n", + "start_date = datetime(2024, 1, 15, 0, 0, tzinfo=pytz.utc).isoformat()\n", + "maturity_date = datetime(2024, 8, 15, 0, 0, tzinfo=pytz.utc).isoformat()\n", + "dom_ccy = \"GBP\"\n", + "principal = 10487\n", + "coupon_rate = 0.07\n", + "bond_identifier = \"Example_Bond_B\"\n", + "bond_name = \"ExampleBond_B\"\n", + "\n", + "luid_1=create_bond(currency, payment_frequency, roll_convention, day_count_convention, payment_calendars, reset_calendars, settle_days, reset_days, start_date, maturity_date, dom_ccy,\n", + " principal, coupon_rate, bond_identifier, bond_name)\n", + "\n", + "luid_1" + ] + }, + { + "cell_type": "markdown", + "id": "11bab45e-eb3e-469e-af35-747e83f53201", + "metadata": {}, + "source": [ + "# 3.1 Instrument Property\n", + "We will create and upsert instrument property for the first instrument so that it can be used when defining amortisation rules\n", + "\n", + "We will do so only for one instrument so that impact of amortisation rules on instruments can be compared based on instrument properties" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdda65d2-d9f2-4e78-8b39-683e394a94a2", + "metadata": {}, + "outputs": [], + "source": [ + "#Creating Instrument Property\n", + "try:\n", + " instrument_prop = property_api.create_property_definition(\n", + " create_property_definition_request=lusid.CreatePropertyDefinitionRequest(\n", + " domain = \"Instrument\",\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " data_type_id= lusid.ResourceId(\n", + " scope = \"system\",\n", + " code = \"string\",\n", + " ),\n", + " life_time= \"Perpetual\",\n", + " constraint_style = \"Property\",\n", + " display_name = \"Bond_Prop_1\",\n", + " property_description = \"Example Bond Property\",\n", + " )\n", + " \n", + " )\n", + " print(\"Instrument Property Created\")\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "286fb7e7-c6d2-4963-86af-dd89df53b68b", + "metadata": {}, + "outputs": [], + "source": [ + "#Upserting property to first Bond Instrument\n", + "try:\n", + " upsert_instrument_prop = instruments_api.upsert_instruments_properties(\n", + " upsert_instrument_property_request=[\n", + " lusid.UpsertInstrumentPropertyRequest(\n", + " identifier_type=\"LusidInstrumentId\",\n", + " identifier=luid,\n", + " properties=[\n", + " lusid.ModelProperty(\n", + " key=\"Instrument/valuation-sample/EQUITY_UK_1\",\n", + " value=lusid.PropertyValue(\n", + " label_value='alpha1'\n", + " )\n", + " )\n", + " ]\n", + " )\n", + " ]\n", + " )\n", + " print(\"Instrument Property Upserted\")\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "markdown", + "id": "c3ed4107-5074-4731-9e05-105ece1e1070", + "metadata": {}, + "source": [ + "# 4. Amortisation RuleSet Creation\n", + "We will create the relevant Amortisation Rule Set" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78b84358-17fc-43f8-9479-729ed2193348", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "#Creating Amortisation Ruleset\n", + "try:\n", + " create_amt = amortisation_api.create_amortisation_rule_set(\n", + " scope= scope,\n", + " create_amortisation_rule_set_request=models.CreateAmortisationRuleSetRequest(\n", + " code=portfolio_code,\n", + " display_name='Example_Amortisation_Rule',\n", + " description='This is an example run'\n", + " )\n", + " )\n", + " print(\"Amortisation RuleSet Created\")\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "markdown", + "id": "84a6b0bf-8a3d-420f-8133-64e608b4c2f0", + "metadata": {}, + "source": [ + "# 5. Portfolio Creation\n", + "We proceed by creating a basic transaction portfolio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12bdbe41-a719-4abc-b980-af6ac9b27650", + "metadata": {}, + "outputs": [], + "source": [ + "# Creating a Transaction Portfolio\n", + "\n", + "try:\n", + " create_portfolio_response = transaction_portfolio_api.create_portfolio(\n", + " scope=scope,\n", + " create_transaction_portfolio_request=models.CreateTransactionPortfolioRequest(\n", + " display_name=\"RuleSet_Portfolio\",\n", + " code=portfolio_code,\n", + " base_currency=\"GBP\",\n", + " created=datetime(2024, 1, 1, 0, 0, tzinfo=pytz.utc).isoformat(),\n", + " sub_holding_keys=[],\n", + " amortisation_method='NoAmortisation',\n", + " amortisation_rule_set_id={\n", + " \"scope\": scope,\n", + " \"code\": portfolio_code\n", + " }\n", + "\n", + " ),\n", + " )\n", + "\n", + " print(\"Portfolio Created\")\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])\n" + ] + }, + { + "cell_type": "markdown", + "id": "5ea98c2c-7be2-4912-87bc-7735482d1f64", + "metadata": {}, + "source": [ + "# 5.1 Portfolio Property\n", + "We will create and upsert portfolio property for the relevant portfolio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18e40c13-2639-4ace-abfa-38f32c75d683", + "metadata": {}, + "outputs": [], + "source": [ + "#Creating Portfolio Property\n", + "try:\n", + " property_creation = property_api.create_property_definition(\n", + " create_property_definition_request=lusid.CreatePropertyDefinitionRequest(\n", + " domain = \"Portfolio\",\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " data_type_id= lusid.ResourceId(\n", + " scope = \"system\",\n", + " code = \"string\",\n", + " ),\n", + " life_time= \"Perpetual\",\n", + " constraint_style = \"Property\",\n", + " display_name = \"Portfolio_property\",\n", + " property_description = \"Example amortisation rule Property\",\n", + " )\n", + " \n", + " )\n", + " print('Portfolio Property Created')\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ebf29d2-a432-4bcd-9dce-7c23dadbcaf7", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "#Upserting property to Portfolio\n", + "try:\n", + " upsert_port= portfolio_api.upsert_portfolio_properties(\n", + " scope= scope,\n", + " code= portfolio_code,\n", + " request_body={\n", + " \"Portfolio/valuation-sample/EQUITY_UK_1\": {\n", + " \n", + " \"key\": \"Portfolio/valuation-sample/EQUITY_UK_1\",\n", + " \"value\": {\n", + " \"labelValue\": \"alpha1\"\n", + " },\n", + " },\n", + " }\n", + " \n", + " ) \n", + " print(\"Portfolio Property Upserted\")\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "markdown", + "id": "974d2d19-18e1-4b17-a592-4d3db16ae125", + "metadata": {}, + "source": [ + "# 6. Setting Amortisation Rule\n", + "We will upsert rules to the Amortisation Rule Set" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1685dc4-f641-4da3-937b-9f858ae8d5ec", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "#Set Amortisation Rules\n", + "\n", + "set_amt_rule = amortisation_api.set_amortisation_rules(\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " set_amortisation_rules_request=models.SetAmortisationRulesRequest(\n", + " rules_interval=models.RulesInterval(\n", + " effective_range={\n", + " \"fromDate\": \"0001-01-01T00:00:00.0000000+00:00\",\n", + " },\n", + " rules=[]\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "6853d35f-8b18-43a6-bc3d-56fde9f5794c", + "metadata": {}, + "source": [ + "# 6.1 List Amortisation RuleSets\n", + "Using this endpoint we can see the list of Amortisation RuleSets and associates Rules" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b2b1797-f5c0-46f9-96d8-baa83319d641", + "metadata": {}, + "outputs": [], + "source": [ + "#List RuleSets\n", + "\n", + "list_amt= amortisation_api.list_amortisation_rule_sets()\n", + "list_amt" + ] + }, + { + "cell_type": "markdown", + "id": "95e7d51f-227a-4834-9656-77faa317e441", + "metadata": {}, + "source": [ + "# 7. Upsert Transactions\n", + "We will upsert similar transactions for the created instruments to observe the impacts of the amortisation rules" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c07f66c-c27a-418a-9d28-3d79faeec7cd", + "metadata": {}, + "outputs": [], + "source": [ + "transactions = pd.read_csv(\"Transaction_Data.csv\")\n", + "transactions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c83f29b0-3530-4e28-bb6f-f32aa5f46ada", + "metadata": {}, + "outputs": [], + "source": [ + "#Upserting Transactions\n", + "transaction_request = [\n", + " lusid.TransactionRequest(\n", + " transaction_id=row[\"txn_id\"],\n", + " type=row[\"type\"],\n", + " instrument_identifiers={\n", + " \"Instrument/default/ClientInternal\": row[\"client_id\"]\n", + " },\n", + " transaction_date=to_date(row[\"trade_date\"]).isoformat(),\n", + " settlement_date=to_date(row[\"settlement_date\"]).isoformat(),\n", + " units=row[\"quantity\"],\n", + " transaction_price=lusid.TransactionPrice(price=row[\"price\"], type=\"Price\"),\n", + " total_consideration=lusid.CurrencyAndAmount(\n", + " amount=row[\"total_consideration\"], currency=row[\"currency\"]\n", + " ),\n", + " )\n", + " for index, row in transactions.iterrows()\n", + "]\n", + "\n", + "response = transaction_portfolio_api.upsert_transactions(\n", + " scope=scope, code=portfolio_code, transaction_request=transaction_request\n", + ")\n", + "\n", + "print(f\"Transactions succesfully updated at time: {response.version.as_at_date}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2d169207-b6ce-48eb-a506-ee8009d5c528", + "metadata": {}, + "source": [ + "# 8. Recipe Creation\n", + "\n", + "Following the initial setup, we can see to configuring how LUSID will conduct valuation. This introduces the concept of recipes, which are a set of steps we specify to the valuation engine relating to market data and model specification.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abacf1ce-1bb8-4c54-806b-4758ef2b6dcf", + "metadata": {}, + "outputs": [], + "source": [ + "# Creating Recipe to perform a valuation\n", + "try:\n", + " configuration_recipe = models.ConfigurationRecipe(\n", + " scope=scope,\n", + " code=portfolio_code,\n", + " market=models.MarketContext(\n", + " market_rules=[\n", + " # define how to resolve the quotes\n", + " models.MarketDataKeyRule(\n", + " key=\"Quote.ClientInternal.*\",\n", + " supplier=\"Lusid\",\n", + " data_scope=scope,\n", + " quote_type=\"Price\",\n", + " field= \"bid\",\n", + " ),\n", + " ],\n", + " options=models.MarketOptions(\n", + " default_supplier=\"Lusid\",\n", + " default_instrument_code_type=\"ClientInternal\",\n", + " default_scope=scope,\n", + " ),\n", + " ),\n", + " pricing=models.PricingContext(\n", + " options={\"AllowPartiallySuccessfulEvaluation\": True},\n", + " ),\n", + " )\n", + " \n", + " upsert_configuration_recipe_response = configuration_recipe_api.upsert_configuration_recipe(\n", + " upsert_recipe_request=models.UpsertRecipeRequest(\n", + " configuration_recipe=configuration_recipe\n", + " )\n", + " )\n", + " print(\"Recipe Created Successfully\")\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "markdown", + "id": "fab0149c-87d9-41aa-87d8-07c7bce5dd5d", + "metadata": {}, + "source": [ + "# 9. Valuation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3e702eb-232d-45dc-ab28-77c798d85a3e", + "metadata": {}, + "outputs": [], + "source": [ + "#Defining function for Valuation\n", + "\n", + "def generate_valuation_request(valuation_effectiveAt):\n", + "\n", + " # Create the valuation request\n", + " valuation_request = models.ValuationRequest(\n", + " recipe_id=models.ResourceId(\n", + " scope=scope, code=portfolio_code\n", + " ),\n", + " metrics=[\n", + " models.AggregateSpec(\"Instrument/default/Name\", \"Value\"),\n", + " models.AggregateSpec(\"Valuation/PvInReportCcy\", \"Proportion\"),\n", + " models.AggregateSpec(\"Valuation/PvInReportCcy\", \"Sum\"),\n", + " models.AggregateSpec(\"Holding/default/Units\", \"Sum\"),\n", + " models.AggregateSpec(\"Holding/AmortisedCost\", \"Value\"),\n", + " ],\n", + " group_by=[\"Instrument/default/Name\"],\n", + " portfolio_entity_ids=[\n", + " models.PortfolioEntityId(scope=scope, code=portfolio_code)\n", + " ],\n", + " valuation_schedule=models.ValuationSchedule(\n", + " effective_at=valuation_effectiveAt.isoformat()\n", + " ),\n", + " )\n", + "\n", + " return valuation_request" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c16f197-ef0c-4f80-9bef-b0cd2404989e", + "metadata": {}, + "outputs": [], + "source": [ + "#Getting Valuation\n", + "\n", + "aggregation = aggregation_api.get_valuation(\n", + " valuation_request=generate_valuation_request(datetime(year=2024, month=4, day=25, tzinfo=pytz.UTC)\n", + " )\n", + ")\n", + "\n", + "pd.DataFrame(aggregation.data)" + ] + }, + { + "cell_type": "markdown", + "id": "f1e86e68-fb62-4d08-a1fc-a99fd59f3a86", + "metadata": {}, + "source": [ + "We can see that the Amortised Cost is calculated based on No Amortisation method as set when creating the portfolio. No Amortisation Rules were defined at this point " + ] + }, + { + "cell_type": "markdown", + "id": "52fe7dbb-1912-4e14-ab4e-026a3ae4d5c3", + "metadata": {}, + "source": [ + "# 9.1 Defining and Setting Amortisation Rules" + ] + }, + { + "cell_type": "markdown", + "id": "0405558f-31bb-431a-9a10-65a9ed68b81f", + "metadata": {}, + "source": [ + "We can change/update Amortisation Rules based on the filters we provide. Following are a few examples of how we can use filters to set amortisation rules, along with imSetting Amortisation Rule filter to True eq True. This means that the amortisation method set in the rule defined will take precedence for all transactions in the relevant portfolio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29228d0a-6dc0-4ebb-8838-dee07397095f", + "metadata": {}, + "outputs": [], + "source": [ + "#Updating Amortisation Rule \n", + "\n", + "set_amt_rule = amortisation_api.set_amortisation_rules(\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " set_amortisation_rules_request=models.SetAmortisationRulesRequest(\n", + " rules_interval=models.RulesInterval(\n", + " effective_range={\n", + " \"fromDate\": \"0001-01-01T00:00:00.0000000+00:00\",\n", + " },\n", + " rules=[\n", + " {\n", + " \"name\" : \"Amortisation_Rule_1\",\n", + " \"description\" : \"Rule setting filter to True equals True\",\n", + " \"filter\" : \"True eq True\",\n", + " \"amortisationMethod\" : \"StraightLine\",\n", + " },\n", + " ]\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa6201bc-1eca-428a-8c30-96905d58ff64", + "metadata": {}, + "outputs": [], + "source": [ + "#Getting Valuation\n", + "\n", + "aggregation = aggregation_api.get_valuation(\n", + " valuation_request=generate_valuation_request(datetime(year=2024, month=4, day=25, tzinfo=pytz.UTC)\n", + " )\n", + ")\n", + "\n", + "pd.DataFrame(aggregation.data)" + ] + }, + { + "cell_type": "markdown", + "id": "7ff51ca5-9075-472b-a4aa-59532eb0fc90", + "metadata": {}, + "source": [ + "We can see that the Amortised Cost is calculate based on StraightLine method as updated in the rule." + ] + }, + { + "cell_type": "markdown", + "id": "0e09aa4e-5060-42b8-b90e-61ab00808730", + "metadata": {}, + "source": [ + "Setting Amortisation Rule filter based on instrument property. This means that the amortisation method set in the rule defined will take precedence over the method set on portfolio creation, for the instrument with the relevant property that is used in the filter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "933a83b6-cee3-435f-bc77-83c19f9a2ca2", + "metadata": {}, + "outputs": [], + "source": [ + "#Updating Amortisation Rule\n", + "\n", + "set_amt_rule = amortisation_api.set_amortisation_rules(\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " set_amortisation_rules_request=models.SetAmortisationRulesRequest(\n", + " rules_interval=models.RulesInterval(\n", + " effective_range={\n", + " \"fromDate\": \"0001-01-01T00:00:00.0000000+00:00\",\n", + " },\n", + " rules=[\n", + " {\n", + " \"name\" : \"Amortisation_Rule_1\",\n", + " \"description\" : \"Rule Description\",\n", + " \"filter\" : \"properties[Instrument/valuation-sample/EQUITY_UK_1] eq 'alpha1'\",\n", + " \"amortisationMethod\" : \"StraightLine\",\n", + " },\n", + " ]\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc4a8e74-fffb-465e-bf92-5eedf43328fc", + "metadata": {}, + "outputs": [], + "source": [ + "#Getting Valuation\n", + "\n", + "aggregation = aggregation_api.get_valuation(\n", + " valuation_request=generate_valuation_request(datetime(year=2024, month=4, day=25, tzinfo=pytz.UTC)\n", + " )\n", + ")\n", + "\n", + "pd.DataFrame(aggregation.data)" + ] + }, + { + "cell_type": "markdown", + "id": "c5f251f3-72dd-49c5-99d1-c7e77054e45c", + "metadata": {}, + "source": [ + "We can see that the Amortised Cost is calculate based on StraightLine method as updated in the rule for ExampleBond_A as it has the relevant property whereas ExampleBond_B has amortised cost based on NoAmortisation method" + ] + }, + { + "cell_type": "markdown", + "id": "4da21ed6-881f-4554-9c60-354ec2b7544c", + "metadata": {}, + "source": [ + "Now, setting Amortisation Rule filter based on Portfolio property. This means that the amortisation method set in the rule defined will take precedence over the method set on portfolio creation, for the instruments belonging to that portfolio." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc0cc542-092a-4aa7-b9ed-a70c871ae97f", + "metadata": {}, + "outputs": [], + "source": [ + "#Set Amortisation Rules\n", + "\n", + "set_amt_rule = amortisation_api.set_amortisation_rules(\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " set_amortisation_rules_request=models.SetAmortisationRulesRequest(\n", + " rules_interval=models.RulesInterval(\n", + " effective_range={\n", + " \"fromDate\": \"0001-01-01T00:00:00.0000000+00:00\",\n", + " },\n", + " rules=[\n", + " {\n", + " \"name\" : \"Amortisation_Rule_2\",\n", + " \"description\" : \"Rule Description\",\n", + " \"filter\" : \"properties[Portfolio/valuation-sample/EQUITY_UK_1] eq 'alpha1'\",\n", + " \"amortisationMethod\" : \"EffectiveYield\",\n", + " },\n", + " \n", + " ]\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ccd9dac-435b-46f7-9498-9c2759627b65", + "metadata": {}, + "outputs": [], + "source": [ + "#Getting Valuation\n", + "\n", + "aggregation = aggregation_api.get_valuation(\n", + " valuation_request=generate_valuation_request(datetime(year=2024, month=4, day=25, tzinfo=pytz.UTC)\n", + " )\n", + ")\n", + "\n", + "pd.DataFrame(aggregation.data)" + ] + }, + { + "cell_type": "markdown", + "id": "d079ced5-9f85-41b4-9293-23c26197f08a", + "metadata": {}, + "source": [ + "We can see that the Amortised Cost is calculated based on EffectiveYield method as updated in the rule." + ] + }, + { + "cell_type": "markdown", + "id": "f163318a-9439-4f59-9836-7989104bfc0f", + "metadata": {}, + "source": [ + "Now, we will show that when multiple rules exist, the first one that matches is implemented." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4ca98a9-1d46-47b1-a0d4-7bfcccfb5ad1", + "metadata": {}, + "outputs": [], + "source": [ + "#Set Amortisation Rules\n", + "\n", + "set_amt_rule = amortisation_api.set_amortisation_rules(\n", + " scope = scope,\n", + " code = portfolio_code,\n", + " set_amortisation_rules_request=models.SetAmortisationRulesRequest(\n", + " rules_interval=models.RulesInterval(\n", + " effective_range={\n", + " \"fromDate\": \"0001-01-01T00:00:00.0000000+00:00\",\n", + " },\n", + " rules=[\n", + " {\n", + " \"name\" : \"Amortisation_Rule_1\",\n", + " \"description\" : \"Rule Description\",\n", + " \"filter\" : \"properties[Instrument/valuation-sample/EQUITY_UK_1] eq 'alpha1'\",\n", + " \"amortisationMethod\" : \"StraightLine\",\n", + " },\n", + " {\n", + " \"name\" : \"Amortisation_Rule_2\",\n", + " \"description\" : \"Rule Description\",\n", + " \"filter\" : \"properties[Portfolio/valuation-sample/EQUITY_UK_1] eq 'alpha1'\",\n", + " \"amortisationMethod\" : \"EffectiveYield\",\n", + " },\n", + " \n", + " ]\n", + " )\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33dcb965-54d8-488b-bdc6-5ecb8c8d84bc", + "metadata": {}, + "outputs": [], + "source": [ + "#Getting Valuation\n", + "\n", + "aggregation = aggregation_api.get_valuation(\n", + " valuation_request=generate_valuation_request(datetime(year=2024, month=4, day=25, tzinfo=pytz.UTC)\n", + " )\n", + ")\n", + "\n", + "pd.DataFrame(aggregation.data)" + ] + }, + { + "cell_type": "markdown", + "id": "b7d031fa-9a9f-451c-b7f8-64b689bf4920", + "metadata": {}, + "source": [ + "We can see that the Amortised Cost is calculated based on StraightLine method for ExampleBond_A as it matches the first rule where as EffectiveYield method is used for ExampleBond_B as it matches the second rule in the list." + ] + }, + { + "cell_type": "markdown", + "id": "699637a4-e94d-492d-ae8f-5c15c370cdd7", + "metadata": {}, + "source": [ + "# 10. Data Cleaning" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b7ae1d9-f0ff-470a-81d7-c82f414bcf3a", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "#Delete Instruments\n", + "def DeleteInstruments(luid): \n", + " try:\n", + " delete_instrument = instruments_api.delete_instrument(\n", + " identifier_type=\"LusidInstrumentId\", identifier= luid\n", + " )\n", + " \n", + " print(delete_instrument)\n", + " \n", + " except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fbcad5e-95d8-4d7c-a869-2398b4138e37", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Deleting Instruments\n", + "\n", + "DeleteInstruments(luid)\n", + "DeleteInstruments(luid_1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46f0ea57-113a-4a0a-bb9d-dd15bbbd77ec", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Rule\n", + "\n", + "delete_amt= amortisation_api.delete_amortisation_ruleset(\n", + " scope=scope,\n", + " code=portfolio_code\n", + ")\n", + "\n", + "delete_amt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "baee566b-9862-45bc-8ad7-a853b0b10683", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Portfolio\n", + "try:\n", + " delete_portfolio = portfolio_api.delete_portfolio(scope, portfolio_code)\n", + " \n", + " print(delete_portfolio)\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b56cd719-5555-4ba9-b3f8-e5efd2117f71", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Recipe\n", + "try:\n", + " delete_recipe = configuration_recipe_api.delete_configuration_recipe(\n", + " scope=scope,\n", + " code=portfolio_code,\n", + " )\n", + "\n", + " print(delete_recipe)\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3026aba6-3ad1-4695-addf-c0e591978d96", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Portfolio Properties\n", + "try:\n", + " delete_portfolio_prop = property_api.delete_property_definition(\n", + " domain= 'Portfolio',\n", + " scope= scope,\n", + " code= portfolio_code,\n", + " )\n", + " print(delete_portfolio_prop)\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4219244-1fd4-462d-9d74-f5e1a37a4f74", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Instrument Properties\n", + "try:\n", + " delete_instrument_prop = property_api.delete_property_definition(\n", + " domain= 'Instrument',\n", + " scope= scope,\n", + " code= portfolio_code,\n", + " )\n", + " print(delete_instrument_prop)\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07ebcd02-59ef-4678-bbb4-2e0830744f6b", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/use-cases/amortisation-ruleset/Transaction_Data.csv b/examples/use-cases/amortisation-ruleset/Transaction_Data.csv new file mode 100644 index 00000000..ba503fe9 --- /dev/null +++ b/examples/use-cases/amortisation-ruleset/Transaction_Data.csv @@ -0,0 +1,3 @@ +txn_id,type,trade_date,settlement_date,quantity,price,currency,client_id,total_consideration +txn001,Buy,2024-01-15T00:00:00Z,2024-01-15T00:00:00Z,1,1444,GBP,Example_Bond_A,0 +txn002,Buy,2024-01-15T00:00:00Z,2024-01-15T00:00:00Z,1,1444,GBP,Example_Bond_B,0 diff --git a/examples/use-cases/irr-calculation/IRR_Sample_Notebook.ipynb b/examples/use-cases/irr-calculation/IRR_Sample_Notebook.ipynb new file mode 100644 index 00000000..3b156c4f --- /dev/null +++ b/examples/use-cases/irr-calculation/IRR_Sample_Notebook.ipynb @@ -0,0 +1,814 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "dea4f0a4-1a34-4c2a-97df-bf90f660d899", + "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", + "\"\"\"IRR Valuation\n", + "\n", + "Attributes\n", + "----------\n", + "instruments\n", + "transactions\n", + "quotes\n", + "Recipe\n", + "IRR Valuation\n", + "\"\"\"\n", + "\n", + "toggle_code(\"Toggle Docstring\")" + ] + }, + { + "cell_type": "markdown", + "id": "ba3551db-87ce-48af-b029-214a79e67cd0", + "metadata": {}, + "source": [ + "# IRR Calculations\n", + "\n", + "In this example we demonstrate how Internal Rate of Return can be calculated using the GetValuation endpoint. We will set up a portfolio, create a simple instrument and upsert a number of related transactions. \n", + "\n", + "Once done, we will create a recipe for valuation and upsert quotes for the the simple instrument that we had created.\n", + "\n", + "Using the valuation function we will illustrate the calculation of the IRR for the series of cashflows." + ] + }, + { + "cell_type": "markdown", + "id": "a49c6cc8-8347-4092-ae4c-50e38ebf98c7", + "metadata": {}, + "source": [ + "## Table of Contents:\n", + "- 1. [Imports](#1.-Imports)\n", + "- 2. [LUSID APIs](#2.-LUSID-APIs)\n", + "- 3. [Portfolio Creation](#3.-Portfolio-Creation)\n", + "- 4. [Instrument Creation](#4.-Instrument-Creation)\n", + "- 5. [Upsert Transactions](#5.-Upsert-Transactions)\n", + "- 6. [Valuation Recipe Creation](#6.-Valuation-Recipe-Creation)\n", + "- 7. [Upserting Market Data / Quotes Creation](#7.-Upserting-Market-Data-/-Quotes-Creation)\n", + "- 8. [Valuation with IRR](#8.-Valuation-with-IRR)\n", + "- 9. [Data Cleaning](#9.-Data-Cleaning)" + ] + }, + { + "cell_type": "markdown", + "id": "ca52fa05-6dbe-496e-9e13-40202f614933", + "metadata": {}, + "source": [ + "# 1. Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2f3d179-2273-408f-9e7a-cf62c445f6a5", + "metadata": {}, + "outputs": [], + "source": [ + "# Import LUSID libraries\n", + "import lusid \n", + "import lusid.models as lm\n", + "\n", + "from lusidtools.pandas_utils.lusid_pandas import lusid_response_to_data_frame\n", + "\n", + "# Import Libraries\n", + "from datetime import datetime, timedelta\n", + "from lusidtools.lpt.lpt import to_date\n", + "import pytz\n", + "import pandas as pd\n", + "import numpy as np\n", + "import json\n", + "import os\n", + "from lusidtools.cocoon.utilities import create_scope_id\n", + "from lusidtools.cocoon.cocoon import load_from_data_frame\n", + "from lusidtools.cocoon.cocoon_printer import (\n", + " format_instruments_response,\n", + " format_portfolios_response,\n", + " format_transactions_response,\n", + " format_quotes_response,\n", + " format_holdings_response,\n", + ")\n", + "from lusidtools.jupyter_tools import toggle_code\n", + "from lusidjam.refreshing_token import RefreshingToken\n", + "\n", + "# Settings and utility functions to display objects and responses more clearly.\n", + "pd.set_option('float_format', '{:,.4f}'.format)\n", + "\n", + "# Set the secrets path\n", + "secrets_path = os.getenv(\"FBN_SECRETS_PATH\")\n", + "\n", + "# Initiate an API Factory which is the client side object for interacting with LUSID APIs\n", + "api_factory = lusid.utilities.ApiClientFactory(\n", + " token=RefreshingToken(),\n", + " api_secrets_filename = secrets_path,\n", + " app_name=\"LusidJupyterNotebook\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "dd902deb-5c00-409b-879c-63f2ac793ed1", + "metadata": {}, + "source": [ + "# 2. LUSID APIs\n", + "\n", + "Firstly, we initialize the LUSID APIs required for the notebook" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ecfff26-4283-4cbf-8023-8bf6bc66c2cd", + "metadata": {}, + "outputs": [], + "source": [ + "# Initiate the LUSID APIs required for the notebook\n", + "instruments_api = api_factory.build(lusid.api.InstrumentsApi)\n", + "transaction_portfolio_api = api_factory.build(lusid.api.TransactionPortfoliosApi)\n", + "portfolio_api = api_factory.build(lusid.api.PortfoliosApi)\n", + "quotes_api = api_factory.build(lusid.api.QuotesApi)\n", + "configuration_recipe_api = api_factory.build(lusid.api.ConfigurationRecipeApi)\n", + "aggregation_api = api_factory.build(lusid.AggregationApi)" + ] + }, + { + "cell_type": "markdown", + "id": "cb46e528-6f37-432a-ad47-28c55aed886a", + "metadata": {}, + "source": [ + "# 3. Portfolio Creation\n", + "\n", + "We proceed by creating a basic transaction portfolio:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3174573e-6a2f-4299-bfda-174eb6ac7ae0", + "metadata": {}, + "outputs": [], + "source": [ + "portfolio_scope= \"Analytics-Examples1\"\n", + "portfolio_code=\"IRR-Notebook-Equity1\"\n", + "portfolio_name=\"IRR-Notebook-Equity1\"\n", + "instrument_scope= \"TestIRR1\"\n", + "effective_at = datetime(2024, 5, 27, 0, 0, tzinfo=pytz.utc)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5397479-ccc4-4227-a07a-67898dc60e79", + "metadata": {}, + "outputs": [], + "source": [ + "def create_portfolio(scope, portfolio_code, name,instrument_scope):\n", + "\n", + " pf_df = pd.DataFrame(data=[\n", + " {\"portfolio_code\": portfolio_code, \"portfolio_name\": name, \"instrument_scope\": instrument_scope},\n", + " ])\n", + " \n", + " portfolio_mapping = {\n", + " \"required\": {\n", + " \"code\": \"portfolio_code\",\n", + " \"display_name\": \"portfolio_name\",\n", + " \"base_currency\": \"$USD\",\n", + " \"instrument_scopes\": \"instrument_scope\"\n", + " },\n", + " \"optional\": {\n", + " \"created\": f\"${'01-01-2024'}\"\n", + " },\n", + " }\n", + " \n", + " result = load_from_data_frame(\n", + " api_factory=api_factory,\n", + " scope=scope,\n", + " data_frame=pf_df,\n", + " mapping_required=portfolio_mapping[\"required\"],\n", + " mapping_optional=portfolio_mapping[\"optional\"],\n", + " file_type=\"portfolios\",\n", + " )\n", + "\n", + " succ, failed = format_portfolios_response(result)\n", + " display(pd.DataFrame(data=[{\"success\": len(succ), \"failed\": len(failed)}])) " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7abe69ac-d122-42f9-9dbd-f3ae6d519491", + "metadata": {}, + "outputs": [], + "source": [ + "create_portfolio(portfolio_scope, portfolio_code, portfolio_name, instrument_scope)" + ] + }, + { + "cell_type": "markdown", + "id": "debaf103-f741-4847-a95e-7aed86bc2be3", + "metadata": {}, + "source": [ + "# 4. Instrument Creation\n", + "\n", + "We create an equity instruments using lumi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6a363e6-bfea-4b71-8d1c-2fff5cd36a62", + "metadata": {}, + "outputs": [], + "source": [ + "instr_df = pd.read_csv(\"IRR_instruments_upsert.csv\")\n", + "display(instr_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dff996fe-b814-4781-babe-ca7fcfa2fd9c", + "metadata": {}, + "outputs": [], + "source": [ + "instrument_mapping = {\n", + " \"identifier_mapping\": {\n", + " \"ClientInternal\": \"client_internal\",\n", + " },\n", + " \"required\": {\n", + " \"name\": \"instrument_name\"\n", + " },\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c05224e4-753c-4da0-a52f-8921ef4d023e", + "metadata": {}, + "outputs": [], + "source": [ + "result = load_from_data_frame(\n", + " api_factory=api_factory,\n", + " scope=portfolio_scope,\n", + " data_frame=instr_df,\n", + " mapping_required=instrument_mapping[\"required\"],\n", + " mapping_optional={},\n", + " file_type=\"instruments\",\n", + " identifier_mapping=instrument_mapping[\"identifier_mapping\"]\n", + ")\n", + "\n", + "succ, failed, errors = format_instruments_response(result)\n", + "pd.DataFrame(data=[{\"success\": len(succ), \"failed\": len(failed), \"errors\": len(errors)}])" + ] + }, + { + "cell_type": "markdown", + "id": "da54df0d-0f42-4c12-9e8e-3ab1edca7ad4", + "metadata": {}, + "source": [ + "# 5. Upsert Transactions\n", + "\n", + "We can enter into a position in the equity, buy 100 @ $400 on 1st March for MSFT_41" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68f12af7-42cd-474d-9a08-d0ee2985d194", + "metadata": {}, + "outputs": [], + "source": [ + "df_transac = pd.read_csv(\"IRR_transactions.csv\")\n", + "df_transac" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25837f5e-c79a-4ea3-b265-250ce761796f", + "metadata": {}, + "outputs": [], + "source": [ + "transaction_mapping = {\n", + " \"identifier_mapping\": {\"ClientInternal\": \"client_internal\",\"LusidInstrumentId\": \"instrument_id\"},\n", + " \"required\": {\n", + " \"code\": \"portfolio_code\",\n", + " \"transaction_id\": \"txn_id\",\n", + " \"type\": \"txn_type\",\n", + " \"transaction_price.price\": \"txn_price\",\n", + " \"transaction_price.type\": \"$Price\",\n", + " \"total_consideration.amount\": \"txn_consideration\",\n", + " \"units\": \"txn_units\",\n", + " \"transaction_date\": \"txn_trade_date\",\n", + " \"total_consideration.currency\": \"portfolio_base_currency\",\n", + " \"settlement_date\": \"txn_settle_date\",\n", + " },\n", + " \"optional\": {},\n", + " \"properties\": [],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4a30187-a2ca-4dfa-b51e-b65e14bee49f", + "metadata": {}, + "outputs": [], + "source": [ + "result = load_from_data_frame(\n", + " api_factory=api_factory,\n", + " scope=portfolio_scope,\n", + " data_frame=df_transac,\n", + " mapping_required=transaction_mapping[\"required\"],\n", + " mapping_optional=transaction_mapping[\"optional\"],\n", + " file_type=\"transactions\",\n", + " identifier_mapping=transaction_mapping[\"identifier_mapping\"],\n", + " property_columns=transaction_mapping[\"properties\"],\n", + ")\n", + "\n", + "succ, failed = format_transactions_response(result)\n", + "pd.DataFrame(\n", + " data=[{\"success\": len(succ), \"failed\": len(failed), \"errors\": len(errors)}]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "3c574de5-5c76-400b-9bc0-0c32c4f52779", + "metadata": {}, + "source": [ + "# 6. Valuation Recipe Creation\n", + "\n", + "Following the initial setup, we can see to configuring how LUSID will conduct valuation on the swap. This introduces the concept of recipes, which are a set of steps we specify to the valuation engine relating to market data and model specification." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b4732d8-9acf-4126-8886-3dfac7ea81f8", + "metadata": {}, + "outputs": [], + "source": [ + "recipe_code = \"TestIRR_RecipeCode1\"\n", + "recipe_scope = \"Analytics-Examples1\"\n", + "model_name = \"SimpleStatic\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4caeaad-68e3-477a-aea7-25f32594e093", + "metadata": {}, + "outputs": [], + "source": [ + "# Create two different recipes depending on the AllowPartiallySuccessfulEvaluation option\n", + "def UpsertRecipe(recipe_scope,recipe_code,model_name): \n", + " try:\n", + " configuration_recipe = lm.ConfigurationRecipe(\n", + " scope=recipe_scope,\n", + " code=recipe_code,\n", + " market=lm.MarketContext(\n", + " market_rules=[\n", + " lm.MarketDataKeyRule(\n", + " key=\"Quote.ClientInternal.*\",\n", + " supplier=\"Lusid\",\n", + " data_scope=recipe_scope,\n", + " quote_type=\"Price\",\n", + " field=\"mid\",\n", + " quote_interval=\"5D\",\n", + " )\n", + " ]\n", + " ),\n", + " pricing=lm.PricingContext(\n", + " model_rules=[\n", + " lm.VendorModelRule(\n", + " supplier = \"Lusid\",\n", + " model_name = model_name,\n", + " instrument_type = \"Equity\",\n", + " parameters = \"{}\",\n", + " )\n", + " ], \n", + " )\n", + " )\n", + " \n", + " upsert_configuration_recipe_response = configuration_recipe_api.upsert_configuration_recipe(\n", + " upsert_recipe_request=lm.UpsertRecipeRequest(\n", + " configuration_recipe=configuration_recipe\n", + " )\n", + " )\n", + " \n", + " print (f\"Recipe {recipe_code} Upserted Successfully!\")\n", + "\n", + " except lusid.ApiException as e:\n", + " print(f\"Recipie Creation Failed!\")\n", + " print(json.loads(e.body))\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4088901d-1d21-4370-8d44-d64bc1c62c04", + "metadata": {}, + "outputs": [], + "source": [ + "UpsertRecipe(recipe_scope,recipe_code,model_name)" + ] + }, + { + "cell_type": "markdown", + "id": "70922a18-8299-42a4-bbfb-533938371a85", + "metadata": {}, + "source": [ + "# 7. Upserting Market Data / Quotes Creation\n", + "We will be upserting quotes for the equity upserted earlier." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64677cb1-ca93-414b-9095-27451cf2ac77", + "metadata": {}, + "outputs": [], + "source": [ + "#For first instrument\n", + "equity_prices = pd.DataFrame({\n", + " 'date' :[\"2024-03-01\", \"2024-03-27\", \"2024-04-01\",\"2024-04-27\", \"2024-05-01\", \"2024-05-27\"],\n", + " 'price' : [410, 420, 420, 430,430,440]\n", + "})\n", + "equity_prices.insert(0, 'ClientInternal', 'MSFT_41')\n", + "equity_prices.insert(3, 'currency', 'USD')\n", + "\n", + "equity_prices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b85924c-22f2-4076-aa26-854cd60ef6de", + "metadata": {}, + "outputs": [], + "source": [ + "quotes_mapping = {\n", + " \"quote_id.effective_at\": \"date\",\n", + " \"quote_id.quote_series_id.provider\": \"$Lusid\",\n", + " \"quote_id.quote_series_id.quote_type\": \"$Price\",\n", + " \"quote_id.quote_series_id.instrument_id_type\": \"$ClientInternal\",\n", + " \"quote_id.quote_series_id.instrument_id\": \"ClientInternal\",\n", + " \"metric_value.unit\": \"currency\",\n", + " \"metric_value.value\": \"price\",\n", + " \"quote_id.quote_series_id.field\": \"$mid\",\n", + " \n", + "}\n", + "\n", + " \n", + "result = load_from_data_frame(\n", + " api_factory = api_factory,\n", + " scope=recipe_scope,\n", + " data_frame=equity_prices,\n", + " mapping_required=quotes_mapping,\n", + " mapping_optional={},\n", + " file_type=\"quotes\"\n", + ")\n", + "\n", + "\n", + "\n", + "succ, failed, errors = format_quotes_response(result)\n", + "display(pd.DataFrame(data=[{\"success\": len(succ), \"failed\": len(failed), \"errors\": len(errors)}]))" + ] + }, + { + "cell_type": "markdown", + "id": "e201c93e-7fb2-4916-b3a5-29e32d49e805", + "metadata": {}, + "source": [ + "# 8. Valuation with IRR" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3be2f7ee-27f5-4804-baa4-8e24b7405777", + "metadata": {}, + "outputs": [], + "source": [ + "#Function to get valuation\n", + "def get_valuation(date: datetime, portfolio_scope: str, portfolio_code: str, recipe_scope: str, recipe_code: str, metrics: list, groupBy: str=[\"Instrument/default/Name\"]) -> pd.DataFrame: \n", + " \n", + " try:\n", + " valuation_request = lm.ValuationRequest(\n", + " recipe_id=lm.ResourceId(\n", + " scope=recipe_scope,\n", + " code=recipe_code\n", + " ),\n", + " metrics=metrics,\n", + " group_by=groupBy,\n", + " portfolio_entity_ids=[\n", + " lm.PortfolioEntityId(scope=portfolio_scope, code=portfolio_code)\n", + " ],\n", + " valuation_schedule=lm.ValuationSchedule(effective_at=date.isoformat()),\n", + " )\n", + " \n", + " val_response = aggregation_api.get_valuation(valuation_request=valuation_request)\n", + " val_data = val_response.data\n", + " vals_df = pd.DataFrame(val_data)\n", + " \n", + " return vals_df\n", + " \n", + " except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"errorDetails\"][0][\"id\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5285a600-6180-4ae2-b3ed-c2ab8b83d70f", + "metadata": {}, + "outputs": [], + "source": [ + "# Set the metrics to be requested from valuation\n", + "metrics = [\n", + " lm.AggregateSpec(\"Instrument/default/Name\", \"Value\"),\n", + " lm.AggregateSpec(\"Instrument/default/ClientInternal\", \"Value\"),\n", + " lm.AggregateSpec(\"Valuation/PV\", \"Value\"),\n", + " lm.AggregateSpec(\"ProfitAndLoss/PortfolioInternalRateOfReturn\", \"Value\", {\"Window\" : \"MTD\"}),\n", + " lm.AggregateSpec(\"Holding/default/Units\", \"Value\")\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67840aee-06ae-43d9-9de5-7feaf23f46be", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "df = get_valuation(effective_at, portfolio_scope,portfolio_code,recipe_scope,recipe_code,metrics,[\"Instrument/default/Name\"])\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "d860c351-af55-41d4-90c1-28aacf9ad766", + "metadata": {}, + "source": [ + "# 8.1 IRR Explained\n", + "We get a Portfolio IRR of 38%, to validate this, we first confirm the valuation at the start of the Month" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76600bd9-a1ca-469d-ad5f-647192179a7e", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "df_start = get_valuation(datetime(2024, 5, 1, 0, 0, tzinfo=pytz.utc), portfolio_scope,portfolio_code,recipe_scope,recipe_code,[ lm.AggregateSpec(\"Instrument/default/Name\", \"Value\"), lm.AggregateSpec(\"Valuation/PV\", \"Value\") ],[\"Portfolio/default/Name\"])\n", + "\n", + "df_start" + ] + }, + { + "cell_type": "markdown", + "id": "f548883e-76ce-45b3-bc07-863ce12b4966", + "metadata": {}, + "source": [ + "We have a value of 43,000 on 1st May 2024 (which is expected as the stock was $430) and then a final value of 44,000 on 27th May 2024.\n", + "\n", + "The IRR value of 38\\% can be validated in Excel using XIRR(), note the inital value should be set to -43,000.\n", + "\n", + "We can also confirm it here by showing that:-43,000 + 44,000 / (1+irr) ^ (26 / 365) = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e104f68-a231-4e85-b054-638382084cdb", + "metadata": {}, + "outputs": [], + "source": [ + "irr = df.iloc[0,3]\n", + "days = 27 - 1\n", + "\n", + "divisor = (1+irr)**(days/365)\n", + "\n", + "-43_000 + 44_000 / divisor" + ] + }, + { + "cell_type": "markdown", + "id": "4003a064-e69a-4baf-9eb3-c3e14628a7cc", + "metadata": {}, + "source": [ + "# 9. Data Cleaning\n", + "The following chunks of code help you clean data by deleting recipes, quotes, instruments and portfolio created during the above sample\n", + "\n", + "(for quotes and instruments you have to specify the instrument individually and effective date for quotes must match the effective date at time of creation)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "613418d8-a1d0-4e3b-bca5-553ed1f41b9b", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Recipe\n", + "try:\n", + " delete_recipe = configuration_recipe_api.delete_configuration_recipe(\n", + " scope=recipe_scope,\n", + " code=recipe_code,\n", + " )\n", + "\n", + " print(delete_recipe)\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a61fd2e1-00e2-4ff6-ad11-ee34d86ae8cb", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Quotes\n", + "\n", + "#Have to run this for individual instruments by changing instrument_id\n", + "\n", + "try:\n", + " delete_quotes = quotes_api.delete_quotes(\n", + " scope=recipe_scope,\n", + " request_body={ \n", + " \"request_1\": lm.QuoteId(\n", + " quote_series_id=lm.QuoteSeriesId(\n", + " provider='Lusid', \n", + " quote_type='Price',\n", + " instrument_id_type= 'ClientInternal',\n", + " instrument_id= 'MSFT_41',\n", + " field='mid'\n", + " ),\n", + " effective_at=\"2024-03-01T00:00:00Z\"\n", + " ),\n", + " \"request_2\": lm.QuoteId(\n", + " quote_series_id=lm.QuoteSeriesId(\n", + " provider='Lusid', \n", + " quote_type='Price',\n", + " instrument_id_type= 'ClientInternal',\n", + " instrument_id= 'MSFT_41',\n", + " field='mid'\n", + " ),\n", + " effective_at=\"2024-03-27T00:00:00Z\"\n", + " ),\n", + " \"request_3\": lm.QuoteId(\n", + " quote_series_id=lm.QuoteSeriesId(\n", + " provider='Lusid', \n", + " quote_type='Price',\n", + " instrument_id_type= 'ClientInternal',\n", + " instrument_id= 'MSFT_41',\n", + " field='mid'\n", + " ),\n", + " effective_at=\"2024-04-01T00:00:00Z\"\n", + " ),\n", + " \"request_4\": lm.QuoteId(\n", + " quote_series_id=lm.QuoteSeriesId(\n", + " provider='Lusid', \n", + " quote_type='Price',\n", + " instrument_id_type= 'ClientInternal',\n", + " instrument_id= 'MSFT_41',\n", + " field='mid'\n", + " ),\n", + " effective_at=\"2024-04-27T00:00:00Z\"\n", + " ),\n", + " \"request_5\": lm.QuoteId(\n", + " quote_series_id=lm.QuoteSeriesId(\n", + " provider='Lusid', \n", + " quote_type='Price',\n", + " instrument_id_type= 'ClientInternal',\n", + " instrument_id= 'MSFT_41',\n", + " field='mid'\n", + " ),\n", + " effective_at=\"2024-05-01T00:00:00Z\"\n", + " ),\n", + " \"request_6\": lm.QuoteId(\n", + " quote_series_id=lm.QuoteSeriesId(\n", + " provider='Lusid', \n", + " quote_type='Price',\n", + " instrument_id_type= 'ClientInternal',\n", + " instrument_id= 'MSFT_41',\n", + " field='mid'\n", + " ),\n", + " effective_at=\"2024-05-27T00:00:00Z\"\n", + " )\n", + " \n", + " \n", + " }\n", + " )\n", + " \n", + " print(delete_quotes)\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1366c6c4-b180-42fd-b5e8-cdeb25e5d6b3", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Instruments\n", + "\n", + "#Have to run this for individual instruments by changing identifier value\n", + "\n", + "try:\n", + " delete_instrument = instruments_api.delete_instrument(\n", + " identifier_type=\"ClientInternal\", identifier= 'MSFT_41'\n", + " )\n", + "\n", + " print(delete_instrument)\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93ffd96f-82bb-416b-9b2c-bf7381056c16", + "metadata": {}, + "outputs": [], + "source": [ + "'''\n", + "#Delete Portfolio\n", + "try:\n", + " delete_portfolio = portfolio_api.delete_portfolio(portfolio_scope, portfolio_code)\n", + " \n", + " print(delete_portfolio)\n", + "\n", + "except lusid.ApiException as e:\n", + " print(json.loads(e.body)[\"title\"])\n" + ] + } + ], + "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.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/use-cases/irr-calculation/IRR_instruments_upsert.csv b/examples/use-cases/irr-calculation/IRR_instruments_upsert.csv new file mode 100644 index 00000000..6fbf9e05 --- /dev/null +++ b/examples/use-cases/irr-calculation/IRR_instruments_upsert.csv @@ -0,0 +1,2 @@ +instrument_name,client_internal,currency,figi,exchange_code,ticker,market_sector +Microsoft_41,MSFT_41,USD,BBG000BVPXP1,UN,msft_41,equity diff --git a/examples/use-cases/irr-calculation/IRR_transactions.csv b/examples/use-cases/irr-calculation/IRR_transactions.csv new file mode 100644 index 00000000..ebcc3b1e --- /dev/null +++ b/examples/use-cases/irr-calculation/IRR_transactions.csv @@ -0,0 +1,3 @@ +portfolio_code,portfolio_name,portfolio_base_currency,instrument_id,client_internal,txn_id,txn_type,txn_trade_date,txn_settle_date,txn_units,txn_price,txn_consideration +IRR-Notebook-Equity1,IRR-Notebook-Equity1,USD,CCY_USD,,TXN_IRRSample_1,FundsIn,01/03/2024,01/03/2024,40000,0,40000 +IRR-Notebook-Equity1,IRR-Notebook-Equity1,USD,,MSFT_41,TXN_IRRSample_2,Buy,01/03/2024,01/03/2024,100,400,40000