diff --git a/absbox/client.py b/absbox/client.py index d16a5df..2eaac2f 100644 --- a/absbox/client.py +++ b/absbox/client.py @@ -12,7 +12,7 @@ from pyspecter import query from absbox.local.util import mkTag, isDate, flat, guess_pool_locale, mapValsBy, guess_pool_flow_header, _read_cf, _read_asset_pricing, mergeStrWithDict, earlyReturnNone, searchByFst -from absbox.local.component import mkPool,mkAssumpType,mkNonPerfAssumps, mkPricingAssump,mkLiqMethod,mkAssetUnion +from absbox.local.component import mkPool,mkAssumpType,mkNonPerfAssumps, mkPricingAssump,mkLiqMethod,mkAssetUnion,mkRateAssumption from absbox.local.base import * from absbox.validation import valReq,valAssumption @@ -31,7 +31,8 @@ class API: check: bool = True server_info = {} version = VERSION_NUM.split(".") - hdrs = {'Content-type': 'application/json', 'Accept': 'text/plain','Accept':'*/*' ,'Accept-Encoding':'gzip'} + hdrs = {'Content-type': 'application/json', 'Accept': '*/*' + , 'Accept-Encoding': 'gzip'} session = None debug = False @@ -57,7 +58,6 @@ def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[]) ''' build run deal requests: (single run, multi-scenario run, multi-struct run) ''' r = None _nonPerfAssump = mkNonPerfAssumps({}, nonPerfAssump) - print("NON PERF",_nonPerfAssump) match run_type: case "Single" | "S": @@ -76,16 +76,20 @@ def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[]) raise RuntimeError(f"Failed to match run type:{run_type}") return json.dumps(r, ensure_ascii=False) - def build_pool_req(self, pool, assumptions, read=None) -> str: + def build_pool_req(self, pool, poolAssump, rateAssumps, read=None) -> str: r = None - if isinstance(assumptions, list): + if isinstance(poolAssump, tuple): r = mkTag(("SingleRunPoolReq" - ,[mkPool(pool) - ,mkAssumption2(assumptions)])) - elif isinstance(assumptions, dict): + ,[mkPool(pool) + ,mkAssumpType(poolAssump) + ,[mkRateAssumption(rateAssump) for rateAssump in rateAssumps] if rateAssumps else None] + )) + elif isinstance(poolAssump, dict): r = mkTag(("MultiScenarioRunPoolReq" - ,[mkPool(pool) - ,mapValsBy(assumptions, mkAssumption2)])) + ,[mkPool(pool) + ,mapValsBy(poolAssump, mkAssumpType) + ,[mkRateAssumption(rateAssump) for rateAssump in rateAssumps] if rateAssumps else None] + )) else: raise RuntimeError("Error in build pool req") return json.dumps(r , ensure_ascii=False) @@ -106,6 +110,9 @@ def run(self, deal, runAssump=None, read=True): + assert isinstance(runAssump,list),f"runAssump must be a list ,but got {type(runAssump)}" + + # if run req is a multi-scenario run multi_run_flag = True if isinstance(poolAssump, dict) else False url = f"{self.url}/runDealByScenarios" if multi_run_flag else f"{self.url}/runDeal" @@ -128,67 +135,76 @@ def run(self, deal, return None # load deal if it is a multi scenario if read and multi_run_flag: - #return {n:deal.read(_r) for (n,_r) in result.items()} return mapValsBy(result, deal.read) elif read: return deal.read(result) else: return result - def runPool(self, pool, assumptions,read=True): + def runPool(self, pool, poolAssump=None, rateAssump=None, read=True): def read_single(pool_resp): - flow_header,idx = guess_pool_flow_header(pool_resp[0],pool_lang) + (pool_flow, pool_bals) = pool_resp + flow_header, idx = guess_pool_flow_header(pool_flow[0],pool_lang) try: - result = pd.DataFrame([_['contents'] for _ in pool_resp] , columns=flow_header) - except ValueError as e: + result = pd.DataFrame([_['contents'] for _ in pool_flow], columns=flow_header) + except ValueError as _: console.print(f"❌[bold red]Failed to match header:{flow_header} with {result[0]['contents']}") + result = result.set_index(idx) result.index.rename(idx, inplace=True) result.sort_index(inplace=True) - return result + return (result, pool_bals) - multi_scenario = True if isinstance(assumptions, dict) else False + multi_scenario = True if isinstance(poolAssump, dict) else False url = f"{self.url}/runPoolByScenarios" if multi_scenario else f"{self.url}/runPool" pool_lang = guess_pool_locale(pool) - req = self.build_pool_req(pool, assumptions=assumptions) + req = self.build_pool_req(pool, poolAssump, rateAssump) result = self._send_req(req, url) if read and (not multi_scenario): return read_single(result) - elif read and multi_scenario : + elif read and multi_scenario: return mapValsBy(result, read_single) else: return result - def runStructs(self, deals, assumptions=None, pricing=None, read=True ): + def runStructs(self, deals, poolAssump=None, nonPoolAssump=None, read=True): assert isinstance(deals, dict),f"Deals should be a dict but got {deals}" url = f"{self.url}/runMultiDeals" - _assumptions = mkAssumption2(assumptions) if assumptions else None - _pricing = mkPricingAssump(pricing) if pricing else None + _poolAssump = mkAssumpType(poolAssump) if poolAssump else None + _nonPerfAssump = mkNonPerfAssumps({}, nonPoolAssump) req = json.dumps(mkTag(("MultiDealRunReq" - ,[{k:v.json for k,v in deals.items()} - ,_assumptions - ,_pricing])) + ,[{k:v.json for k,v in deals.items()} + ,_poolAssump + ,_nonPerfAssump])) ,ensure_ascii=False) - result = self._send_req(req,url) - if read : - return {k:deals[k].read(v) for k,v in result.items()} + result = self._send_req(req, url) + if read: + return {k: deals[k].read(v) for k, v in result.items()} else: return result - def runAsset(self, date, _assets, assumptions=None, pricing=None, read=True): + def runAsset(self, date, _assets, poolAssump=None, rateAssump=None, pricing=None, read=True): assert isinstance(_assets, list),f"Assets passed in must be a list" def readResult(x): - (cfs,pr) = x - cfs = _read_cf(cfs, self.lang) - pricingResult = _read_asset_pricing(pr, self.lang) if pr else None - return (cfs,pricingResult) + try: + ((cfs,cfBalance),pr) = x + cfs = _read_cf(cfs, self.lang) + pricingResult = _read_asset_pricing(pr, self.lang) if pr else None + except Exception as e: + print(f"Failed to read result {x}") + return (cfs,cfBalance,pricingResult) url = f"{self.url}/runAsset" - _assumptions = mkAssumption2(assumptions) - _pricing = mkLiqMethod(pricing) if pricing else None - assets = [ mkAssetUnion(_) for _ in _assets] - req = json.dumps([date ,assets ,_assumptions ,_pricing] + _assumptions = mkAssumpType(poolAssump) if poolAssump else None + _pricing = mkLiqMethod(pricing) if pricing else None + _rate = mkRateAssumption(rateAssump) if rateAssump else None + assets = [ mkAssetUnion(_) for _ in _assets ] + req = json.dumps([date + ,assets + ,_assumptions + ,_rate + ,_pricing] ,ensure_ascii=False) result = self._send_req(req,url) if read : @@ -293,7 +309,6 @@ def _send_req(self,_req,_url,timeout=10,headers={})->dict: return None if r.status_code != 200: console.print_json(_req) - print(">>>",r.text) console.print_json(r.text) return None try: diff --git a/absbox/local/base.py b/absbox/local/base.py index 52099d6..eea8d9a 100644 --- a/absbox/local/base.py +++ b/absbox/local/base.py @@ -73,7 +73,9 @@ rateLikeFormula = set(["bondFactor","poolFactor","cumPoolDefaultedRate","资产池累计违约率","债券系数","资产池系数"]) intLikeFormula = set(["borrowerNumber","monthsTillMaturity"]) - +boolLikeFormula = set(["trigger","事件","isMostSenior","最优先"]) + + #pool income mapping poolSourceMapping = {"利息回款": "CollectedInterest" , "本金回款": "CollectedPrincipal" diff --git a/absbox/local/component.py b/absbox/local/component.py index 614d46a..885023b 100644 --- a/absbox/local/component.py +++ b/absbox/local/component.py @@ -153,10 +153,16 @@ def mkDs(x): return mkTag(("MonthsTillMaturity", bn)) case ("资产池余额",) | ("poolBalance",): return mkTag("CurrentPoolBalance") + case ("资产池期初余额",) | ("poolBegBalance",): + return mkTag("CurrentPoolBegBalance") case ("初始资产池余额",) | ("originalPoolBalance",): return mkTag("OriginalPoolBalance") case ("资产池违约余额",) | ("currentPoolDefaultedBalance",): return mkTag("CurrentPoolDefaultedBalance") + case ("资产池累计损失余额",) | ("cumPoolNetLoss",): + return mkTag("CumulativeNetLoss") + case ("资产池累计损失率",) | ("cumPoolNetLossRate",): + return mkTag("CumulativeNetLossRatio") case ("资产池累计违约余额",) | ("cumPoolDefaultedBalance",): return mkTag("CumulativePoolDefaultedBalance") case ("资产池累计回收额",) | ("cumPoolRecoveries",): @@ -167,14 +173,20 @@ def mkDs(x): return mkTag(("CumulativePoolDefaultedRateTill",n)) case ("资产池累计",*i) | ("cumPoolCollection",*i): return mkTag(("PoolCumCollection", [mkPoolSource(_) for _ in i] )) + case ("资产池累计至",idx,*i) | ("cumPoolCollectionTill",idx,*i): + return mkTag(("PoolCumCollectionTill", [idx, [mkPoolSource(_) for _ in i]] )) case ("资产池当期",*i) | ("curPoolCollection",*i): return mkTag(("PoolCurCollection", [mkPoolSource(_) for _ in i])) + case ("资产池当期至",idx,*i) | ("curPoolCollectionStats",idx,*i): + return mkTag(("PoolCurCollectionStats", [idx, [mkPoolSource(_) for _ in i]])) case ("债券系数",) | ("bondFactor",): return mkTag("BondFactor") case ("资产池系数",) | ("poolFactor",): return mkTag("PoolFactor") case ("债券利率",bn) | ("bondRate",bn): return mkTag(("BondRate", bn)) + case ("债券加权利率",*bn) | ("bondWaRate",*bn): + return mkTag(("BondWaRate", bn)) case ("资产池利率",) | ("poolWaRate",): return mkTag("PoolWaRate") case ("所有账户余额",) | ("accountBalance"): @@ -193,7 +205,9 @@ def mkDs(x): return mkTag(("LastBondIntPaid", bnds)) case ("债券低于目标余额", bn) | ("behindTargetBalance", bn): return mkTag(("BondBalanceGap", bn)) - case ("已提供流动性", *liqName) | ("liqCredit", *liqName): + case ("已提供流动性", *liqName) | ("liqBalance", *liqName): + return mkTag(("LiqBalance", liqName)) + case ("流动性额度", *liqName) | ("liqCredit", *liqName): return mkTag(("LiqCredit", liqName)) case ("债务人数量",) | ("borrowerNumber",): return mkTag(("CurrentPoolBorrowerNum")) @@ -201,7 +215,9 @@ def mkDs(x): dealCycleM = chinaDealCycle | englishDealCycle if not loc in dealCycleM: raise RuntimeError(f" {loc} not in map {dealCycleM}") - return mkTag(("TriggersStatusAt", [dealCycleM[loc], idx])) + return mkTag(("TriggersStatus", [dealCycleM[loc], idx])) + case ("阶段", st) | ("status", st): + return mkTag(("IsDealStatus", mkStatus(st))) case ("待付费用", *fns) | ("feeDue", *fns): return mkTag(("CurrentDueFee", fns)) case ("已付费用", *fns) | ("lastFeePaid", *fns): @@ -212,11 +228,11 @@ def mkDs(x): return mkTag(("BondTxnAmt", [bns, cmt])) case ("账户变动总额", cmt, *ans) | ("accountTxnAmount", cmt, *ans): return mkTag(("AccTxnAmt", [ans, cmt])) - case ("系数", ds, f) | ("factor", ds, f) if isinstance(float, f): + case ("系数", ds, f) | ("factor", ds, f) if isinstance(f,float): return mkTag(("Factor", [mkDs(ds), f])) - case ("Min", *ds): + case ("Min", *ds) | ("min", *ds): return mkTag(("Min", [mkDs(s) for s in ds])) - case ("Max", *ds): + case ("Max", *ds) | ("max", *ds): return mkTag(("Max", [mkDs(s) for s in ds])) case ("合计", *ds) | ("sum", *ds): return mkTag(("Sum", [mkDs(_ds) for _ds in ds])) @@ -269,6 +285,8 @@ def queryType(y): return "IfRate" case (_y,*_) if _y in intLikeFormula: return "IfInt" + # case (_y,*_) if _y in boolLikeFormula: + # return "IfBool" case _: return "If" @@ -441,31 +459,28 @@ def mkAccountCapType(x): case _: raise RuntimeError(f"Failed to match {x}:mkAccountCapType") -def mkTransferLimit(x): - match x: - case {"余额百分比": pct} | {"balPct": pct}: - return mkTag(("DuePct", pct)) - case {"金额上限": amt} | {"balCapAmt": amt}: - return mkTag(("DueCapAmt", amt)) - case {"公式": formula} | {"formula": formula}: - return mkTag(("DS", mkDs(formula))) - case {"冲销":an} | {"clearLedger":an}: - return mkTag(("ClearLedger", an)) - case {"簿记":an} | {"bookLedger":an}: - return mkTag(("BookLedger", an)) - case {"系数":[limit,factor]} | {"multiple":[limit,factor]}: - return mkTag(("Multiple", [mkLimit(limit),factor])) - case {"储备":"缺口"} | {"reserve":"gap"} : - return mkTag(("TillTarget")) - case {"储备":"盈余"} | {"reserve":"excess"} : - return mkTag(("TillSource")) - case None: - return None - case _: - raise RuntimeError(f"Failed to match :{x}:mkTransferLimit") - def mkLimit(x): - return mkTransferLimit(x) + match x: + case {"余额百分比": pct} | {"balPct": pct}: + return mkTag(("DuePct", pct)) + case {"金额上限": amt} | {"balCapAmt": amt}: + return mkTag(("DueCapAmt", amt)) + case {"公式": formula} | {"formula": formula}: + return mkTag(("DS", mkDs(formula))) + case {"冲销":an} | {"clearLedger":an}: + return mkTag(("ClearLedger", an)) + case {"簿记":an} | {"bookLedger":an}: + return mkTag(("BookLedger", an)) + case {"系数":[limit,factor]} | {"multiple":[limit,factor]}: + return mkTag(("Multiple", [mkLimit(limit),factor])) + case {"储备":"缺口"} | {"reserve":"gap"} : + return mkTag(("TillTarget")) + case {"储备":"盈余"} | {"reserve":"excess"} : + return mkTag(("TillSource")) + case None: + return None + case _: + raise RuntimeError(f"Failed to match :{x}:mkLimit") def mkComment(x): match x: @@ -521,27 +536,27 @@ def mkLiqDrawType(x): def mkLiqRepayType(x): match x: - case "余额" | "bal": + case "余额" | "bal" | "balance": return mkTag(("LiqBal")) case "费用" | "premium": return mkTag(("LiqPremium")) - case "利息" | "int": + case "利息" | "int" | "interest": return mkTag(("LiqInt")) case _: raise RuntimeError(f"Failed to match :{x}:Liquidation Repay Type") -def mkRateSwapType(pr,rr): +def mkRateSwapType(pr, rr): def isFloater(y): if isinstance(y, tuple): return True return False - match (isFloater(pr),isFloater(rr)): - case (True,True): + match (isFloater(pr), isFloater(rr)): + case (True, True): return mkTag(("FloatingToFloating", [pr, rr])) - case (False,True): + case (False, True): return mkTag(("FixedToFloating", [pr, rr])) - case (True,False): + case (True, False): return mkTag(("FloatingToFixed", [pr, rr])) case _: raise RuntimeError(f"Failed to match :{rr,pr}:Interest Swap Type") @@ -581,20 +596,21 @@ def mkRateType(x): match x : case {"fix":r} | {"固定":r} | ["fix",r] | ["固定",r]: return mkTag(("Fix",[DC.DC_ACT_365F.value, r])) - case {"floater":(idx,spd),"rate":r,"resets":dp,**p} | \ + case {"floater":(idx,spd),"rate":r,"reset":dp,**p} | \ {"浮动":(idx,spd),"利率":r,"重置":dp,**p}: mf = getValWithKs(p,["floor"]) mc = getValWithKs(p,["cap"]) mrnd = getValWithKs(p,["rounding"]) - return mkTag(("Floater",[idx,spd,r,mkDatePattern(dp),mf,mc,mrnd])) - case ["浮动",r,{"基准":idx,"利差":spd,"重置频率":dp,**p}] | ["floater",r,{"index":idx,"spread":spd,"resets":dp,**p}]: + dc = p.get("dayCount",DC.DC_ACT_365F.value) + return mkTag(("Floater",[dc,idx,spd,r,mkDatePattern(dp),mf,mc,mrnd])) + case ["浮动",r,{"基准":idx,"利差":spd,"重置频率":dp,**p}] | \ + ["floater",r,{"index":idx,"spread":spd,"reset":dp,**p}] : mf = getValWithKs(p,["floor"]) mc = getValWithKs(p,["cap"]) mrnd = getValWithKs(p,["rounding"]) - __r = mkTag(("Floater",[idx,spd,r,mkDatePattern(dp),mf,mc,mrnd])) - return __r + dc = p.get("dayCount",DC.DC_ACT_365F.value) + return mkTag(("Floater",[dc,idx,spd,r,mkDatePattern(dp),mf,mc,mrnd])) case None: - print("NONE") return None case _ : raise RuntimeError(f"Failed to match :{x}: Rate Type") @@ -735,8 +751,10 @@ def mkStatus(x): def readStatus(x, locale): m = {"en": {'amort': "Amortizing", 'def': "Defaulted", 'acc': "Accelerated", 'end': "Ended", + 'called': "Called", 'pre': "PreClosing",'revol':"Revolving"} - , "cn": {'amort': "摊销", 'def': "违约", 'acc': "加速清偿", 'end': "结束", 'pre': "设计","revol":"循环"}} + , "cn": {'amort': "摊销", 'def': "违约", 'acc': "加速清偿", 'end': "结束", 'pre': "设计","revol":"循环" + ,'called':"清仓回购"}} match x: case {"tag": "Amortizing"}: return m[locale]['amort'] @@ -750,6 +768,8 @@ def readStatus(x, locale): return m[locale]['pre'] case {"tag": "Revolving"}: return m[locale]['revol'] + case {"tag": "Called"}: + return m[locale]['called'] case _: raise RuntimeError( f"Failed to read deal status:{x} with locale: {locale}") @@ -1092,6 +1112,8 @@ def mkDelinqAssumption(x): def mkPerfAssumption(x): "Make assumption on performing assets" + def mkExtraStress(y): + return None #TODO match x: case ("Mortgage",md,mp,mr,mes): d = earlyReturnNone(mkAssumpDefault,md) @@ -1121,15 +1143,16 @@ def mkPerfAssumption(x): case _: raise RuntimeError(f"failed to match {x}") +def mkPDF(a,b,c): + ''' make assumps asset with 3 status: performing/delinq/defaulted ''' + return [mkPerfAssumption(a),mkDelinqAssumption(b),mkDefaultedAssumption(c)] + def mkAssumpType(x): - ''' ''' - def mkPDF(a,b,c): - return [mkPerfAssumption(a),mkDelinqAssumption(b),mkDefaultedAssumption(c)] - + ''' make assumps either on pool level or asset level ''' match x: case ("Pool", p, d, f): - return mkTag(("PoolLevel",mkPDF(p,d,f))) - case ("ByIndex", ps): + return mkTag(("PoolLevel",mkPDF(p, d, f))) + case ("ByIndex", *ps): return mkTag(("ByIndex",[ [idx, mkPDF(a,b,c)] for (idx,(a,b,c)) in ps ])) case _ : raise RuntimeError(f"failed to match {x} | mkAssumpType") @@ -1149,9 +1172,9 @@ def mkAssetUnion(x): def mkRevolvingPool(x): match x: - case ["constant",asts]|["固定",asts]: + case ["constant",*asts]|["固定",*asts]: return mkTag(("ConstantAsset",[ mkAssetUnion(_) for _ in asts])) - case ["static",asts]|["静态",asts]: + case ["static",*asts]|["静态",*asts]: return mkTag(("StaticAsset",[ mkAssetUnion(_) for _ in asts])) case ["curve",astsWithDates]|["曲线",astsWithDates]: assetCurve = [ [d, [mkAssetUnion(a) for a in asts]] for (d,asts) in astsWithDates ] @@ -1260,7 +1283,7 @@ def mkCf(x): if len(x) == 0: return None else: - return [mkTag(("MortgageFlow", _x+[0.0]*5+[None,None])) for _x in x] + return [mkTag(("MortgageFlow", _x+[0.0]*6+[None,None])) for _x in x] def mkCollection(x): @@ -1438,7 +1461,7 @@ def aggAccs(x, locale): return agg_acc -def readIssuance(pool): +def readCutoffFields(pool): _map = {'cn': "发行", 'en': "Issuance"} lang_flag = None @@ -1449,7 +1472,7 @@ def readIssuance(pool): else: return None - validIssuanceFields = { + validCutoffFields = { "资产池规模": "IssuanceBalance", "IssuanceBalance": "IssuanceBalance" } @@ -1466,7 +1489,7 @@ def readIssuance(pool): def mkRateAssumption(x): match x: case (idx,r) if isinstance(r, list): - return mkTag(("RateCurve",[idx, r])) + return mkTag(("RateCurve",[idx, mkCurve("IRateCurve",r)])) case (idx,r) : return mkTag(("RateFlat" ,[idx, r])) case _ : @@ -1477,12 +1500,13 @@ def translate(y): match y: case ("stop",d): return {"stopRunBy":d} - case ("estimateExpense",fn,ts): - return {"projectedExpense":(fn,ts)} - case ("call",opts): - return {"callWhen":mkCallOptions(opts)} + case ("estimateExpense",*projectExps): + return {"projectedExpense":[(fn,mkTs("BalanceCurve",ts)) for (fn,ts) in projectExps]} + case ("call",*opts): + return {"callWhen":[mkCallOptions(opt) for opt in opts]} case ("revolving",rPool,rPerf): - return {"revolving":mkTag(("AvailableAssets",[mkRevolvingPool(rPool),rPerf]))} + return {"revolving":mkTag(("AvailableAssets" + ,[mkRevolvingPool(rPool),[mkPerfAssumption(rPerf),[],mkTag(("DummyDefaultAssump"))]]))} case ("interest",*ints): return {"interest":[mkRateAssumption(_) for _ in ints]} case ("inspect",*tps): diff --git a/absbox/local/generic.py b/absbox/local/generic.py index f0e67af..9b0cdd3 100644 --- a/absbox/local/generic.py +++ b/absbox/local/generic.py @@ -44,7 +44,8 @@ def json(self): "pool": {"assets": [mkAsset(x) for x in getValWithKs(self.pool,['assets',"清单"],defaultReturn=[])] , "asOfDate": lastAssetDate , "issuanceStat": getValWithKs(self.pool,["issuanceStat","统计"]) - , "futureCf":mkCf(getValWithKs(self.pool,['cashflow','现金流归集表','归集表'], [])) }, + , "futureCf":mkCf(getValWithKs(self.pool,['cashflow','现金流归集表','归集表'], [])) + , "extendPeriods":mkDatePattern(getValWithKs(self.pool,['extendBy'],"MonthEnd"))}, "bonds": { bn: mkBnd(bn,bo) for (bn,bo) in self.bonds}, "waterfall": mkWaterfall({},self.waterfall.copy()), "fees": {fn: mkFee(fo|{"name":fn},fsDate = lastCloseDate) diff --git a/absbox/local/plot.py b/absbox/local/plot.py index ec0e6d5..c6a2522 100644 --- a/absbox/local/plot.py +++ b/absbox/local/plot.py @@ -3,7 +3,7 @@ from absbox.local.util import guess_locale,aggStmtByDate,consolStmtByDate from pyspecter import query, S #from itertools import reduce -from itertools import reduce +from functools import reduce import numpy as np import logging diff --git a/absbox/tests/benchmark/us/__init__.py b/absbox/tests/benchmark/us/__init__.py new file mode 100644 index 0000000..a644031 --- /dev/null +++ b/absbox/tests/benchmark/us/__init__.py @@ -0,0 +1,15 @@ +import absbox.tests.benchmark.us.test01 as t1 +import absbox.tests.benchmark.us.test02 as t2 +import absbox.tests.benchmark.us.test03 as t3 +import absbox.tests.benchmark.us.test04 as t4 +import absbox.tests.benchmark.us.test05 as t5 +import absbox.tests.benchmark.us.test06 as t6 + +translate_pair = [(t1.test01, "test01.json") + ,(t2.test01, "test02.json") + ,(t3.test01, "test03.json") + ,(t4.test01, "test04.json") + ,(t5.test01, "test05.json") + ,(t6.test01, "test06.json") +] + diff --git a/absbox/tests/benchmark/us/stepup_sample.py b/absbox/tests/benchmark/us/stepup_sample.py index c13557b..65ec331 100644 --- a/absbox/tests/benchmark/us/stepup_sample.py +++ b/absbox/tests/benchmark/us/stepup_sample.py @@ -1,6 +1,6 @@ from absbox.local.generic import Generic -test01 = Generic( +test06 = Generic( "Step Up bond" ,{"cutoff":"2021-03-01","closing":"2021-06-15","firstPay":"2021-07-26" ,"payFreq":["DayOfMonth",20],"poolFreq":"MonthEnd","stated":"2030-01-01"} @@ -17,7 +17,8 @@ ,"originBalance":1000 ,"originRate":0.07 ,"startDate":"2020-01-03" - ,"rateType":{"StepUp":0.08,"Spread":0.01,"When":["After","2022-04-01","MonthEnd"]} + ,"rateType":{"StepUp":0.08,"Spread":0.01 + ,"When":["After","2022-04-01","MonthEnd"]} ,"bondType":{"Sequential":None}}) ,("B",{"balance":1000 ,"rate":0.0 diff --git a/absbox/tests/benchmark/us/test01.py b/absbox/tests/benchmark/us/test01.py index ae8248b..165cda4 100644 --- a/absbox/tests/benchmark/us/test01.py +++ b/absbox/tests/benchmark/us/test01.py @@ -33,7 +33,7 @@ ,["accrueAndPayInt","acc01",["A1"]] ,["payPrin","acc01",["A1"]] ,["payPrin","acc01",["B"]] - ,["payPrinResidual","acc01",["B"]] + ,["payIntResidual","acc01","B"] ]} ,[["CollectedInterest","acc01"] ,["CollectedPrincipal","acc01"] diff --git a/absbox/tests/benchmark/us/test02.py b/absbox/tests/benchmark/us/test02.py index cb72c13..aa20241 100644 --- a/absbox/tests/benchmark/us/test02.py +++ b/absbox/tests/benchmark/us/test02.py @@ -60,11 +60,11 @@ ,None ,None ,None - ,{"AfterCollect":[ - {"condition":[("cumPoolDefaultedRate",),">",0.05], - "effects":("newStatus","Accelerated"), - "status":False, - "curable":False} ] - } + ,{"AfterCollect":{ + "DefaultTrigger": + {"condition":[("cumPoolDefaultedRate",),">",0.05] + ,"effects":("newStatus","Accelerated") + ,"status":False + ,"curable":False}}} ) diff --git a/absbox/tests/benchmark/us/test03.py b/absbox/tests/benchmark/us/test03.py index b2b5590..66c4c29 100644 --- a/absbox/tests/benchmark/us/test03.py +++ b/absbox/tests/benchmark/us/test03.py @@ -1,13 +1,13 @@ from absbox.local.generic import Generic -test01 = Generic( +test03 = Generic( "If in Waterfall" ,{"cutoff":"2021-03-01","closing":"2021-06-15","firstPay":"2021-07-26" ,"payFreq":["DayOfMonth",20],"poolFreq":"MonthEnd","stated":"2030-01-01"} ,{'assets':[["Mortgage" - ,{"originBalance":2200,"originRate":["fix",0.045],"originTerm":30 + ,{"originBalance":2500,"originRate":["fix",0.045],"originTerm":30 ,"freq":"Monthly","type":"Level","originDate":"2021-02-01"} - ,{"currentBalance":2200 + ,{"currentBalance":2500 ,"currentRate":0.08 ,"remainTerm":20 ,"status":"current"}]]} @@ -43,7 +43,7 @@ ,[("monthsTillMaturity","A1"),"<",3] ,["payPrin","acc01",["A1","A2"]]] ,["payPrin","acc01",["B"]] - ,["payPrinResidual","acc01",["B"]]] + ,["payIntResidual","acc01","B"]] } ,[["CollectedInterest","acc01"] ,["CollectedPrincipal","acc01"] diff --git a/absbox/tests/benchmark/us/test04.py b/absbox/tests/benchmark/us/test04.py index 7067a81..4a172bd 100644 --- a/absbox/tests/benchmark/us/test04.py +++ b/absbox/tests/benchmark/us/test04.py @@ -1,13 +1,13 @@ from absbox.local.generic import Generic -test01 = Generic( +test04 = Generic( "split pool income" ,{"cutoff":"2021-03-01","closing":"2021-06-15","firstPay":"2021-07-26" ,"payFreq":["DayOfMonth",20],"poolFreq":"MonthEnd","stated":"2030-01-01"} ,{'assets':[["Mortgage" - ,{"originBalance":2200,"originRate":["fix",0.045],"originTerm":30 + ,{"originBalance":2500,"originRate":["fix",0.045],"originTerm":30 ,"freq":"Monthly","type":"Level","originDate":"2021-02-01"} - ,{"currentBalance":2200 + ,{"currentBalance":2500 ,"currentRate":0.08 ,"remainTerm":20 ,"status":"current"}]]} @@ -40,4 +40,4 @@ ,["CollectedPrincipal",[[0.8,"acc01"],[0.2,"acc02"]]] ,["CollectedPrepayment","acc01"] ,["CollectedRecoveries","acc01"]] - ) +) diff --git a/absbox/tests/benchmark/us/test05.py b/absbox/tests/benchmark/us/test05.py index aed356b..3169e38 100644 --- a/absbox/tests/benchmark/us/test05.py +++ b/absbox/tests/benchmark/us/test05.py @@ -1,6 +1,6 @@ from absbox.local.generic import Generic -test01 = Generic( +test05 = Generic( "liquidation provider with interest" ,{"cutoff":"2021-03-01","closing":"2021-06-15","firstPay":"2021-07-26" ,"payFreq":["DayOfMonth",20],"poolFreq":"MonthEnd","stated":"2030-01-01"} @@ -11,7 +11,8 @@ ,"currentRate":0.08 ,"remainTerm":20 ,"status":"current"}]]} - ,(("acc01",{"balance":0}),("acc02",{"balance":0})) + ,(("acc01",{"balance":0}) + ,("acc02",{"balance":0})) ,(("A1",{"balance":1000 ,"rate":0.08 ,"originBalance":1000 @@ -31,25 +32,22 @@ ,{"amortizing":[ ["calcInt","A1"] ,["liqAccrue","insuranceProvider"] - ,["liqSupport", "insuranceProvider","account","acc01" - ,{"formula": - ("Max" - ,("substract",("bondDueInt","A1","B"),("accountBalance","acc01")) - ,("constant",0.0))}] + ,["liqSupport", "insuranceProvider","interest","A1"] + ,["liqSupport", "insuranceProvider","interest","B"] ,["accrueAndPayInt","acc01",["A1","B"]] ,["payPrin","acc02",["A1"]] ,["payPrin","acc02",["B"]] ,["If" ,[("bondBalance","A1","B"),"=",0] ,["accrueAndPayInt","acc02",["A1","B"]] - ,["liqRepay","bal","acc01","insuranceProvider"] - ,["liqRepay","bal","acc02","insuranceProvider"]] + ,["liqRepay","balance","acc01","insuranceProvider"] + ,["liqRepay","balance","acc02","insuranceProvider"]] ]} ,[["CollectedInterest","acc01"] ,["CollectedPrincipal","acc02"] ,["CollectedPrepayment","acc02"] ,["CollectedRecoveries","acc02"]] ,{"insuranceProvider": - {"lineOfCredit":100,"start":"2021-06-15" - ,"rate":{"fix":0.01}} + {"lineOfCredit":100,"start":"2021-06-15","fixRate":0.01 + ,"rateAccDates":"MonthEnd","lastAccDate":"2021-06-15"} }) diff --git a/absbox/tests/benchmark/us/test07.py b/absbox/tests/benchmark/us/test07.py index 0d58b28..9084636 100644 --- a/absbox/tests/benchmark/us/test07.py +++ b/absbox/tests/benchmark/us/test07.py @@ -66,7 +66,8 @@ ,["2027-06-28",6283905.88,7070447.52,77370.71] ,["2027-07-28",1919297.32,4364608.56,35979.93] ,["2027-08-28",0,1919297.32,10964.13] - ]} + ] + ,"extendBy":"MonthEnd"} ,(("distAcc",{"balance":0}) ,("cashReserve",{"balance":80_000_000.02 ,"type":{"fixReserve":80_000_000.02}}) @@ -91,22 +92,22 @@ ,{"default":[ ["transfer",'revolBuyAcc',"distAcc"] ,["transfer",'cashReserve',"distAcc"] - ,["payFee","distAcc",["admFee"],{"support":["suppportAccount",'cashReserve']}] - ,["payFee","distAcc",["serviceFee"],{"support":["suppportAccount",'cashReserve']}] + ,["payFee","distAcc",["admFee"],{"support":["account","cashReserve"]}] + ,["payFee","distAcc",["serviceFee"],{"support":["account","cashReserve"]}] ,["accrueAndPayInt","distAcc",["A"]] ,["accrueAndPayInt","cashReserve",["A"]] - ,["runTrigger",0] # update the trigger status during the waterfall + ,["runTrigger","ExcessTrigger"] # update the trigger status during the waterfall ,["If" - ,[("trigger","InDistribution",0),False] # if it was triggered + ,[("trigger","InDistribution","ExcessTrigger"),False] # if it was triggered ,["transfer","distAcc",'cashReserve',{"reserve":"gap"}]] # trasnfer amt to cash reserver account ,["IfElse" ,["status","Revolving"] # acitons in the revolving period ,[["transfer","distAcc",'revolBuyAcc',{"formula":("substract",("bondBalance",),("poolBalance",))}] ,["buyAsset",["Current|Defaulted",1.0,0],"revolBuyAcc",None] # buy asset with 1:1 if asset with performing - ,["payPrinResidual","distAcc",["Sub"]] ] + ,["payIntResidual","distAcc","Sub"] ] ,[["payPrin","distAcc",["A"]] # actions if deal is in Amortizing status ,["payPrin","distAcc",["Sub"]] ,["payFeeResidual", "distAcc", "bmwFee"]]]] @@ -124,19 +125,45 @@ ,None ,None ,None - ,{"BeforeDistribution":[ + ,{"BeforeDistribution": + {"DefTrigger": {"condition":["any" ,[">=","2024-05-26"] ,[("cumPoolDefaultedRate",),">",0.016]] ,"effects":("newStatus","Amortizing") ,"status":False - ,"curable":False}] - ,"InDistribution":[ + ,"curable":False}} + ,"InDistribution": + {"ExcessTrigger": {"condition":[("accountBalance","distAcc"),">",("bondBalance","A")] ,"effects":("newReserveBalance","cashReserve",{"fixReserve":0}) # if was triggered, change reserve account amount to 0 ,"status":False ,"curable":False} - ] + } } ,"Revolving" # start deal with "Revolving" status ) + +#perf = ("Mortgage",{"CDR":0.15},{"CPR":0.0015},{"Rate":0.3,"Lag":4},None) +# +#r = localAPI.run(BMW202301, +# poolAssump = ("Pool" +# ,("Mortgage",{"CDR":0.15},{"CPR":0.0015},{"Rate":0.3,"Lag":4},None) +# ,None +# ,None) +# ,runAssump = [("inspect",("MonthEnd",("bondBalance",)) +# ,("MonthFirst",("poolBalance",))) +# ,("interest" +# ,("LPR5Y",0.05)) +# ,("revolving" +# ,["constant" +# ,["Mortgage" +# ,{"originBalance":2200,"originRate":["fix",0.045],"originTerm":30 +# ,"freq":"Monthly","type":"Level","originDate":"2021-02-01"} +# ,{"currentBalance":2200 +# ,"currentRate":0.08 +# ,"remainTerm":20 +# ,"status":"current"}]] +# ,perf)] +# ,read=True) +#r['pool']['flow'] diff --git a/absbox/tests/benchmark/us/test08.py b/absbox/tests/benchmark/us/test08.py index 82ce5d4..77ec022 100644 --- a/absbox/tests/benchmark/us/test08.py +++ b/absbox/tests/benchmark/us/test08.py @@ -2,9 +2,9 @@ asset = ["AdjustRateMortgage" ,{"originBalance":73_875.00 - ,"originRate":{"floater":("USCMT1Y",0.01) - ,"rate":0.04 - ,"resets":"YearFirst"} + ,"originRate":["floater",0.04,{"index":"USCMT1Y" + ,"spread":0.01 + ,"reset":"YearFirst"}] ,"originTerm":360 ,"freq":"Monthly","type":"Level","originDate":"1999-05-01" ,"arm":{"initPeriod":2,"firstCap":0.01,"periodicCap":0.01,"lifeCap":0.09}} @@ -13,24 +13,7 @@ ,"remainTerm":77 ,"status":"current"}] -from absbox.local.generic import Generic - -asset = ["AdjustRateMortgage" - ,{"originBalance":73_875.00 - ,"originRate":{"floater":("USCMT1Y",0.01) - ,"rate":0.04 - ,"resets":"YearFirst"} - ,"originTerm":360 - ,"freq":"Monthly","type":"Level","originDate":"1999-05-01" - ,"arm":{"initPeriod":2,"firstCap":0.01,"periodicCap":0.01,"lifeCap":0.09}} - ,{"currentBalance":20_788.41 - ,"currentRate":0.0215 - ,"remainTerm":77 - ,"status":"current"}] - -from absbox.local.generic import Generic - -test01 = Generic( +GNMA_36208ALG4 = Generic( "820146/36208ALG4/G2-Custom AR" ,{"collect":["2023-05-01","2023-05-31"] ,"pay":["2023-05-26","2023-06-28"] @@ -44,9 +27,7 @@ ,"originBalance":1_553_836.00 ,"originRate":0.07 ,"startDate":"2020-01-03" - ,"rateType":{"floater": - [0.07,"USCMT1Y",0.01,"YearFirst"] - ,"dayCount":"DC_30_360_US"} + ,"rateType":{"floater": [0.07,"USCMT1Y",0.01,"YearFirst"],"dayCount":"DC_30_360_US"} ,"bondType":{"Sequential":None} ,"lastAccrueDate":"2023-04-30"}) ,) @@ -67,8 +48,10 @@ ,"endOfCollection":[ ["liqSupport","Ginnie_Mae","account","acc01" ,{"formula": - ("substract",("cumPoolDefaultedBalance",) - ,("liqCredit","Ginnie_Mae"))}] + ("floorWithZero" + ,("substract",("cumPoolDefaultedBalance",) + ,("liqBalance","Ginnie_Mae")))} + ] ,["calcInt","A1"]]} ,[["CollectedInterest","acc01"] ,["CollectedPrincipal","acc01"] @@ -78,22 +61,23 @@ ,None) -if __name__ == '__main__': - - localAPI = API("https://absbox.org/api/dev",lang='english') +r = localAPI.run(GNMA_36208ALG4 + ,runAssump = [("inspect",("MonthEnd",("cumPoolDefaultedBalance",)) + ,("MonthEnd",("liqBalance","Ginnie_Mae"))) + ,("interest",("USCMT1Y",0.0468))] + ,poolAssump = ("Pool" + ,("Mortgage",{"CDR":0.005},None,{"Rate":0.3,"Lag":4},None) + ,None + ,None) + ,read=True) + +# Inspect cumulative defaulted balance +r['result']['inspect'][''] + +# Inspect credit provided by Ginnie Mae +r['result']['inspect'][''] - r = localAPI.run(GNMA_36208ALG4 - ,assumptions = [{"Rate":["USCMT1Y",0.0468]} - ,{"CDR":0.005} - ,{"Inspect":[("MonthEnd",("cumPoolDefaultedBalance",)) - ,("MonthEnd",("liqCredit","Ginnie_Mae"))]}] - ,read=True) - - # Inspect cumulative defaulted balance - r['result']['inspect'][''] +# the cash deposited to SPV in account `acc01` +r['accounts']['acc01'][r['accounts']['acc01']["memo"]==""] - # Inspect credit provided by Ginnie Mae - r['result']['inspect'][''] - # the cash deposited to SPV in account `acc01` - r['accounts']['acc01'][r['accounts']['acc01']["memo"]==""] diff --git a/absbox/tests/config.json b/absbox/tests/config.json index 7512d3c..9244ed7 100644 --- a/absbox/tests/config.json +++ b/absbox/tests/config.json @@ -1,3 +1,3 @@ { - "test_server":"https://absbox.org/api/dev" + "test_server":"http://localhost:8081" }