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».

Ответить