{ "cells": [ { "cell_type": "markdown", "id": "f8e67c23", "metadata": {}, "source": [ "### Distill the verbose waterfall in CLO\n", "\n", "When modeling with `absbox`, the rookie may model the waterfall payment action with 1:1 maping to deal prospectus.\n", "\n", "That's `OKayish` as it is clear and straightforward. But it just increase the burden of mind which is not `absbox` 's intention.\n", "\n", "With a `data-drive` design of `absbox`, the whole deal model can be very clean and straightforward.\n", "\n", "I'll demo a `fairly-complex` CLO deal to show how to distill the verbose waterfall.\n", "\n", "* FIDELITY GRAND HARBOUR CLO 2023-1 DAC Prospectus : https://live.euronext.com/en/product/bonds-detail/30218/documents \n", "\n", "#### Model Bonds\n", "\n", "It can be very clear just model the bonds with `absbox` syntax, but we just assign it to a variable `bonds` for later use.\n", "\n", "And create a `bndNames` holding the bond names for future reference.\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "cd5b455d", "metadata": {}, "outputs": [], "source": [ "bonds = (\n", " (\"AR\",{\"balance\":198_000_000\n", " ,\"rate\":0.07\n", " ,\"originBalance\":198_000_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"floater\":[0.05,\"EURIBOR3M\",0.0123,\"QuarterEnd\"]}\n", " ,\"bondType\":{\"Sequential\":None}})\n", " ,(\"B1R\",{\"balance\":29_000_000\n", " ,\"rate\":0.0\n", " ,\"originBalance\":29_000_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"floater\":[0.05,\"EURIBOR3M\",0.0175,\"QuarterEnd\"]}\n", " ,\"bondType\":{\"Sequential\":None}\n", " })\n", " ,(\"B2R\",{\"balance\":15_000_000\n", " ,\"rate\":0.046\n", " ,\"originBalance\":15_000_000\n", " ,\"originRate\":0.046\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"Fixed\":0.046}\n", " ,\"bondType\":{\"Sequential\":None}\n", " })\n", " ,(\"CR\",{\"balance\":24_000_000\n", " ,\"rate\":0.0\n", " ,\"originBalance\":24_000_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"floater\":[0.05,\"EURIBOR3M\",0.0205,\"QuarterEnd\"]}\n", " ,\"bondType\":{\"Sequential\":None}\n", " })\n", " ,(\"DR\",{\"balance\":28_000_000\n", " ,\"rate\":0.0\n", " ,\"originBalance\":28_000_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"floater\":[0.05,\"EURIBOR3M\",0.027,\"QuarterEnd\"]}\n", " ,\"bondType\":{\"Sequential\":None}\n", " })\n", " ,(\"ER\",{\"balance\":18_000_000\n", " ,\"rate\":0.0\n", " ,\"originBalance\":18_000_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"floater\":[0.05,\"EURIBOR3M\",0.0475,\"QuarterEnd\"]}\n", " ,\"bondType\":{\"Sequential\":None}\n", " })\n", " ,(\"FR\",{\"balance\":12_000_000\n", " ,\"rate\":0.0\n", " ,\"originBalance\":12_000_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"floater\":[0.05,\"EURIBOR3M\",0.0757,\"QuarterEnd\"]}\n", " ,\"bondType\":{\"Sequential\":None}\n", " })\n", " ,(\"SUB\",{\"balance\":28_427_000\n", " ,\"rate\":0.0\n", " ,\"originBalance\":28_427_000\n", " ,\"originRate\":0.07\n", " ,\"startDate\":\"2025-02-18\"\n", " ,\"rateType\":{\"Fixed\":0.00}\n", " ,\"bondType\":{\"Equity\":None}\n", " ,'stmt': [\n", " [\"2025-02-18\",0,0,0,0,-28_427_000,0,0,None,\"\"]\n", " ]\n", " }) \n", ")\n", "## Tricks: extract the bond names for later use\n", "bndNames = [ bn for bn,_ in bonds ]" ] }, { "cell_type": "markdown", "id": "32c4dfec", "metadata": {}, "source": [ "`Adjusted_Collateral_Principal_Amount` is defined as the sum of the principal amount of the collateral pool and the accrued interest of the collateral pool. It is being used for OC ratio." ] }, { "cell_type": "code", "execution_count": 16, "id": "d77f50fe", "metadata": {}, "outputs": [], "source": [ "Adjusted_Collateral_Principal_Amount = (\"sum\", (\"poolBalance\",),(\"accountBalance\",\"prinAcc\"),(\"poolAccruedInterest\",)) " ] }, { "cell_type": "markdown", "id": "de97515d", "metadata": {}, "source": [ "#### Model Coverage Test Triggers\n", "\n", "One of the verbose items in the waterfall is the coverage test. To model each test, a rookie may follow each item and build a single expression for each threshold test.\n", "\n", "**Class Required Par Value Ratio**\n", "\n", "| Class | Required Ratio |\n", "|---|---:|\n", "| A/B | 127.99% |\n", "| C | 119.58% |\n", "| D | 110.28% |\n", "| E | 105.50% |\n", "| F | 102.45% |\n", "\n", "**Class Required Interest Coverage Ratio**\n", "\n", "| Class | Required Ratio |\n", "|---|---:|\n", "| A/B | 120.0% |\n", "| C | 110.0% |\n", "| D | 105.0% |\n", "\n", "#### Par Value ratios\n", "\n", "The verbose way to model each formula to represent the Par Value Ratio" ] }, { "cell_type": "code", "execution_count": 4, "id": "c3c20744", "metadata": {}, "outputs": [], "source": [ "Class_A_B_Par_Value_Ratio = (\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",\"AR\",\"B1R\",\"B2R\")) \n", "TargetAB = (\"ratio\", Adjusted_Collateral_Principal_Amount, (\"const\", 1.2799 ))\n", "\n", "Class_C_Par_Value_Ratio = (\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",\"AR\",\"B1R\",\"B2R\",\"CR\"))\n", "TargetC = (\"ratio\", Adjusted_Collateral_Principal_Amount, (\"const\", 1.1958 ))\n", "\n", "Class_D_Par_Value_Ratio = (\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",\"AR\",\"B1R\",\"B2R\",\"CR\",\"DR\"))\n", "TargetD = (\"ratio\", Adjusted_Collateral_Principal_Amount, (\"const\", 1.1028 ))\n", "\n", "Class_E_Par_Value_Ratio = (\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",\"AR\",\"B1R\",\"B2R\",\"CR\",\"DR\",\"ER\"))\n", "TargetE = (\"ratio\", Adjusted_Collateral_Principal_Amount, (\"const\", 1.055 ))\n", "\n", "Class_F_Par_Value_Ratio = (\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",\"AR\",\"B1R\",\"B2R\",\"CR\",\"DR\",\"ER\",\"FR\"))\n", "TargetF = (\"ratio\", Adjusted_Collateral_Principal_Amount, (\"const\", 1.0245 ))\n" ] }, { "cell_type": "markdown", "id": "3f86bc99", "metadata": {}, "source": [ "Oh man, that's *verbose* ! \n", "\n", "If you look the pattern carefully, the only `variable` part is for each `threshold` ,there is associate with a list of bond names.i.e (1.2799 vs \"AR\",\"B1R\",\"B2R\") \n", "\n", "Let's simplify it with bond name list with the candy function from `absbox`" ] }, { "cell_type": "code", "execution_count": 5, "id": "4b69ed4a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[['AR', 'B1R', 'B2R', 'CR', 'DR', 'ER', 'FR'],\n", " ['AR', 'B1R', 'B2R', 'CR', 'DR', 'ER'],\n", " ['AR', 'B1R', 'B2R', 'CR', 'DR'],\n", " ['AR', 'B1R', 'B2R', 'CR'],\n", " ['AR', 'B1R', 'B2R']]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from absbox.local.util import genDescList\n", "\n", "genDescList(bndNames[:-1],5)" ] }, { "cell_type": "markdown", "id": "c25f344d", "metadata": {}, "source": [ "#### OC Formulas \n", "\n", "With the shortcut function above ,we can build ratios list like " ] }, { "cell_type": "code", "execution_count": 18, "id": "f3f3e942", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true } }, "outputs": [ { "data": { "text/plain": [ "[[('ratio',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " ('bondBalance', 'AR', 'B1R', 'B2R')),\n", " '<=',\n", " 1.2799],\n", " [('ratio',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " ('bondBalance', 'AR', 'B1R', 'B2R', 'CR')),\n", " '<=',\n", " 1.1958],\n", " [('ratio',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR')),\n", " '<=',\n", " 1.1028],\n", " [('ratio',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER')),\n", " '<=',\n", " 1.055],\n", " [('ratio',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER', 'FR')),\n", " '<=',\n", " 1.0245]]" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[ \n", " [(\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",*bndNames)),\"<=\", threshold]\n", " \n", " for (n, threshold, bndNames) in \n", " zip([\"OC_AB\",\"OC_C\",\"OC_D\",\"OC_E\",\"OC_F\"],[1.2799,1.1958,1.1028,1.055,1.0245],genDescList(bndNames[:-1],5,reverse=True))\n", "]" ] }, { "cell_type": "markdown", "id": "86ec5dcb", "metadata": {}, "source": [ "Now with a simple dictionary comprehension, we plug the data into a `trigger syntax` ({`condition`:...,`effects`:None,`status`:False,`curable`:True})\n", "\n", "Now you just build the 5 OC triggers! \n", "\n", "#### OC Triggers" ] }, { "cell_type": "code", "execution_count": 21, "id": "1f1fecd5", "metadata": {}, "outputs": [], "source": [ "OC_Triggers = \\\n", "{n: \n", " {\n", " \"condition\":[\"all\"\n", " ,[(\"sum\",(\"bondBalance\",*bndNames)),\">\",0]\n", " ,[(\"ratio\", Adjusted_Collateral_Principal_Amount , (\"bondBalance\",*bndNames)) \n", " ,\"<=\"\n", " ,threshold]\n", " ],\n", " \"effects\":None,\n", " \"status\":False,\n", " \"curable\":True\n", " } \n", " for (n, threshold, bndNames) in \n", " zip([\"OC_AB\",\"OC_C\",\"OC_D\",\"OC_E\",\"OC_F\"],[1.2799,1.1958,1.1028,1.055,1.0245],genDescList(bndNames[:-1],5,reverse=True))\n", "}" ] }, { "cell_type": "markdown", "id": "212304f9", "metadata": {}, "source": [ "Same method for IC tests trigger !\n", "\n", "#### IC Triggers" ] }, { "cell_type": "code", "execution_count": 22, "id": "ef79f6b6", "metadata": {}, "outputs": [], "source": [ "IC_Triggers = \\\n", "{n: \n", " {\n", " \"condition\":[\"all\"\n", " ,[(\"sum\",(\"bondDueInt\",*bndNames)),\">\",0]\n", " ,[(\"ratio\", (\"accountBalance\",'intAcc') , (\"bondDueInt\",*bndNames)) \n", " ,\"<=\"\n", " ,threshold]\n", " ],\n", " \"effects\":None,\n", " \"status\":False,\n", " \"curable\":True\n", " } \n", " for (n, threshold, bndNames) in \n", " zip([\"IC_AB\",\"IC_C\",\"IC_D\"],[1.20,1.1,1.05],genDescList(bndNames[:-1],3,reverse=True))\n", "}" ] }, { "cell_type": "markdown", "id": "05751928", "metadata": {}, "source": [ "Now, we'd done with the Coverage Tests. Let's move to the interest payment waterfall. \n", "\n", "The crazy part of CLO waterfall is its really verbose /repeated waterfall as legal terms shall be rigids. The total steps of Interest Payment waterfall is 26 ! ( from A -> Z) , I do feel future CLO may run out of alphabets .\n", "\n", "That's say, if go with `rookie` way, there are 26 lists in the deal model ! that's crazy ! The good news : we are able to identify the repeated part and abstract them out. For example:\n", "\n", "\n", "> 5. Coverage Test Cures (Class A/B)\n", "> \n", "> (H) Coverage Test Cure: If certain financial ratios (Par Value/Interest Coverage tests) are failing, funds are used to redeem senior debt until the tests are satisfied.\n", "> \n", "> 6. Junior Mezzanine Note Interest (Classes C, D, E, F)\n", "> \n", "> (I) Class C Interest: Payment of current interest on Class C Notes.\n", "> \n", "> (J) Class C Deferred Interest: Payment of any previously deferred interest on Class C Notes.\n", "> \n", "> (K) Coverage Test Cure (Class C): Cure for Class C coverage tests.\n", "> \n", "> (L) Class D Interest: Payment of current interest on Class D Notes.\n", "> \n", "> (M) Class D Deferred Interest: Payment of any previously deferred interest on Class D Notes.\n", "> \n", "> (N) Coverage Test Cure (Class D): Cure for Class D coverage tests.\n", "> \n", "> (O) Class E Interest: Payment of current interest on Class E Notes.\n", "> \n", "> (P) Class E Deferred Interest: Payment of any previously deferred interest on Class E Notes.\n", "> \n", "> (Q) Coverage Test Cure (Class E): Cure for Class E coverage tests.\n", "> \n", "> (R) Class F Interest: Payment of current interest on Class F Notes.\n", "> \n", "> (S) Class F Deferred Interest: Payment of any previously deferred interest on Class F Notes.\n", "> \n", "> (T) Coverage Test Cure (Class F): Cure for Class F coverage tests (post-reinvestment period).\n", "> \n", "> 7. Rating Agency Cure\n", "> \n", "> (U) Rating Event Cure: Redemption of debt if a specific \"Rating Event\" is ongoing.\n", "\n", "Here, in the steps above , there is a repeated pattern for class C/D/E/F:\n", "\n", " 1. Class X Interest\n", " 2. Class X Deferred Interest\n", " 3. Run Coverage Test on Class X and Cure\n", "\n", "Then we can compress (4*3) steps into a loop!\n", "\n", "| Feature | Class C Notes | Class D Notes | Class E Notes | Class F Notes |\n", "| :-------------------------- | :------------------------------------------------------- | :------------ | :------------ | :------------ |\n", "| **Par Value Test Ratio** | 127.99% (for A/B) then 119.58% | **110.28%** | **105.50%** | **102.45%** |\n", "| **Interest Coverage Test** | **Yes** (110.0%) | **Yes** (105.0%) | **No** | **No** |\n", "| **When Par Value Test Applies** | On and after the Effective Date | On and after the Effective Date | On and after the Effective Date | **After Reinvestment Period** |\n", "| **Subordination** | Subordinated to A & B | Subordinated to A, B, & C | Subordinated to A, B, C, & D | Subordinated to A, B, C, D, & E |\n", "\n", "In code below, it says:\n", "\n", "1. accure and pay interest for class `x` with account `interestAcc`\n", "2. if any `trigger` (for IC and OC) is failed, then repay the principal portion of class\n", " * the repayment amount is defined by a upper limit, hard code to 100.00" ] }, { "cell_type": "code", "execution_count": 3, "id": "7600e238-91ff-4ab6-84bb-5902f87cbede", "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "[[['accrueAndPayInt', 'interestAcc', ['C']],\n", " ['if',\n", " ['any',\n", " ('trigger', 'InDistribution', 'OC_C'),\n", " ('trigger', 'InDistribution', 'IC_C')],\n", " ['payPrin', 'interestAcc', ['C'], {'ds': ('const', 100)}]]],\n", " [['accrueAndPayInt', 'interestAcc', ['D']],\n", " ['if',\n", " ['any',\n", " ('trigger', 'InDistribution', 'OC_D'),\n", " ('trigger', 'InDistribution', 'IC_D')],\n", " ['payPrin', 'interestAcc', ['D'], {'ds': ('const', 100)}]]],\n", " [['accrueAndPayInt', 'interestAcc', ['E']],\n", " ['if',\n", " ['any', ('trigger', 'InDistribution', 'OC_E'), ('always', False)],\n", " ['payPrin', 'interestAcc', ['E'], {'ds': ('const', 100)}]]],\n", " [['accrueAndPayInt', 'interestAcc', ['F']],\n", " ['if',\n", " ['any', ('trigger', 'InDistribution', 'OC_F'), ('always', False)],\n", " ['payPrin', 'interestAcc', ['F'], {'ds': ('const', 100)}]]]]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[\n", " [\n", " [\"accrueAndPayInt\",'interestAcc', [x] ]\n", " ,[\"if\",[\"any\",(\"trigger\",\"InDistribution\",ocTrigger),(\"trigger\",\"InDistribution\",icTrigger) if icTrigger else (\"always\",False)]\n", " , [\"payPrin\",'interestAcc',[x],{\"limit\":{\"ds\": (\"const\",100)}}]\n", " ]\n", " ]\n", " for x,ocTrigger,icTrigger in zip(['C','D','E','F']\n", " ,[\"OC_C\",\"OC_D\",\"OC_E\",\"OC_F\"]\n", " ,[\"IC_C\",\"IC_D\",None,None])\n", "]" ] }, { "cell_type": "markdown", "id": "859ddb08-4ecb-4d34-a8f5-5886d621c3e4", "metadata": {}, "source": [ "Now we can spot the hardcode value `(const,100)`, in the `payPrin`, there is a dict with key `limit` to describle how much cash to pay down the principal portion of the bond.\n", "\n", "But how to describle the amount to be paid out ? we are using a `formula` which is a dict with `{\"ds\": }` to describle the cure amount .\n", "\n", "#### Build Cure Amount Formula\n", "\n", "wit a little algebra transformation, the amount to cure the test will be:\n", "> max(0, sum of bond balance - (expected bond balance))\n", ">> expected bond balance = Adjusted_Collateral_Principal_Amount / threshold\n", ">>\n", "\n", "I'll make it a dict, then it can be referenced by name later" ] }, { "cell_type": "code", "execution_count": 11, "id": "a2b91211-17cf-405c-a3a2-40f2c5b87a41", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'OC_AB': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.2799)),\n", " 'OC_C': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.1958)),\n", " 'OC_D': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.1028)),\n", " 'OC_E': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.055)),\n", " 'OC_F': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER', 'FR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.0245))}" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cureFormula = {ocName: (\"excess\"\n", " , (\"sum\", ('bondBalance',*bonds))\n", " , (\"/\", Adjusted_Collateral_Principal_Amount, threshold)\n", " )\n", " for (ocName,threshold,bonds) in \n", " zip([\"OC_AB\",\"OC_C\",\"OC_D\",\"OC_E\",\"OC_F\"],[1.2799,1.1958,1.1028,1.055,1.0245],genDescList(bndNames[:-1],5,reverse=True))\n", "}\n", "cureFormula" ] }, { "cell_type": "markdown", "id": "8c5b514f-cde7-476d-965d-aab8dea62cdb", "metadata": {}, "source": [ "Now, we can plugin back the data back to the waterfall actions" ] }, { "cell_type": "code", "execution_count": 14, "id": "7e65363f-7647-405e-8ed8-10cab458b685", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[['accrueAndPayInt', 'interestAcc', ['C']],\n", " ['if',\n", " ['any',\n", " ('trigger', 'InDistribution', 'OC_C'),\n", " ('trigger', 'InDistribution', 'IC_C')],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['C'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.1958))}}]]],\n", " [['accrueAndPayInt', 'interestAcc', ['D']],\n", " ['if',\n", " ['any',\n", " ('trigger', 'InDistribution', 'OC_D'),\n", " ('trigger', 'InDistribution', 'IC_D')],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['D'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.1028))}}]]],\n", " [['accrueAndPayInt', 'interestAcc', ['E']],\n", " ['if',\n", " ['any', ('trigger', 'InDistribution', 'OC_E'), ('always', False)],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['E'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.055))}}]]],\n", " [['accrueAndPayInt', 'interestAcc', ['F']],\n", " ['if',\n", " ['any', ('trigger', 'InDistribution', 'OC_F'), ('always', False)],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['F'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER', 'FR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.0245))}}]]]]" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "partialWaterfallActions = [\n", " [\n", " [\"accrueAndPayInt\",'interestAcc', [x] ]\n", " ,[\"if\",[\"any\",(\"trigger\",\"InDistribution\",ocTrigger),(\"trigger\",\"InDistribution\",icTrigger) if icTrigger else (\"always\",False)]\n", " , [\"payPrin\",'interestAcc',[x],{\"limit\":{\"ds\": cureFormula[ocTrigger] }}]\n", " ]\n", " ]\n", " for x,ocTrigger,icTrigger in zip(['C','D','E','F']\n", " ,[\"OC_C\",\"OC_D\",\"OC_E\",\"OC_F\"]\n", " ,[\"IC_C\",\"IC_D\",None,None])\n", "]\n", "partialWaterfallActions" ] }, { "cell_type": "markdown", "id": "4e09606d-bc9e-448e-b266-46c152822271", "metadata": {}, "source": [ "Wait, it's a nested list of action ! Let's just use the my favorite package `toolz` to flatten the list" ] }, { "cell_type": "code", "execution_count": 15, "id": "8bfe5e02-b992-4850-a6ff-49a9563d44b4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[['accrueAndPayInt', 'interestAcc', ['C']],\n", " ['if',\n", " ['any',\n", " ('trigger', 'InDistribution', 'OC_C'),\n", " ('trigger', 'InDistribution', 'IC_C')],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['C'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.1958))}}]],\n", " ['accrueAndPayInt', 'interestAcc', ['D']],\n", " ['if',\n", " ['any',\n", " ('trigger', 'InDistribution', 'OC_D'),\n", " ('trigger', 'InDistribution', 'IC_D')],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['D'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.1028))}}]],\n", " ['accrueAndPayInt', 'interestAcc', ['E']],\n", " ['if',\n", " ['any', ('trigger', 'InDistribution', 'OC_E'), ('always', False)],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['E'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.055))}}]],\n", " ['accrueAndPayInt', 'interestAcc', ['F']],\n", " ['if',\n", " ['any', ('trigger', 'InDistribution', 'OC_F'), ('always', False)],\n", " ['payPrin',\n", " 'interestAcc',\n", " ['F'],\n", " {'limit': {'ds': ('excess',\n", " ('sum', ('bondBalance', 'AR', 'B1R', 'B2R', 'CR', 'DR', 'ER', 'FR')),\n", " ('/',\n", " ('sum',\n", " ('poolBalance',),\n", " ('accountBalance', 'prinAcc'),\n", " ('poolAccruedInterest',)),\n", " 1.0245))}}]]]" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from toolz import concat\n", "list(concat(partialWaterfallActions))" ] }, { "cell_type": "markdown", "id": "ec571bc5-d5d7-417b-9999-90bf7113aa7b", "metadata": {}, "source": [ "Whoa, that's much cleaner way ! \n", "\n", "Happy hacking !" ] }, { "cell_type": "code", "execution_count": null, "id": "c6a39665-0bb1-41b9-87a6-e3ff7d9ab423", "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.13.3" } }, "nbformat": 4, "nbformat_minor": 5 }