Giter Club home page Giter Club logo

tmval's Introduction

TmVal

PyPI version

Time Value of Money

Introduction

Documentation | Development Blog | PyPI

TmVal is a package that provides tools for the valuation of various financial instruments (annuities, bonds).

It can be used to study for Actuarial Exam FM, and (hopefully) used on the job for projects where time value of money is relevant.

Installation

pip install tmval

or

git clone https://github.com/genedan/TmVal
cd TmVal
python3 -m setup sdist bdist_wheel
cd dist
sudo pip3 install 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, 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 bonds, stocks, and options.

  • TmVal's classes are intended to correspond closely to symbols used in actuarial notation. Well-known symbols encountered by actuaries 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?

from tmval import Accumulation

def f(t):
    return .05 * (t **2) + .05 * t + 1


my_acc = Accumulation(gr=f)

print(my_acc.val(5))
2.5

If we were to invest 5000 today, how long would it take to reach 2% and 3% interest, assuming no future contributions?

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:

  1. Effective Interest
  2. Effective Discount
  3. Nominal Interest
  4. Nominal Discount
  5. Force of Interest
  6. Simple Interest
  7. Simple Discount

The relationships between compound interest rates can be represented with the following expression:

interest conversion

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:

from tmval import Rate

i = Rate(.05)

nom_d = i.convert_rate(
    pattern="Nominal Discount",
    freq=12
)

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:

nom_i = nom_d.convert_rate(
    pattern="Nominal Interest",
    freq=4
)

print(nom_i)
Pattern: Nominal Interest
Rate: 0.04908893771615652
Compounding Frequency: 4 times per year

delta = nom_i.convert_rate(
    pattern="Force of Interest"
)

print(delta)
Pattern: Force of Interest
Rate: 0.04879016416943141

i2 = delta.convert_rate(
    pattern="Effective Interest",
    interval=1
)

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:

equation of value

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:

from tmval import Payments

pmts = Payments(
    amounts=[-10000, 5000, 6000],
    times=[0, 1, 2]
)

# internal rate of return
print(pmts.irr())
[0.0639410298049854, -1.5639410298049854]

We can also use the Payments class to find the time-weighted yield:

time weighted yield

where

time weighted factor

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:

pmts = Payments(
   amounts=[100000, 5000],
   times=[0, 1]
)

i = pmts.time_weighted_yield(
   balance_times=[0, 1, 2],
   balance_amounts=[100000, 105000, 115000],
   annual=True
)

# time-weighted yield
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:

  1. Annuity-immediate
  2. Annuity-due
  3. Perpetuity-immediate
  4. Perpetuity-due
  5. Arithmetically increasing annuity-immediate
  6. Arithmetically increasing annuity-due
  7. Arithmetically increasing perpetuity-immediate
  8. Arithmetically increasing perpetuity-due
  9. Geometrically increasing annuity-immediate
  10. Geometrically increasing annuity-due
  11. Geometrically increasing perpetuity-immediate
  12. Geometrically increasing perpetuity-due
  13. Level annuity-immediate with payments more frequent than each interest period
  14. 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 be embedded in more complex insurance products.

We can perform simple calculations, such as finding the present value of a basic annuity-immediate with a five year term and a compound interest rate of 5%:

from tmval import Annuity

print(Annuity(gr=.05, n=5).pv())
4.329476670630819

To more complex ones, such as the accumulated value of an arithmetically increasing annuity-due with a starting payment of 5,000, and subsequent payments of 100 over a 5-year term, at 5% compound interest:

ann = Annuity(
    amount=5000,
    gr=.05,
    n=5,
    aprog=100,
    imd='due'
)

print(ann.sv())
30113.389687500014

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?

import pandas as pd

from tmval import Loan, Rate

gr = Rate(
    rate=.04,
    pattern="Nominal Interest",
    freq=4
)

my_loan = Loan(
    amt=50000,
    period=1/12,
    term=2,
    gr=gr,
    cents=True
)

amort = pd.DataFrame(my_loan.amortization())

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:

olb retrospective

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?

pmts = Payments(
    amounts=[3000] * 2 + [2170.06] * 10,
    times=[(x + 1) / 12 for x in range(12)]
)

print(my_loan.olb_r(t=1, payments=pmts))
23789.6328174795

Development Status

TmVal is currently in the alpha stage of development. In the coming weeks, I expect to add many more features, such as:

  1. Bonds
  2. Stocks
  3. Options
  4. 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.

tmval's People

Contributors

genedan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

tmval's Issues

calculate delta_t for any function

so far I'm only able to calculate this for the very special cases of simple interest/discount, and compound rates

however, for more general cases, we'd need calculus since \delta_t = \frac{a`(t)}{a(t)}, which requires taking the derivative of of the accumulation function

create alternative discount options for npv function

currently only accepts a discount function, although it works if payments already have discount factors provided

but, if payments do not have discount factors attached, then we should be able to supply just an interest rate or accumulation object/function as well

Create NPV solver

Although an npv function exists to calculate net present value, it would be nice to have a solver that can solve for missing amounts. For example, if a payment happens on an unknown date, but the npv and the rest of the information is provided, we should be able to solve for that date.

refactor cases for convert_rate

convert_rate can be refactored to reduce the number of cases

we should have an if/else for the known patterns, each of which then calls its own specialized conversion function (which I need to make) for the case.

for example, there are 4 x 4 different cases for the 4 different patterns, if each pattern had its own function for converting to the other patterns, we'd need just 4 cases, otherwise the function will grow exponentially as we discover more patterns

Enable automatic K deduction for Accumulation

Not all accumulation functions have K explicitly defined. It would be nice to have k as an optional argument, and when it is not given, the value for k should be extracted from the given accumulation function.

Initialize classes by passing a solver

Since a solver solves for a missing variable, it should be possible to pass a solver directly to a class (Annuity, Accumulation, etc.) to initialize it without needing to repeat the other arguments.

Allow initialization of Annuities using number of payments

Right now you must supply the period and term, from which the number of payments will be derived.

An alternative approach would be to allow for supplying the period and number of payments, from which the term will be derived. This would better match actuarial notation since the n in \ax{\angln} refers to the number of payments.

allow more complex growth functions to be applied to compound solver

See Vaaler & Daniel, problem 1.7.5. We can solve this by supplying a TieredTime to an Accumulation object, and then using the discount function.

I wonder if some people will try to use the compound solver in this scenario. The function does not support complex growth functions such as TieredTime, so it may be worthwhile to implement this feature. This is not high priority since there is already a way to do this using the Accumulation class.

create single interest rate converter

it may be convenient for users to have a single interest rate conversion function rather than having to memorize or look up all the different specific converters:

  1. apr
  2. apy
  3. nominal_from_eff
  4. effective_from_nominal
  5. interest_from_discount
  6. discount_from_interest

There may be more, so I might wait until I get further in my sources before getting to this.

npv() should be able to work without a discount function

Expected behavior:

When supplying npv() with discounted payments, I would expect for it to work without needing to supply a discount function.

Observed behavior:

Traceback (most recent call last):
  ...
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-3-822aaf6bee2c>", line 7, in <module>
    test = npv(payments=payments)
TypeError: npv() missing 1 required positional argument: 'discount_func'

create generalized GrowthRate class

I've so far counted at least 16 different types of interest-discount conversions, leading me to think that it may be difficult for the user to handle.

All these different rates can be abstracted to a general class, which can then be handled by the existing amount/accumulation classes.

I will need to make sure that the underlying mechanics work first, so it may be some time until I get to this.

possibly deprecate Annuity.pt_bal()

See Vaaler and Daniel example 3.5.3. This method was originally written to solve for the value of a deferred annuity several years beyond the last payment.

Now that the Annuity class takes an argument for deferrals, pt_bal() returns the same answer as eq_val(), the equation of value for the underlying payments class. Because of this, I'm not so sure if pt_bal() needs to exist or repurposed for something else.

An alternative would be to rewrite the method to return the value of an annuity t years after the last payment.

Break up solvers into different functions.

I haven't really seen solvers in other packages that are general in the sense that the return type can change depending on what parts of an equation are missing. It may be better to break up a general function into multiple functions - one for each case. By doing so, we can avoid confusion on what a function is supposed to return and reduce semantic errors.

Simple interest/discount may cause equation of value problems and issues for classes that depend on it

This is kind of an edge case since most TVM problems involve compound interest, so somewhat low priority.

This might also apply to growth functions where accumulation and discounting do not yield the same value for certain points of time. For example, if we invest $1 at 5% simple interest, it grows to $1 x (1.15) = $1.15 at time 3. We can get the present value by multiplying by the discount function, (1 + .05 * t) ^ -1. But what we can't do is get the time 2 value by multiplying 1.15 by the discount function with t=1.

That is, $1 x (1.10) is not equal to $1.15 / 1.05.

When textbooks transition to compound interest, students may get used to its convenient properties and forget that you cannot just multiply by v * t when going backwards in time for 2 periods for certain growth functions other than compound interest.

In the general case, one must first bring the value back to time zero and then grow the pv up to the point in time for which you want to have the value.

The value module is the most obvious place I can think of where this may be a problem, so we should check there first.

allow composition of different investment classes

Maybe there ought to be a way to combine financial instruments governed by different growth rules so we can calculate the combined value at desired points in time. See Daniel & Vaaler example 3.12.10.

Allow CompoundAmt/Acc, SimpleAmt/Acc instantiation with a discount rate, possibly merge the discount classes

It may be confusing to users to have to call a separate class each time they want to use the discount rate, so maybe it would be better to just enable the compound accumulation/amount class to accept a discount rate instead.

However, it could be confusing if users realize there are two available arguments - interest and discount rate - without knowing the difference between the two. I'll need to work through more problems and reach out to the community on their opinion.

add inflation adjustments

My sources don't go into great detail about inflation, except in the case of annual effective interest. However, handling general inflation for all the different interest rates (nominal, force, discount, etc.) is another story. It may be a concept worth exploring.

Update demo

The demo should be update with something other than simple interest, just to demonstrate more advanced capabilities.

Re-add delta_t

I might want to re-add delta_t which was only implemented for simple interest but was taken out with the deprecation of the SimpleAmt and SimpleAcc classes. It has known forms for certain growth patterns so I may add limited support for those forms.

create_payments() should accept multiple class inputs

The create_payments() function currently accepts lists of times, amounts, and discount factors, along with an optional discount function.

I'd like for the user to be able to have more options for creating payments, by supplying other information that is mathematically equivalent to the above. Off the top of my head, I can think of:

  1. A dictionary of payments, which include times, amounts, and optionally, discounts
  2. A compound interest rate
  3. An accumulation object

Yet, at the same time I want to avoid redundancy. If a person inputs an accumulation object, an interest rate, and a discount function, for instance, there's a chance they will be inconsistent with each other, so the user should not be able to insert all of these simultaneously.

Rework APR/APY examples

Since the NominalInt/Disc classes have been removed, this section of the documentation needs to be redone.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.