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: enter image description here

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']

    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:

enter image description here

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,

  • 1
    $\begingroup$ FTP = Fund Transfer Pricing ? $\endgroup$
    – Alex C
    Commented Sep 11, 2019 at 19:21
  • $\begingroup$ Yes, for ALM purpose $\endgroup$ Commented Sep 11, 2019 at 19:42
  • 1
    $\begingroup$ It might be helpful if you listed the features of structured loans that you want to use to project the cash flows. For example, I remember an Indonesian loan where at every reset the issuer had the option: pay 3mo libor + spread in 3 months, or 6mo libor + spread and reset in 6 months. That's not what you meant, right? $\endgroup$ Commented Sep 13, 2019 at 2:42
  • $\begingroup$ I see your point, i'll update the post with an example. $\endgroup$ Commented Sep 15, 2019 at 0:03
  • 1
    $\begingroup$ You assume that the redemptions (amortizations) happen on coupon days, but I've seen loans where the notional way paid on arbitrary days in the middle of coupon period. The coupon paid in the ens of the period accrues on different notional amounts before and after. I've also seen loans whose notional increased through PIK or disbursements. $\endgroup$ Commented Sep 15, 2019 at 18:58

2 Answers 2


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.

  • $\begingroup$ Hi Luigi, thanks for the answer. I did what you mention (remove the assumption and recompile the library) and everything seems to be working correctly, even the notional is being increased as time passes. Could you point me out an example of what could be in conflict with that? Also, what do you think about creating a class for this type of instrument in the base library, would it be worth it? $\endgroup$ Commented Sep 20, 2019 at 0:57
  • 1
    $\begingroup$ I was thinking of the normalization of the price to the current notional, but it should work, so mostly I was being prudent :) A separate instrument would make sense, though. This way we could make different assumptions explicitly. $\endgroup$ Commented Sep 20, 2019 at 8:24
  • $\begingroup$ Thanks again, I'll give it a look to see how can be implemented. One last question, usually in loans one is interested in the coupon rate that returns certain NPV, instead of the usual IRR. I think one easy way of doing this calculation is iterate over the coupon rate and create an instance of the bond/loan class every time until the NPV is reached, but I feel like it's a bit dirty. I was looking at the InterestRate class and the rate attribute is private and even if I change it I don't think that would update the bond cashflows. Is there any class that allows you to do something like this? $\endgroup$ Commented Sep 20, 2019 at 14:59
  • $\begingroup$ No, there isn't. $\endgroup$ Commented Sep 21, 2019 at 20:34

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)

  • $\begingroup$ Thank you David, it's a good idea, but i think -i'm not sure- that the atmRate() method does the same. $\endgroup$ Commented Feb 4, 2020 at 15:15
  • $\begingroup$ You are right! ql.BondFunctions.atmRate(bond, crv, date, cleanPrice) would do the same thing. I initially though it would only solve for par. So what exactly were you looking for that Luigi answered "No, there isn't" $\endgroup$ Commented Feb 5, 2020 at 12:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.