Go Financial — A pkg for elementary financial functions

Gyanesh Malhotra
Razorpay Engineering
6 min readDec 2, 2021

--

At Razorpay Capital, we build a suite of next-generation lending products such as Loans, Cash Advances and Corporate Cards. For each of these products is powered by our Automated Collection System and for that, we needed an Amortization Schedule Generator.

We were looking for an Amortization Schedule Generator in Golang and while there are excellent libraries like NumPy-financial in other languages, we could not find any packages in Golang which offered a complete solution.

So we decided to build it ourselves. Here comes, Go-Financial 🎉 !

To understand how we built the Go Financial pkg, we’ll use Amortization Schedules as an illustration. Let’s understand what an amortization schedule is:

Amortization Schedule is a complete table of periodic loan payments, showing the amount of principal and the amount of interest that comprise each payment until the loan is paid off at the end of its term.

* source: Investopedia

Understanding Go-Financial

Go-Financial is a go-native port of the NumPy-financial library and includes an amortization schedule generation. You can either use the low-level functions for financial calculations or utilise the higher-level amortization functions. Features are much like NumPy-financial offerings, however, with a few differences.

  • Go-Financial implements an amortization function, which is not found in NumPy-financial.
  • NumPy-financial functions can accept Scalar, Array of Floats or Decimal as input. However, Go-Financial functions accept only scalar Decimal as inputs.

Go-financial implements some of the functions defined in OpenFormula. OpenFormula is a specification of Open Document Format for exchanging standard formulas between different spreadsheet software. The OASIS organisation maintains it. Some of the functions defined in OpenFormula includes:

  • Internal Rate of Return (IRR)
  • Future Value (FV)
  • Net Present Value (NPV)

Example

Let’s try to understand how Go-Financial works using an example.

If you have to generate the yearly instalments for a loan of Rs 20 lakhs over 15 years at 12% per year, you can use the Go-Financial package.

https://goplay.tools/snippet/5QBs8y4pNpF

The graph generated for the above example is as follows:

interactive plot for 20 lakh repayment schedule

Choosing the Right Data Type for Money

Using Int for Money

We first tried using int64 to represent money but we quickly realized the following limitations:

  • Reducing Principal/Interest formulas have exponential calculations and are defined for floats/decimals as input.
  • Using int64 in exponential formulas would not be feasible since it may go out of bounds. Apart from this, it would not be accurate whenever dividing with ints.
  • Go doesn’t have exponent support for ints in its standard library. Exp function is only defined for floats. https://pkg.go.dev/math@go1.17.2#Exp

Hmm, sounds complicated? Let’s simplify it with an example.

Example

Using this sample code, we will try to calculate the accrued amount for compound interest. The formula is modified so that we can use int64 for representing the interest rate.

The formula for Float:

The modified formula for Int:

https://goplay.tools/snippet/nOfVNVWb3bL

Signed int64 has 63 bits for representing a number. It fails here because all the bits in signed int64 get flipped to 0 to represent a number higher than the int_max. The program panics while calculating the compounded value because it will go out of bounds for int64. So, based on the challenges listed above, int64 wasn’t a feasible option.

Using Float for Money

Next, we tried float to represent money. There were challenges here as well.

Approximation Errors

Any float number is always an approximation to the number but never equal to that number. So, we can never compare them with certainty but only with approximations up to some tolerance. Hence, whenever we perform any operation on floats, we lose precision and the errors in representation increase.

Example

  • ⅓ != 0.33
  • float( 100/ 10 ) != 10
https://goplay.tools/snippet/7GrQa6GjrNi

Here, we try to add 0.03, 100 times. However, the result will never be equal to 3 but always tend towards 3(2.999999999999995) due to approximation errors in representing 0.03 correctly.

Rounding Errors

We had to round the numbers to represent the floats in the minimum precision required for the Indian rupee (₹ ) currency. There are different strategies by which we can round a number. We used Rounding half away from zero (round half towards infinity). It treats positive and negative values symmetrically.

Example

  • Round(23.5): 24
  • Round(-23.5): -24
  • Round(23.6): 24
  • Round(23.4): 23
  • Round(-23.4): -23
  • Round(-23.6): -24

However, whenever we do rounding, we might end up either overestimating or underestimating the value.

Example

If we have ⅓ ,⅓ ,⅓ and we try to round their float representations, we will end up with:

round(0.33) + round (0.33) + round(0.33)= 0

This is a case of underrepresentation. Similarly, if we had to round 0.7, we might end up having an overestimation. To fix rounding errors, we need to perform error correction.

Tackling Approximation & Rounding Errors

Approximation Errors:

We identified a few approaches to handle the approximation errors.

Decimals are an abstract precision representation and not an approximation. We don’t lose precision even after we have performed any number of operations on them. So, we decided to use decimals: https://github.com/shopspring/decimal.

Rounding Errors:

There will always be a case where we overestimate or underestimate a number whenever we round it. So, this is a real issue and needs to be handled separately.

Example

Let’s revisit the example where we were trying to generate the yearly instalments for a loan of Rs 20 lakhs over 15 years at 12% p.a. The only difference here is that the interest rate type is flat. This will make it easier to understand the rounding error scenario and how we can tackle it.

https://goplay.tools/snippet/pJl0QDgmNOc

If we look closely at the payments made in each period, we find that the payment is different from the rest of the periods for the last period. That is because if, after rounding, we distributed the principal equally for the 15th year, our sum of principal collection would be Rs 19,99,999.95, which is Rs 0.05 less than the initial principal amount. This error crept in due to underestimations after rounding. To fix this, we add the difference back to the Principal and Payment.

Try it Out

Go-financial is published at https://github.com/razorpay/go-financial.

A few examples which you can try out are :

We have been using Go-Financial for some time and it has worked well across multiple applications. Since the beginning, we have aimed at building a reusable package that we wanted to open source to the community. We hope the Golang community finds it useful too!

We are hiring!

If you love working on exciting challenges, we are actively hiring for our Engineering team. We are a bunch of ignited minds simplifying financial infrastructure for businesses, and we’re always looking out for great folks. We are looking for both Engineering Managers and senior engineers to break barriers and build for the future. Apply now.

--

--