Valuing Structured Loans with Cash Flows and QuantLib
Summary
The document discusses representing loans with unusual cash flows in QuantLib, including structures where principal can increase rather than amortize. QuantLib’s Bond class assumes an initial notional that is later repaid in full or through amortization, so a loan with increasing nominal amounts may fail its checks. Removing that check and recompiling is discouraged because other parts of the library may rely on the assumption.
One suggested approach is to combine coupon and redemption cash flows into a sorted leg and analyze it with the CashFlows methods. This provides measures such as net present value, basis-point sensitivity, yield, and duration, but does not provide bond-specific features such as callability or automatic recalculation. For a different loan question—solving for a coupon that gives a target present value—the document derives a linear formula: subtract discounted principal repayments from the target value, then divide by the discounted accrual-weighted notionals. The example is illustrative and would need adaptation to a particular loan’s dates, curve, and conventions.
Key ideas
- QuantLib’s Bond class assumes principal is paid up front and returned through redemption or amortization.
- For increasing-notional loans, combined and sorted cash flows can be analyzed with the CashFlows methods.
- CashFlows analysis can provide valuation and risk measures but not every feature of a specialized bond class.
- A coupon for a target present value can be solved from discounted principal flows and coupon cash flows.
- The example formula depends on the loan’s schedule, discount factors, day-count convention, and outstanding notionals.
Tags
Full text
# Valuing structured loans in QuantLib
# Valuing structured loans in QuantLib
I'm trying to figure out if it's possible to value structured products, mainly loans, in quantlib. The idea is to build a bond class with different cash flows. For example, a loan could have coupons that only pay interest, could be only-amortizing or even the coupons can increase the nominal amount.
Also, for FTP purposes, one could be interested in the yield that returns the par value of the loan. Is there a native function for that?
It's possible to do this with the current release in python or c++?
Being more precise, imagine these two examples:
First, I have a loan that is issued with a face value of 2000 and has two redemptions of 1000:
I've been successful building the cashflows with the Bond class with a simple code:
```
def bond_from_table(df,notional, outlay_date, day_counter, r=0.03):
eval_date = ql.Settings.instance().evaluationDate
coupons = []
redemptions = []
redem = 0
for i in range(df.shape[0]):
start_date = df.at[i,'Start']
end_date = df.at[i,'End']
notional -= redem
redem = df.at[i,'Capital']
redemptions.append(ql.Redemption(redem,end_date))
coupons.append(ql.FixedRateCoupon(end_date,notional,r,day_counter,start_date,end_date))
leg = ql.Leg(coupons)
loan = ql.Bond(0,calendar,eval_date,leg)
return loan
```
As it seems that the Bond class constructs the redemptions from the nominals that are being paid. The good thing is that using the bond class we can access all the bond functions and other things as callability and so on.
But! if I try to build something like this:
Where after the initial 2000 payment to the client, another 400 are paid in the next coming dates (that's why the - sign) -at the end, you receive the total lend- I would get an error saying the nominal is increasing, which is true. I guess there might be another route for this, but I think I'll be losing the functionalities of the bond class.
Any ideas? Thanks in advance,
## Answer by Luigi Ballabio (score 3, accepted)
https://quant.stackexchange.com/a/48767
As you've found, the `Bond` class assumes that you're buying a bond; that is, you're paying a notional upfront and it will be returned to you in one solution or in a series of amortizing payments. The assumption is coded here, and you might try removing it and recompiling but I don't suggest it. It might be that other parts of the code rely on that check having passed successfully.
What you might do instead is to take both sets of cash flows (`coupons` and `redemptions`), put together, sort them, and consider the resulting leg as your loan. It probably won't give you all the functionality of the bond (e.g., lazy recalculation when something changes) but you'll be able to analyze them using the methods of the `CashFlows` class. This will give you NPV, BPS, yield, and measures like duration, as well as some static info. Unfortunately, it won't let you access features like callability which are coded inside some specific bond class.
## Answer by David Duarte (score 1)
https://quant.stackexchange.com/a/50858
To add a possible solution for your last comment: "usually in loans one is interested in the coupon rate that returns certain NPV, instead of the usual IRR"...
In python you could extend the Bond class with your own function to get the coupon with matrix algebra.
$$PV_{Loan} = PV_{amortizaitons} + PV_{coupons}$$
Decomposing the PV of the coupon by vectors:
$$PV_{Loan} = PV_{amortizaitons} + \big[ notionals \times dcf \times coupon \times dfs \big]$$
So you can basically solve for the coupon by:
$$coupon = (PV_{Loan} - PV_{amortizaitons}) / \big[ notionals \times dcf \times dfs\big]$$
Here is an example that could probably be improved but hopefully you'll get the idea...
```
crv = ql.FlatForward(ql.Date(16,9,2019), 0.025, ql.Actual360())
fixedRate = 0.03
notionals = [2000] * 9 + [1000] * 10
class Loan(ql.AmortizingFixedRateBond):
def couponRate(self, pv):
notional_flows = np.array([cf.amount() for cf in self.redemptions()])
notional_dfs = np.array([crv.discount(cf.date()) for cf in self.redemptions()])
notional_pv = pv - (notional_flows * notional_dfs).sum()
dates = list(schedule)
dcf = np.array([ql.Actual360().yearFraction(d0, d1) for d0,d1 in zip(dates[:-1], dates[1:])])
dfs = np.array([crv.discount(dt) for dt in dates[1:]])
return notional_pv / (dcf * dfs * notionals).sum()
schedule = ql.MakeSchedule(ql.Date(16,9,2019), ql.Date(30,4,2021), ql.Period('1m'), firstDate=ql.Date(31,10,2019), endOfMonth=True)
loan = Loan(2, notionals, schedule, [fixedRate], ql.Actual360(), ql.ModifiedFollowing)
loan.couponRate(2000)
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.