-
Notifications
You must be signed in to change notification settings - Fork 22
/
amortization.go
193 lines (179 loc) · 5.65 KB
/
amortization.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package gofinancial
import (
"encoding/json"
"fmt"
"io"
"os"
"path"
"time"
"github.com/shopspring/decimal"
"github.com/go-echarts/go-echarts/v2/charts"
"github.com/go-echarts/go-echarts/v2/opts"
"github.com/razorpay/go-financial/enums/interesttype"
)
// Amortization struct holds the configuration and financial details.
type Amortization struct {
Config *Config
Financial Financial
}
// NewAmortization return a new amortisation object with config and financial fields initialised.
func NewAmortization(c *Config) (*Amortization, error) {
a := Amortization{Config: c}
if err := a.Config.setPeriodsAndDates(); err != nil {
return nil, err
}
switch a.Config.InterestType {
case interesttype.REDUCING:
a.Financial = &Reducing{}
case interesttype.FLAT:
a.Financial = &Flat{}
}
return &a, nil
}
// Row represents a single row in an amortization schedule.
type Row struct {
Period int64
StartDate time.Time
EndDate time.Time
Payment decimal.Decimal
Interest decimal.Decimal
Principal decimal.Decimal
}
// GenerateTable constructs the amortization table based on the configuration.
func (a Amortization) GenerateTable() ([]Row, error) {
var result []Row
for i := int64(1); i <= a.Config.periods; i++ {
var row Row
row.Period = i
row.StartDate = a.Config.startDates[i-1]
row.EndDate = a.Config.endDates[i-1]
payment := a.Financial.GetPayment(*a.Config)
principalPayment := a.Financial.GetPrincipal(*a.Config, i)
interestPayment := a.Financial.GetInterest(*a.Config, i)
if a.Config.EnableRounding {
row.Payment = payment.Round(a.Config.RoundingPlaces)
row.Principal = principalPayment.Round(a.Config.RoundingPlaces)
// to avoid rounding errors.
row.Interest = row.Payment.Sub(row.Principal)
} else {
row.Payment = payment
row.Principal = principalPayment
row.Interest = interestPayment
}
if i == a.Config.periods {
DoPrincipalAdjustmentDueToRounding(&row, result, a.Config.AmountBorrowed, a.Config.EnableRounding, a.Config.RoundingPlaces)
}
if err := sanityCheckUpdate(&row, a.Config.RoundingErrorTolerance); err != nil {
return nil, err
}
result = append(result, row)
}
return result, nil
}
// DoPrincipalAdjustmentDueToRounding takes care of errors in total principal to be collected and adjusts it against the
// the final principal and payment amount.
func DoPrincipalAdjustmentDueToRounding(finalRow *Row, rows []Row, principal decimal.Decimal, round bool, places int32) {
principalCollected := finalRow.Principal
for _, row := range rows {
principalCollected = principalCollected.Add(row.Principal)
}
diff := principal.Abs().Sub(principalCollected.Abs())
if round {
// subtracting diff coz payment, principal and interest are -ve.
finalRow.Payment = finalRow.Payment.Sub(diff).Round(places)
finalRow.Principal = finalRow.Principal.Sub(diff).Round(places)
} else {
finalRow.Payment = finalRow.Payment.Sub(diff)
finalRow.Principal = finalRow.Principal.Sub(diff)
}
}
// sanityCheckUpdate verifies the equation,
// payment = principal + interest for every row.
// If there is a mismatch due to rounding error and it is withing the tolerance,
// the difference is adjusted against the interest.
func sanityCheckUpdate(row *Row, tolerance decimal.Decimal) error {
if !row.Payment.Equal(row.Principal.Add(row.Interest)) {
diff := row.Payment.Abs().Sub(row.Principal.Add(row.Interest).Abs())
if diff.LessThanOrEqual(tolerance) {
row.Interest = row.Interest.Sub(diff)
} else {
return ErrPayment
}
}
return nil
}
// PrintRows outputs a formatted json for given rows as input.
func PrintRows(rows []Row) {
bytes, _ := json.MarshalIndent(rows, "", "\t")
fmt.Printf("%s", bytes)
}
// PlotRows uses the go-echarts package to generate an interactive plot from the Rows array.
func PlotRows(rows []Row, fileName string) (err error) {
bar := getStackedBarPlot(rows)
completePath, err := os.Getwd()
if err != nil {
return err
}
filePath := path.Join(completePath, fileName)
f, err := os.Create(fmt.Sprintf("%s.html", filePath))
if err != nil {
return err
}
defer func() {
// setting named err
ferr := f.Close()
if err == nil {
err = ferr
}
}()
return renderer(bar, f)
}
// getStackedBarPlot returns an instance for stacked bar plot.
func getStackedBarPlot(rows []Row) *charts.Bar {
bar := charts.NewBar()
bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
Title: "Loan repayment schedule",
},
),
charts.WithInitializationOpts(opts.Initialization{
Width: "1200px",
Height: "600px",
}),
charts.WithToolboxOpts(opts.Toolbox{Show: true}),
charts.WithLegendOpts(opts.Legend{Show: true}),
charts.WithDataZoomOpts(opts.DataZoom{
Type: "inside",
Start: 0,
End: 50,
}),
charts.WithDataZoomOpts(opts.DataZoom{
Type: "slider",
Start: 0,
End: 50,
}),
)
var xAxis []string
var interestArr []opts.BarData
var principalArr []opts.BarData
var paymentArr []opts.BarData
minusOne := decimal.NewFromInt(-1)
for _, row := range rows {
xAxis = append(xAxis, row.EndDate.Format("2006-01-02"))
interestArr = append(interestArr, opts.BarData{Value: row.Interest.Mul(minusOne).String()})
principalArr = append(principalArr, opts.BarData{Value: row.Principal.Mul(minusOne).String()})
paymentArr = append(paymentArr, opts.BarData{Value: row.Payment.Mul(minusOne).String()})
}
// Put data into instance
bar.SetXAxis(xAxis).
AddSeries("Principal", principalArr).
AddSeries("Interest", interestArr).
AddSeries("Payment", paymentArr).SetSeriesOptions(
charts.WithBarChartOpts(opts.BarChart{
Stack: "stackA",
}))
return bar
}
// renderer renders the bar into the writer interface
func renderer(bar *charts.Bar, writer io.Writer) error {
return bar.Render(writer)
}