Skip to content

Commit 66df64e

Browse files
committed
expose Receivable Asset Class
1 parent 01354db commit 66df64e

File tree

6 files changed

+180
-34
lines changed

6 files changed

+180
-34
lines changed

absbox/client.py

+35-33
Original file line numberDiff line numberDiff line change
@@ -143,18 +143,18 @@ def __post_init__(self) -> None:
143143
:raises VersionMismatch: Failed to match version between client and server
144144
"""
145145
self.url = isValidUrl(self.url).rstrip("/")
146-
with console.status(f"{MsgColor.Info.value}Connecting engine server -> {self.url}") as status:
147-
try:
148-
_r = requests.get(f"{self.url}/{Endpoints.Version.value}", verify=False, timeout=5).text
149-
except (ConnectionRefusedError, ConnectionError):
150-
raise AbsboxError(f"❌{MsgColor.Error.value}Error: Can't not connect to API server {self.url}")
151-
if _r is None:
152-
raise RuntimeError(f"Failed to get version from url:{self.url}")
153-
self.server_info = self.server_info | json.loads(_r)
154-
engine_version = self.server_info['_version'].split(".")
155-
if self.check and (self.version[1] != engine_version[1]):
156-
console.print("pls upgrade your api package by: pip -U absbox")
157-
raise VersionMismatch('.'.join(self.version), '.'.join(engine_version))
146+
console.print(f"{MsgColor.Info.value}Connecting engine server -> {self.url}")
147+
try:
148+
_r = requests.get(f"{self.url}/{Endpoints.Version.value}", verify=False, timeout=5, headers = {"Origin":"http://localhost:8001"}).text
149+
except (ConnectionRefusedError, ConnectionError):
150+
raise AbsboxError(f"❌{MsgColor.Error.value}Error: Can't not connect to API server {self.url}")
151+
if _r is None:
152+
raise RuntimeError(f"Failed to get version from url:{self.url}")
153+
self.server_info = self.server_info | json.loads(_r)
154+
engine_version = self.server_info['_version'].split(".")
155+
if self.check and (self.version[1] != engine_version[1]):
156+
console.print("pls upgrade your api package by: pip -U absbox")
157+
raise VersionMismatch('.'.join(self.version), '.'.join(engine_version))
158158
console.print(f"✅{MsgColor.Success.value}Connected, local lib:{'.'.join(self.version)}, server:{'.'.join(engine_version)}")
159159
self.session = requests.Session()
160160

@@ -371,12 +371,15 @@ def readResult(x):
371371
except Exception as e:
372372
print(f"Failed to read result {x} \n with error {e}")
373373
return (None, None, None)
374+
374375
url = f"{self.url}/{Endpoints.RunAsset.value}"
375376
_assumptions = mkAssumpType(poolAssump) if poolAssump else None
376-
_pricing = mkLiqMethod(pricing) if pricing else None
377377
_rate = lmap(mkRateAssumption, rateAssump) if rateAssump else None
378-
assets = lmap(mkAssetUnion, _assets) # [mkAssetUnion(_) for _ in _assets]
379-
req = json.dumps([date, assets, _assumptions, _rate, _pricing], ensure_ascii=False)
378+
_pricing = mkLiqMethod(pricing) if pricing else None
379+
assets = lmap(mkAssetUnion, _assets)
380+
req = json.dumps([date, assets, _assumptions, _rate, _pricing]
381+
, ensure_ascii=False)
382+
#console.print_json(req)
380383
result = self._send_req(req, url)
381384
if read:
382385
return readResult(result)
@@ -539,21 +542,20 @@ def _send_req(self, _req, _url: str, timeout=10, headers={})-> dict | None:
539542
:return: response in dict
540543
:rtype: dict | None
541544
"""
542-
with console.status("") as status:
543-
try:
544-
hdrs = self.hdrs | headers
545-
r = None
546-
if self.session:
547-
r = self.session.post(_url, data=_req.encode('utf-8'), headers=hdrs, verify=False, timeout=timeout)
548-
else:
549-
raise AbsboxError(f"❌: None type for session")
550-
except (ConnectionRefusedError, ConnectionError):
551-
raise AbsboxError(f"❌ Failed to talk to server {_url}")
552-
except ReadTimeout:
553-
raise AbsboxError(f"❌ Failed to get response from server")
554-
if r.status_code != 200:
555-
raise EngineError(r)
556-
try:
557-
return json.loads(r.text)
558-
except JSONDecodeError as e:
559-
raise EngineError(e)
545+
try:
546+
hdrs = self.hdrs | headers
547+
r = None
548+
if self.session:
549+
r = self.session.post(_url, data=_req.encode('utf-8'), headers=hdrs, verify=False, timeout=timeout)
550+
else:
551+
raise AbsboxError(f"❌: None type for session")
552+
except (ConnectionRefusedError, ConnectionError):
553+
raise AbsboxError(f"❌ Failed to talk to server {_url}")
554+
except ReadTimeout:
555+
raise AbsboxError(f"❌ Failed to get response from server")
556+
if r.status_code != 200:
557+
raise EngineError(r)
558+
try:
559+
return json.loads(r.text)
560+
except JSONDecodeError as e:
561+
raise EngineError(e)

absbox/local/base.py

+7
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
english_rental_flow = ['Balance', 'Rental']
3232
china_rental_flow_d = ["日期"] + china_rental_flow
3333
english_rental_flow_d = ["Date"] + english_rental_flow
34+
3435
## Loan flow
3536
china_loan_flow = ["余额", "本金", "利息", "早偿金额", "违约金额", "回收金额", "损失金额", "利率"]
3637
english_loan_flow = ["Balance", "Principal", "Interest", "Prepayment", "Default", "Recovery", "Loss", "WAC"]
@@ -43,6 +44,12 @@
4344
china_fixed_flow_d = ["日期"] + china_fixed_flow
4445
english_fixed_flow_d = ["Date"] + english_fixed_flow
4546

47+
## Receivable
48+
china_receivable_flow = ["余额", "应计费用", "本金", "费用", "违约", "回收", "损失"]
49+
english_receivable_flow = ["Balance", "AccuredFee", "Principal", "Fee", "Default", "Recovery", "Loss"]
50+
china_receivable_flow_d = ["日期"] + china_receivable_flow
51+
english_receivable_flow_d = ["Date"] + english_receivable_flow
52+
4653
## Underlying Bond Flow
4754
china_uBond_flow = ["余额", "本金", "利息"]
4855
english_uBond_flow = ["Balance", "Principal", "Interest"]

absbox/local/component.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ def mkPoolSource(x):
164164
return "CollectedRental"
165165
case "现金" | "Cash" | "现金回款" | "CollectedCash":
166166
return "CollectedCash"
167+
case "费用" | "Fee" | "现金回款" | "CollectedFeePaid":
168+
return "CollectedFeePaid"
167169
case "新增违约" | "Defaults":
168170
return "NewDefaults"
169171
case "新增拖欠" | "Delinquencies":
@@ -1131,6 +1133,20 @@ def mkAccRule(x):
11311133
case _ :
11321134
raise RuntimeError(f"Failed to match {x}:mkAccRule")
11331135

1136+
def mkInvoiceFeeType(x):
1137+
match x :
1138+
case ("Fixed", amt) | ("固定", amt):
1139+
return mkTag(("FixedFee", vNum(amt)))
1140+
case ("FixedRate", rate) | ("固定比例", rate):
1141+
return mkTag(("FixedRateFee", vNum(rate)))
1142+
case ("FactorFee", rate, days, rnd) | ("周期计费", rate, days, rnd):
1143+
return mkTag(("FactorFee", [vNum(rate), vInt(days), mkRoundingType(rnd)]))
1144+
case ("AdvanceRate", rate) | ("提前比例", rate):
1145+
return mkTag(("AdvanceFee", vNum(rate)))
1146+
case ("CompoundFee", *fs) | ("复合计费", *fs):
1147+
return mkTag(("CompoundFee", lmap(mkInvoiceFeeType, fs)))
1148+
case _:
1149+
raise RuntimeError(f"Failed to match {x}:mkInvoiceFeeType")
11341150

11351151
def mkCapacity(x):
11361152
match x:
@@ -1231,6 +1247,12 @@ def mkAsset(x):
12311247
,"period":freqMap[p],"accRule":mkAccRule(ar)
12321248
,"capacity":mkCapacity(cap)} | mkTag("FixedAssetInfo")
12331249
,vInt(rt)]))
1250+
case ["Invoice", {"start":sd,"originBalance":ob,"originAdvance":oa,"dueDate":dd,"feeType":ft},{"status":status}] :
1251+
return mkTag(("Invoice",[{"startDate":vDate(sd),"originBalance":vNum(ob),"originAdvance":vNum(oa),"dueDate":vDate(dd),"feeType":mkInvoiceFeeType(ft)} | mkTag("ReceivableInfo")
1252+
,mkAssetStatus(status)]))
1253+
case ["Invoice", {"start":sd,"originBalance":ob,"originAdvance":oa,"dueDate":dd},{"status":status}] :
1254+
return mkTag(("Invoice",[{"startDate":vDate(sd),"originBalance":vNum(ob),"originAdvance":vNum(oa),"dueDate":vDate(dd),"feeType":None} | mkTag("ReceivableInfo")
1255+
,mkAssetStatus(status)]))
12341256
case _:
12351257
raise RuntimeError(f"Failed to match {x}:mkAsset")
12361258

@@ -1254,6 +1276,8 @@ def id_by_pool_assets(z):
12541276
return "RDeal"
12551277
case {"assets": [{'tag': 'FixedAsset'}, *rest]}:
12561278
return "FDeal"
1279+
case {"assets": [{'tag': 'Invoice'}, *rest]}:
1280+
return "VDeal"
12571281
case _:
12581282
raise RuntimeError(f"Failed to identify deal type {z}")
12591283
y = None
@@ -1311,6 +1335,8 @@ def mkAssumpDefault(x):
13111335
return mkTag(("DefaultCDR", vNum(r)))
13121336
case {"ByAmount": (bal, rs)}:
13131337
return mkTag(("DefaultByAmt", (vNum(bal), vList(rs,float))))
1338+
case "DefaultAtEnd":
1339+
return mkTag(("DefaultAtEnd"))
13141340
case _ :
13151341
raise RuntimeError(f"failed to match {x}")
13161342

@@ -1428,6 +1454,10 @@ def mkExtraStress(y):
14281454
case ("Fixed",utilCurve,priceCurve):
14291455
return mkTag(("FixedAssetAssump",[mkTs("RatioCurve",utilCurve)
14301456
,mkTs("BalanceCurve",priceCurve)]))
1457+
case ("Receivable", md, mr, mes):
1458+
d = earlyReturnNone(mkAssumpDefault,md)
1459+
r = earlyReturnNone(mkAssumpRecovery,mr)
1460+
return mkTag(("ReceivableAssump",[d, r, mkExtraStress(mes)]))
14311461
case _:
14321462
raise RuntimeError(f"failed to match {x}")
14331463

@@ -1467,6 +1497,8 @@ def mkAssetUnion(x):
14671497
return mkTag(("LS", mkAsset(x)))
14681498
case "固定资产" | "FixedAsset" :
14691499
return mkTag(("FA", mkAsset(x)))
1500+
case "应收帐款" | "Invoice" :
1501+
return mkTag(("RE", mkAsset(x)))
14701502
case _:
14711503
raise RuntimeError(f"Failed to match AssetUnion {x}")
14721504

@@ -1507,7 +1539,8 @@ def mkPoolComp(asOfDate, x, mixFlag) -> dict:
15071539

15081540
def mkPool(x: dict):
15091541
mapping = {"LDeal": "LPool", "MDeal": "MPool",
1510-
"IDeal": "IPool", "RDeal": "RPool", "FDeal":"FPool"}
1542+
"IDeal": "IPool", "RDeal": "RPool", "FDeal":"FPool",
1543+
"VDeal": "VPool"}
15111544
match x:
15121545
case {"清单": assets, "封包日": d} | {"assets": assets, "cutoffDate": d}:
15131546
_pool = {"assets": [mkAsset(a) for a in assets] , "asOfDate": d}

absbox/local/util.py

+4
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ def guess_pool_flow_header(x, l):
269269
return (china_fixed_flow_d, "日期", False)
270270
case ('FixedFlow', 6, 'english'):
271271
return (english_fixed_flow_d, "Date", False)
272+
case ('ReceivableFlow', 9, 'chinese'):
273+
return (china_receivable_flow_d+china_cumStats, "日期", True)
274+
case ('ReceivableFlow', 9, 'english'):
275+
return (english_receivable_flow_d+english_cumStats, "Date", True)
272276
case ('BondFlow', 4, 'chinese'):
273277
return (china_uBond_flow_d, "日期", False)
274278
case ('BondFlow', 4, 'english'):

docs/source/analytics.rst

+32
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,38 @@ Installment
168168
* <default assump> : ``{"CDR":<%>}``
169169
* <prepayment assump> : ``{"CPR":<%>}``
170170

171+
Receivable
172+
^^^^^^^^^^^^^^^^^^^^^
173+
174+
user can set assumption on receivable asset class:
175+
176+
* assume default at last period ( 0 cash received )
177+
* a CDR way ,whcih is a percentage of current balance remains.
178+
179+
.. code-block:: python
180+
181+
# apply on asset level
182+
r = localAPI.run(test01
183+
,runAssump=[]
184+
,poolAssump = ("ByIndex"
185+
,([0],(("Receivable", {"CDR":0.12}, None, None)
186+
,None,None))
187+
,([1],(("Receivable", "DefaultAtEnd", None, None)
188+
,None,None))
189+
)
190+
,read=True)
191+
192+
receivableAssump = ("Pool"
193+
,("Receivable", {"CDR":0.01}, None, None)
194+
,None
195+
,None)
196+
197+
# apply on pool level
198+
r = localAPI.run(test01
199+
,runAssump=[]
200+
,poolAssump = receivableAssump
201+
,read=True)
202+
171203
Extra Stress
172204
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173205

docs/source/modeling.rst

+68
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,74 @@ This type of asset can be used to model `future flow of securitization` : `Hotel
13491349
To project cashflow , user MUST set assumption for this type asset.
13501350
13511351
1352+
Receivable
1353+
^^^^^^^^^^^^^^^^
1354+
1355+
.. versionadded:: 0.26.5
1356+
1357+
``Receivable`` is a type of asset which has a fixed amount of receivable in last period, with optional fee collected at end
1358+
1359+
* if No fee was set, the receivable will be paid off at last period without fee.
1360+
* if there are multiple fee being setup, then ALL fees are sum up and paid off at last period.
1361+
1362+
syntax
1363+
``["Invoice", {<asset description>}, {<status>}]``
1364+
1365+
* ``asset description`` -> a map describe the asset
1366+
* ``status`` -> a map describe the status of asset
1367+
1368+
* feeType
1369+
1370+
* ``Fixed`` -> fixed amount of fee
1371+
* ``FixedRate`` -> fixed rate of fee, the base was the ``originBalance``
1372+
* ``AdvanceRate`` -> annualized rate of advance amount, the base was the ``originAdvance``
1373+
* ``(FactorFee,<service rate>,<days of period>,<rounding>)`` -> total fee = ``service rate`` * ``originBalance`` * (``days of the invioce``/ ``<days of periods>``)
1374+
* ``(CompoundFee,<feeType1>,<feeType2>.....)`` -> a compound fee with multiple fee type
1375+
1376+
1377+
.. code-block:: python
1378+
1379+
receivable = ["Invoice"
1380+
,{"start":"2024-04-01","originBalance":2000
1381+
,"originAdvance":1500,"dueDate":"2024-06-01"}
1382+
,{"status":"Current"}]
1383+
1384+
1385+
receivable1 = ["Invoice"
1386+
,{"start":"2024-04-01","originBalance":2000
1387+
,"originAdvance":1500,"dueDate":"2024-08-01"
1388+
,"feeType":("Fixed",150)}
1389+
,{"status":"Current"}]
1390+
1391+
receivable2 = ["Invoice"
1392+
,{"start":"2024-04-01","originBalance":2000
1393+
,"originAdvance":1500,"dueDate":"2024-06-01"
1394+
,"feeType":("FixedRate",0.1)}
1395+
,{"status":"Current"}]
1396+
1397+
receivable3 = ["Invoice"
1398+
,{"start":"2024-04-01","originBalance":2000
1399+
,"originAdvance":1500,"dueDate":"2024-06-01"
1400+
,"feeType":("AdvanceRate", 0.12)}
1401+
,{"status":"Current"}]
1402+
1403+
receivable4 = ["Invoice"
1404+
,{"start":"2024-04-01","originBalance":2000
1405+
,"originAdvance":1500,"dueDate":"2024-06-01"
1406+
,"feeType":("FactorFee", 0.01, 25,["floor",0.1])}
1407+
,{"status":"Current"}]
1408+
1409+
receivable5 = ["Invoice"
1410+
,{"start":"2024-04-01","originBalance":2000
1411+
,"originAdvance":1500,"dueDate":"2024-06-01"
1412+
,"feeType":("CompoundFee"
1413+
,("AdvanceRate", 0.12)
1414+
,("Fixed",150)
1415+
)}
1416+
,{"status":"Current"}]
1417+
1418+
1419+
13521420
Collection Rules
13531421
-------------------
13541422

0 commit comments

Comments
 (0)