float64 to decimal
This commit is contained in:
parent
2a84ff60c7
commit
941cb0d199
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
paisa.go
2
paisa.go
|
@ -2,8 +2,10 @@ package main
|
|||
|
||||
import (
|
||||
"github.com/ananthakumaran/paisa/cmd"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
decimal.MarshalJSONWithoutQuotes = true
|
||||
cmd.Execute()
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue