Skip to content

Commit bac5be9

Browse files
committed
Merge branch 'main' of https://github.com/yellowbean/AbsBox
2 parents e86200e + 0c0087f commit bac5be9

File tree

13 files changed

+558
-86
lines changed

13 files changed

+558
-86
lines changed

absbox/client.py

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .local.base import ValidationMsg
2222
from .exception import AbsboxError, VersionMismatch, EngineError
2323
from .local.interface import mkTag, readAeson
24+
from .rootFinder import mkTweak, mkStop
2425

2526
VERSION_NUM = version("absbox")
2627
console = Console()
@@ -241,6 +242,12 @@ def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[])
241242
if mRunAssump == {}:
242243
mRunAssump = {"Empty":{}}
243244
r = mkTag((RunReqType.ComboSensitivity.value, [mDeal, mAssump, mRunAssump]))
245+
case ("RootFinder", tweak, stop):
246+
_deal = deal.json if hasattr(deal, "json") else deal
247+
_perfAssump = earlyReturnNone(mkAssumpType, perfAssump)
248+
_nonPerfAssump = mkNonPerfAssumps({}, nonPerfAssump)
249+
dealRunInput= (_deal, _perfAssump, _nonPerfAssump)
250+
r = mkTag((RunReqType.RootFinder.value, [dealRunInput, mkTweak(tweak), mkStop(stop)]))
244251
case ("FirstLoss", bn) | ("FL", bn):
245252
_deal = deal.json if hasattr(deal, "json") else deal
246253
_perfAssump = mkAssumpType(perfAssump)
@@ -661,46 +668,35 @@ def runFirstLoss(self, deal, bName, poolAssump=None, runAssump=[], read=True, de
661668
:return: result of run, a dict of dataframe if `read` is True.
662669
:rtype: dict
663670
"""
671+
return self.runRootFinder(deal, poolAssump, runAssump, ("firstLoss", bName), read, debug)
664672

665-
if (poolAssump is None):
666-
raise AbsboxError(f"❌ poolAssump must be set for first loss run")
667-
668-
url = f"{self.url}/{Endpoints.RunRootFinder.value}"
669-
670-
req = self.build_run_deal_req(("FL", bName), deal, poolAssump, runAssump)
671-
672-
if debug:
673-
return req
674-
675-
result = self._send_req(req, url)
676-
677-
if result is None or 'error' in result or 'Left' in result:
678-
leftVal = result.get("Left","")
679-
raise AbsboxError(f"❌ Failed to get response from run: {leftVal}")
680-
681-
if read:
682-
return readAeson(result['Right'])
683-
else:
684-
result['Right']
685-
686-
def runRootFinder(self, p, read=True, debug=False) -> dict:
673+
def runRootFinder(self, deal, poolAssump, runAssump, p, read=True, debug=False) -> dict:
687674
"""run root finder with deal and pool assumptions
688-
675+
:param deal: a deal object
676+
:type deal: Generic | SPV
677+
:param poolAssump: pool performance assumption, a tuple for single run/ a dict for multi-scenario run, defaults to None
678+
:type poolAssump: tuple, optional
679+
:param runAssump: deal level assumption, defaults to []
680+
:type runAssump: list, optional
681+
:param p: a tuple of root finder parameters
682+
:type p: tuple/string
689683
:param read: flag to convert result to pandas dataframe, defaults to True
690684
:type read: bool, optional
691685
:param debug: return request text instead of sending out such request, defaults to False
692686
:type debug: bool, optional
693-
:return: result of run, a dict of dataframe if `read` is True.
687+
:return: result of run
694688
:rtype: dict
695689
"""
696690

697691
url = f"{self.url}/{Endpoints.RunRootFinder.value}"
698692
req = None
699693
match p:
700-
case ("maxSpreadToFace",deal,poolAssump,runAssump,bn,bFlag,fFlag):
701-
req = self.build_run_deal_req(("MaxSpreadToFaceReq",bn, bFlag, fFlag), deal, poolAssump, runAssump)
702-
case ("firstLoss",deal,poolAssump,runAssump,bn):
703-
req = self.build_run_deal_req(("FirstLoss",bn), deal, poolAssump, runAssump)
694+
case ("firstLoss", bn):
695+
req = self.build_run_deal_req(("RootFinder", "stressDefault", ("bondIncurLoss", bn)), deal, poolAssump, runAssump)
696+
case ("maxSpreadToFace", bn, bFlag, fFlag):
697+
req = self.build_run_deal_req(("RootFinder",("maxSpread", bn), ("bondPricingEqOriginBal", bn, bFlag, fFlag)), deal, poolAssump, runAssump)
698+
case (tweak,stop):
699+
req = self.build_run_deal_req(("RootFinder", tweak, stop), deal, poolAssump, runAssump)
704700
if debug:
705701
return req
706702

absbox/local/component.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,8 @@ def mkTriggerEffect(x):
13321332
return mkTag(("ChangeReserveBalance", [vStr(accName), mkAccType(newReserve)]))
13331333
case ["结果", *efs] | ["Effects", *efs] | ("Effects", *efs):
13341334
return mkTag(("TriggerEffects", [mkTriggerEffect(e) for e in efs]))
1335+
case ("changeBondRate", bndName, bondRateType, newRate):
1336+
return mkTag(("ChangeBondRate", [vStr(bndName), mkBondRate(bondRateType), vNum(newRate)]))
13351337
case None:
13361338
return mkTag(("DoNothing"))
13371339
case _:
@@ -1815,9 +1817,13 @@ def mkAssumpLeaseRent(x):
18151817
def mkAssumpLeaseEndType(x):
18161818
match x:
18171819
case {"byDate":d} | ("byDate", d):
1818-
return mkTag("CutByDate", vDate(d))
1820+
return mkTag(("CutByDate", vDate(d)))
18191821
case {"stopByExtNum":n} | ("byExtTimes",n):
1820-
return mkTag("StopByExtTimes", vInt(n))
1822+
return mkTag(("StopByExtTimes", vInt(n)))
1823+
case ("earlierOf", d, n):
1824+
return mkTag(("EarlierOf", [vDate(d), vInt(n)]))
1825+
case ("laterOf", d, n):
1826+
return mkTag(("LaterOf", [vDate(d), vInt(n)]))
18211827
case _:
18221828
raise RuntimeError(f"failed to match {x}:mkAssumpLeaseEndType")
18231829

@@ -1855,19 +1861,8 @@ def mkDefaultedAssumption(x):
18551861

18561862

18571863
def mkDelinqAssumption(x):
1858-
#return "DummyDelinqAssump"
1859-
#return mkTag("DummyDelinqAssump")
18601864
return []
18611865

1862-
def mkEndType(x):
1863-
match x:
1864-
case ("byDate", d):
1865-
return mkTag(("CutByDate",vDate(d)))
1866-
case ("byExtTimes",n):
1867-
return mkTag(("StopByExtTimes", vNum(n)))
1868-
case _:
1869-
raise RuntimeError(f"failed to match {x} : mkEndType")
1870-
18711866
def mkPerfAssumption(x):
18721867
"Make assumption on performing assets"
18731868
def mkExtraStress(y):
@@ -1901,7 +1896,7 @@ def mkExtraStress(y):
19011896
return mkTag(("LeaseAssump",[earlyReturnNone(mkAssumpLeaseDefaultType,md)
19021897
,mkAssumpLeaseGap(gap)
19031898
,mkAssumpLeaseRent(rent)
1904-
,mkEndType(endType)
1899+
,mkAssumpLeaseEndType(endType)
19051900
]))
19061901
case ("Loan",md,mp,mr,mes):
19071902
d = earlyReturnNone(mkAssumpDefault,md)

absbox/local/interface.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def readAeson(x:dict):
1313
match x:
1414
case {"tag": tag,"contents": contents} if isinstance(contents,list):
1515
return {tag: [readAeson(c) for c in contents]}
16+
case {"tag": tag,"contents": {'numerator': n,'denominator': de}} :
17+
return {tag: (n/de)}
1618
case {"tag": tag,"contents": contents} if isinstance(contents,dict):
1719
return {tag: {k: readAeson(v) for k,v in contents.items()} }
1820
case {"tag": tag,"contents": contents} if isinstance(contents,str):
@@ -42,9 +44,4 @@ def readAeson(x:dict):
4244
case _:
4345
raise RuntimeError("failed to match",x)
4446

45-
def toAeson(x:dict | tuple):
46-
pass
4747

48-
49-
def fromAeson(x:dict):
50-
pass

absbox/rootFinder.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from .local.interface import mkTag
2+
from .validation import vStr,vBool,vNum
3+
4+
def mkTweak(x):
5+
match x:
6+
case "stressDefault":
7+
return mkTag(("StressPoolDefault"))
8+
case "stressPrepayment":
9+
return mkTag(("StressPoolPrepayment"))
10+
case ("maxSpread", bn):
11+
return mkTag(("MaxSpreadTo", vStr(bn)))
12+
case ("splitBalance", bn1 ,bn2):
13+
return mkTag(("SplitFixedBalance", [vStr(bn1), vStr(bn2)]))
14+
case _:
15+
raise RuntimeError(f"failed to match {x}:mkTweak")
16+
17+
def mkStop(x):
18+
match x:
19+
case ("bondIncurLoss", bn):
20+
return mkTag(("BondIncurLoss", vStr(bn)))
21+
case ("bondIncurPrinLoss", bn, amt):
22+
return mkTag(("BondIncurPrinLoss", [vStr(bn), vNum(amt)]))
23+
case ("bondIncurIntLoss", bn, amt):
24+
return mkTag(("BondIncurIntLoss", [vStr(bn), vNum(amt)]))
25+
case ("bondPricingEqOriginBal", bn, f1, f2):
26+
return mkTag(("BondPricingEqOriginBal", [vStr(bn), vBool(f1), vBool(f2)] ))
27+
case ("bondMetTargetIrr", bn, irr):
28+
return mkTag(("BondMetTargetIrr", [vStr(bn), vNum(irr)]))
29+
case _:
30+
raise RuntimeError(f"failed to match {x}:mkStop")

absbox/tests/regression/assets.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@
3030
,"currentRate": 0.075
3131
,"remainTerm": 12
3232
,"status": "Current"}]
33+
34+
l1 = ["Lease"
35+
,{"rental":("byDay", 12.0, ["DayOfMonth",15])
36+
,"originTerm": 12
37+
,"originDate": "2022-01-05"}
38+
,{"status":"Current" ,"remainTerm":6 ,"currentBalance":150}]

absbox/tests/regression/test_main.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def test_first_loss(setup_api):
206206
,None)
207207
,read=True
208208
)
209-
closeTo(r0['FirstLossResult'][0], 31.60100353659348, r=6)
209+
closeTo(r0[0], 31.60100353659348, r=6)
210210

211211
@pytest.mark.analytics
212212
def test_irr_01(setup_api):
@@ -217,7 +217,6 @@ def test_irr_01(setup_api):
217217
,runAssump = [("pricing",{"IRR":{"B":("holding",[("2021-04-01",-500)],500)}})]
218218
,read=True
219219
)
220-
#print(">>>", r0)
221220
closeTo(r0['pricing']['summary'].loc["B"].IRR, 0.264238, r=6)
222221

223222
r1 = setup_api.run(Irr01
@@ -245,6 +244,31 @@ def test_irr_01(setup_api):
245244
closeTo(r3['pricing']['summary'].loc["A1"].IRR, 0.12248, r=6)
246245

247246

247+
@pytest.mark.analytics
248+
def test_rootfind_stressppy(setup_api):
249+
poolPerf = ("Pool",("Mortgage",{"CDR":0.002},{"CPR":0.001},{"Rate":0.1,"Lag":18},None)
250+
,None
251+
,None)
252+
pricing = ("pricing",{"IRR":{"B":("holding",[("2021-04-15",-1000)],1000)}})
253+
r = setup_api.runRootFinder(test01, poolPerf ,[pricing]
254+
,("stressPrepayment",("bondMetTargetIrr", "B", 0.25))
255+
)
256+
assert r[1][1]['PoolLevel'][0]['MortgageAssump'][1] == {'PrepaymentCPR': 0.38642105474696914 }
257+
258+
@pytest.mark.analytics
259+
def test_rootfind_stressdef(setup_api):
260+
poolPerf = ("Pool",("Mortgage",{"CDR":0.002},{"CPR":0.001},{"Rate":0.1,"Lag":18},None)
261+
,None
262+
,None)
263+
pricing = ("pricing",{"IRR":{"B":("holding",[("2021-04-15",-1000)],1000)}})
264+
r = setup_api.runRootFinder(test01, poolPerf ,[pricing]
265+
,("stressDefault",("bondMetTargetIrr", "B", 0.10))
266+
)
267+
assert r[1][1]['PoolLevel'][0]['MortgageAssump'][0] == {'DefaultCDR': 0.07603587859615266}
268+
269+
270+
271+
248272
@pytest.mark.bond
249273
def test_pac_01(setup_api):
250274
r = setup_api.run(pac01 , read=True , runAssump = [])
@@ -274,4 +298,45 @@ def test_bondGrp(setup_api):
274298
r = setup_api.run(bondGrp , read=True , runAssump = [])
275299
grpFlow = readBondsCf(r['bonds'])['A'].xs('balance',axis=1,level=1)
276300
assert grpFlow.A1.to_list()[:8] == [1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 1000.0, 993.93]
277-
assert grpFlow.A2.to_list()[:8] == [510.6, 436.68, 364.06, 290.75, 217.26, 143.1, 68.74, 0.0,]
301+
assert grpFlow.A2.to_list()[:8] == [510.6, 436.68, 364.06, 290.75, 217.26, 143.1, 68.74, 0.0,]
302+
303+
@pytest.mark.trigger
304+
def test_trigger_chgBondRate(setup_api):
305+
withTrigger = test01 & lens.trigger.set({
306+
"BeforeDistribution":{
307+
"changeBndRt":{"condition":[">=","2021-08-20"]
308+
,"effects":("changeBondRate", "A1", {"floater":[0.05, "SOFR1Y",-0.02,"MonthEnd"]}, 0.12)
309+
,"status":False
310+
,"curable":False}
311+
}
312+
})
313+
r = setup_api.run(withTrigger , read=True ,runAssump = [("interest",("SOFR1Y",0.04))])
314+
assert r['bonds']['A1'].rate.to_list() == [0.07, 0.12, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02, 0.02]
315+
316+
@pytest.mark.pool
317+
def test_pool_lease_end(setup_api):
318+
""" Lease end date """
319+
myPool = {'assets':[l1],'cutoffDate':"2022-01-01"}
320+
r = setup_api.runPool(myPool
321+
,poolAssump=("Pool",("Lease", None, ('days', 20) , ('byAnnualRate', 0.0), ("byExtTimes", 2))
322+
,None
323+
,None
324+
)
325+
,read=True)
326+
assert r['PoolConsol'][0].index[-1] == '2024-12-15'
327+
328+
r = setup_api.runPool(myPool
329+
,poolAssump=("Pool",("Lease", None, ('days', 20) , ('byAnnualRate', 0.0), ("earlierOf", "2023-11-15", 2))
330+
,None
331+
,None
332+
)
333+
,read=True)
334+
assert r['PoolConsol'][0].index[-1] == '2023-12-15'
335+
336+
r = setup_api.runPool(myPool
337+
,poolAssump=("Pool",("Lease", None, ('days', 20) , ('byAnnualRate', 0.0), ("laterOf", "2023-11-15", 3))
338+
,None
339+
,None
340+
)
341+
,read=True)
342+
assert r['PoolConsol'][0].index[-1] == '2025-12-15'

0 commit comments

Comments
 (0)