float64 to decimal

This commit is contained in:
Anantha Kumaran 2023-08-20 17:33:14 +05:30
parent 2a84ff60c7
commit 941cb0d199
38 changed files with 406 additions and 359 deletions

View File

@ -151,12 +151,12 @@ func emitTransaction(file *os.File, date time.Time, payee string, from string, t
func emitCommodityBuy(file *os.File, date time.Time, commodity string, from string, to string, amount float64) float64 {
pc := utils.BTreeDescendFirstLessOrEqual(pricesTree[commodity], price.Price{Date: date})
units := amount / pc.Value
units := amount / pc.Value.InexactFloat64()
_, err := file.WriteString(fmt.Sprintf(`
%s Investment
%s %s %s @ %s INR
%s
`, date.Format("2006/01/02"), to, formatFloat(units), commodity, formatFloat(pc.Value), from))
`, date.Format("2006/01/02"), to, formatFloat(units), commodity, formatFloat(pc.Value.InexactFloat64()), from))
if err != nil {
log.Fatal(err)
}
@ -165,9 +165,9 @@ func emitCommodityBuy(file *os.File, date time.Time, commodity string, from stri
func emitCommoditySell(file *os.File, date time.Time, commodity string, from string, to string, amount float64, availableUnits float64) (float64, float64) {
pc := utils.BTreeDescendFirstLessOrEqual(pricesTree[commodity], price.Price{Date: date})
requiredUnits := amount / pc.Value
requiredUnits := amount / pc.Value.InexactFloat64()
units := math.Min(availableUnits, requiredUnits)
return emitCommodityBuy(file, date, commodity, from, to, -units*pc.Value), units * pc.Value
return emitCommodityBuy(file, date, commodity, from, to, -units*pc.Value.InexactFloat64()), units * pc.Value.InexactFloat64()
}
func loadPrices(schemeCode string, commodityType config.CommodityType, commodityName string, pricesTree map[string]*btree.BTree) {

1
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/manifoldco/promptui v0.9.0
github.com/samber/lo v1.38.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.0
github.com/shopspring/decimal v1.3.1
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.8.4

2
go.sum
View File

@ -82,6 +82,8 @@ github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=

View File

@ -10,6 +10,7 @@ import (
"github.com/ananthakumaran/paisa/internal/service"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/samber/lo"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@ -17,15 +18,15 @@ import (
type Balance struct {
Date time.Time
Commodity string
Quantity float64
Quantity decimal.Decimal
}
func Register(postings []posting.Posting) []Balance {
balances := make([]Balance, 0)
current := Balance{Quantity: 0}
current := Balance{Quantity: decimal.Zero}
for _, p := range postings {
sameDay := p.Date == current.Date
current = Balance{Date: p.Date, Quantity: p.Quantity + current.Quantity, Commodity: p.Commodity}
current = Balance{Date: p.Date, Quantity: p.Quantity.Add(current.Quantity), Commodity: p.Commodity}
if sameDay {
balances = balances[:len(balances)-1]
}
@ -51,35 +52,35 @@ func FIFO(postings []posting.Posting) []posting.Posting {
var available []posting.Posting
for _, p := range postings {
if utils.IsCurrency(p.Commodity) {
if p.Amount > 0 {
if p.Amount.GreaterThan(decimal.Zero) {
available = append(available, p)
} else {
amount := -p.Amount
for amount > 0 && len(available) > 0 {
amount := p.Amount.Neg()
for amount.GreaterThan(decimal.Zero) && len(available) > 0 {
first := available[0]
if first.Amount > amount {
first.AddAmount(-amount)
if first.Amount.GreaterThan(amount) {
first.AddAmount(amount.Neg())
available[0] = first
amount = 0
amount = decimal.Zero
} else {
amount -= first.Amount
amount = amount.Sub(first.Amount)
available = available[1:]
}
}
}
} else {
if p.Quantity > 0 {
if p.Quantity.GreaterThan(decimal.Zero) {
available = append(available, p)
} else {
quantity := -p.Quantity
for quantity > 0 && len(available) > 0 {
quantity := p.Quantity.Neg()
for quantity.GreaterThan(decimal.Zero) && len(available) > 0 {
first := available[0]
if first.Quantity > quantity {
first.AddQuantity(-quantity)
if first.Quantity.GreaterThan(quantity) {
first.AddQuantity(quantity.Neg())
available[0] = first
quantity = 0
quantity = decimal.Zero
} else {
quantity -= first.Quantity
quantity = quantity.Sub(first.Quantity)
available = available[1:]
}
}
@ -90,31 +91,31 @@ func FIFO(postings []posting.Posting) []posting.Posting {
return available
}
func CostBalance(postings []posting.Posting) float64 {
func CostBalance(postings []posting.Posting) decimal.Decimal {
byAccount := lo.GroupBy(postings, func(p posting.Posting) string { return p.Account })
return lo.SumBy(lo.Values(byAccount), func(ps []posting.Posting) float64 {
return lo.SumBy(FIFO(ps), func(p posting.Posting) float64 {
return utils.SumBy(lo.Values(byAccount), func(ps []posting.Posting) decimal.Decimal {
return utils.SumBy(FIFO(ps), func(p posting.Posting) decimal.Decimal {
return p.Amount
})
})
}
func CurrentBalance(postings []posting.Posting) float64 {
return lo.SumBy(postings, func(p posting.Posting) float64 {
func CurrentBalance(postings []posting.Posting) decimal.Decimal {
return utils.SumBy(postings, func(p posting.Posting) decimal.Decimal {
return p.MarketAmount
})
}
func CostSum(postings []posting.Posting) float64 {
return lo.SumBy(postings, func(p posting.Posting) float64 {
func CostSum(postings []posting.Posting) decimal.Decimal {
return utils.SumBy(postings, func(p posting.Posting) decimal.Decimal {
return p.Amount
})
}
type Point struct {
Date time.Time `json:"date"`
Value float64 `json:"value"`
Date time.Time `json:"date"`
Value decimal.Decimal `json:"value"`
}
func RunningBalance(db *gorm.DB, postings []posting.Posting) []Point {
@ -135,7 +136,7 @@ func RunningBalance(db *gorm.DB, postings []posting.Posting) []Point {
pastPostings = append(pastPostings, p)
}
balance := lo.SumBy(pastPostings, func(p posting.Posting) float64 {
balance := utils.SumBy(pastPostings, func(p posting.Posting) decimal.Decimal {
return service.GetMarketPrice(db, p, start)
})
series = append(series, Point{Date: start, Value: balance})

View File

@ -13,6 +13,7 @@ import (
"github.com/gofrs/uuid"
"github.com/google/btree"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"encoding/json"
@ -82,7 +83,7 @@ func (LedgerCLI) Parse(journalPath string, _prices []price.Price) ([]*posting.Po
log.Fatal(err)
}
command := exec.Command("ledger", "-f", journalPath, "csv", "--csv-format", "%(quoted(date)),%(quoted(payee)),%(quoted(display_account)),%(quoted(commodity(scrub(display_amount)))),%(quoted(quantity(scrub(display_amount)))),%(quoted(to_int(scrub(market(amount,date,'"+config.DefaultCurrency()+"') * 100000)))),%(quoted(xact.filename)),%(quoted(xact.id)),%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\"))),%(quoted(tag('Recurring'))),%(quoted(xact.beg_line)),%(quoted(xact.end_line))\n")
command := exec.Command("ledger", "-f", journalPath, "csv", "--csv-format", "%(quoted(date)),%(quoted(payee)),%(quoted(display_account)),%(quoted(commodity(scrub(display_amount)))),%(quoted(quantity(scrub(display_amount)))),%(quoted(to_int(scrub(market(amount,date,'"+config.DefaultCurrency()+"') * 100000000)))),%(quoted(xact.filename)),%(quoted(xact.id)),%(quoted(cleared ? \"*\" : (pending ? \"!\" : \"\"))),%(quoted(tag('Recurring'))),%(quoted(xact.beg_line)),%(quoted(xact.end_line))\n")
var output, error bytes.Buffer
command.Stdout = &output
command.Stderr = &error
@ -117,7 +118,7 @@ func (LedgerCLI) Parse(journalPath string, _prices []price.Price) ([]*posting.Po
if err != nil {
return nil, err
}
amount = amount / 100000
amount = amount / 100000000
fileName, err := filepath.Rel(dir, record[6])
if err != nil {
@ -156,8 +157,8 @@ func (LedgerCLI) Parse(journalPath string, _prices []price.Price) ([]*posting.Po
Payee: record[1],
Account: record[2],
Commodity: record[3],
Quantity: quantity,
Amount: amount,
Quantity: decimal.NewFromFloat(quantity),
Amount: decimal.NewFromFloat(amount),
TransactionID: transactionID,
Status: status,
TagRecurring: tagRecurring,
@ -305,17 +306,17 @@ func (HLedgerCLI) Parse(journalPath string, prices []price.Price) ([]*posting.Po
for _, p := range t.Postings {
amount := p.Amount[0]
totalAmount := amount.Quantity.Value
totalAmount := decimal.NewFromFloat(amount.Quantity.Value)
if amount.Commodity != config.DefaultCurrency() {
if amount.Price.Contents.Quantity.Value != 0 {
totalAmount = amount.Price.Contents.Quantity.Value * amount.Quantity.Value
totalAmount = decimal.NewFromFloat(amount.Price.Contents.Quantity.Value).Mul(decimal.NewFromFloat(amount.Quantity.Value))
} else {
pt := pricesTree[amount.Commodity]
if pt != nil {
pc := utils.BTreeDescendFirstLessOrEqual(pt, price.Price{Date: date})
if pc.Value != 0 {
totalAmount = amount.Quantity.Value * pc.Value
if !pc.Value.Equal(decimal.Zero) {
totalAmount = decimal.NewFromFloat(amount.Quantity.Value).Mul(pc.Value)
}
}
}
@ -347,7 +348,7 @@ func (HLedgerCLI) Parse(journalPath string, prices []price.Price) ([]*posting.Po
Payee: t.Description,
Account: p.Account,
Commodity: amount.Commodity,
Quantity: amount.Quantity.Value,
Quantity: decimal.NewFromFloat(amount.Quantity.Value),
Amount: totalAmount,
TransactionID: strconv.FormatInt(t.ID, 10),
Status: strings.ToLower(t.Status),
@ -406,7 +407,7 @@ func parseLedgerPrices(output string, defaultCurrency string) ([]price.Price, er
return nil, err
}
prices = append(prices, price.Price{Date: date, CommodityName: commodity, CommodityID: commodity, CommodityType: config.Unknown, Value: value})
prices = append(prices, price.Price{Date: date, CommodityName: commodity, CommodityID: commodity, CommodityType: config.Unknown, Value: decimal.NewFromFloat(value)})
}
return prices, nil
@ -434,7 +435,7 @@ func parseHLedgerPrices(output string, defaultCurrency string) ([]price.Price, e
return nil, err
}
prices = append(prices, price.Price{Date: date, CommodityName: commodity, CommodityID: commodity, CommodityType: config.Unknown, Value: value})
prices = append(prices, price.Price{Date: date, CommodityName: commodity, CommodityID: commodity, CommodityType: config.Unknown, Value: decimal.NewFromFloat(value)})
}
return prices, nil

View File

@ -10,7 +10,7 @@ import (
func assertPriceEqual(t *testing.T, actual price.Price, date string, commodityName string, value float64) {
assert.Equal(t, commodityName, actual.CommodityName, "they should be equal")
assert.Equal(t, date, actual.Date.Format("2006/01/02"), "they should be equal")
assert.Equal(t, value, actual.Value, "they should be equal")
assert.Equal(t, value, actual.Value.InexactFloat64(), "they should be equal")
}
func TestParseLegerPrices(t *testing.T) {

View File

@ -2,6 +2,7 @@ package portfolio
import (
"github.com/ananthakumaran/paisa/internal/config"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@ -15,7 +16,7 @@ type Portfolio struct {
SecurityType string `json:"security_type"`
SecurityRating string `json:"security_rating"`
SecurityIndustry string `json:"security_industry"`
Percentage float64 `json:"percentage"`
Percentage decimal.Decimal `json:"percentage"`
}
func UpsertAll(db *gorm.DB, commodityType config.CommodityType, parentCommodityID string, portfolios []*Portfolio) {

View File

@ -4,26 +4,27 @@ import (
"strings"
"time"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type Posting struct {
ID uint `gorm:"primaryKey" json:"id"`
TransactionID string `json:"transaction_id"`
Date time.Time `json:"date"`
Payee string `json:"payee"`
Account string `json:"account"`
Commodity string `json:"commodity"`
Quantity float64 `json:"quantity"`
Amount float64 `json:"amount"`
Status string `json:"status"`
TagRecurring string `json:"tag_recurring"`
TransactionBeginLine uint64 `json:"transaction_begin_line"`
TransactionEndLine uint64 `json:"transaction_end_line"`
FileName string `json:"file_name"`
ID uint `gorm:"primaryKey" json:"id"`
TransactionID string `json:"transaction_id"`
Date time.Time `json:"date"`
Payee string `json:"payee"`
Account string `json:"account"`
Commodity string `json:"commodity"`
Quantity decimal.Decimal `json:"quantity"`
Amount decimal.Decimal `json:"amount"`
Status string `json:"status"`
TagRecurring string `json:"tag_recurring"`
TransactionBeginLine uint64 `json:"transaction_begin_line"`
TransactionEndLine uint64 `json:"transaction_end_line"`
FileName string `json:"file_name"`
MarketAmount float64 `gorm:"-:all" json:"market_amount"`
MarketAmount decimal.Decimal `gorm:"-:all" json:"market_amount"`
}
func (p Posting) GroupDate() time.Time {
@ -36,29 +37,29 @@ func (p *Posting) RestName(level int) string {
func (p Posting) Negate() Posting {
clone := p
clone.Quantity = -p.Quantity
clone.Amount = -p.Amount
clone.Quantity = p.Quantity.Neg()
clone.Amount = p.Amount.Neg()
return clone
}
func (p *Posting) Price() float64 {
return p.Amount / p.Quantity
func (p *Posting) Price() decimal.Decimal {
return p.Amount.Div(p.Quantity)
}
func (p *Posting) AddAmount(amount float64) {
p.Amount += amount
func (p *Posting) AddAmount(amount decimal.Decimal) {
p.Amount = p.Amount.Add(amount)
}
func (p *Posting) AddQuantity(quantity float64) {
func (p *Posting) AddQuantity(quantity decimal.Decimal) {
price := p.Price()
p.Quantity += quantity
p.Amount = p.Quantity * price
p.Quantity = p.Quantity.Add(quantity)
p.Amount = p.Quantity.Mul(price)
}
func (p Posting) WithQuantity(quantity float64) Posting {
func (p Posting) WithQuantity(quantity decimal.Decimal) Posting {
clone := p
clone.Quantity = quantity
clone.Amount = quantity * p.Price()
clone.Amount = quantity.Mul(p.Price())
return clone
}

View File

@ -7,6 +7,7 @@ import (
"github.com/ananthakumaran/paisa/internal/config"
"github.com/google/btree"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
)
@ -16,7 +17,7 @@ type Price struct {
CommodityType config.CommodityType `json:"commodity_type"`
CommodityID string `json:"commodity_id"`
CommodityName string `json:"commodity_name"`
Value float64 `json:"value"`
Value decimal.Decimal `json:"value"`
}
func (p Price) Less(o btree.Item) bool {

View File

@ -53,7 +53,7 @@ func buldIndex(postings []posting.Posting) index {
if idx.Docs[p.Account] == nil {
idx.Docs[p.Account] = make(map[string]int64)
}
for _, token := range tokenize(strings.Join([]string{strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", p.Amount), "0"), "."), p.Payee}, " ")) {
for _, token := range tokenize(strings.Join([]string{strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", p.Amount.InexactFloat64()), "0"), "."), p.Payee}, " ")) {
if idx.Tokens[token] == nil {
idx.Tokens[token] = make(map[string]int64)
}

View File

@ -3,12 +3,14 @@ package mutualfund
import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"github.com/ananthakumaran/paisa/internal/config"
"github.com/ananthakumaran/paisa/internal/model/price"
)
@ -52,7 +54,7 @@ func GetNav(schemeCode string, commodityName string) ([]*price.Price, error) {
return nil, err
}
price := price.Price{Date: date, CommodityType: config.MutualFund, CommodityID: schemeCode, CommodityName: commodityName, Value: value}
price := price.Price{Date: date, CommodityType: config.MutualFund, CommodityID: schemeCode, CommodityName: commodityName, Value: decimal.NewFromFloat(value)}
prices = append(prices, &price)
}
return prices, nil

View File

@ -7,6 +7,7 @@ import (
"net/http"
"strings"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"github.com/ananthakumaran/paisa/internal/config"
@ -48,12 +49,12 @@ WHERE s.code = %s
}
type Data struct {
Name string `json:"name"`
PercentageToNav float64 `json:"percentage_to_nav"`
ISIN string `json:"isin"`
Type string `json:"type"`
Rating string `json:"rating"`
Industry string `json:"industry"`
Name string `json:"name"`
PercentageToNav decimal.Decimal `json:"percentage_to_nav"`
ISIN string `json:"isin"`
Type string `json:"type"`
Rating string `json:"rating"`
Industry string `json:"industry"`
}
type Result struct {
Data []Data

View File

@ -3,11 +3,13 @@ package nps
import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"io/ioutil"
"net/http"
"time"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"github.com/ananthakumaran/paisa/internal/config"
"github.com/ananthakumaran/paisa/internal/model/price"
)
@ -28,7 +30,7 @@ func GetNav(schemeCode string, commodityName string) ([]*price.Price, error) {
type Data struct {
Date string
Nav float64
Nav decimal.Decimal
}
type Result struct {
Data []Data

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/google/btree"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"github.com/ananthakumaran/paisa/internal/config"
@ -85,7 +86,7 @@ func GetHistory(ticker string, commodityName string) ([]*price.Price, error) {
value = value * exchangePrice.Close
}
price := price.Price{Date: date, CommodityType: config.Stock, CommodityID: ticker, CommodityName: commodityName, Value: value}
price := price.Price{Date: date, CommodityType: config.Stock, CommodityID: ticker, CommodityName: commodityName, Value: decimal.NewFromFloat(value)}
prices = append(prices, &price)
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"github.com/ananthakumaran/paisa/internal/accounting"
"github.com/ananthakumaran/paisa/internal/config"
@ -16,22 +17,22 @@ import (
)
type Aggregate struct {
Date time.Time `json:"date"`
Account string `json:"account"`
Amount float64 `json:"amount"`
MarketAmount float64 `json:"market_amount"`
Date time.Time `json:"date"`
Account string `json:"account"`
Amount decimal.Decimal `json:"amount"`
MarketAmount decimal.Decimal `json:"market_amount"`
}
type AllocationTargetConfig struct {
Name string
Target float64
Target decimal.Decimal
Accounts []string
}
type AllocationTarget struct {
Name string `json:"name"`
Target float64 `json:"target"`
Current float64 `json:"current"`
Target decimal.Decimal `json:"target"`
Current decimal.Decimal `json:"current"`
Aggregates map[string]Aggregate `json:"aggregates"`
}
@ -71,7 +72,7 @@ func computeAllocationTargets(postings []posting.Posting) []AllocationTarget {
var targetAllocations []AllocationTarget
allocationTargetConfigs := config.GetConfig().AllocationTargets
totalMarketAmount := lo.Reduce(postings, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
totalMarketAmount := accounting.CurrentBalance(postings)
for _, allocationTargetConfig := range allocationTargetConfigs {
targetAllocations = append(targetAllocations, computeAllocationTarget(postings, allocationTargetConfig, totalMarketAmount))
@ -80,12 +81,12 @@ func computeAllocationTargets(postings []posting.Posting) []AllocationTarget {
return targetAllocations
}
func computeAllocationTarget(postings []posting.Posting, allocationTargetConfig config.AllocationTarget, total float64) AllocationTarget {
func computeAllocationTarget(postings []posting.Posting, allocationTargetConfig config.AllocationTarget, total decimal.Decimal) AllocationTarget {
date := time.Now()
postings = accounting.FilterByGlob(postings, allocationTargetConfig.Accounts)
aggregates := computeAggregate(postings, date)
currentTotal := lo.Reduce(postings, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
return AllocationTarget{Name: allocationTargetConfig.Name, Target: allocationTargetConfig.Target, Current: (currentTotal / total) * 100, Aggregates: aggregates}
currentTotal := accounting.CurrentBalance(postings)
return AllocationTarget{Name: allocationTargetConfig.Name, Target: decimal.NewFromFloat(allocationTargetConfig.Target), Current: (currentTotal.Div(total)).Mul(decimal.NewFromInt(100)), Aggregates: aggregates}
}
func computeAggregate(postings []posting.Posting, date time.Time) map[string]Aggregate {
@ -99,8 +100,8 @@ func computeAggregate(postings []posting.Posting, date time.Time) map[string]Agg
result[parent] = Aggregate{Account: parent}
}
amount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.Amount }, 0.0)
marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
amount := accounting.CostSum(ps)
marketAmount := accounting.CurrentBalance(ps)
result[account] = Aggregate{Date: date, Account: account, Amount: amount, MarketAmount: marketAmount}
}

View File

@ -4,6 +4,7 @@ import (
"strings"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"github.com/ananthakumaran/paisa/internal/accounting"
"github.com/ananthakumaran/paisa/internal/model/posting"
@ -15,13 +16,13 @@ import (
)
type AssetBreakdown struct {
Group string `json:"group"`
InvestmentAmount float64 `json:"investment_amount"`
WithdrawalAmount float64 `json:"withdrawal_amount"`
MarketAmount float64 `json:"market_amount"`
BalanceUnits float64 `json:"balance_units"`
LatestPrice float64 `json:"latest_price"`
XIRR float64 `json:"xirr"`
Group string `json:"group"`
InvestmentAmount decimal.Decimal `json:"investment_amount"`
WithdrawalAmount decimal.Decimal `json:"withdrawal_amount"`
MarketAmount decimal.Decimal `json:"market_amount"`
BalanceUnits decimal.Decimal `json:"balance_units"`
LatestPrice decimal.Decimal `json:"latest_price"`
XIRR decimal.Decimal `json:"xirr"`
}
func GetBalance(db *gorm.DB) gin.H {
@ -47,29 +48,29 @@ func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]AssetB
for group, leaf := range accounts {
ps := lo.Filter(postings, func(p posting.Posting, _ int) bool { return utils.IsSameOrParent(p.Account, group) })
investmentAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
if utils.IsCheckingAccount(p.Account) || p.Amount < 0 || service.IsInterest(db, p) {
investmentAmount := lo.Reduce(ps, func(acc decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if utils.IsCheckingAccount(p.Account) || p.Amount.LessThan(decimal.Zero) || service.IsInterest(db, p) {
return acc
} else {
return acc + p.Amount
return acc.Add(p.Amount)
}
}, 0.0)
withdrawalAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
if utils.IsCheckingAccount(p.Account) || p.Amount > 0 || service.IsInterest(db, p) {
}, decimal.Zero)
withdrawalAmount := lo.Reduce(ps, func(acc decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if utils.IsCheckingAccount(p.Account) || p.Amount.GreaterThan(decimal.Zero) || service.IsInterest(db, p) {
return acc
} else {
return acc + -p.Amount
return acc.Add(p.Amount.Neg())
}
}, 0.0)
}, decimal.Zero)
marketAmount := accounting.CurrentBalance(ps)
var balanceUnits float64
var balanceUnits decimal.Decimal
if leaf {
balanceUnits = lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
balanceUnits = lo.Reduce(ps, func(acc decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if !utils.IsCurrency(p.Commodity) {
return acc + p.Quantity
return acc.Add(p.Quantity)
}
return 0.0
}, 0.0)
return decimal.Zero
}, decimal.Zero)
}
xirr := service.XIRR(db, ps)

View File

@ -9,6 +9,7 @@ import (
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -19,11 +20,11 @@ type PostingPair struct {
}
type FYCapitalGain struct {
Units float64 `json:"units"`
PurchasePrice float64 `json:"purchase_price"`
SellPrice float64 `json:"sell_price"`
Tax taxation.Tax `json:"tax"`
PostingPairs []PostingPair `json:"posting_pairs"`
Units decimal.Decimal `json:"units"`
PurchasePrice decimal.Decimal `json:"purchase_price"`
SellPrice decimal.Decimal `json:"sell_price"`
Tax taxation.Tax `json:"tax"`
PostingPairs []PostingPair `json:"posting_pairs"`
}
type CapitalGain struct {
@ -49,41 +50,41 @@ func computeCapitalGains(db *gorm.DB, account string, commodity config.Commodity
capitalGain := CapitalGain{Account: account, TaxCategory: string(commodity.TaxCategory), FY: make(map[string]FYCapitalGain)}
var available []posting.Posting
for _, p := range postings {
if p.Quantity > 0 {
if p.Quantity.GreaterThan(decimal.Zero) {
available = append(available, p)
} else {
quantity := -p.Quantity
quantity := p.Quantity.Neg()
totalTax := taxation.Tax{}
purchasePrice := 0.0
purchasePrice := decimal.Zero
postingPairs := make([]PostingPair, 0)
for quantity > 0 && len(available) > 0 {
for quantity.GreaterThan(decimal.Zero) && len(available) > 0 {
first := available[0]
q := 0.0
q := decimal.Zero
if first.Quantity > quantity {
first.AddQuantity(-quantity)
if first.Quantity.GreaterThan(quantity) {
first.AddQuantity(quantity.Neg())
q = quantity
available[0] = first
quantity = 0
quantity = decimal.Zero
} else {
quantity -= first.Quantity
quantity = quantity.Sub(first.Quantity)
q = first.Quantity
available = available[1:]
}
purchasePrice += q * first.Price()
purchasePrice = purchasePrice.Add(q.Mul(first.Price()))
tax := taxation.Calculate(db, q, commodity, first.Price(), first.Date, p.Price(), p.Date)
totalTax = taxation.Add(totalTax, tax)
postingPair := PostingPair{Purchase: first.WithQuantity(q), Sell: p.WithQuantity(-q), Tax: tax}
postingPair := PostingPair{Purchase: first.WithQuantity(q), Sell: p.WithQuantity(q.Neg()), Tax: tax}
postingPairs = append(postingPairs, postingPair)
}
fy := utils.FY(p.Date)
fyCapitalGain := capitalGain.FY[fy]
fyCapitalGain.Tax = taxation.Add(fyCapitalGain.Tax, totalTax)
fyCapitalGain.Units += -p.Quantity
fyCapitalGain.PurchasePrice += purchasePrice
fyCapitalGain.SellPrice += -p.Amount
fyCapitalGain.Units.Add(p.Quantity.Neg())
fyCapitalGain.PurchasePrice.Add(purchasePrice)
fyCapitalGain.SellPrice.Add(p.Amount.Neg())
fyCapitalGain.PostingPairs = append(fyCapitalGain.PostingPairs, postingPairs...)
capitalGain.FY[fy] = fyCapitalGain

View File

@ -7,18 +7,19 @@ import (
"github.com/ananthakumaran/paisa/internal/query"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type CashFlow struct {
Date time.Time `json:"date"`
Income float64 `json:"income"`
Expenses float64 `json:"expenses"`
Liabilities float64 `json:"liabilities"`
Investment float64 `json:"investment"`
Tax float64 `json:"tax"`
Checking float64 `json:"checking"`
Balance float64 `json:"balance"`
Date time.Time `json:"date"`
Income decimal.Decimal `json:"income"`
Expenses decimal.Decimal `json:"expenses"`
Liabilities decimal.Decimal `json:"liabilities"`
Investment decimal.Decimal `json:"investment"`
Tax decimal.Decimal `json:"tax"`
Checking decimal.Decimal `json:"checking"`
Balance decimal.Decimal `json:"balance"`
}
func (c CashFlow) GroupDate() time.Time {
@ -26,7 +27,7 @@ func (c CashFlow) GroupDate() time.Time {
}
func GetCashFlow(db *gorm.DB) gin.H {
return gin.H{"cash_flows": computeCashFlow(db, query.Init(db), 0)}
return gin.H{"cash_flows": computeCashFlow(db, query.Init(db), decimal.Zero)}
}
func GetCurrentCashFlow(db *gorm.DB) []CashFlow {
@ -34,7 +35,7 @@ func GetCurrentCashFlow(db *gorm.DB) []CashFlow {
return computeCashFlow(db, query.Init(db).LastNMonths(3), balance)
}
func computeCashFlow(db *gorm.DB, q *query.Query, balance float64) []CashFlow {
func computeCashFlow(db *gorm.DB, q *query.Query, balance decimal.Decimal) []CashFlow {
var cashFlows []CashFlow
expenses := utils.GroupByMonth(q.Clone().Like("Expenses:%").NotLike("Expenses:Tax").All())
@ -61,12 +62,12 @@ func computeCashFlow(db *gorm.DB, q *query.Query, balance float64) []CashFlow {
ps, ok = incomes[key]
if ok {
cashFlow.Income = -accounting.CostSum(ps)
cashFlow.Income = accounting.CostSum(ps).Neg()
}
ps, ok = liabilities[key]
if ok {
cashFlow.Liabilities = -accounting.CostSum(ps)
cashFlow.Liabilities = accounting.CostSum(ps).Neg()
}
ps, ok = investments[key]
@ -84,7 +85,7 @@ func computeCashFlow(db *gorm.DB, q *query.Query, balance float64) []CashFlow {
cashFlow.Checking = accounting.CostSum(ps)
}
balance += cashFlow.Checking
balance = balance.Add(cashFlow.Checking)
cashFlow.Balance = balance
cashFlows = append(cashFlows, cashFlow)

View File

@ -3,7 +3,6 @@ package server
import (
"errors"
"fmt"
"math"
"github.com/ananthakumaran/paisa/internal/accounting"
"github.com/ananthakumaran/paisa/internal/config"
@ -13,6 +12,7 @@ import (
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -84,7 +84,7 @@ func ruleAssetRegisterNonNegative(db *gorm.DB) []error {
assets := query.Init(db).Like("Assets:%").All()
for account, ps := range lo.GroupBy(assets, func(posting posting.Posting) string { return posting.Account }) {
for _, balance := range accounting.Register(ps) {
if balance.Quantity < -0.01 {
if balance.Quantity.LessThan(decimal.NewFromFloat(0.01).Neg()) {
errs = append(errs, errors.New(fmt.Sprintf("<b>%s</b> account went negative on %s", account, balance.Date.Format(DATE_FORMAT))))
break
}
@ -97,8 +97,8 @@ func ruleNonCreditAccount(db *gorm.DB) []error {
errs := make([]error, 0)
incomes := query.Init(db).Like("Income:%").All()
for _, p := range incomes {
if p.Amount > 0.01 {
errs = append(errs, errors.New(fmt.Sprintf("<b>%.4f</b> got credited to <b>%s</b> on %s", p.Amount, p.Account, p.Date.Format(DATE_FORMAT))))
if p.Amount.GreaterThan(decimal.NewFromFloat(0.01)) {
errs = append(errs, errors.New(fmt.Sprintf("<b>%.4f</b> got credited to <b>%s</b> on %s", p.Amount.InexactFloat64(), p.Account, p.Date.Format(DATE_FORMAT))))
}
}
return errs
@ -108,8 +108,8 @@ func ruleNonDebitAccount(db *gorm.DB) []error {
errs := make([]error, 0)
incomes := query.Init(db).Like("Expenses:%").All()
for _, p := range incomes {
if p.Amount < -0.01 {
errs = append(errs, errors.New(fmt.Sprintf("<b>%.4f</b> got debited from <b>%s</b> on %s", p.Amount, p.Account, p.Date.Format(DATE_FORMAT))))
if p.Amount.LessThan(decimal.NewFromFloat(0.01).Neg()) {
errs = append(errs, errors.New(fmt.Sprintf("<b>%.4f</b> got debited from <b>%s</b> on %s", p.Amount.InexactFloat64(), p.Account, p.Date.Format(DATE_FORMAT))))
}
}
return errs
@ -121,9 +121,9 @@ func ruleJournalPriceMismatch(db *gorm.DB) []error {
for _, p := range postings {
if !utils.IsCurrency(p.Commodity) {
externalPrice := service.GetUnitPrice(db, p.Commodity, p.Date)
diff := math.Abs(externalPrice.Value - p.Price())
if externalPrice.CommodityType != config.Unknown && diff >= 0.0001 {
errs = append(errs, errors.New(fmt.Sprintf("%s\t%s\t%.4f @ <b>%.4f</b> %s <br />doesn't match the price %s <b>%.4f</b> fetched from external system", p.Date.Format(DATE_FORMAT), p.Account, p.Quantity, p.Price(), config.DefaultCurrency(), externalPrice.Date.Format(DATE_FORMAT), externalPrice.Value)))
diff := externalPrice.Value.Sub(p.Price()).Abs()
if externalPrice.CommodityType != config.Unknown && diff.GreaterThanOrEqual(decimal.NewFromFloat(0.0001)) {
errs = append(errs, errors.New(fmt.Sprintf("%s\t%s\t%.4f @ <b>%.4f</b> %s <br />doesn't match the price %s <b>%.4f</b> fetched from external system", p.Date.Format(DATE_FORMAT), p.Account, p.Quantity.InexactFloat64(), p.Price().InexactFloat64(), config.DefaultCurrency(), externalPrice.Date.Format(DATE_FORMAT), externalPrice.Value.InexactFloat64())))
}
}
}

View File

@ -1,14 +1,13 @@
package server
import (
"math"
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/model/transaction"
"github.com/ananthakumaran/paisa/internal/query"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -18,9 +17,9 @@ type Node struct {
}
type Link struct {
Source uint `json:"source"`
Target uint `json:"target"`
Value float64 `json:"value"`
Source uint `json:"source"`
Target uint `json:"target"`
Value decimal.Decimal `json:"value"`
}
type Pair struct {
@ -67,7 +66,7 @@ func GetExpense(db *gorm.DB) gin.H {
func computeGraph(postings []posting.Posting) Graph {
nodes := make(map[string]Node)
links := make(map[Pair]float64)
links := make(map[Pair]decimal.Decimal)
var nodeID uint = 0
@ -83,19 +82,19 @@ func computeGraph(postings []posting.Posting) Graph {
}
for _, t := range transactions {
from := lo.Filter(t.Postings, func(p posting.Posting, _ int) bool { return p.Amount < 0 })
to := lo.Filter(t.Postings, func(p posting.Posting, _ int) bool { return p.Amount > 0 })
from := lo.Filter(t.Postings, func(p posting.Posting, _ int) bool { return p.Amount.LessThan(decimal.Zero) })
to := lo.Filter(t.Postings, func(p posting.Posting, _ int) bool { return p.Amount.GreaterThan(decimal.Zero) })
for _, f := range from {
for math.Abs(f.Amount) > 0.1 && len(to) > 0 {
for f.Amount.Abs().GreaterThan(decimal.NewFromFloat(0.1)) && len(to) > 0 {
top := to[0]
if top.Amount > -f.Amount {
links[Pair{Source: nodes[f.Account].ID, Target: nodes[top.Account].ID}] += -f.Amount
top.Amount -= f.Amount
f.Amount = 0
if top.Amount.GreaterThan(f.Amount.Neg()) {
links[Pair{Source: nodes[f.Account].ID, Target: nodes[top.Account].ID}] = links[Pair{Source: nodes[f.Account].ID, Target: nodes[top.Account].ID}].Add(f.Amount.Neg())
top.Amount = top.Amount.Sub(f.Amount)
f.Amount = decimal.Zero
} else {
links[Pair{Source: nodes[f.Account].ID, Target: nodes[top.Account].ID}] += top.Amount
f.Amount += top.Amount
links[Pair{Source: nodes[f.Account].ID, Target: nodes[top.Account].ID}] = links[Pair{Source: nodes[f.Account].ID, Target: nodes[top.Account].ID}].Add(top.Amount)
f.Amount = f.Amount.Add(top.Amount)
to = to[1:]
}
}

View File

@ -6,20 +6,21 @@ import (
"github.com/ananthakumaran/paisa/internal/service"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type Gain struct {
Account string `json:"account"`
Networth Networth `json:"networth"`
XIRR float64 `json:"xirr"`
XIRR decimal.Decimal `json:"xirr"`
Postings []posting.Posting `json:"postings"`
}
type AccountGain struct {
Account string `json:"account"`
NetworthTimeline []Networth `json:"networthTimeline"`
XIRR float64 `json:"xirr"`
XIRR decimal.Decimal `json:"xirr"`
Postings []posting.Posting `json:"postings"`
}

View File

@ -12,27 +12,28 @@ import (
"github.com/ananthakumaran/paisa/internal/taxation"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type HarvestBreakdown struct {
Units float64 `json:"units"`
PurchaseDate time.Time `json:"purchase_date"`
PurchasePrice float64 `json:"purchase_price"`
CurrentPrice float64 `json:"current_price"`
PurchaseUnitPrice float64 `json:"purchase_unit_price"`
Tax taxation.Tax `json:"tax"`
Units decimal.Decimal `json:"units"`
PurchaseDate time.Time `json:"purchase_date"`
PurchasePrice decimal.Decimal `json:"purchase_price"`
CurrentPrice decimal.Decimal `json:"current_price"`
PurchaseUnitPrice decimal.Decimal `json:"purchase_unit_price"`
Tax taxation.Tax `json:"tax"`
}
type Harvestable struct {
Account string `json:"account"`
TaxCategory string `json:"tax_category"`
TotalUnits float64 `json:"total_units"`
HarvestableUnits float64 `json:"harvestable_units"`
UnrealizedGain float64 `json:"unrealized_gain"`
TaxableUnrealizedGain float64 `json:"taxable_unrealized_gain"`
TotalUnits decimal.Decimal `json:"total_units"`
HarvestableUnits decimal.Decimal `json:"harvestable_units"`
UnrealizedGain decimal.Decimal `json:"unrealized_gain"`
TaxableUnrealizedGain decimal.Decimal `json:"taxable_unrealized_gain"`
HarvestBreakdown []HarvestBreakdown `json:"harvest_breakdown"`
CurrentUnitPrice float64 `json:"current_unit_price"`
CurrentUnitPrice decimal.Decimal `json:"current_unit_price"`
CurrentUnitDate time.Time `json:"current_unit_date"`
}
@ -57,17 +58,17 @@ func computeHarvestable(db *gorm.DB, account string, commodity config.Commodity,
harvestable := Harvestable{Account: account, TaxCategory: string(commodity.TaxCategory), HarvestBreakdown: []HarvestBreakdown{}, CurrentUnitPrice: currentPrice.Value, CurrentUnitDate: currentPrice.Date}
cutoff := time.Now().AddDate(0, 0, -commodity.Harvest)
for _, p := range available {
harvestable.TotalUnits += p.Quantity
harvestable.TotalUnits = harvestable.TotalUnits.Add(p.Quantity)
if p.Date.Before(cutoff) {
tax := taxation.Calculate(db, p.Quantity, commodity, p.Price(), p.Date, currentPrice.Value, currentPrice.Date)
harvestable.HarvestableUnits += p.Quantity
harvestable.UnrealizedGain += tax.Gain
harvestable.TaxableUnrealizedGain += tax.Taxable
harvestable.HarvestableUnits = harvestable.HarvestableUnits.Add(p.Quantity)
harvestable.UnrealizedGain = harvestable.UnrealizedGain.Add(tax.Gain)
harvestable.TaxableUnrealizedGain = harvestable.TaxableUnrealizedGain.Add(tax.Taxable)
harvestable.HarvestBreakdown = append(harvestable.HarvestBreakdown, HarvestBreakdown{
Units: p.Quantity,
PurchaseDate: p.Date,
PurchasePrice: p.Amount,
CurrentPrice: currentPrice.Value * p.Quantity,
CurrentPrice: currentPrice.Value.Mul(p.Quantity),
PurchaseUnitPrice: p.Price(),
Tax: tax,
})

View File

@ -7,7 +7,7 @@ import (
"github.com/ananthakumaran/paisa/internal/query"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -15,9 +15,9 @@ type IncomeYearlyCard struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Postings []posting.Posting `json:"postings"`
GrossIncome float64 `json:"gross_income"`
NetTax float64 `json:"net_tax"`
NetIncome float64 `json:"net_income"`
GrossIncome decimal.Decimal `json:"gross_income"`
NetTax decimal.Decimal `json:"net_tax"`
NetIncome decimal.Decimal `json:"net_income"`
}
type Income struct {
@ -91,10 +91,10 @@ func computeIncomeYearlyCard(start time.Time, taxes []posting.Posting, incomes [
end := time.Now()
for start = utils.BeginningOfFinancialYear(start); start.Before(end); start = start.AddDate(1, 0, 0) {
yearEnd := utils.EndOfFinancialYear(start)
var netTax float64 = 0
var netTax decimal.Decimal = decimal.Zero
for len(taxes) > 0 && utils.IsWithDate(taxes[0].Date, start, yearEnd) {
p, taxes = taxes[0], taxes[1:]
netTax += p.Amount
netTax = netTax.Add(p.Amount)
}
var currentYearIncomes []posting.Posting = make([]posting.Posting, 0)
@ -103,8 +103,8 @@ func computeIncomeYearlyCard(start time.Time, taxes []posting.Posting, incomes [
currentYearIncomes = append(currentYearIncomes, p)
}
grossIncome := lo.SumBy(currentYearIncomes, func(p posting.Posting) float64 {
return -p.Amount
grossIncome := utils.SumBy(currentYearIncomes, func(p posting.Posting) decimal.Decimal {
return p.Amount.Neg()
})
yearlyCards = append(yearlyCards, IncomeYearlyCard{
@ -113,7 +113,7 @@ func computeIncomeYearlyCard(start time.Time, taxes []posting.Posting, incomes [
Postings: currentYearIncomes,
NetTax: netTax,
GrossIncome: grossIncome,
NetIncome: grossIncome - netTax,
NetIncome: grossIncome.Sub(netTax),
})
}

View File

@ -4,11 +4,12 @@ import (
"strings"
"time"
"github.com/ananthakumaran/paisa/internal/accounting"
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/query"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -16,13 +17,13 @@ type InvestmentYearlyCard struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Postings []posting.Posting `json:"postings"`
GrossSalaryIncome float64 `json:"gross_salary_income"`
GrossOtherIncome float64 `json:"gross_other_income"`
NetTax float64 `json:"net_tax"`
NetIncome float64 `json:"net_income"`
NetInvestment float64 `json:"net_investment"`
NetExpense float64 `json:"net_expense"`
SavingsRate float64 `json:"savings_rate"`
GrossSalaryIncome decimal.Decimal `json:"gross_salary_income"`
GrossOtherIncome decimal.Decimal `json:"gross_other_income"`
NetTax decimal.Decimal `json:"net_tax"`
NetIncome decimal.Decimal `json:"net_income"`
NetInvestment decimal.Decimal `json:"net_investment"`
NetExpense decimal.Decimal `json:"net_expense"`
SavingsRate decimal.Decimal `json:"savings_rate"`
}
func GetInvestment(db *gorm.DB) gin.H {
@ -65,8 +66,8 @@ func computeInvestmentYearlyCard(start time.Time, assets []posting.Posting, expe
}
}
netTax := lo.SumBy(currentYearTaxes, func(p posting.Posting) float64 { return p.Amount })
netExpense := lo.SumBy(currentYearExpenses, func(p posting.Posting) float64 { return p.Amount })
netTax := accounting.CostSum(currentYearTaxes)
netExpense := accounting.CostSum(currentYearExpenses)
var currentYearIncomes []posting.Posting = make([]posting.Posting, 0)
for len(incomes) > 0 && utils.IsWithDate(incomes[0].Date, start, yearEnd) {
@ -74,27 +75,27 @@ func computeInvestmentYearlyCard(start time.Time, assets []posting.Posting, expe
currentYearIncomes = append(currentYearIncomes, p)
}
grossSalaryIncome := lo.SumBy(currentYearIncomes, func(p posting.Posting) float64 {
grossSalaryIncome := utils.SumBy(currentYearIncomes, func(p posting.Posting) decimal.Decimal {
if strings.HasPrefix(p.Account, "Income:Salary") {
return -p.Amount
return p.Amount.Neg()
} else {
return 0
return decimal.Zero
}
})
grossOtherIncome := lo.SumBy(currentYearIncomes, func(p posting.Posting) float64 {
grossOtherIncome := utils.SumBy(currentYearIncomes, func(p posting.Posting) decimal.Decimal {
if !strings.HasPrefix(p.Account, "Income:Salary") {
return -p.Amount
return p.Amount.Neg()
} else {
return 0
return decimal.Zero
}
})
netInvestment := lo.SumBy(currentYearPostings, func(p posting.Posting) float64 { return p.Amount })
netInvestment := accounting.CostSum(currentYearPostings)
netIncome := grossSalaryIncome + grossOtherIncome - netTax
var savingsRate float64 = 0
if netIncome != 0 {
savingsRate = (netInvestment / netIncome) * 100
netIncome := grossSalaryIncome.Add(grossOtherIncome).Sub(netTax)
var savingsRate decimal.Decimal = decimal.Zero
if !netIncome.Equal(decimal.Zero) {
savingsRate = (netInvestment.Div(netIncome)).Mul(decimal.NewFromInt(100))
}
yearlyCards = append(yearlyCards, InvestmentYearlyCard{

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/query"
@ -15,12 +16,12 @@ import (
)
type AssetBreakdown struct {
Group string `json:"group"`
DrawnAmount float64 `json:"drawn_amount"`
RepaidAmount float64 `json:"repaid_amount"`
InterestAmount float64 `json:"interest_amount"`
BalanceAmount float64 `json:"balance_amount"`
APR float64 `json:"apr"`
Group string `json:"group"`
DrawnAmount decimal.Decimal `json:"drawn_amount"`
RepaidAmount decimal.Decimal `json:"repaid_amount"`
InterestAmount decimal.Decimal `json:"interest_amount"`
BalanceAmount decimal.Decimal `json:"balance_amount"`
APR decimal.Decimal `json:"apr"`
}
func GetBalance(db *gorm.DB) gin.H {
@ -51,31 +52,31 @@ func computeBreakdown(db *gorm.DB, postings, expenses []posting.Posting) map[str
sort.Slice(ps, func(i, j int) bool { return ps[i].Date.Before(ps[j].Date) })
ps = append(ps, es...)
drawn := lo.Reduce(ps, func(agg float64, p posting.Posting, _ int) float64 {
if p.Amount > 0 || service.IsInterest(db, p) {
drawn := lo.Reduce(ps, func(agg decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if p.Amount.GreaterThan(decimal.Zero) || service.IsInterest(db, p) {
return agg
} else {
return -p.Amount + agg
return p.Amount.Neg().Add(agg)
}
}, 0)
}, decimal.Zero)
repaid := lo.Reduce(ps, func(agg float64, p posting.Posting, _ int) float64 {
if p.Amount < 0 {
repaid := lo.Reduce(ps, func(agg decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if p.Amount.LessThan(decimal.Zero) {
return agg
} else {
return p.Amount + agg
return p.Amount.Add(agg)
}
}, 0)
}, decimal.Zero)
balance := lo.Reduce(ps, func(agg float64, p posting.Posting, _ int) float64 {
balance := lo.Reduce(ps, func(agg decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if service.IsInterest(db, p) {
return agg
} else {
return -p.MarketAmount + agg
return p.MarketAmount.Neg().Add(agg)
}
}, 0)
}, decimal.Zero)
interest := balance + repaid - drawn
interest := balance.Add(repaid).Sub(drawn)
apr := service.APR(db, ps)
breakdown := AssetBreakdown{DrawnAmount: drawn, RepaidAmount: repaid, BalanceAmount: balance, APR: apr, Group: group, InterestAmount: interest}

View File

@ -1,7 +1,6 @@
package liabilities
import (
"math"
"sort"
"time"
@ -11,20 +10,21 @@ import (
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type Overview struct {
Date time.Time `json:"date"`
DrawnAmount float64 `json:"drawn_amount"`
RepaidAmount float64 `json:"repaid_amount"`
InterestAmount float64 `json:"interest_amount"`
Date time.Time `json:"date"`
DrawnAmount decimal.Decimal `json:"drawn_amount"`
RepaidAmount decimal.Decimal `json:"repaid_amount"`
InterestAmount decimal.Decimal `json:"interest_amount"`
}
type Interest struct {
Account string `json:"account"`
OverviewTimeline []Overview `json:"overview_timeline"`
APR float64 `json:"apr"`
Account string `json:"account"`
OverviewTimeline []Overview `json:"overview_timeline"`
APR decimal.Decimal `json:"apr"`
}
func GetInterest(db *gorm.DB) gin.H {
@ -60,34 +60,34 @@ func computeOverviewTimeline(db *gorm.DB, postings []posting.Posting) []Overview
pastPostings = append(pastPostings, p)
}
drawn := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
if p.Amount > 0 || service.IsInterest(db, p) {
drawn := lo.Reduce(pastPostings, func(agg decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if p.Amount.GreaterThan(decimal.Zero) || service.IsInterest(db, p) {
return agg
} else {
return -p.Amount + agg
return p.Amount.Neg().Add(agg)
}
}, 0)
}, decimal.Zero)
repaid := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
if p.Amount < 0 {
repaid := lo.Reduce(pastPostings, func(agg decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if p.Amount.LessThan(decimal.Zero) {
return agg
} else {
return p.Amount + agg
return p.Amount.Add(agg)
}
}, 0)
}, decimal.Zero)
balance := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
balance := lo.Reduce(pastPostings, func(agg decimal.Decimal, p posting.Posting, _ int) decimal.Decimal {
if service.IsInterest(db, p) {
return agg
} else {
return -service.GetMarketPrice(db, p, start) + agg
return service.GetMarketPrice(db, p, start).Neg().Add(agg)
}
}, 0)
}, decimal.Zero)
interest := balance + repaid - drawn
interest := balance.Add(repaid).Sub(drawn)
netliabilities = append(netliabilities, Overview{Date: start, DrawnAmount: drawn, RepaidAmount: repaid, InterestAmount: interest})
if len(postings) == 0 && math.Abs(balance) < 0.01 {
if len(postings) == 0 && balance.Abs().LessThan(decimal.NewFromFloat(0.01)) {
break
}
}

View File

@ -1,7 +1,6 @@
package server
import (
"math"
"time"
"github.com/ananthakumaran/paisa/internal/model/posting"
@ -9,16 +8,17 @@ import (
"github.com/ananthakumaran/paisa/internal/service"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
type Networth struct {
Date time.Time `json:"date"`
InvestmentAmount float64 `json:"investmentAmount"`
WithdrawalAmount float64 `json:"withdrawalAmount"`
GainAmount float64 `json:"gainAmount"`
BalanceAmount float64 `json:"balanceAmount"`
NetInvestmentAmount float64 `json:"netInvestmentAmount"`
Date time.Time `json:"date"`
InvestmentAmount decimal.Decimal `json:"investmentAmount"`
WithdrawalAmount decimal.Decimal `json:"withdrawalAmount"`
GainAmount decimal.Decimal `json:"gainAmount"`
BalanceAmount decimal.Decimal `json:"balanceAmount"`
NetInvestmentAmount decimal.Decimal `json:"netInvestmentAmount"`
}
func GetNetworth(db *gorm.DB) gin.H {
@ -46,31 +46,31 @@ func computeNetworth(db *gorm.DB, postings []posting.Posting) Networth {
return networth
}
var investment float64 = 0
var withdrawal float64 = 0
var balance float64 = 0
var investment decimal.Decimal = decimal.Zero
var withdrawal decimal.Decimal = decimal.Zero
var balance decimal.Decimal = decimal.Zero
now := utils.BeginingOfDay(time.Now())
for _, p := range postings {
isInterest := service.IsInterest(db, p)
if p.Amount > 0 && !isInterest {
investment += p.Amount
if p.Amount.GreaterThan(decimal.Zero) && !isInterest {
investment = investment.Add(p.Amount)
}
if p.Amount < 0 && !isInterest {
withdrawal += -p.Amount
if p.Amount.LessThan(decimal.Zero) && !isInterest {
withdrawal = withdrawal.Add(p.Amount.Neg())
}
if isInterest {
balance += p.Amount
balance = balance.Add(p.Amount)
} else {
balance += service.GetMarketPrice(db, p, now)
balance = balance.Add(service.GetMarketPrice(db, p, now))
}
}
gain := balance + withdrawal - investment
net_investment := investment - withdrawal
gain := balance.Add(withdrawal).Sub(investment)
net_investment := investment.Sub(withdrawal)
networth = Networth{Date: now, InvestmentAmount: investment, WithdrawalAmount: withdrawal, GainAmount: gain, BalanceAmount: balance, NetInvestmentAmount: net_investment}
return networth
@ -93,33 +93,33 @@ func computeNetworthTimeline(db *gorm.DB, postings []posting.Posting) []Networth
pastPostings = append(pastPostings, p)
}
var investment float64 = 0
var withdrawal float64 = 0
var balance float64 = 0
var investment decimal.Decimal = decimal.Zero
var withdrawal decimal.Decimal = decimal.Zero
var balance decimal.Decimal = decimal.Zero
for _, p := range pastPostings {
isInterest := service.IsInterest(db, p)
if p.Amount > 0 && !isInterest {
investment += p.Amount
if p.Amount.GreaterThan(decimal.Zero) && !isInterest {
investment = investment.Add(p.Amount)
}
if p.Amount < 0 && !isInterest {
withdrawal += -p.Amount
if p.Amount.LessThan(decimal.Zero) && !isInterest {
withdrawal = withdrawal.Add(p.Amount.Neg())
}
if isInterest {
balance += p.Amount
balance = balance.Add(p.Amount)
} else {
balance += service.GetMarketPrice(db, p, start)
balance = balance.Add(service.GetMarketPrice(db, p, start))
}
}
gain := balance + withdrawal - investment
net_investment := investment - withdrawal
gain := balance.Add(withdrawal).Sub(investment)
net_investment := investment.Sub(withdrawal)
networths = append(networths, Networth{Date: start, InvestmentAmount: investment, WithdrawalAmount: withdrawal, GainAmount: gain, BalanceAmount: balance, NetInvestmentAmount: net_investment})
if len(postings) == 0 && math.Abs(balance) < 0.01 {
if len(postings) == 0 && balance.Abs().LessThan(decimal.NewFromFloat(0.01)) {
break
}
}

View File

@ -4,6 +4,7 @@ import (
"strings"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"sort"
@ -14,6 +15,7 @@ import (
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/query"
"github.com/ananthakumaran/paisa/internal/service"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@ -27,23 +29,23 @@ type PortfolioDimension struct {
}
type CommodityBreakdown struct {
ParentCommodityID string `json:"parent_commodity_id"`
CommodityName string `json:"commodity_name"`
SecurityName string `json:"security_name"`
SecurityRating string `json:"security_rating"`
SecurityIndustry string `json:"security_industry"`
Percentage float64 `json:"percentage"`
SecurityID string `json:"security_id"`
SecurityType string `json:"security_type"`
Amount float64 `json:"amount"`
ParentCommodityID string `json:"parent_commodity_id"`
CommodityName string `json:"commodity_name"`
SecurityName string `json:"security_name"`
SecurityRating string `json:"security_rating"`
SecurityIndustry string `json:"security_industry"`
Percentage decimal.Decimal `json:"percentage"`
SecurityID string `json:"security_id"`
SecurityType string `json:"security_type"`
Amount decimal.Decimal `json:"amount"`
}
type PortfolioAggregate struct {
Group string `json:"group"`
SubGroup string `json:"sub_group"`
ID string `json:"id"`
Percentage float64 `json:"percentage"`
Amount float64 `json:"amount"`
Percentage decimal.Decimal `json:"percentage"`
Amount decimal.Decimal `json:"amount"`
Breakdowns []CommodityBreakdown `json:"breakdowns"`
}
@ -78,7 +80,7 @@ func GetAccountPortfolioAllocation(db *gorm.DB, account string) PortfolioAllocat
cbs := lo.FlatMap(lo.Keys(byCommodity), func(commodity string, _ int) []CommodityBreakdown {
ps := byCommodity[commodity]
balance := accounting.CurrentBalance(ps)
if balance <= 0.0001 {
if balance.LessThanOrEqual(decimal.NewFromFloat(0.0001)) {
supportedCommodities = lo.Without(supportedCommodities, commodity)
return []CommodityBreakdown{}
}
@ -138,11 +140,11 @@ func GetAccountPortfolioAllocation(db *gorm.DB, account string) PortfolioAllocat
}
}
func computePortfolioAggregate(db *gorm.DB, commodityName string, total float64) []CommodityBreakdown {
func computePortfolioAggregate(db *gorm.DB, commodityName string, total decimal.Decimal) []CommodityBreakdown {
commodity := commodity.FindByName(commodityName)
portfolios := portfolio.GetPortfolios(db, commodity.Code)
return lo.Map(portfolios, func(p portfolio.Portfolio, _ int) CommodityBreakdown {
amount := (total * p.Percentage) / 100
amount := (total.Mul(p.Percentage)).Div(decimal.NewFromInt(100))
return CommodityBreakdown{
SecurityName: p.SecurityName,
CommodityName: commodity.Name,
@ -166,7 +168,7 @@ func mergeBreakdowns(cbs []CommodityBreakdown) []CommodityBreakdown {
SecurityName: bs[0].SecurityName,
CommodityName: bs[0].CommodityName,
ParentCommodityID: bs[0].ParentCommodityID,
Amount: lo.SumBy(bs, func(b CommodityBreakdown) float64 { return b.Amount }),
Amount: utils.SumBy(bs, func(b CommodityBreakdown) decimal.Decimal { return b.Amount }),
SecurityID: strings.Join(lo.Map(bs, func(b CommodityBreakdown, _ int) string { return b.SecurityID }), ","),
SecurityRating: bs[0].SecurityRating,
SecurityIndustry: bs[0].SecurityIndustry,
@ -176,22 +178,22 @@ func mergeBreakdowns(cbs []CommodityBreakdown) []CommodityBreakdown {
func rollupPortfolioAggregate(dimension PortfolioDimension, cbs []CommodityBreakdown) []PortfolioAggregate {
cbs = lo.Filter(cbs, dimension.FilterFn)
total := lo.SumBy(cbs, func(b CommodityBreakdown) float64 { return b.Amount })
total := utils.SumBy(cbs, func(b CommodityBreakdown) decimal.Decimal { return b.Amount })
grouped := lo.GroupBy(cbs, func(c CommodityBreakdown) string {
return strings.Join([]string{dimension.GroupFn(c), dimension.SubGroupFn(c)}, ":")
})
pas := lo.Map(lo.Keys(grouped), func(key string, _ int) PortfolioAggregate {
breakdowns := mergeBreakdowns(grouped[key])
portfolioTotal := lo.SumBy(breakdowns, func(b CommodityBreakdown) float64 { return b.Amount })
portfolioTotal := utils.SumBy(breakdowns, func(b CommodityBreakdown) decimal.Decimal { return b.Amount })
breakdowns = lo.Map(breakdowns, func(breakdown CommodityBreakdown, _ int) CommodityBreakdown {
breakdown.Percentage = (breakdown.Amount / portfolioTotal) * 100
breakdown.Percentage = (breakdown.Amount.Div(portfolioTotal)).Mul(decimal.NewFromInt(100))
return breakdown
})
totalPercentage := (portfolioTotal / total) * 100
totalPercentage := (portfolioTotal.Div(total)).Mul(decimal.NewFromInt(100))
return PortfolioAggregate{Group: dimension.GroupFn(breakdowns[0]), SubGroup: dimension.SubGroupFn(breakdowns[0]), ID: key, Amount: portfolioTotal, Percentage: totalPercentage, Breakdowns: breakdowns}
})
sort.Slice(pas, func(i, j int) bool { return pas[i].Percentage > pas[j].Percentage })
sort.Slice(pas, func(i, j int) bool { return pas[i].Percentage.GreaterThan(pas[j].Percentage) })
return pas
}

View File

@ -10,7 +10,7 @@ import (
"github.com/ananthakumaran/paisa/internal/service"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -21,18 +21,18 @@ func GetRetirementProgress(db *gorm.DB) gin.H {
savings = service.PopulateMarketPrice(db, savings)
savingsTotal := accounting.CurrentBalance(savings)
yearlyExpenses := retirementConfig.YearlyExpenses
if !(yearlyExpenses > 0) {
yearlyExpenses := decimal.NewFromFloat(retirementConfig.YearlyExpenses)
if !(yearlyExpenses.GreaterThan(decimal.Zero)) {
yearlyExpenses = calculateAverageExpense(db, retirementConfig)
}
return gin.H{"savings_timeline": accounting.RunningBalance(db, savings), "savings_total": savingsTotal, "swr": retirementConfig.SWR, "yearly_expense": yearlyExpenses, "xirr": service.XIRR(db, savings)}
}
func calculateAverageExpense(db *gorm.DB, retirementConfig config.Retirement) float64 {
func calculateAverageExpense(db *gorm.DB, retirementConfig config.Retirement) decimal.Decimal {
now := time.Now()
end := utils.BeginningOfMonth(now)
start := end.AddDate(-2, 0, 0)
expenses := accounting.FilterByGlob(query.Init(db).Like("Expenses:%").Where("date between ? AND ?", start, end).All(), retirementConfig.Expenses)
return lo.SumBy(expenses, func(p posting.Posting) float64 { return p.Amount }) / 2
return utils.SumBy(expenses, func(p posting.Posting) decimal.Decimal { return p.Amount }).Div(decimal.NewFromInt(2))
}

View File

@ -10,6 +10,7 @@ import (
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -38,7 +39,7 @@ func init() {
type ScheduleALEntry struct {
Section ScheduleALSection `json:"section"`
Amount float64 `json:"amount"`
Amount decimal.Decimal `json:"amount"`
}
type ScheduleAL struct {
@ -74,7 +75,7 @@ func computeScheduleAL(postings []posting.Posting) []ScheduleALEntry {
return scheduleALConfig.Code == section.Code
})
var amount float64
var amount decimal.Decimal
if found {
ps := accounting.FilterByGlob(postings, config.Accounts)
@ -85,7 +86,7 @@ func computeScheduleAL(postings []posting.Posting) []ScheduleALEntry {
}
amount = accounting.CostBalance(ps)
} else {
amount = 0
amount = decimal.Zero
}
return ScheduleALEntry{

View File

@ -41,7 +41,7 @@ func IsInterest(db *gorm.DB, p posting.Posting) bool {
for _, ip := range icache.postings[p.Date.Unix()] {
if ip.Date.Equal(p.Date) &&
-ip.Amount == p.Amount &&
ip.Amount.Neg().Equal(p.Amount) &&
ip.Payee == p.Payee {
return true
}

View File

@ -10,6 +10,7 @@ import (
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/google/btree"
"github.com/samber/lo"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@ -83,7 +84,7 @@ func GetAllPrices(db *gorm.DB, commodity string) []price.Price {
return utils.BTreeToSlice[price.Price](pt)
}
func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) float64 {
func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) decimal.Decimal {
pcache.Do(func() { loadPriceCache(db) })
if utils.IsCurrency(p.Commodity) {
@ -93,8 +94,8 @@ func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) float64 {
pt := pcache.pricesTree[p.Commodity]
if pt != nil {
pc := utils.BTreeDescendFirstLessOrEqual(pt, price.Price{Date: date})
if pc.Value != 0 {
return p.Quantity * pc.Value
if !pc.Value.Equal(decimal.Zero) {
return p.Quantity.Mul(pc.Value)
}
} else {
log.Info("Price not found ", p)

View File

@ -5,43 +5,49 @@ import (
"github.com/ChizhovVadim/xirr"
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/samber/lo"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)
func XIRR(db *gorm.DB, ps []posting.Posting) float64 {
func XIRR(db *gorm.DB, ps []posting.Posting) decimal.Decimal {
today := time.Now()
marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
marketAmount := utils.SumBy(ps, func(p posting.Posting) decimal.Decimal {
return p.MarketAmount
})
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}
return xirr.Payment{Date: p.Date, Amount: p.Amount.Neg().Round(4).InexactFloat64()}
}
}))
payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount})
payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount.Round(4).InexactFloat64()})
returns, err := xirr.XIRR(payments)
if err != nil {
log.Warn(err)
return 0
return decimal.Zero
}
return (returns - 1) * 100
return decimal.NewFromFloat(returns).Sub(decimal.NewFromInt(1)).Mul(decimal.NewFromInt(100))
}
func APR(db *gorm.DB, ps []posting.Posting) float64 {
func APR(db *gorm.DB, ps []posting.Posting) decimal.Decimal {
today := time.Now()
marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
payments := lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment {
return xirr.Payment{Date: p.Date, Amount: p.Amount}
marketAmount := utils.SumBy(ps, func(p posting.Posting) decimal.Decimal {
return p.MarketAmount
})
payments = append(payments, xirr.Payment{Date: today, Amount: -marketAmount})
payments := lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment {
return xirr.Payment{Date: p.Date, Amount: p.Amount.Round(4).InexactFloat64()}
})
payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount.Neg().Round(4).InexactFloat64()})
returns, err := xirr.XIRR(payments)
if err != nil {
log.Warn(err)
return 0
return decimal.Zero
}
return (returns - 1) * -100
return decimal.NewFromFloat(returns).Sub(decimal.NewFromInt(1)).Mul(decimal.NewFromInt(-100))
}

View File

@ -7,6 +7,7 @@ import (
"github.com/ananthakumaran/paisa/internal/model/cii"
"github.com/ananthakumaran/paisa/internal/service"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -22,24 +23,24 @@ func init() {
}
type Tax struct {
Gain float64 `json:"gain"`
Taxable float64 `json:"taxable"`
Slab float64 `json:"slab"`
LongTerm float64 `json:"long_term"`
ShortTerm float64 `json:"short_term"`
Gain decimal.Decimal `json:"gain"`
Taxable decimal.Decimal `json:"taxable"`
Slab decimal.Decimal `json:"slab"`
LongTerm decimal.Decimal `json:"long_term"`
ShortTerm decimal.Decimal `json:"short_term"`
}
func Add(a, b Tax) Tax {
return Tax{Gain: a.Gain + b.Gain, Taxable: a.Taxable + b.Taxable, LongTerm: a.LongTerm + b.LongTerm, ShortTerm: a.ShortTerm + b.ShortTerm, Slab: a.Slab + b.Slab}
return Tax{Gain: a.Gain.Add(b.Gain), Taxable: a.Taxable.Add(b.Taxable), LongTerm: a.LongTerm.Add(b.LongTerm), ShortTerm: a.ShortTerm.Add(b.ShortTerm), Slab: a.Slab.Add(b.Slab)}
}
func Calculate(db *gorm.DB, quantity float64, commodity config.Commodity, purchasePrice float64, purchaseDate time.Time, sellPrice float64, sellDate time.Time) Tax {
func Calculate(db *gorm.DB, quantity decimal.Decimal, commodity config.Commodity, purchasePrice decimal.Decimal, purchaseDate time.Time, sellPrice decimal.Decimal, sellDate time.Time) Tax {
dateDiff := sellDate.Sub(purchaseDate)
gain := sellPrice*quantity - purchasePrice*quantity
gain := sellPrice.Mul(quantity).Sub(purchasePrice.Mul(quantity))
if (commodity.TaxCategory == config.Equity || commodity.TaxCategory == config.Equity65) && sellDate.Before(EQUITY_GRANDFATHER_DATE) {
return Tax{Gain: gain, Taxable: 0, ShortTerm: 0, LongTerm: 0, Slab: 0}
return Tax{Gain: gain, Taxable: decimal.Zero, ShortTerm: decimal.Zero, LongTerm: decimal.Zero, Slab: decimal.Zero}
}
if (commodity.TaxCategory == config.Equity || commodity.TaxCategory == config.Equity65) && purchaseDate.Before(EQUITY_GRANDFATHER_DATE) {
@ -47,30 +48,30 @@ func Calculate(db *gorm.DB, quantity float64, commodity config.Commodity, purcha
}
if commodity.TaxCategory == config.Debt && purchaseDate.After(CII_START_DATE) && dateDiff > THREE_YEAR {
purchasePrice = (purchasePrice * float64(cii.GetIndex(db, utils.FY(sellDate)))) / float64(cii.GetIndex(db, utils.FY(purchaseDate)))
purchasePrice = purchasePrice.Mul(decimal.NewFromInt(int64(cii.GetIndex(db, utils.FY(sellDate)))).Div(decimal.NewFromInt(int64(cii.GetIndex(db, utils.FY(purchaseDate))))))
}
if commodity.TaxCategory == config.UnlistedEquity && purchaseDate.After(CII_START_DATE) && dateDiff > TWO_YEAR {
purchasePrice = (purchasePrice * float64(cii.GetIndex(db, utils.FY(sellDate)))) / float64(cii.GetIndex(db, utils.FY(purchaseDate)))
purchasePrice = purchasePrice.Mul(decimal.NewFromInt(int64(cii.GetIndex(db, utils.FY(sellDate)))).Div(decimal.NewFromInt(int64(cii.GetIndex(db, utils.FY(purchaseDate))))))
}
taxable := sellPrice*quantity - purchasePrice*quantity
shortTerm := 0.0
longTerm := 0.0
slab := 0.0
taxable := sellPrice.Mul(quantity).Sub(purchasePrice.Mul(quantity))
shortTerm := decimal.Zero
longTerm := decimal.Zero
slab := decimal.Zero
if commodity.TaxCategory == config.Equity || commodity.TaxCategory == config.Equity65 {
if dateDiff > ONE_YEAR {
longTerm = taxable * 0.10
longTerm = taxable.Mul(decimal.NewFromFloat(0.10))
} else {
shortTerm = taxable * 0.15
shortTerm = taxable.Mul(decimal.NewFromFloat(0.15))
}
}
if commodity.TaxCategory == config.Debt {
if dateDiff > THREE_YEAR && purchaseDate.Before(DEBT_INDEXATION_REVOCATION_DATE) {
longTerm = taxable * 0.20
longTerm = taxable.Mul(decimal.NewFromFloat(0.20))
} else {
slab = taxable
}
@ -78,7 +79,7 @@ func Calculate(db *gorm.DB, quantity float64, commodity config.Commodity, purcha
if commodity.TaxCategory == config.Equity35 {
if dateDiff > THREE_YEAR {
longTerm = taxable * 0.20
longTerm = taxable.Mul(decimal.NewFromFloat(0.20))
} else {
slab = taxable
}
@ -86,7 +87,7 @@ func Calculate(db *gorm.DB, quantity float64, commodity config.Commodity, purcha
if commodity.TaxCategory == config.UnlistedEquity {
if dateDiff > TWO_YEAR {
longTerm = taxable * 0.20
longTerm = taxable.Mul(decimal.NewFromFloat(0.20))
} else {
slab = taxable
}

View File

@ -7,6 +7,8 @@ import (
"github.com/ananthakumaran/paisa/internal/config"
"github.com/google/btree"
"github.com/samber/lo"
"github.com/shopspring/decimal"
)
func BTreeDescendFirstLessOrEqual[I btree.Item](tree *btree.BTree, item I) I {
@ -143,3 +145,9 @@ func GroupByFY[G GroupableByDate](groupables []G) map[string][]G {
}
return grouped
}
func SumBy[C any](collection []C, iteratee func(item C) decimal.Decimal) decimal.Decimal {
return lo.Reduce(collection, func(acc decimal.Decimal, item C, _ int) decimal.Decimal {
return iteratee(item).Add(acc)
}, decimal.Zero)
}

View File

@ -2,8 +2,10 @@ package main
import (
"github.com/ananthakumaran/paisa/cmd"
"github.com/shopspring/decimal"
)
func main() {
decimal.MarshalJSONWithoutQuotes = true
cmd.Execute()
}

View File

@ -845,3 +845,7 @@ export function getColorPreference() {
export function setColorPreference(theme: string) {
localStorage.setItem(storageKey, theme);
}
export function isZero(n: number) {
return n < 0.0001 && n > -0.0001;
}

View File

@ -6,7 +6,8 @@
formatCurrency,
formatFloat,
lastName,
type AssetBreakdown
type AssetBreakdown,
isZero
} from "$lib/utils";
import _ from "lodash";
import { onMount } from "svelte";
@ -56,22 +57,22 @@
<a href="/assets/gain/{b.group}">{lastName(b.group)}</a></td
>
<td class="has-text-right"
>{b.investment_amount != 0 ? formatCurrency(b.investment_amount) : ""}</td
>{!isZero(b.investment_amount) ? formatCurrency(b.investment_amount) : ""}</td
>
<td class="has-text-right"
>{b.withdrawal_amount != 0 ? formatCurrency(b.withdrawal_amount) : ""}</td
>{!isZero(b.withdrawal_amount) ? formatCurrency(b.withdrawal_amount) : ""}</td
>
<td class="has-text-right"
>{b.balance_units > 0 ? formatFloat(b.balance_units, 4) : ""}</td
>
<td class="has-text-right"
>{b.market_amount != 0 ? formatCurrency(b.market_amount) : ""}</td
>{!isZero(b.market_amount) ? formatCurrency(b.market_amount) : ""}</td
>
<td class="{changeClass} has-text-right"
>{b.investment_amount != 0 && gain != 0 ? formatCurrency(gain) : ""}</td
>{!isZero(b.investment_amount) && !isZero(gain) ? formatCurrency(gain) : ""}</td
>
<td class="{changeClass} has-text-right"
>{b.xirr > 0.0001 || b.xirr < -0.0001 ? formatFloat(b.xirr) : ""}</td
>{!isZero(b.xirr) ? formatFloat(b.xirr) : ""}</td
>
</tr>
{/each}