add the concept of interest transaction
If the posting amount is being debited from Income:Interest:*** account, then consider it as an interest transaction.
This commit is contained in:
parent
48bf4c6214
commit
a7b4a1ab0c
|
@ -1,16 +1,18 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||||
|
"github.com/ananthakumaran/paisa/internal/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetInvestment(db *gorm.DB) gin.H {
|
func GetInvestment(db *gorm.DB) gin.H {
|
||||||
var postings []posting.Posting
|
var postings []posting.Posting
|
||||||
result := db.Where("account like ?", "Asset:%").Find(&postings)
|
result := db.Where("account like ?", "Asset:%").Find(&postings)
|
||||||
|
postings = lo.Filter(postings, func(p posting.Posting, _ int) bool { return !service.IsInterest(db, p) })
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.Fatal(result.Error)
|
log.Fatal(result.Error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,11 @@ func GetLedger(db *gorm.DB) gin.H {
|
||||||
p.MarketAmount = service.GetMarketPrice(db, p, date)
|
p.MarketAmount = service.GetMarketPrice(db, p, date)
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
breakdowns := computeBreakdown(lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, "Asset:") }))
|
breakdowns := computeBreakdown(db, lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, "Asset:") }))
|
||||||
return gin.H{"postings": postings, "breakdowns": breakdowns}
|
return gin.H{"postings": postings, "breakdowns": breakdowns}
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeBreakdown(postings []posting.Posting) map[string]Breakdown {
|
func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]Breakdown {
|
||||||
accounts := make(map[string]bool)
|
accounts := make(map[string]bool)
|
||||||
for _, p := range postings {
|
for _, p := range postings {
|
||||||
var parts []string
|
var parts []string
|
||||||
|
@ -53,9 +53,22 @@ func computeBreakdown(postings []posting.Posting) map[string]Breakdown {
|
||||||
|
|
||||||
for group := range accounts {
|
for group := range accounts {
|
||||||
ps := lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, group) })
|
ps := lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, group) })
|
||||||
amount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.Amount }, 0.0)
|
amount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
|
||||||
|
if service.IsInterest(db, p) {
|
||||||
|
return acc
|
||||||
|
} else {
|
||||||
|
return acc + p.Amount
|
||||||
|
}
|
||||||
|
}, 0.0)
|
||||||
marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
|
marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0)
|
||||||
payments := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment { return xirr.Payment{Date: p.Date, Amount: -p.Amount} }))
|
payments := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment {
|
||||||
|
if service.IsInterest(db, p) {
|
||||||
|
return xirr.Payment{Date: p.Date, Amount: 0}
|
||||||
|
} else {
|
||||||
|
return xirr.Payment{Date: p.Date, Amount: -p.Amount}
|
||||||
|
}
|
||||||
|
|
||||||
|
}))
|
||||||
payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount})
|
payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount})
|
||||||
returns, err := xirr.XIRR(payments)
|
returns, err := xirr.XIRR(payments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -42,11 +42,19 @@ func ComputeTimeline(db *gorm.DB, postings []posting.Posting) []Networth {
|
||||||
}
|
}
|
||||||
|
|
||||||
actual := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
|
actual := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
|
||||||
|
if service.IsInterest(db, p) {
|
||||||
|
return agg
|
||||||
|
} else {
|
||||||
return p.Amount + agg
|
return p.Amount + agg
|
||||||
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
gain := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
|
gain := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 {
|
||||||
|
if service.IsInterest(db, p) {
|
||||||
|
return p.Amount + agg
|
||||||
|
} else {
|
||||||
return service.GetMarketPrice(db, p, start) - p.Amount + agg
|
return service.GetMarketPrice(db, p, start) - p.Amount + agg
|
||||||
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
networths = append(networths, Networth{Date: start, Actual: actual, Gain: gain})
|
networths = append(networths, Networth{Date: start, Actual: actual, Gain: gain})
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,5 +26,8 @@ func Listen(db *gorm.DB) {
|
||||||
c.JSON(200, GetLedger(db))
|
c.JSON(200, GetLedger(db))
|
||||||
})
|
})
|
||||||
log.Info("Listening on 7500")
|
log.Info("Listening on 7500")
|
||||||
router.Run(":7500")
|
err := router.Run(":7500")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type interestCache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
loaded bool
|
||||||
|
postings map[time.Time][]posting.Posting
|
||||||
|
}
|
||||||
|
|
||||||
|
var icache interestCache
|
||||||
|
|
||||||
|
func loadInterestCache(db *gorm.DB) {
|
||||||
|
icache.mu.Lock()
|
||||||
|
defer icache.mu.Unlock()
|
||||||
|
|
||||||
|
if icache.loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var postings []posting.Posting
|
||||||
|
result := db.Where("account like ?", "Income:Interest:%").Find(&postings)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Fatal(result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
icache.postings = lo.GroupBy(postings, func(p posting.Posting) time.Time { return p.Date })
|
||||||
|
icache.loaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsInterest(db *gorm.DB, p posting.Posting) bool {
|
||||||
|
if p.Commodity != "INR" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
loadInterestCache(db)
|
||||||
|
|
||||||
|
for _, ip := range icache.postings[p.Date] {
|
||||||
|
if ip.Date.Equal(p.Date) &&
|
||||||
|
-ip.Amount == p.Amount &&
|
||||||
|
ip.Payee == p.Payee {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -15,16 +15,17 @@ import (
|
||||||
|
|
||||||
type priceCache struct {
|
type priceCache struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
loaded bool
|
||||||
pricesTree map[string]*btree.BTree
|
pricesTree map[string]*btree.BTree
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache priceCache
|
var pcache priceCache
|
||||||
|
|
||||||
func loadCache(db *gorm.DB) {
|
func loadPriceCache(db *gorm.DB) {
|
||||||
cache.mu.Lock()
|
pcache.mu.Lock()
|
||||||
defer cache.mu.Unlock()
|
defer pcache.mu.Unlock()
|
||||||
|
|
||||||
if cache.pricesTree != nil {
|
if pcache.loaded {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,14 +34,14 @@ func loadCache(db *gorm.DB) {
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.Fatal(result.Error)
|
log.Fatal(result.Error)
|
||||||
}
|
}
|
||||||
cache.pricesTree = make(map[string]*btree.BTree)
|
pcache.pricesTree = make(map[string]*btree.BTree)
|
||||||
|
|
||||||
for _, price := range prices {
|
for _, price := range prices {
|
||||||
if cache.pricesTree[price.CommodityName] == nil {
|
if pcache.pricesTree[price.CommodityName] == nil {
|
||||||
cache.pricesTree[price.CommodityName] = btree.New(2)
|
pcache.pricesTree[price.CommodityName] = btree.New(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.pricesTree[price.CommodityName].ReplaceOrInsert(price)
|
pcache.pricesTree[price.CommodityName].ReplaceOrInsert(price)
|
||||||
}
|
}
|
||||||
|
|
||||||
var postings []posting.Posting
|
var postings []posting.Posting
|
||||||
|
@ -50,22 +51,24 @@ func loadCache(db *gorm.DB) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for commodityName, postings := range lo.GroupBy(postings, func(p posting.Posting) string { return p.Commodity }) {
|
for commodityName, postings := range lo.GroupBy(postings, func(p posting.Posting) string { return p.Commodity }) {
|
||||||
if postings[0].Commodity != "INR" && cache.pricesTree[commodityName] == nil {
|
if postings[0].Commodity != "INR" && pcache.pricesTree[commodityName] == nil {
|
||||||
cache.pricesTree[commodityName] = btree.New(2)
|
pcache.pricesTree[commodityName] = btree.New(2)
|
||||||
for _, p := range postings {
|
for _, p := range postings {
|
||||||
cache.pricesTree[commodityName].ReplaceOrInsert(price.Price{Date: p.Date, CommodityID: p.Commodity, CommodityName: p.Commodity, Value: p.Amount / p.Quantity})
|
pcache.pricesTree[commodityName].ReplaceOrInsert(price.Price{Date: p.Date, CommodityID: p.Commodity, CommodityName: p.Commodity, Value: p.Amount / p.Quantity})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pcache.loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) float64 {
|
func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) float64 {
|
||||||
loadCache(db)
|
loadPriceCache(db)
|
||||||
if p.Commodity == "INR" {
|
if p.Commodity == "INR" {
|
||||||
return p.Amount
|
return p.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
pt := cache.pricesTree[p.Commodity]
|
pt := pcache.pricesTree[p.Commodity]
|
||||||
if pt != nil {
|
if pt != nil {
|
||||||
|
|
||||||
pc := utils.BTreeDescendFirstLessOrEqual(pt, price.Price{Date: date})
|
pc := utils.BTreeDescendFirstLessOrEqual(pt, price.Price{Date: date})
|
||||||
|
|
Loading…
Reference in New Issue