Project cashflow with a pool of assets with Absbox
Quick Start:
Step 1: Connect to Engine
Again, user can connect to public server as well as his/her own server
It’s fairly easy that it just need a one-line command to pull a docker image or download executable from github directly.
from absbox import API,EnginePath
localAPI = API(EnginePath.DEV,lang="english",check=False)
Connecting engine server -> https://absbox.org/api/dev
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
✅Connected, local lib:0.52.3, server:0.52.3
Step 2: Prepare asset data
Asset data is just plain Python data structures: list and map, as long as they are assembled in a correct way. They shall be consumed by absbox without question.
why using Python’s basic type(list/map) ? Because , user may save the data in different storage place , maybe Redis, maybe mongodb, maybe just 1.44MB disk, or just pandas dataframe. The commonality is all data sources with variuos formats are able to be converted into Python’s basic strcutres.
ast1 = ["Mortgage"
,{"originBalance": 12000.0
,"originRate": ["fix",0.045]
,"originTerm": 120
,"freq": "monthly"
,"type": "level"
,"originDate": "2021-02-01"}
,{"currentBalance": 10000.0
,"currentRate": 0.075
,"remainTerm": 80
,"status": "current"}]
ast2 = ["Mortgage"
,{"originBalance": 12000.0
,"originRate": ["fix",0.045]
,"originTerm": 120
,"freq": "monthly"
,"type": "level"
,"originDate": "2021-02-01"}
,{"currentBalance": 10000.0
,"currentRate": 0.075
,"remainTerm": 80
,"status": "current"}]
Step 3: Construct Pool
Just build a map with fields:
assets: a list of assetscutoffDate: all cashflow before this date will be truncated
myPool = {'assets':[ ast1, ast2 ],
'cutoffDate':"2022-03-01"}
Step 4: Run with assumption
4.1 Performance assumption
The pool performance assumption should be setup base on the asset type in the fields assets. The argument should be passed into field poolAssump.
Pls noted that, given a specific asset class, there are multiply way to stress instead of one.
4.2 Interest rate assumption
In case there are assets with a floater setup , user need to pass interest rate curve assumption to rateAssump
r = localAPI.runPool(myPool
,poolAssump=("Pool",("Mortgage",{"CDR":0.01},None,None,None)
,None
,None)
,rateAssump=[("SOFR6M", 0.03),("SOFR1M",[["2021-01-01",0.025]
,["2022-08-01",0.029]])]
,read=True)
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
4.3 Cashflow Result
if it’s a single pool , the default key is PoolConsol ( Pool Consolidated ), it’s a Dataframe if read is True
r['PoolConsol']['flow'].head()
| Balance | Principal | Interest | Prepayment | Default | Recovery | Loss | WAC | BorrowerNum | PrepayPenalty | CumPrincipal | CumPrepay | CumDelinq | CumDefault | CumRecovery | CumLoss | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 2024-06-01 | 20000.00 | 0.00 | 0.00 | 0 | 0.00 | 0 | 0.00 | 0.075 | None | None | 0.00 | 0 | 0 | 0.00 | 0 | 0.00 |
| 2024-07-01 | 19790.20 | 193.30 | 124.88 | 0 | 16.50 | 0 | 16.50 | 0.075 | None | None | 193.30 | 0 | 0 | 16.50 | 0 | 16.50 |
| 2024-08-01 | 19579.00 | 194.32 | 123.58 | 0 | 16.88 | 0 | 16.88 | 0.075 | None | None | 387.62 | 0 | 0 | 33.38 | 0 | 33.38 |
| 2024-09-01 | 19366.92 | 195.38 | 122.26 | 0 | 16.70 | 0 | 16.70 | 0.075 | None | None | 583.00 | 0 | 0 | 50.08 | 0 | 50.08 |
| 2024-10-01 | 19154.50 | 196.44 | 120.94 | 0 | 15.98 | 0 | 15.98 | 0.075 | None | None | 779.44 | 0 | 0 | 66.06 | 0 | 66.06 |
Let’s rock with unicorn
Run with mulitiple scenarios
Before you write code like this ( a loop just run with different pool performance input ):
scenarioDefaults = [0.01,0.02,0.03]
rs = []
for d in scenarioDefaults:
r = localAPI.runPool(myPool
,poolAssump=("Pool",("Mortgage",{"CDR":d},None,None,None)
,None
,None)
,rateAssump=[("SOFR6M", 0.03),("SOFR1M",[["2021-01-01",0.025]
,["2022-08-01",0.029]])]
,read=True)
rs.append(r)
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
let try with a new funciton runPoolByScenarios() from the api
multiScenario = {
"Stress01":("Pool",("Mortgage",{"CDR":0.01},None,None,None)
,None
,None)
,"Stress02":("Pool",("Mortgage",{"CDR":0.05},None,None,None)
,None
,None)
}
rs = localAPI.runPoolByScenarios(myPool
,poolAssump = multiScenario
,read=True)
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
Now the rs is the map with keys "Stress01","Stress02", with values from corresponding performance input.
rs['Stress01']['PoolConsol']['flow'].head()
| Balance | Principal | Interest | Prepayment | Default | Recovery | Loss | WAC | BorrowerNum | PrepayPenalty | CumPrincipal | CumPrepay | CumDelinq | CumDefault | CumRecovery | CumLoss | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 2024-06-01 | 20000.00 | 0.00 | 0.00 | 0 | 0.00 | 0 | 0.00 | 0.075 | None | None | 0.00 | 0 | 0 | 0.00 | 0 | 0.00 |
| 2024-07-01 | 19790.20 | 193.30 | 124.88 | 0 | 16.50 | 0 | 16.50 | 0.075 | None | None | 193.30 | 0 | 0 | 16.50 | 0 | 16.50 |
| 2024-08-01 | 19579.00 | 194.32 | 123.58 | 0 | 16.88 | 0 | 16.88 | 0.075 | None | None | 387.62 | 0 | 0 | 33.38 | 0 | 33.38 |
| 2024-09-01 | 19366.92 | 195.38 | 122.26 | 0 | 16.70 | 0 | 16.70 | 0.075 | None | None | 583.00 | 0 | 0 | 50.08 | 0 | 50.08 |
| 2024-10-01 | 19154.50 | 196.44 | 120.94 | 0 | 15.98 | 0 | 15.98 | 0.075 | None | None | 779.44 | 0 | 0 | 66.06 | 0 | 66.06 |
Make code more concise
Let’s get back to why absbox is obsessed with native python structures. Here is the example how to make code more concise and robust.
Here, we can factor out the data structure ("Pool", ("Mortgage"..)) by focus on the essense of performance input [("stress01",0.01,0.02),("stress02",0.03,0.04)]
perfAssumpPairs = [("stress01",0.01,0.02),("stress02",0.03,0.04)]
multiScenario = {stressName: ("Pool",("Mortgage",{"CDR":defaultRate},{"CPR":prepayRate},None,None)
,None
,None)
for stressName,defaultRate,prepayRate in perfAssumpPairs}
rs = localAPI.runPoolByScenarios(myPool
,poolAssump = multiScenario
,read=True)
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
Finer assumption on pool
Given a same asset class, absbox provides a mulitple stress type. like Life default amount, Default rate by curve. absbox also extend its stressing capabilities to a finer granularity.
Set Assumption by Index
Let’s rewind a little bit about pool model syntax , the assets field accepts a List . The question will be asked ” how to specifiy elements in a list ? ” ,Index ! Now it will make sense that why there is a Pool in the ("Pool",("Mortgage", because that will be applied to whole pool instead of assets of the pool.
Now the first asset 0 will use the CDR=0% and second asset will use CDR=1%
perfByIndex = ("ByIndex"
,([0,],(("Mortgage",{"CDR":0.0},None,None,None)
,None
,None))
,([1,],(("Mortgage",{"CDR":0.01},None,None,None)
,None
,None))
)
r = localAPI.runPool(myPool
,poolAssump=perfByIndex
,read=True)
r['PoolConsol']['flow'].head()
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
| Balance | Principal | Interest | Prepayment | Default | Recovery | Loss | WAC | BorrowerNum | PrepayPenalty | CumPrincipal | CumPrepay | CumDelinq | CumDefault | CumRecovery | CumLoss | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 2024-06-01 | 20000.00 | 0.00 | 0.00 | 0 | 0.00 | 0 | 0.00 | 0.075 | None | None | 0.00 | 0 | 0 | 0.00 | 0 | 0.00 |
| 2024-07-01 | 19798.38 | 193.37 | 124.94 | 0 | 8.25 | 0 | 8.25 | 0.075 | None | None | 193.37 | 0 | 0 | 8.25 | 0 | 8.25 |
| 2024-08-01 | 19595.45 | 194.49 | 123.68 | 0 | 8.44 | 0 | 8.44 | 0.075 | None | None | 387.86 | 0 | 0 | 16.69 | 0 | 16.69 |
| 2024-09-01 | 19391.47 | 195.63 | 122.41 | 0 | 8.35 | 0 | 8.35 | 0.075 | None | None | 583.49 | 0 | 0 | 25.04 | 0 | 25.04 |
| 2024-10-01 | 19186.71 | 196.77 | 121.14 | 0 | 7.99 | 0 | 7.99 | 0.075 | None | None | 780.26 | 0 | 0 | 33.03 | 0 | 33.03 |
Set Assumption by Tag
But setting assumption is precise while a little bit lack-off-flexibility, for example, with the amortizing of the pool, new pool data came in , then the number of assets will be changed, as a result of that , indexing will be off the track.
Also, the setting assumption base on index doesn’t carry much business sense, like judging personality by driver license id, there is no “correlation” about that.
That’s why absbox supports tagging on assets, then user can applpy pool performancing by tags. User can define ANY tag on the assets via field Obligor tag.
Model asset with Tag
ob1 = {
"id":"A1",
"tag":["LowRisk","HighPrepay","NewYork"]
}
ob2 = {
"id":"A2",
"tag":["LowRisk","LowPrepay","LA"]
}
ast3 = ["Mortgage"
,{"originBalance": 12000.0
,"originRate": ["fix",0.045]
,"originTerm": 120
,"freq": "monthly"
,"type": "level"
,"originDate": "2021-02-01"
,"obligor":ob1}
,{"currentBalance": 15000.0
,"currentRate": 0.075
,"remainTerm": 80
,"status": "current"}]
ast4 = ["Mortgage"
,{"originBalance": 12000.0
,"originRate": ["fix",0.045]
,"originTerm": 120
,"freq": "monthly"
,"type": "even"
,"originDate": "2021-02-01"
,"obligor":ob2}
,{"currentBalance": 5000.0
,"currentRate": 0.075
,"remainTerm": 80
,"status": "current"}]
Tag Operation
There are 5 types of matching rule for tag:
TagEq-> extact matchTagSubset-> Asset’s tags are subset or equal to input tagTagSuperset-> Asset’s tags are superset or equal to input tagTagAny("not", <TagOperate>)ByDefaultwill catch all the asset not matchedByIdwill match by obligor id ( string equal operation )
ppyAssump = (("Mortgage",None ,{"CPR":0.1}, None, None)
,None
,None)
defAssump = (("Mortgage",{"CDR":0.2} ,None, None, None)
,None
,None)
r = localAPI.runPool(myPool | {"assets":[ast3,ast4]}
,poolAssump=("ByObligor"
#,("ById",["A1"],ppyAssump)
#,("ByTag",["LowRisk","LowPrepay","LA"],"TagEq",defAssump)
#,("ByTag",["LowRisk","LowPrepay","LA"],"TagSubset",defAssump)
#,("ByTag",["LowRisk","LowPrepay"],"TagSuperset",defAssump)
#,("ByTag",["LowRisk"],"TagAny",defAssump)
,("ByTag",["LowRisk"],("not","TagAny"),defAssump)
#,("ByDefault",<assumption>)
)
,read=True)
r['PoolConsol']['flow'].head()
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
| Balance | Principal | Interest | Prepayment | Default | Recovery | Loss | WAC | BorrowerNum | PrepayPenalty | CumPrincipal | CumPrepay | CumDelinq | CumDefault | CumRecovery | CumLoss | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 2024-06-01 | 20000.00 | 0.00 | 0.00 | 0 | 0 | 0 | 0 | 0.075 | None | None | 0.00 | 0 | 0 | 0 | 0 | 0 |
| 2024-07-01 | 19792.42 | 207.58 | 125.00 | 0 | 0 | 0 | 0 | 0.075 | None | None | 207.58 | 0 | 0 | 0 | 0 | 0 |
| 2024-08-01 | 19583.93 | 208.49 | 123.69 | 0 | 0 | 0 | 0 | 0.075 | None | None | 416.07 | 0 | 0 | 0 | 0 | 0 |
| 2024-09-01 | 19374.53 | 209.40 | 122.39 | 0 | 0 | 0 | 0 | 0.075 | None | None | 625.47 | 0 | 0 | 0 | 0 | 0 |
| 2024-10-01 | 19164.21 | 210.32 | 121.08 | 0 | 0 | 0 | 0 | 0.075 | None | None | 835.79 | 0 | 0 | 0 | 0 | 0 |
Set Assumption By Field Value
Other than Tag based way to set assumption , user can also setup selector to apply assumption to asset by testing the numeric field value .
("not" , <field matching rule>)=> negate the matching rule(<fieldName>, "cmp", <cmp>, <value>)=> only for numeric field value, hit when asset field value compare with value by cmp operator(<fieldName>, "range", <rangeType>, <lowValue>, <highValue>)=> only for numeric field value, hit when asset field value in the range
Lets’ start with patching extra numeric field value FICO and status( in string)
ob1 = {
"id":"A1",
"tag":["LowRisk","HighPrepay","NewYork"],
"fields":{"fico":500,"status":"current"}
}
ob2 = {
"id":"A2",
"tag":["LowRisk","LowPrepay","LA"],
"fields":{"fico":600,"status":"defaulted"}
}
ast5 = ["Mortgage"
,{"originBalance": 12000.0
,"originRate": ["fix",0.045]
,"originTerm": 120
,"freq": "monthly"
,"type": "level"
,"originDate": "2021-02-01"
,"obligor":ob1}
,{"currentBalance": 15000.0
,"currentRate": 0.075
,"remainTerm": 80
,"status": "current"}]
ast6 = ["Mortgage"
,{"originBalance": 12000.0
,"originRate": ["fix",0.045]
,"originTerm": 120
,"freq": "monthly"
,"type": "even"
,"originDate": "2021-02-01"
,"obligor":ob2}
,{"currentBalance": 5000.0
,"currentRate": 0.075
,"remainTerm": 80
,"status": "current"}]
User can always combine the ByTag and ByField rules together !
r = localAPI.runPool(myPool | {"assets":[ast5,ast6]}
,poolAssump=("ByObligor"
,("ById",["A1"],ppyAssump)
#,("ByTag",["LowRisk","LowPrepay","LA"],"TagEq",defAssump)
#,("ByTag",["LowRisk","LowPrepay","LA"],"TagSubset",defAssump)
#,("ByTag",["LowRisk","LowPrepay"],"TagSuperset",defAssump)
#,("ByTag",["LowRisk"],"TagAny",defAssump)
#,("ByField",[("status","in",["defaulted"])],defAssump)
#,("ByField",[("fico","cmp","G",550),],defAssump)
#,("ByField",[("fico","cmp","G",550),("status","in",["defaulted"])],defAssump)
,("ByField",[("fico","range","IE",500,600)],defAssump)
#,("ByDefault",<assumption>)
)
,read=True)
r['PoolConsol']['flow'].head()
/home/docs/checkouts/readthedocs.org/user_builds/absbox-doc/envs/stable/lib/python3.11/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host 'absbox.org'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
warnings.warn(
| Balance | Principal | Interest | Prepayment | Default | Recovery | Loss | WAC | BorrowerNum | PrepayPenalty | CumPrincipal | CumPrepay | CumDelinq | CumDefault | CumRecovery | CumLoss | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 2024-06-01 | 20000.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0 | 0.00 | 0.075 | None | None | 0.00 | 0.00 | 0 | 0.00 | 0 | 0.00 |
| 2024-07-01 | 19574.62 | 205.19 | 123.62 | 129.33 | 90.86 | 0 | 90.86 | 0.075 | None | None | 205.19 | 129.33 | 0 | 90.86 | 0 | 90.86 |
| 2024-08-01 | 19148.78 | 203.65 | 120.94 | 131.19 | 91.00 | 0 | 91.00 | 0.075 | None | None | 408.84 | 260.52 | 0 | 181.86 | 0 | 181.86 |
| 2024-09-01 | 18729.74 | 202.13 | 118.32 | 128.74 | 88.17 | 0 | 88.17 | 0.075 | None | None | 610.97 | 389.26 | 0 | 270.03 | 0 | 270.03 |
| 2024-10-01 | 18324.10 | 200.71 | 115.77 | 122.26 | 82.67 | 0 | 82.67 | 0.075 | None | None | 811.68 | 511.52 | 0 | 352.70 | 0 | 352.70 |
Conclusion
Absbox ships extream flexible way for user to apply different guranuality
User can set assumption on pool level , which applies to all assets
set assumption by Index machine friendly but not to human
set by Tag/Field ,user first model the tag/field value by any field name, the engine will apply pool assumptions with the tag/field matching rule.
Happy hacking let me know anything I can help with , cheers
Xiaoyu, xiaoyu@asset-backed.org