Go Financial — A pkg for elementary financial functions
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.
The graph generated for the above example is as follows:
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:
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
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.
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 :
- https://github.com/razorpay/go-financial/blob/master/example_amortization_test.go
- https://github.com/razorpay/go-financial/blob/master/example_reducing_utils_test.go
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.