From 572cc14c572103460b52dde48b46b1001fc39e1a Mon Sep 17 00:00:00 2001 From: Anantha Kumaran Date: Sun, 24 Apr 2022 09:17:39 +0530 Subject: [PATCH] add xirr and other details in gain page --- cmd/init.go | 15 ++++++- internal/server/gain.go | 5 ++- internal/server/ledger.go | 24 ++--------- internal/service/market.go | 8 ++++ internal/service/xirr.go | 28 +++++++++++++ web/src/gain.ts | 81 ++++++++++++++++++++++++++++---------- web/src/utils.ts | 1 + web/static/index.html | 2 +- 8 files changed, 120 insertions(+), 44 deletions(-) create mode 100644 internal/service/xirr.go diff --git a/cmd/init.go b/cmd/init.go index e5305d0c..0746cf18 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -81,7 +81,7 @@ func formatFloat(num float64) string { } func emitSalary(file *os.File, start time.Time) { - var salary float64 = 100000 + (float64(start.Year())-2019)*20000 + var salary float64 = 100000 + (float64(start.Year())-2019)*(100000*0.05) _, err := file.WriteString(fmt.Sprintf(` %s Salary Income:Salary @@ -92,6 +92,19 @@ func emitSalary(file *os.File, start time.Time) { log.Fatal(err) } + if start.Year() > 2019 && start.Month() == time.March { + _, err = file.WriteString(fmt.Sprintf(` +%s EPF Interest + Income:Interest:EPF + Asset:Debt:EPF %s INR +`, start.Format("2006/01/02"), formatFloat(salary*0.12*((float64(start.Year())-2019)*12)*0.075))) + + if err != nil { + log.Fatal(err) + } + + } + } func emitEquityMutualFund(file *os.File, start time.Time, pricesTree map[string]*btree.BTree) { diff --git a/internal/server/gain.go b/internal/server/gain.go index 1290c3a1..0c0c167c 100644 --- a/internal/server/gain.go +++ b/internal/server/gain.go @@ -4,6 +4,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/ananthakumaran/paisa/internal/model/posting" + "github.com/ananthakumaran/paisa/internal/service" "github.com/gin-gonic/gin" "github.com/samber/lo" "gorm.io/gorm" @@ -12,6 +13,7 @@ import ( type Gain struct { Account string `json:"account"` OverviewTimeline []Overview `json:"overview_timeline"` + XIRR float64 `json:"xirr"` } func GetGain(db *gorm.DB) gin.H { @@ -21,10 +23,11 @@ func GetGain(db *gorm.DB) gin.H { log.Fatal(result.Error) } + postings = service.PopulateMarketPrice(db, postings) byAccount := lo.GroupBy(postings, func(p posting.Posting) string { return p.Account }) var gains []Gain for account, ps := range byAccount { - gains = append(gains, Gain{Account: account, OverviewTimeline: computeOverviewTimeline(db, ps)}) + gains = append(gains, Gain{Account: account, XIRR: service.XIRR(db, ps), OverviewTimeline: computeOverviewTimeline(db, ps)}) } return gin.H{"gain_timeline_breakdown": gains} diff --git a/internal/server/ledger.go b/internal/server/ledger.go index 382398d3..c8386ad6 100644 --- a/internal/server/ledger.go +++ b/internal/server/ledger.go @@ -2,12 +2,10 @@ package server import ( "strings" - "time" "github.com/samber/lo" log "github.com/sirupsen/logrus" - "github.com/ChizhovVadim/xirr" "github.com/ananthakumaran/paisa/internal/model/posting" "github.com/ananthakumaran/paisa/internal/service" "github.com/gin-gonic/gin" @@ -29,11 +27,7 @@ func GetLedger(db *gorm.DB) gin.H { log.Fatal(result.Error) } - date := time.Now() - postings = lo.Map(postings, func(p posting.Posting, _ int) posting.Posting { - p.MarketAmount = service.GetMarketPrice(db, p, date) - return p - }) + postings = service.PopulateMarketPrice(db, postings) breakdowns := computeBreakdown(db, lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, "Asset:") })) return gin.H{"postings": postings, "breakdowns": breakdowns} } @@ -49,7 +43,6 @@ func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]Breakd } - today := time.Now() result := make(map[string]Breakdown) for group := range accounts { @@ -69,20 +62,9 @@ func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]Breakd } }, 0.0) marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0) - payments := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment { - if service.IsInterest(db, p) { - return xirr.Payment{Date: p.Date, Amount: 0} - } else { - return xirr.Payment{Date: p.Date, Amount: -p.Amount} - } - })) - payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount}) - returns, err := xirr.XIRR(payments) - if err != nil { - log.Fatal(err) - } - breakdown := Breakdown{InvestmentAmount: investmentAmount, WithdrawalAmount: withdrawalAmount, MarketAmount: marketAmount, XIRR: (returns - 1) * 100, Group: group} + xirr := service.XIRR(db, ps) + breakdown := Breakdown{InvestmentAmount: investmentAmount, WithdrawalAmount: withdrawalAmount, MarketAmount: marketAmount, XIRR: xirr, Group: group} result[group] = breakdown } diff --git a/internal/service/market.go b/internal/service/market.go index 19f29c6a..670d459e 100644 --- a/internal/service/market.go +++ b/internal/service/market.go @@ -81,3 +81,11 @@ func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) float64 { return p.Amount } + +func PopulateMarketPrice(db *gorm.DB, ps []posting.Posting) []posting.Posting { + date := time.Now() + return lo.Map(ps, func(p posting.Posting, _ int) posting.Posting { + p.MarketAmount = GetMarketPrice(db, p, date) + return p + }) +} diff --git a/internal/service/xirr.go b/internal/service/xirr.go new file mode 100644 index 00000000..21f32900 --- /dev/null +++ b/internal/service/xirr.go @@ -0,0 +1,28 @@ +package service + +import ( + "github.com/ChizhovVadim/xirr" + "github.com/ananthakumaran/paisa/internal/model/posting" + "github.com/samber/lo" + "gorm.io/gorm" + "time" +) + +func XIRR(db *gorm.DB, ps []posting.Posting) float64 { + today := time.Now() + marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0) + payments := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment { + if IsInterest(db, p) { + return xirr.Payment{Date: p.Date, Amount: 0} + } else { + return xirr.Payment{Date: p.Date, Amount: -p.Amount} + } + })) + payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount}) + returns, err := xirr.XIRR(payments) + if err != nil { + return 0 + } + + return (returns - 1) * 100 +} diff --git a/web/src/gain.ts b/web/src/gain.ts index 646bd7b8..d6495875 100644 --- a/web/src/gain.ts +++ b/web/src/gain.ts @@ -2,7 +2,14 @@ import * as d3 from "d3"; import legend from "d3-svg-legend"; import dayjs from "dayjs"; import _ from "lodash"; -import { ajax, formatCurrencyCrude, Overview } from "./utils"; +import { + ajax, + formatCurrency, + formatCurrencyCrude, + formatFloat, + Gain, + Overview +} from "./utils"; export default async function () { const { gain_timeline_breakdown: gains } = await ajax("/api/gain"); @@ -17,43 +24,83 @@ export default async function () { ), end = dayjs(); - const svgs = d3 + const divs = d3 .select("#d3-gain-timeline-breakdown") - .selectAll("svg") + .selectAll("div") .data(_.sortBy(gains, (g) => g.account)); - svgs.exit().remove(); + divs.exit().remove(); - svgs - .enter() + const columns = divs.enter().append("div").attr("class", "columns"); + + const leftColumn = columns.append("div").attr("class", "column is-2"); + leftColumn + .append("table") + .attr("class", "table is-narrow is-fullwidth is-size-7") + .append("tbody") + .each(renderTable); + + const rightColumn = columns.append("div").attr("class", "column is-10"); + rightColumn .append("svg") .attr("width", "100%") .attr("height", "150") .each(function (gain) { - renderOverviewSmall(gain.overview_timeline, this, gain.account, [ - start, - end - ]); + renderOverviewSmall(gain.overview_timeline, this, [start, end]); }); } const areaKeys = ["gain", "loss"]; const colors = ["#b2df8a", "#fb9a99"]; const areaScale = d3.scaleOrdinal().domain(areaKeys).range(colors); -const lineKeys = ["networth", "investment", "withdrawal"]; +const lineKeys = ["balance", "investment", "withdrawal"]; const lineScale = d3 .scaleOrdinal() .domain(lineKeys) .range(["#1f77b4", "#17becf", "#ff7f0e"]); +function renderTable(gain: Gain) { + const tbody = d3.select(this); + const current = _.last(gain.overview_timeline); + tbody.html(function (d) { + return ` + + Account + ${gain.account} + + + Investment + ${formatCurrency(current.investment_amount)} + + + Withdrawal + ${formatCurrency(current.withdrawal_amount)} + + + Gain + ${formatCurrency(current.gain_amount)} + + + Balance + ${formatCurrency( + current.investment_amount + current.gain_amount - current.withdrawal_amount + )} + + + XIRR + ${formatFloat(gain.xirr)} + +`; + }); +} + function renderOverviewSmall( points: Overview[], element: Element, - account: string, xDomain: [dayjs.Dayjs, dayjs.Dayjs] ) { const svg = d3.select(element), - margin = { top: 15, right: 80, bottom: 20, left: 40 }, + margin = { top: 5, right: 80, bottom: 20, left: 40 }, width = element.parentElement.clientWidth - margin.left - margin.right, height = +svg.attr("height") - margin.top - margin.bottom, g = svg @@ -189,7 +236,7 @@ function renderOverviewSmall( layer .append("path") - .style("stroke", lineScale("networth")) + .style("stroke", lineScale("balance")) .style("fill", "none") .attr( "d", @@ -199,12 +246,6 @@ function renderOverviewSmall( .x((d) => x(d.timestamp)) .y((d) => y(d.investment_amount + d.gain_amount - d.withdrawal_amount)) ); - - svg - .append("g") - .append("text") - .attr("transform", "translate(80,12)") - .text(account); } function renderLegend() { diff --git a/web/src/utils.ts b/web/src/utils.ts index 675f6384..e4f8e994 100644 --- a/web/src/utils.ts +++ b/web/src/utils.ts @@ -29,6 +29,7 @@ export interface Overview { export interface Gain { account: string; overview_timeline: Overview[]; + xirr: number; } export interface Breakdown { diff --git a/web/static/index.html b/web/static/index.html index 544979ec..ccbda688 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -121,7 +121,7 @@ -
+