Skip to content

Commit

Permalink
add xirr and other details in gain page
Browse files Browse the repository at this point in the history
  • Loading branch information
ananthakumaran committed Apr 24, 2022
1 parent 51ee391 commit 572cc14
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 44 deletions.
15 changes: 14 additions & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion internal/server/gain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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}
Expand Down
24 changes: 3 additions & 21 deletions internal/server/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}
}
Expand All @@ -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 {
Expand All @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions internal/service/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
28 changes: 28 additions & 0 deletions internal/service/xirr.go
Original file line number Diff line number Diff line change
@@ -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
}
81 changes: 61 additions & 20 deletions web/src/gain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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<string>()
.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 `
<tr>
<td>Account</td>
<td class='has-text-right has-text-weight-bold'>${gain.account}</td>
</tr>
<tr>
<td>Investment</td>
<td class='has-text-right'>${formatCurrency(current.investment_amount)}</td>
</tr>
<tr>
<td>Withdrawal</td>
<td class='has-text-right'>${formatCurrency(current.withdrawal_amount)}</td>
</tr>
<tr>
<td>Gain</td>
<td class='has-text-right'>${formatCurrency(current.gain_amount)}</td>
</tr>
<tr>
<td>Balance</td>
<td class='has-text-right'>${formatCurrency(
current.investment_amount + current.gain_amount - current.withdrawal_amount
)}</td>
</tr>
<tr>
<td>XIRR</td>
<td class='has-text-right'>${formatFloat(gain.xirr)}</td>
</tr>
`;
});
}

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
Expand Down Expand Up @@ -189,7 +236,7 @@ function renderOverviewSmall(

layer
.append("path")
.style("stroke", lineScale("networth"))
.style("stroke", lineScale("balance"))
.style("fill", "none")
.attr(
"d",
Expand All @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions web/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface Overview {
export interface Gain {
account: string;
overview_timeline: Overview[];
xirr: number;
}

export interface Breakdown {
Expand Down
2 changes: 1 addition & 1 deletion web/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
</div>
</div>
</div>
<div class="container is-fluid">
<div class="container is-fluid d3-gain-timeline-breakdown">
<div class="columns">
<div id="d3-gain-timeline-breakdown" class="column is-12">
</div>
Expand Down

0 comments on commit 572cc14

Please sign in to comment.