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 assets

  • cutoffDate : 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 match

  • TagSubset -> Asset’s tags are subset or equal to input tag

  • TagSuperset -> Asset’s tags are superset or equal to input tag

  • TagAny

  • ("not", <TagOperate>)

  • ByDefault will catch all the asset not matched

  • ById will 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