Go-Financial – пакет для элементарных финансовых функций

В Razorpay Capital мы создаем набор кредитных продуктов следующего поколения, таких как ссуды , денежные авансы и корпоративные карты . Для каждого из этих продуктов используется наша автоматизированная система сбора, и для этого нам потребовался генератор графика амортизации.
Мы искали генератор графика амортизации в Golang, и хотя есть отличные библиотеки, такие как NumPy-financial, на других языках, мы не смогли найти в Golang никаких пакетов, которые предлагали бы полное решение.
Поэтому мы решили построить его сами. А вот и Go-Financial 🎉!
Чтобы понять, как мы создали Go Financial pkg, мы будем использовать графики амортизации в качестве иллюстрации. Давайте разберемся, что такое график амортизации:
Mortization Schedule является полной таблицей периодических платежей по кредиту, с указанием суммы основных платежей и сумм процентов , которые включают каждый платеж, пока кредит не погашается
Понимание Go-Financial
Go-Financial – это стандартный порт библиотеки NumPy-financial, который включает создание графика амортизации. Вы можете использовать функции нижнего уровня для финансовых расчетов или функции амортизации более высокого уровня. Функции очень похожи на предложения NumPy-financial, однако с некоторыми отличиями .
- Go-Financial реализует функцию амортизации, которой нет в NumPy-financial.
- Функции NumPy-financial могут принимать в качестве входных данных Scalar, Array of Floats или Decimal. Однако функции Go-Financial принимают в качестве входных данных только скалярное Decimal.

Go-financial реализует некоторые функции, определенные в OpenFormula . OpenFormula – это спецификация формата открытого документа для обмена стандартными формулами между различными программами для работы с электронными таблицами. Организация OASIS поддерживает его. Некоторые из функций, определенных в OpenFormula, включают:
- Внутренняя норма прибыли (IRR)
- Будущая стоимость (FV)
- Чистая приведенная стоимость (NPV)
Пример
Попробуем разобраться, как работает Go-Financial, на примере.
Если вам необходимо генерировать ежегодные платежи по кредиту в размере 20 лакхов рупий в течение 15 лет под 12 % годовых, вы можете воспользоваться пакетом Go-Financial.
package main
import (
"time"
"github.com/shopspring/decimal"
financial "github.com/razorpay/go-financial"
"github.com/razorpay/go-financial/enums/frequency"
"github.com/razorpay/go-financial/enums/interesttype"
"github.com/razorpay/go-financial/enums/paymentperiod"
)
func main() {
loc, err := time.LoadLocation("Asia/Kolkata")
if err != nil {
panic("location loading error")
}
currentDate := time.Date(2009, 11, 11, 4, 30, 0, 0, loc)
config := financial.Config{
// start date is inclusive
StartDate: currentDate,
// end date is inclusive.
EndDate: currentDate.AddDate(15, 0, 0).AddDate(0, 0, -1),
Frequency: frequency.ANNUALLY,
// AmountBorrowed is in paisa
AmountBorrowed: decimal.NewFromInt(200000000),
// InterestType can be flat or reducing
InterestType: interesttype.REDUCING,
// interest is in basis points
Interest: decimal.NewFromInt(1200),
// amount is paid at the end of the period
PaymentPeriod: paymentperiod.ENDING,
// all values will be rounded
EnableRounding: true,
// it will be rounded to nearest int
RoundingPlaces: 0,
// no error is tolerated
RoundingErrorTolerance: decimal.Zero,
}
amortization, err := financial.NewAmortization(&config)
if err != nil {
panic(err)
}
rows, err := amortization.GenerateTable()
if err != nil {
panic(err)
}
// Generates json output of the data
financial.PrintRows(rows)
// Generates a html file with plots of the given data.
financial.PlotRows(rows, "20lakh-loan-repayment-schedule")
}
График, созданный для приведенного выше примера, выглядит следующим образом:

Выбор правильного типа данных для денег
Использование Int для денег
Сначала мы попытались использовать int64 для представления денег, но быстро осознали следующие ограничения:
- Формулы уменьшения основной суммы / процента имеют экспоненциальные вычисления и в качестве вводимых данных определены для чисел с плавающей запятой / десятичных дробей .
- Использование int64 в экспоненциальных формулах нецелесообразно, поскольку оно может выходить за рамки.
- В стандартной библиотеке Go нет поддержки экспоненты для целых чисел . Функция Exp определена только для чисел Float .
Пример
Используя этот пример кода, мы попытаемся рассчитать сумму сложных процентов. Формула изменена так, что мы можем использовать int64 для представления процентной ставки.
Формула для Float :

Модифицированная формула для Int :

package main
import (
"fmt"
)
func main() {
a := int64(100000000) // RS. 10 LAKH in paise
r := int64(2000) // 20% INTEREST
t := int64(15) // 15 YEARS
n := int64(365) // COMPOUNDED DAILY
// Compound value formula : A(1 + r/n)**(n*t)
// Modified for int values.
// Signed int 64 has 63 bits for representing a number.
// It fails here because, all the bits in signed int 64 get flipped to 0 in trying to represent
// a number higher than the int_max.
compoundedValue := a*pow((10000 + r/n),n*t)/pow(int64(10),4*t*n)
fmt.Println("value is:",compoundedValue)
}
// pow computes a to the power of b
// go doesn't have support for power function for int in its standard lib
func pow( a int64, b int64) int64 {
ans := int64(1)
for b >0{
ans = ans * a
b -=1
}
return ans
}
Знаковое int64 имеет 63 бита для представления числа. Здесь он не работает, потому что все биты в подписанном int64 сбрасываются на 0, чтобы представить число, превышающее int_max. Программа не понимает этого, потому что это выйдет за пределы для int64 . Итак, исходя из перечисленных выше проблем, использование int64 было невозможным.
Использование Float для денег
Затем мы попытались использовать float для представления денег. Здесь тоже были проблемы.
Ошибки приближения
Любое число с float всегда является приближением к числу, но никогда не равно этому числу. Таким образом, мы никогда не можем сравнивать их с достоверностью, а только с приблизительными значениями. Следовательно, всякий раз, когда мы выполняем какую-либо операцию с числами float , мы теряем точность и увеличиваем ошибки в представлении.
Пример
- ⅓! = 0,33
- float (100/10)! = 10
package main
import "fmt"
func main() {
var n float64 = 0
for i := 0; i < 100; i++ {
n += .03
}
fmt.Println(n)
// Output:
// 2.999999999999995
}
Здесь мы пытаемся прибавить 0,03, 100 раз. Однако результат никогда не будет равен 3, а всегда будет стремиться к 3 (2,9999999999999995) из-за ошибок аппроксимации при представлении 0,03.
Ошибки округления
Нам пришлось округлить числа, чтобы представить числа с плавающей запятой с минимальной точностью, необходимой для валюты индийская рупия (₹). Есть разные стратегии, с помощью которых мы можем округлить число. Мы использовали Округление половины от нуля (округление половины до бесконечности). Это симметрично обрабатывает положительные и отрицательные значения.
Пример
- Округление (23,5): 24
- Округление (-23,5): -24
- Округление (23,6): 24
- Округление (23,4): 23
- Округление (-23,4): -23
- Округление (-23,6): -24
Однако всякий раз, когда мы выполняем округление, мы можем либо переоценить, либо недооценить значение.
Пример
Если у нас есть ⅓, ⅓, ⅓ и мы попытаемся округлить их представления с плавающей запятой, мы получим:
округление (0,33) + округление (0,33) + округление (0,33) = 0
Это случай недопредставленности. Точно так же, если бы нам пришлось округлить 0,7, мы могли бы получить завышенную оценку.
Устранение ошибок приближения и округления
Ошибки приближения:
Мы определили несколько подходов к обработке ошибок аппроксимации.
Десятичные дроби (Decimals) – это абстрактное прецизионное представление, а не приближение. Мы не теряем точности даже после того, как проделали с ними любое количество операций.
Ошибки округления:
Всегда будет случай, когда мы переоцениваем или недооцениваем число, когда мы его округляем. Итак, это реальная проблема, и ее нужно решать отдельно.
Пример
Давайте вернемся к примеру, в котором мы пытались генерировать ежегодные взносы по ссуде в размере 20 лакхов на 15 лет под 12% годовых. Единственное отличие здесь состоит в том, что тип процентной ставки является фиксированным. Это упростит понимание сценария ошибки округления и того, как с ней бороться
package main
import (
"time"
"github.com/shopspring/decimal"
financial "github.com/razorpay/go-financial"
"github.com/razorpay/go-financial/enums/frequency"
"github.com/razorpay/go-financial/enums/interesttype"
"github.com/razorpay/go-financial/enums/paymentperiod"
)
func main() {
loc, err := time.LoadLocation("Asia/Kolkata")
if err != nil {
panic("location loading error")
}
currentDate := time.Date(2009, 11, 11, 4, 30, 0, 0, loc)
config := financial.Config{
// start date is inclusive
StartDate: currentDate,
// end date is inclusive.
EndDate: currentDate.AddDate(15, 0, 0).AddDate(0, 0, -1),
Frequency: frequency.ANNUALLY,
// AmountBorrowed is in paisa
AmountBorrowed: decimal.NewFromInt(200000000),
// InterestType can be flat or reducing
InterestType: interesttype.FLAT,
// interest is in basis points
Interest: decimal.NewFromInt(1200),
// amount is paid at the end of the period
PaymentPeriod: paymentperiod.ENDING,
// all values will be rounded
EnableRounding: true,
// it will be rounded to nearest int
RoundingPlaces: 0,
// no error is tolerated
RoundingErrorTolerance: decimal.Zero,
}
amortization, err := financial.NewAmortization(&config)
if err != nil {
panic(err)
}
rows, err := amortization.GenerateTable()
if err != nil {
panic(err)
}
// Generates json output of the data
financial.PrintRows(rows)
// Generates a html file with plots of the given data.
financial.PlotRows(rows, "20lakh-loan-repayment-schedule")
}
Если мы внимательно посмотрим на платежи, произведенные в каждый период, мы обнаружим, что платеж отличается от остальных периодов за последний период . Это связано с тем, что если после округления мы распределим основную сумму поровну на 15-й год, наша сумма сбора от основной суммы составит 19,99 999,95 рупий, что на 0,05 рупий меньше первоначальной суммы основного долга. Эта ошибка закралась из-за занижения оценок после округления. Чтобы исправить это, мы добавляем разницу обратно к основной сумме «Principal» и «Payment».
