Introducing TmVal¶
TmVal is a Python library for mathematical interest theory, annuity, and bond calculations. This package arose from the need to have more powerful computational finance tools for another project of mine, Miniature Economic Insurance Simulator (MIES). What began as a simple submodule of MIES quickly spun off into its own repository as its complexity grew and as its potential viability in commercial applications became more apparent.
This article begins by highlighting the advantages TmVal has over existing time value of money packages, and then proceeds to demonstrate how TmVal can be used to solve problems found in actuarial science. Feel free to visit the project repository and examine its source code at https://github.com/genedan/tmval.
Feature Highlights¶
TmVal supports growth patterns that are more complex than compound interest. In addition to supporting simple, compound, and nominal interest, TmVal handles growth patterns that may be of theoretical interest to actuaries, such as continuously compounded rates (force of interest), polynomial growth, and arbitrary amount and accumulation functions.
TmVal provides equations of value computations for core financial instruments in actuarial science, such as annuities, loans, bonds, and arbitrary cash flow streams. As development is still in the alpha stage, the types of investments TmVal supports is rapidly expanding. I expect the package to soon offer classes for stocks and options.
TmVal’s classes are intended to correspond closely to symbols used in actuarial notation. Well-known symbols encountered by actuaries, such as , , , etc., are supported. Refer to the Notation Guide in this documentation to see the available symbols.
Amount and Accumulation Functions¶
TmVal supports the core growth functions of mathematical interest theory, the amount () and accumulation () functions, implemented via the Amount
and Accumulation
classes. These classes support all sorts of growth patterns, from simple and compound interest to more complex cases such as tiered investment accounts and polynomial growth.
For instance, suppose we have the tiered investment account with annually compounded interest rates:
Required Minimum Balance |
Interest Rate |
---|---|
0 |
1% |
10,000 |
2% |
20,000 |
3% |
If we invest 18,000 today, to what value does it grow after 10 years?
In [1]: from tmval import Amount, TieredBal
In [2]: tb = TieredBal(
...: tiers=[0, 10000, 20000],
...: rates=[.01, .02, .03]
...: )
...:
In [3]: amt = Amount(gr=tb, k=18000)
In [4]: print(amt.val(10))
22966.846915945713
If we were to invest 5,000 today, how long would it take to reach 2% and 3% interest, assuming no future contributions?
In [5]: print(tb.get_jump_times(k=5000))
[69.66071689357483, 104.66350567472134]
It will take almost 70 years to reach 2%, and about 105 years to reach 3%. That’s a long time!
Interest Rate Conversions¶
Interest rates are represented by a core data type in TmVal, the Rate
class. This custom data type offers a convenient way to perform computations with a variety of interest rate patterns as well as conversions between them. The main patterns supported by the Rate
class are:
Effective Interest
Effective Discount
Nominal Interest
Nominal Discount
Force of Interest
Simple Interest
Simple Discount
The relationships between compound interest rates can be represented with the following expression:
Since there are so many varieties of rates, as well as relationships between them, an actuary would have to write over twenty conversion functions to handle the full spectrum of interest rates if they weren’t using a package like TmVal. The good news is that TmVal handles all these conversions with a single method, Rate.convert_rate()
.
For example, if we needed to convert 5% rate compounded annually to a nominal discount rate convertible monthly, we could do the following:
In [6]: from tmval import Rate
In [7]: i = Rate(.05)
In [8]: nom_d = i.convert_rate(
...: pattern="Nominal Discount",
...: freq=12
...: )
...:
In [9]: print(nom_d)
Pattern: Nominal Discount
Rate: 0.048691111787194874
Compounding Frequency: 12 times per year
Furthermore, we can demonstrate a conversion to nominal interest compounded quarterly, and then to , the force of interest, and then back to compound annual effective interest:
In [10]: nom_i = nom_d.convert_rate(
....: pattern="Nominal Interest",
....: freq=4
....: )
....:
In [11]: print(nom_i)
Pattern: Nominal Interest
Rate: 0.04908893771615652
Compounding Frequency: 4 times per year
In [12]: delta = nom_i.convert_rate(
....: pattern="Force of Interest"
....: )
....:
In [13]: print(delta)
Pattern: Force of Interest
Rate: 0.04879016416943141
In [14]: i2 = delta.convert_rate(
....: pattern="Effective Interest",
....: interval=1
....: )
....:
In [15]: print(i2)
Pattern: Effective Interest
Rate: 0.04999999999999938
Unit of time: 1 year
For more details, see The Rate Class, Revisited of the Usage Tutorial.
Equations of Value¶
TmVal can solve for the time equation of value for common financial instruments such as annuities and loans, as well as for arbitrary cash flows. This is done via the Payments
class:
For example, we can solve for the internal rate of return of an investment of 10,000 at time 0 which returns 5,000 at time 1 and 6,000 at time 2:
In [16]: from tmval import Payments
In [17]: pmts = Payments(
....: amounts=[-10000, 5000, 6000],
....: times=[0, 1, 2]
....: )
....:
# internal rate of return - two roots
In [18]: print(pmts.irr())
[0.0639410298049854, -1.5639410298049854]
We can also use the Payments
class to find the time-weighted yield:
where
Suppose we deposit 100,000 in a bank account at time 0. It grows to 105,000 at time 1, and we immediately deposit an additional 5,000. It then grows to 115,000 at time 2. The time-weighted yield is:
In [19]: pmts = Payments(
....: amounts=[100000, 5000],
....: times=[0, 1]
....: )
....:
In [20]: i = pmts.time_weighted_yield(
....: balance_times=[0, 1, 2],
....: balance_amounts=[100000, 105000, 115000],
....: annual=True
....: )
....:
# time-weighted yield
In [21]: print(i)
Pattern: Effective Interest
Rate: 0.0477248077273309
Unit of time: 1 year
Annuities¶
Annuities are one of the core financial instruments underlying life insurance products. TmVal provides support for many kinds of annuities via its Annuity
class, such as:
Annuity-immediate:
Annuity-due:
Perpetuity-immediate:
Perpetuity-due:
Arithmetically increasing annuity-immediate:
Arithmetically increasing annuity-due:
Arithmetically increasing perpetuity-immediate:
Arithmetically increasing perpetuity-due:
Geometrically increasing annuity-immediate
Geometrically increasing annuity-due
Geometrically increasing perpetuity-immediate
Geometrically increasing perpetuity-due
Level annuity-immediate with payments more frequent than each interest period:
Continuously-paying annuity:
… and many more. To see what other symbols are supported, consult the Notation Guide.
Unlike other packages, which tend to use functions to represent the different types of annuities, TmVal represents annuities as a class, which gives it access to several methods that can be performed on the annuity, such as equations of value. So rather than simply returning a float value via a function, TmVal expands the manipulations that can be done with an annuity. My aim is to allow the Annuity
class to serve as a base class for, or to be embedded into more complex insurance products.
We can perform simple calculations, such as finding the present value of a basic annuity-immediate, :
In [22]: from tmval import Annuity
In [23]: print(Annuity(gr=.05, n=5).pv())
4.329476670630819
to more complex ones, such as the accumulated value of an arithmetically increasing annuity-due… :
In [24]: ann = Annuity(
....: amount=5000,
....: gr=.05,
....: n=5,
....: aprog=100,
....: imd='due'
....: )
....:
In [25]: print(ann.sv())
30113.389687500006
…or even the present value of continuously paying annuities with continually varying payments, such as this one at a simple discount rate of .036:
In [26]: def f(t):
....: return t
....:
In [27]: ann = Annuity(
....: amount=f,
....: period=0,
....: term=5,
....: gr=Rate(sd=.036)
....: )
....:
In [28]: print(ann.pv())
11.0
Amortization¶
TmVal’s Loan
class has methods for obtaining information that we might want about loans, such as amortization schedules and outstanding loan balances.
The output for several TmVal’s classes are intended to be compatible with Pandas, a popular data analysis library. The output for the Loan
class’s amortization()
method is one such example.
For example, suppose we were to obtain a 2-year loan of 50,000, to be paid back with monthly payments made at the end of each month. If the interest rate were 4% convertible quarterly, what is the amortization schedule?
In [29]: import pandas as pd
In [30]: from tmval import Loan, Rate
In [31]: gr = Rate(
....: rate=.04,
....: pattern="Nominal Interest",
....: freq=4)
....:
In [32]: my_loan = Loan(
....: amt=50000,
....: period=1/12,
....: term=2,
....: gr=gr,
....: cents=True
....: )
....:
In [33]: amort = pd.DataFrame(my_loan.amortization())
In [34]: print(amort)
time payment_amt interest_paid principal_paid remaining_balance
0 0.00 NaN NaN NaN 50000.00
1 0.08 2170.96 166.11 2004.85 47995.15
2 0.17 2170.96 159.45 2011.51 45983.65
3 0.25 2170.96 152.77 2018.19 43965.46
4 0.33 2170.96 146.07 2024.89 41940.56
5 0.42 2170.96 139.34 2031.62 39908.94
6 0.50 2170.96 132.59 2038.37 37870.57
7 0.58 2170.96 125.82 2045.14 35825.43
8 0.67 2170.96 119.02 2051.94 33773.49
9 0.75 2170.96 112.21 2058.75 31714.74
10 0.83 2170.96 105.37 2065.59 29649.14
11 0.92 2170.96 98.50 2072.46 27576.68
12 1.00 2170.96 91.62 2079.34 25497.34
13 1.08 2170.96 84.71 2086.25 23411.09
14 1.17 2170.96 77.78 2093.18 21317.91
15 1.25 2170.96 70.82 2100.14 19217.77
16 1.33 2170.96 63.85 2107.11 17110.66
17 1.42 2170.96 56.85 2114.11 14996.55
18 1.50 2170.96 49.82 2121.14 12875.41
19 1.58 2170.96 42.78 2128.18 10747.22
20 1.67 2170.96 35.71 2135.25 8611.97
21 1.75 2170.96 28.61 2142.35 6469.62
22 1.83 2170.96 21.49 2149.47 4320.16
23 1.92 2170.96 14.35 2156.61 2163.55
24 2.00 2170.74 7.19 2163.55 -0.00
Using the Loan
class’s olb_r()
method, we can calculate the outstanding loan balance at any time, such as after 1 year, using the retrospective method:
In [35]: print(my_loan.olb_r(t=1))
25497.34126843426
Now, what if we choose to overpay during the first two months, with payments of 3,000 each, and then returning to normal payments? What is the outstanding loan balance after 1 year?
In [36]: pmts = Payments(
....: amounts=[3000] * 2 + [2170.06] * 10,
....: times=[(x + 1) / 12 for x in range(12)]
....: )
....:
In [37]: print(my_loan.olb_r(t=1, payments=pmts))
23789.6328174795
Bonds¶
TmVal’s Bond
class supports all sorts of bond calculations. For example, suppose we have a 5-year, 1,000 bond that pays 5% annual coupons and is redeemable for 1,250. Let’s s find the price of this bond if it has an 8% yield:
In [38]: from tmval import Bond
In [39]: bd = Bond(
....: face=1000,
....: red=1250,
....: alpha=.05,
....: cfreq=1,
....: term=5,
....: gr=.08
....: )
....:
In [40]: print(bd.price)
1050.3644981460952
We can also price bonds that have more complex, nonlevel coupon payments. Suppose instead that the bond in the previous example instead pays 5% annual coupons in the first two years and 6% coupons in the last three years:
In [41]: bd = Bond(
....: face=1000,
....: red=1250,
....: alpha=[(.05, 0), (.06, 2)],
....: cfreq=[1,1],
....: term=5,
....: gr=.08
....: )
....:
# verify coupon amounts
In [42]: print(bd.coupons.amounts)
[50.0, 50.0, 60.0, 60.0, 60.0]
# verify coupon times
In [43]: print(bd.coupons.times)
[1.0, 2.0, 3.0, 4.0, 5.0]
In [44]: print(bd.price)
1072.458951054599
Term Structure¶
TmVal supports term structure of interest rate calculations. Suppose we have the following yields to maturity for 5% par-value bonds with annual coupons:
Term |
Yield |
---|---|
1 Year |
1.8% |
2 Years |
3% |
3 Years |
3.6% |
4 Years |
3.9% |
5 Years |
4.4% |
We can calculate the forward rates…
… for a 3-year bond:
In [45]: from tmval import forward_rates
In [46]: ytms = [.018, .03, .036, .039, .044]
In [47]: forward_rates(yields=ytms, alpha=.05, term=3)
Out[47]:
{(0,
3): Pattern: Effective Interest
Rate: 0.036501659647665496
Unit of time: 1 year,
(1,
4): Pattern: Effective Interest
Rate: 0.046910420267653796
Unit of time: 1 year,
(2,
5): Pattern: Effective Interest
Rate: 0.054953242172293804
Unit of time: 1 year}
Development Status¶
TmVal is currently in the alpha stage of development. In the coming weeks, I expect to add many more features, such as:
Stocks
Options
Immunization
I anticipate declaring the project to be in beta stage once I’ve incorporated all of the main concepts on the syllabus of the SOA’s financial mathematics exam. The beta stage of the project will involve the construction of a testing suite to insure the accuracy of the computations in preparation for commercial use.
Further Reading¶
Go ahead and give TmVal a try! The next section is the Installation and Quickstart followed by the Usage Tutorial. For technical documentation, consult the API Reference, which links to the source code of the project.
If you encounter bugs, in TmVal or its documentation, feel free to create a ticket or pull request on the GitHub Repository.