Skip to content

Commit dca5b8b

Browse files
committed
expose AssetLevelCashflow
1 parent 8ed7fcc commit dca5b8b

File tree

4 files changed

+46
-43
lines changed

4 files changed

+46
-43
lines changed

absbox/client.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import requests
66
from rich.console import Console
77
import toolz as tz
8+
from functools import partial
89
from lenses import lens
910

1011
from requests.exceptions import ConnectionError, ReadTimeout
@@ -193,7 +194,7 @@ def __post_init__(self) -> None:
193194
self.session = requests.Session()
194195
console.print(f"✅Connected, local lib:{'.'.join(self.version)}, server:{'.'.join(engine_version)}")
195196

196-
def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[]) -> str:
197+
def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[], rtn = []) -> str:
197198
"""build run deal requests: (single run, multi-scenario run, multi-struct run) 2
198199
199200
:meta private:
@@ -217,22 +218,22 @@ def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[])
217218
_deal = deal.json if hasattr(deal, "json") else deal
218219
_perfAssump = earlyReturnNone(mkAssumpType, perfAssump)
219220
_nonPerfAssump = mkNonPerfAssumps({}, nonPerfAssump)
220-
r = mkTag((RunReqType.Single.value, [_deal, _perfAssump, _nonPerfAssump]))
221+
r = mkTag((RunReqType.Single.value, [rtn, _deal, _perfAssump, _nonPerfAssump]))
221222
case "MultiScenarios" | "MS":
222223
_nonPerfAssump = mkNonPerfAssumps({}, nonPerfAssump)
223224
_deal = deal.json if hasattr(deal, "json") else deal
224225
mAssump = mapValsBy(perfAssump, mkAssumpType)
225-
r = mkTag((RunReqType.MultiScenarios.value, [_deal, mAssump, _nonPerfAssump]))
226+
r = mkTag((RunReqType.MultiScenarios.value, [rtn, _deal, mAssump, _nonPerfAssump]))
226227
case "MultiStructs" | "MD" :
227228
_nonPerfAssump = mkNonPerfAssumps({}, nonPerfAssump)
228229
mDeal = {k: v.json if hasattr(v, "json") else v for k, v in deal.items()}
229230
_perfAssump = mkAssumpType(perfAssump)
230-
r = mkTag((RunReqType.MultiStructs.value, [mDeal, _perfAssump, _nonPerfAssump]))
231+
r = mkTag((RunReqType.MultiStructs.value, [rtn, mDeal, _perfAssump, _nonPerfAssump]))
231232
case "MultiRunScenarios" | "MRS" if isinstance(nonPerfAssump,dict):
232233
_deal = deal.json if hasattr(deal, "json") else deal
233234
_perfAssump = earlyReturnNone(mkAssumpType, perfAssump)
234235
mRunAssump = mapValsBy(nonPerfAssump, lambda x: mkNonPerfAssumps({}, x))
235-
r = mkTag((RunReqType.MultiRunScenarios.value, [_deal, _perfAssump, mRunAssump]))
236+
r = mkTag((RunReqType.MultiRunScenarios.value, [rtn, _deal, _perfAssump, mRunAssump]))
236237
case "ComboSensitivity" | "CS" if isinstance(nonPerfAssump,dict) and isinstance(perfAssump,dict) and isinstance(deal,dict):
237238
mDeal = {k: v.json if hasattr(v, "json") else v for k, v in deal.items()}
238239
mAssump = mapValsBy(perfAssump, mkAssumpType)
@@ -241,7 +242,7 @@ def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[])
241242
mRunAssump = mapValsBy(nonPerfAssump, lambda x: mkNonPerfAssumps({}, x))
242243
if mRunAssump == {}:
243244
mRunAssump = {"Empty":{}}
244-
r = mkTag((RunReqType.ComboSensitivity.value, [mDeal, mAssump, mRunAssump]))
245+
r = mkTag((RunReqType.ComboSensitivity.value, [rtn, mDeal, mAssump, mRunAssump]))
245246
case ("RootFinder", tweak, stop):
246247
_deal = deal.json if hasattr(deal, "json") else deal
247248
_perfAssump = earlyReturnNone(mkAssumpType, perfAssump)
@@ -269,7 +270,7 @@ def build_run_deal_req(self, run_type, deal, perfAssump=None, nonPerfAssump=[])
269270

270271

271272

272-
def build_pool_req(self, pool, poolAssump, rateAssumps, isMultiScenario=False) -> str:
273+
def build_pool_req(self, pool, poolAssump, rateAssumps, isMultiScenario=False, breakdown = False) -> str:
273274
"""build pool run request: (single run, multi-scenario run)
274275
275276
:meta private:
@@ -298,9 +299,9 @@ def buildPoolType(p) -> dict:
298299
return mkTag((assetTag, mkPoolType(assetDate, _p, False)))
299300

300301
if not isMultiScenario:
301-
r = mkTag((RunReqType.SinglePool.value, [buildPoolType(pool), mkAssumpType(poolAssump), _rateAssump]))
302+
r = mkTag((RunReqType.SinglePool.value, [breakdown, buildPoolType(pool), mkAssumpType(poolAssump), _rateAssump]))
302303
else:
303-
r = mkTag((RunReqType.MultiPoolScenarios.value, [buildPoolType(pool), mapValsBy(poolAssump, mkAssumpType), _rateAssump]))
304+
r = mkTag((RunReqType.MultiPoolScenarios.value, [breakdown, buildPoolType(pool), mapValsBy(poolAssump, mkAssumpType), _rateAssump]))
304305

305306
return json.dumps(r, ensure_ascii=False)
306307

@@ -320,6 +321,7 @@ def run(self, deal,
320321
runAssump=[],
321322
read=True,
322323
showWarning=True,
324+
rtn = [],
323325
debug=False) -> dict:
324326
""" run deal with pool and deal run assumptions
325327
@@ -348,7 +350,7 @@ def run(self, deal,
348350
url = f"{self.url}/{Endpoints.RunDeal.value}"
349351

350352
# construct request
351-
req = self.build_run_deal_req("Single", deal, poolAssump, runAssump)
353+
req = self.build_run_deal_req("Single", deal, poolAssump, runAssump, rtn=rtn)
352354
if debug:
353355
return req
354356

@@ -418,19 +420,28 @@ def runByScenarios(self, deal,
418420
else:
419421
return result
420422

421-
def read_single(self, pool_resp) -> tuple:
423+
def read_single(self, breakdown, pool_resp) -> tuple:
422424
""" read pool run response from engine and convert to dataframe
423425
424426
:param pool_resp: (pool raw cashflow, pool statistics)
425427
:type pool_resp: tuple
426428
:return: (pool Cashflow in dataFrame, pool statistics)
427429
:rtype: tuple
428430
"""
429-
(pool_flow, pool_bals) = pool_resp
430-
result = _read_cf(pool_flow['contents'][1], self.lang)
431-
return (result, pool_bals)
432431

433-
def runPoolByScenarios(self, pool, poolAssump, rateAssump=None, read=True, debug=False) -> dict :
432+
((pool_flow, pool_bals), pool_breakdown_flow) = pool_resp
433+
result = _read_cf(pool_flow['contents'][1], self.lang)
434+
if not breakdown:
435+
return {"flow":result, "stat":pool_bals}
436+
else:
437+
assert pool_breakdown_flow is not None, "Breakdown flow is None"
438+
assert len(pool_breakdown_flow)>0, "Breakdown flow is empty"
439+
return {"flow":result, "stat":pool_bals
440+
,"breakdown": [ {"flow": _read_cf(_[0]['contents'][1], self.lang), "stat":_[1]}
441+
for _ in pool_breakdown_flow ]
442+
}
443+
444+
def runPoolByScenarios(self, pool, poolAssump, rateAssump=None, read=True, breakdown = False,debug=False) -> dict :
434445
""" run a pool with multiple scenario ,return result as map , with key same to pool assumption map
435446
436447
:param pool: pool map
@@ -462,10 +473,10 @@ def runPoolByScenarios(self, pool, poolAssump, rateAssump=None, read=True, debug
462473
result = tz.valmap(lambda x:x['Right'] ,result)
463474

464475
if read:
465-
return result & lens.Values().Values().modify(self.read_single)
476+
return result & lens.Values().Values().modify(partial(self.read_single, breakdown))
466477
return result
467478

468-
def runPool(self, pool, poolAssump=None, rateAssump=None, read=True, debug=False, **kwargs) -> tuple:
479+
def runPool(self, pool, poolAssump=None, rateAssump=None, read=True, debug=False, breakdown = False, **kwargs) -> tuple:
469480
"""perform pool run with pool and rate assumptions
470481
471482
:param pool: a pool object
@@ -487,7 +498,7 @@ def runPool(self, pool, poolAssump=None, rateAssump=None, read=True, debug=False
487498

488499
url = f"{self.url}/{Endpoints.RunPool.value}"
489500

490-
req = self.build_pool_req(pool, poolAssump, rateAssump, isMultiScenario=False)
501+
req = self.build_pool_req(pool, poolAssump, rateAssump, isMultiScenario=False, breakdown = breakdown)
491502

492503
if debug:
493504
return req
@@ -497,10 +508,11 @@ def runPool(self, pool, poolAssump=None, rateAssump=None, read=True, debug=False
497508
if result is None or 'error' in result or 'Left' in result:
498509
leftVal = result.get("Left","")
499510
raise AbsboxError(f"❌ Failed to get response from run: {leftVal}")
511+
500512
result = result['Right']
501-
513+
502514
if read:
503-
return result & lens.Values().modify(self.read_single)
515+
return result & lens.Values().modify(partial(self.read_single, breakdown))
504516
else:
505517
return result
506518

absbox/local/component.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ def mkDs(x):
304304
if pNames:
305305
return mkTag(("PoolScheduleCfPv", [mkLiqMethod(pricingMethod), lmap(mkPid,pNames)]))
306306
return mkTag(("PoolScheduleCfPv", [mkLiqMethod(pricingMethod), None]))
307-
case ("资产池加权利差", pNames) | ("PoolWaSpread", pNames):
307+
case ("资产池加权利差", pNames) | ("poolWaSpread", pNames):
308308
if pNames:
309309
return mkTag(("PoolWaSpread", lmap(mkPid,pNames)))
310310
return mkTag(("PoolWaSpread", None))
@@ -1967,17 +1967,17 @@ def mkFieldRule(z):
19671967
def mkAssumpType(x):
19681968
''' make assumps either on pool level or asset level '''
19691969
match x:
1970-
case ("Pool", p, d, f):
1970+
case ("Pool", p, d, f) | ("pool", p, d, f):
19711971
return mkTag(("PoolLevel",mkPDF(p, d, f)))
1972-
case ("ByIndex", *ps):
1972+
case ("ByIndex", *ps) | ("byIndex", *ps):
19731973
return mkTag(("ByIndex",[ [idx, mkPDF(a,b,c)] for (idx,(a,b,c)) in ps ]))
1974-
case ("ByObligor",*rules):
1974+
case ("ByObligor",*rules) | ("byObligor",*rules):
19751975
return mkTag(("ByObligor",[mkObligorStrategy(r) for r in rules]))
1976-
case ("ByName", assumpMap):
1976+
case ("ByName", assumpMap) | ("byName", assumpMap):
19771977
return mkTag(("ByName",{f"PoolName:{k}":mkPDF(*v) for k,v in assumpMap.items()}))
1978-
case ("ByPoolId", assumpMap):
1978+
case ("ByPoolId", assumpMap) | ("byPoolId", assumpMap):
19791979
return mkTag(("ByPoolId",{f"PoolName:{k}": mkAssumpType(v) for k,v in assumpMap.items()}))
1980-
case ("ByDealName", assumpMap):
1980+
case ("ByDealName", assumpMap) | ("byDealName", assumpMap):
19811981
return mkTag(("ByDealName",{k:(mkAssumpType(perfAssump),mkNonPerfAssumps({},nonPerfAssump))
19821982
for k,(perfAssump,nonPerfAssump) in assumpMap.items()}))
19831983
case None:
@@ -2020,18 +2020,6 @@ def mkRevolvingPool(x):
20202020

20212021

20222022
def mkPoolType(assetDate, x, mixedFlag) -> dict:
2023-
# try:
2024-
# if 'assets' in x or "清单" in x or "归集表" in x:
2025-
# return mkTag(("MultiPool" ,{"PoolConsol":mkPoolComp(vDate(assetDate), x, False)}))
2026-
# elif 'deals' in x and isinstance(x['deals'],dict):
2027-
# return mkTag(("ResecDeal",{f"{dealObj.json['contents']['name']}:{bn}:{sd}:{str(pct)}": \
2028-
# {"deal":dealObj.json['contents'],"future":None,"futureScheduleCf":None,"issuanceStat":None}\
2029-
# for ((bn,pct,sd),dealObj) in x['deals'].items()} ))
2030-
# else:
2031-
# return mkTag(("MultiPool" ,{f"PoolName:{k}":mkPoolComp(vDate(assetDate),v,mixedFlag) for (k,v) in x.items()}))
2032-
# except Exception as e:
2033-
# print("Error in mk pool type",e)
2034-
20352023
match x:
20362024
case {"assets": y} | {"清单": y} | {"归集表": y}:
20372025
return mkTag(("MultiPool" ,{"PoolConsol":mkPoolComp(vDate(assetDate), x, False)}))
@@ -2055,7 +2043,8 @@ def mkPoolComp(asOfDate, x, mixFlag) -> dict:
20552043
, "issuanceStat": tz.pipe(getValWithKs(x, ["issuanceStat", "统计", "发行", "Issuance"],defaultReturn={})
20562044
, lambda y: updateKs(y, validCutoffFields)
20572045
)
2058-
, "futureCf":mkCf(getValWithKs(x, ['cashflow', '现金流归集表', '归集表'], []))
2046+
, "futureCf":[mkCf(getValWithKs(x, ['cashflow', '现金流归集表', '归集表'], [])),None]
2047+
, "futureScheduleCf" : [mkCf([]), None]
20592048
, "extendPeriods":mkDatePattern(getValWithKs(x, ['extendBy'], "MonthEnd"))}
20602049
return r
20612050

@@ -2152,7 +2141,7 @@ def mkLedger(n: str, x: dict=None):
21522141
def mkCf(x:list):
21532142
""" Make project cashflow ( Mortgage Only ) """
21542143
if len(x) == 0:
2155-
return None
2144+
return mkTag(("CashFlowFrame", [[0,"1900-01-01",None],[]]))
21562145
else:
21572146
cfs = [mkTag(("MortgageFlow", _x+[0.0]*5+[None,None,None])) for _x in x]
21582147
return mkTag(("CashFlowFrame", [[0,"1900-01-01",None],cfs]))

absbox/local/generic.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ def read(resp):
145145
output['pool']['flow'] = readPoolCf(deal_content['pool']['contents']['futureCf']['contents'])
146146
elif deal_content['pool']['tag']=='MultiPool':
147147
poolMap = deal_content['pool']['contents']
148-
output['pool']['flow'] = tz.valmap(lambda v: readPoolCf(v['futureCf']['contents']) if (not v['futureCf'] is None) else pd.DataFrame(), poolMap)
148+
output['pool']['flow'] = tz.valmap(lambda v: readPoolCf(v['futureCf'][0]['contents']) if (not v['futureCf'] is None) else pd.DataFrame(), poolMap)
149+
output['pool']['breakdown'] = tz.valmap(lambda v: list(tz.map(readPoolCf, v['futureCf'][1] & lens.Each()['contents'].collect() )) if (not v['futureCf'][1] is None) else [], poolMap)
149150
elif deal_content['pool']['tag']=='ResecDeal':
150151
poolMap = deal_content['pool']['contents']
151152
output['pool']['flow'] = {tz.get([1,2,4],k.split(":")): readPoolCf(v['futureCf']['contents']) for (k,v) in poolMap.items() }

absbox/local/util.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ def _read_cf(x, lang):
323323
''' read cashflow from a list , and set index to date'''
324324
if x == []:
325325
return []
326+
assert isinstance(x, list), f"Input is not a list but {type(x)}"
326327
flow_header, idx, expandFlag = guess_pool_flow_header(x[0], lang)
327328
result = None
328329
try:
@@ -331,7 +332,7 @@ def _read_cf(x, lang):
331332
else:
332333
result = pd.DataFrame([_['contents'] for _ in x], columns=flow_header)
333334
except ValueError as e:
334-
print(e)
335+
logging.error(f"{e}")
335336
logging.error(f"Failed to match header:{flow_header} with {result}")
336337
return False
337338
result.set_index(idx, inplace=True)

0 commit comments

Comments
 (0)