Skip to content

Commit 9db875b

Browse files
committed
refactor on lease projection/model
1 parent cb45133 commit 9db875b

File tree

5 files changed

+135
-62
lines changed

5 files changed

+135
-62
lines changed

absbox/local/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@
3939
china_mortgage_delinq_flow_fields_d = [china_date] + china_mortgage_delinq_flow_fields
4040
english_mortgage_delinq_flow_fields_d = [english_date] + english_mortgage_delinq_flow_fields
4141

42-
## Rental
43-
china_rental_flow = ['待收金额', '租金']
44-
english_rental_flow = ['Balance', 'Rental']
42+
## Rental/Lease flow
43+
china_rental_flow = ['待收金额', '租金', '违约']
44+
english_rental_flow = ['Balance', 'Rental', 'Default']
4545
china_rental_flow_d = [china_date] + china_rental_flow
4646
english_rental_flow_d = [english_date] + english_rental_flow
4747

absbox/local/component.py

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,9 +1410,9 @@ def mkAmortPlan(x) -> dict:
14101410
return mkTag(("NO_FirstN", [n, mkAmortPlan(_pt)]))
14111411
case ("IO_FirstN", n, _pt):
14121412
return mkTag(("IO_FirstN", [n, mkAmortPlan(_pt)]))
1413-
case ("计划还款", ts, Dp) | ("Schedule", ts, Dp):
1413+
case ("计划还款", ts, Dp) | ("Schedule", ts, Dp) | ("schedule", ts, Dp):
14141414
return mkTag(("ScheduleRepayment", [mkTs("RatioCurve", ts), mkDatePattern(Dp)]))
1415-
case ("计划还款", ts) | ("Schedule", ts):
1415+
case ("计划还款", ts) | ("Schedule", ts) | ("schedule", ts):
14161416
return mkTag(("ScheduleRepayment", [mkTs("RatioCurve", ts), None]))
14171417
case ("Balloon", n):
14181418
return mkTag(("Balloon", n))
@@ -1524,6 +1524,15 @@ def mkLeaseStepUp(x):
15241524
return mkTag(("ByAmountCurve", vList(bs, vNum)))
15251525
case _ :
15261526
raise RuntimeError(f"Failed to match {x}:mkLeaseStepUp")
1527+
1528+
def mkLeaseCalc(x):
1529+
match x:
1530+
case ("byDay", dr, dp):
1531+
return mkTag(("ByDayRate", [dr, mkDatePattern(dp)]))
1532+
case ("byPeriod", rental, period):
1533+
return mkTag(("ByPeriodRetnal", [rental, period]))
1534+
case _ :
1535+
raise RuntimeError(f"Failed to match {x}:mkLeaseCalc")
15271536

15281537

15291538
def mkAsset(x):
@@ -1600,25 +1609,28 @@ def mkAsset(x):
16001609
vNum(currentBalance),
16011610
vInt(remainTerms),
16021611
mkAssetStatus(status)]))
1603-
case ["租赁", {"固定租金": dailyRate, "初始期限": originTerm, "频率": dp, "起始日": startDate, "状态": status, "剩余期限": remainTerms}] \
1604-
| ["Lease", {"fixRental": dailyRate, "originTerm": originTerm, "freq": dp, "originDate": startDate, "status": status, "remainTerm": remainTerms}]:
1612+
case ["租赁", {"租金": rental, "初始期限": originTerm, "起始日": startDate}
1613+
,{"当前余额": bal, "剩余期限": remainTerms,"状态": status}] \
1614+
| ["Lease", {"rental": rental, "originTerm": originTerm, "originDate": startDate}
1615+
, {"currentBalance": bal,"status": status, "remainTerm": remainTerms}]:
16051616
obligorInfo = getValWithKs(x[1],["obligor","借款人"], mapping=mkObligor)
16061617
return mkTag(("RegularLease"
1607-
, [{"originTerm": originTerm, "startDate": startDate, "paymentDates": mkDatePattern(dp), "originRental": dailyRate
1618+
, [{"originTerm": originTerm, "startDate": startDate, "originRental": mkLeaseCalc(rental)
16081619
,"obligor": obligorInfo} | mkTag("LeaseInfo")
1609-
, 0
1610-
, remainTerms
1620+
, vNum(bal)
1621+
, vInt(remainTerms)
16111622
, mkAssetStatus(status)]))
1612-
case ["租赁", {"初始租金": dailyRate, "初始期限": originTerm, "频率": dp, "起始日": startDate, "状态": status, "剩余期限": remainTerms,"调整":sut}] \
1613-
| ["Lease", {"initRental": dailyRate, "stepUp":sut, "originTerm": originTerm, "freq": dp, "originDate": startDate, "status": status, "remainTerm": remainTerms}]:
1614-
1623+
case ["租赁", {"租金": rental, "初始期限": originTerm, "起始日": startDate, "调整":sut}
1624+
,{"当前余额":bal,"状态": status, "剩余期限": remainTerms}] \
1625+
| ["Lease", {"rental": rental, "stepUp":sut, "originTerm": originTerm, "originDate": startDate}
1626+
,{"currentBalance":bal, "status": status, "remainTerm": remainTerms}]:
16151627
obligorInfo = getValWithKs(x[1],["obligor","借款人"], mapping=mkObligor)
16161628
_stepUpType = mkLeaseStepUp(sut)
16171629
return mkTag(("StepUpLease"
1618-
, [{"originTerm": originTerm, "startDate": startDate, "paymentDates": mkDatePattern(dp), "originRental": dailyRate, "obligor": obligorInfo} | mkTag("LeaseInfo")
1630+
, [{"originTerm": originTerm, "startDate": startDate, "originRental": mkLeaseCalc(rental), "obligor": obligorInfo} | mkTag("LeaseInfo")
16191631
, _stepUpType
1620-
, 0
1621-
, remainTerms
1632+
, vNum(bal)
1633+
, vInt(remainTerms)
16221634
, mkAssetStatus(status)]))
16231635
case ["固定资产",{"起始日":sd,"初始余额":ob,"初始期限":ot,"残值":rb,"周期":p,"摊销":ar,"产能":cap}
16241636
,{"剩余期限":rt,"余额":bal}] \
@@ -1745,7 +1757,7 @@ def mkAssumpDefault(x):
17451757
case {"byTerm": rs}:
17461758
return mkTag(("DefaultByTerm", vListOfList(rs, numVal)))
17471759
case _ :
1748-
raise RuntimeError(f"failed to match {x}")
1760+
raise RuntimeError(f"failed to match {x}:mkAssumpDefault")
17491761

17501762

17511763
def mkAssumpPrepay(x):
@@ -1764,7 +1776,7 @@ def mkAssumpPrepay(x):
17641776
case {"byTerm": rs}:
17651777
return mkTag(("PrepaymentByTerm", vListOfList(rs, numVal)))
17661778
case _ :
1767-
raise RuntimeError(f"failed to match {x}")
1779+
raise RuntimeError(f"failed to match {x}:mkAssumpPrepay")
17681780

17691781

17701782
def mkAssumpDelinq(x):
@@ -1773,19 +1785,17 @@ def mkAssumpDelinq(x):
17731785
case {"DelinqCDR": cdr, "Lag": lag, "DefaultPct": pct}:
17741786
return mkTag(("DelinqCDR", [cdr, (lag, pct)]))
17751787
case _:
1776-
raise RuntimeError(f"failed to match {x}")
1788+
raise RuntimeError(f"failed to match {x}:mkAssumpDelinq")
17771789

17781790

17791791
def mkAssumpLeaseGap(x):
17801792
match x:
1781-
case {"Days":d}:
1793+
case {"Days":d} | ("days",d):
17821794
return mkTag(("GapDays",vInt(d)))
1783-
case {"DaysByAmount":(tbl,d)}:
1784-
return mkTag(("GapDaysByAmount",[tbl, vInt(d)]))
1785-
case {"DaysByCurve": ts}:
1786-
return mkTag(("GapDaysByCurve",mkTs("IntCurve",ts)))
1795+
case ("byCurve", c):
1796+
return mkTag(("GapDaysByCurve", mkTs("IntCurve",c)))
17871797
case _:
1788-
raise RuntimeError(f"failed to match {x}")
1798+
raise RuntimeError(f"failed to match {x}:mkAssumpLeaseGap")
17891799

17901800

17911801
def mkAssumpLeaseRent(x):
@@ -1795,18 +1805,26 @@ def mkAssumpLeaseRent(x):
17951805
case {"CurveIncrease":rc} | ("byRateCurve", rc):
17961806
return mkTag(("BaseCurve", mkTs("RateCurve",rc)))
17971807
case _:
1798-
raise RuntimeError(f"failed to match {x}")
1808+
raise RuntimeError(f"failed to match {x}:mkAssumpLeaseRent")
17991809

18001810

18011811
def mkAssumpLeaseEndType(x):
18021812
match x:
1803-
case {"byDate":d} | ("byDate",d):
1804-
return mkTag("CutByDate",vDate(d))
1813+
case {"byDate":d} | ("byDate", d):
1814+
return mkTag("CutByDate", vDate(d))
18051815
case {"stopByExtNum":n} | ("byExtTimes",n):
1806-
return mkTag("StopByExtTimes",vInt(n))
1816+
return mkTag("StopByExtTimes", vInt(n))
18071817
case _:
1808-
raise RuntimeError(f"failed to match {x}")
1818+
raise RuntimeError(f"failed to match {x}:mkAssumpLeaseEndType")
18091819

1820+
def mkAssumpLeaseDefaultType(x):
1821+
match x:
1822+
case ("byContinuation",r):
1823+
return mkTag(("DefaultByContinuation", vNum(r)))
1824+
case ("byTermination",r):
1825+
return mkTag(("DefaultByTermination", vNum(r)))
1826+
case _:
1827+
raise RuntimeError(f"failed to match {x}")
18101828

18111829

18121830
def mkAssumpRecovery(x):
@@ -1876,7 +1894,7 @@ def mkExtraStress(y):
18761894
r = earlyReturnNone(mkAssumpRecovery,mr)
18771895
return mkTag(("MortgageAssump",[d,p,r,mkExtraStress(mes)]))
18781896
case ("Lease", md, gap, rent, endType):
1879-
return mkTag(("LeaseAssump",[earlyReturnNone(mkAssumpDefault,md)
1897+
return mkTag(("LeaseAssump",[earlyReturnNone(mkAssumpLeaseDefaultType,md)
18801898
,mkAssumpLeaseGap(gap)
18811899
,mkAssumpLeaseRent(rent)
18821900
,mkEndType(endType)

absbox/local/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,9 @@ def guess_pool_flow_header(x, l):
255255
return (english_loan_flow_d, "Date", False)
256256
case ('LoanFlow', 10, 'english'):
257257
return (english_loan_flow_d+english_cumStats, "Date", True)
258-
case ('LeaseFlow', 3, 'chinese'):
258+
case ('LeaseFlow', 4, 'chinese'):
259259
return (china_rental_flow_d, "日期", False)
260-
case ('LeaseFlow', 3, 'english'):
260+
case ('LeaseFlow', 4, 'english'):
261261
return (english_rental_flow_d, "Date", False)
262262
case ('FixedFlow', 6, 'chinese'):
263263
return (china_fixed_flow_d, "日期", False)

docs/source/analytics.rst

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ Lease
433433
.. code-block:: python
434434
435435
r = localAPI.run(deal
436-
,poolAssump = ("Pool",("Lease",<turnover gap>,<rental assump>,<end date>)
436+
,poolAssump = ("Pool",("Lease", <default assumption>,<turnover gap>, <rental assump>, <end type>)
437437
,<delinq assumption>
438438
,<defaulted assumption>
439439
)
@@ -442,9 +442,34 @@ Lease
442442
443443
Notes:
444444

445+
* ``<default assumption>`` -> optional, assumption on gap days between new lease and old lease
445446
* ``<turnover gap>`` -> assumption on gap days between new lease and old lease
446447
* ``<rental assump>`` -> describe the rental increase/decrease over time
447-
* ``<end date>`` -> the date when lease projection ends
448+
* ``<end type>``
449+
* ``("byDate", "2026-09-20")``-> the date when lease projection ends
450+
* ``("byExtTimes",1)``-> how many times lease will roll over
451+
452+
Lease Default
453+
""""""""""""""""
454+
User can pass it as ``None`` or default assumptions as below:
455+
456+
* ``('byConitnuation', <default rate in annual>)``
457+
* ``('byTermination', <default rate in annual>)``
458+
459+
460+
Lease Gap
461+
""""""""""""""
462+
463+
* ``('days', x)`` : the number of days between old lease and new lease
464+
* ``('byCurve', c)`` : the number of days between old lease and new lease depends on a curve
465+
466+
467+
Lease End
468+
""""""""""""""
469+
Describle the end type of lease projection,, either by a ``Date`` or a ``Extend Time``
470+
471+
* ``("byDate", "2026-09-20")`` : the date when lease projection ends
472+
* ``("byExtTimes", 1)`` : how many times lease will roll over for 1 time
448473

449474
Summary
450475
""""""""""""""""
@@ -462,10 +487,18 @@ Summary
462487
Lease -> Defaulted
463488
Performing -> "Lease Gap"
464489
Performing -> "Rental Curve"
465-
"Lease Gap" -> "{'Days':x}"
466-
"Lease Gap" -> "{'DaysByAmount':(tbl,x)}"
467-
"Rental Curve" -> "{'AnnualIncrease':x}"
468-
"Rental Curve" -> "{'CurveIncrease':x}"
490+
Performing -> "Default Assumption"
491+
Performing -> "End Type"
492+
"Default Assumption" -> "By Continuation"
493+
"By Continuation" -> "('byContinuation', x)"
494+
"Default Assumption" -> "By Termination"
495+
"By Termination" -> "('byTermination', x)"
496+
"Lease Gap" -> "('days', x)"
497+
"Lease Gap" -> "('byCurve', curve)"
498+
"Rental Curve" -> "('byAnnualRate', x)"
499+
"Rental Curve" -> "('byRateCurve', x)"
500+
"End Type" -> "end by date"
501+
"End Type" -> "end by extend time"
469502
}
470503

471504

docs/source/modeling.rst

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,48 +1427,70 @@ Schedule Repayment
14271427
Lease
14281428
^^^^^^^^^
14291429
1430-
`Lease` is an asset with have evenly distributed rental as income or step up feature on the rental over the projection timeline.
1430+
`Lease` is an asset with have evenly distributed rental as income or step up/down feature on the rental over the projection timeline.
14311431
1432+
syntax:
1433+
[``"Lease"``, {'rental':<Rental Type>,'originTerm':..,'originDate':..}
1434+
, {'currentBalance':..,'status':<Status>,'remainTerm':..}]
1435+
1436+
1437+
Rental Type
1438+
""""""""""""""
1439+
* Rental by days
1440+
* DailyRate -> fix amount on each day
1441+
* Payment Dates -> :ref:`DatePattern`
1442+
1443+
.. code-block:: python
1444+
1445+
("byDay", <DailyRate>, <Payment Dates>)
1446+
1447+
* Rental by periods
1448+
* Rental -> a fix amount on each period
1449+
* Period -> ``Monthly``, ``Weekly``, ``BiWeekly``, ``Quarterly``, ``SemiAnnually``, ``Annually``
1450+
1451+
.. code-block:: python
14321452
1453+
("byPeriod", <Rental each period>, <Period>)
1454+
1455+
Lease with fixed retnal
1456+
""""""""""""""""""""""""""
14331457
14341458
.. code-block:: python
14351459
14361460
["Lease"
1437-
,{"fixRental": 12.0
1461+
,{"rental":("byDay", 12.0, ["DayOfMonth",15])
14381462
,"originTerm": 96
1439-
,"freq": ["DayOfMonth",15]
1440-
,"originDate": "2022-01-05"
1441-
,"status":"Current"
1442-
,"remainTerm":80}]
1463+
,"originDate": "2022-01-05"}
1464+
,{"status":"Current" ,"remainTerm":80 ,"currentBalance":150}]
14431465
14441466
step up type lease which rental will increase by pct after each accrue period
14451467
1468+
Lease with various rental
1469+
""""""""""""""""""""""""""
1470+
1471+
The ``retnal`` can be increasing/decreasing by a fixed rate or a vector of rate.
1472+
14461473
.. code-block:: python
14471474
14481475
["Lease"
1449-
,{"initRental": 24.0
1450-
,"originTerm": 36
1451-
,"freq": ["DayOfMonth",25]
1452-
,"originDate": "2023-01-01"
1453-
,"status":"Current"
1454-
,"remainTerm":30
1455-
,"accrue": ["DayOfMonth",1]
1456-
,"pct": 0.05}]
1476+
,{"originTerm": 36
1477+
,"rental":("byDay", 24.0, ["DayOfMonth",25])
1478+
,"originDate": "2023-01-01"
1479+
,"stepUp": ("flatRate", 0.05)},
1480+
{"currentBalance":150 ,"status":"Current" ,"remainTerm":30}]
14571481
14581482
or user can specify the vector for the rental change
14591483
14601484
.. code-block:: python
14611485
14621486
["Lease"
1463-
,{"initRental": 24.0
1464-
,"originTerm": 36
1465-
,"freq": ["DayOfMonth",25]
1466-
,"originDate": "2023-01-01"
1467-
,"status":"Current"
1468-
,"remainTerm":30
1469-
,"accrue": ["DayOfMonth",3]
1470-
,"pct": [0.05,0.065,0.06,-0.07]}]
1471-
1487+
,{"originTerm": 36
1488+
,"rental":("byDay", 24.0, ["DayOfMonth",25])
1489+
,"originDate": "2023-01-01"
1490+
,"stepUp": ("byRates",[0.05,0.065,0.06,-0.07])
1491+
},
1492+
,{"status":"Current" ,"remainTerm":30 ,"currentBalance":150}
1493+
]
14721494
14731495
14741496
Installment

0 commit comments

Comments
 (0)