add support for NPS fund price tracking
This commit is contained in:
parent
4a23646076
commit
a63386ea77
|
@ -0,0 +1,87 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ananthakumaran/paisa/internal/model/nps/scheme"
|
||||||
|
"github.com/ananthakumaran/paisa/internal/scraper/nps"
|
||||||
|
"github.com/logrusorgru/aurora"
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var npsUpdate bool
|
||||||
|
|
||||||
|
var npsCmd = &cobra.Command{
|
||||||
|
Use: "nps",
|
||||||
|
Short: "Search nps fund",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(viper.GetString("db_path")), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.AutoMigrate(&scheme.Scheme{})
|
||||||
|
count := scheme.Count(db)
|
||||||
|
if update || count == 0 {
|
||||||
|
schemes, err := nps.GetSchemes()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
scheme.UpsertAll(db, schemes)
|
||||||
|
} else {
|
||||||
|
log.Info("Using cached results; pass '-u' to update the cache")
|
||||||
|
}
|
||||||
|
pfm := promptPFM(db)
|
||||||
|
name := npsPromptName(db, pfm)
|
||||||
|
scheme := scheme.FindScheme(db, pfm, name)
|
||||||
|
log.Info("NPS Fund Scheme Code: ", aurora.Bold(scheme.SchemeID))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
searchCmd.AddCommand(npsCmd)
|
||||||
|
npsCmd.Flags().BoolVarP(&update, "update", "u", false, "update the NPS Fund Scheme list")
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptPFM(db *gorm.DB) string {
|
||||||
|
pfms := scheme.GetPFMs(db)
|
||||||
|
return npsPrompt("Pension Fund Manager", pfms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func npsPromptName(db *gorm.DB, pfm string) string {
|
||||||
|
names := scheme.GetSchemeNames(db, pfm)
|
||||||
|
return npsPrompt("Fund Name", names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func npsPrompt(label string, list []string) string {
|
||||||
|
searcher := func(input string, index int) bool {
|
||||||
|
item := list[index]
|
||||||
|
item = strings.Replace(strings.ToLower(item), " ", "", -1)
|
||||||
|
words := strings.Split(strings.ToLower(input), " ")
|
||||||
|
for _, word := range words {
|
||||||
|
if strings.TrimSpace(word) != "" && !strings.Contains(item, word) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := promptui.Select{
|
||||||
|
Label: label,
|
||||||
|
Items: list,
|
||||||
|
Size: 10,
|
||||||
|
Searcher: searcher,
|
||||||
|
StartInSearchMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, item, err := prompt.Run()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
mutualfund "github.com/ananthakumaran/paisa/internal/model/mutualfund/scheme"
|
||||||
|
nps "github.com/ananthakumaran/paisa/internal/model/nps/scheme"
|
||||||
|
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||||
|
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||||
"github.com/ananthakumaran/paisa/internal/server"
|
"github.com/ananthakumaran/paisa/internal/server"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -14,6 +18,11 @@ var serveCmd = &cobra.Command{
|
||||||
Short: "serve the WEB UI",
|
Short: "serve the WEB UI",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
db, err := gorm.Open(sqlite.Open(viper.GetString("db_path")), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open(viper.GetString("db_path")), &gorm.Config{})
|
||||||
|
db.AutoMigrate(&nps.Scheme{})
|
||||||
|
db.AutoMigrate(&mutualfund.Scheme{})
|
||||||
|
db.AutoMigrate(&posting.Posting{})
|
||||||
|
db.AutoMigrate(&price.Price{})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,22 +20,22 @@ tracked as a commodity. Few example transactions can be found below.
|
||||||
Checking
|
Checking
|
||||||
```
|
```
|
||||||
|
|
||||||
**paisa** comes with inbuilt support for fetching the latest price of some
|
**paisa** comes with inbuilt support for fetching the latest price of
|
||||||
commodities like mutual fund. For others, it will try to use the
|
some commodities like mutual fund and NPS. For others, it will try to
|
||||||
latest purchase price specified in the journal. For example, when you
|
use the latest purchase price specified in the journal. For example,
|
||||||
enter the second NPS transaction on `2019/02/21`, the valuation of
|
when you enter the second NPS transaction on `2019/02/21`, the
|
||||||
your existing holdings will be adjusted based on the new purchase
|
valuation of your existing holdings will be adjusted based on the new
|
||||||
price.
|
purchase price.
|
||||||
|
|
||||||
## Mutual Fund
|
## Mutual Fund
|
||||||
|
|
||||||
To automatically track the latest value of your mutual fund holdings,
|
To automatically track the latest value of your mutual funds holdings,
|
||||||
you need to link the commodity and the fund scheme code.
|
you need to link the commodity and the fund scheme code.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
commodities:
|
commodities:
|
||||||
- name: NIFTY # commodity name
|
- name: NIFTY # commodity name
|
||||||
type: mutualfund # type (only mutualfund supported as of now)
|
type: mutualfund # type
|
||||||
code: 120716 # mutual fund scheme code
|
code: 120716 # mutual fund scheme code
|
||||||
- name: NIFTY_JR
|
- name: NIFTY_JR
|
||||||
type: mutualfund
|
type: mutualfund
|
||||||
|
@ -55,6 +55,31 @@ INFO Using cached results; pass '-u' to update the cache
|
||||||
INFO Mutual Fund Scheme Code: 120684
|
INFO Mutual Fund Scheme Code: 120684
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## NPS
|
||||||
|
|
||||||
|
To automatically track the latest value of your nps funds holdings,
|
||||||
|
you need to link the commodity and the fund scheme code.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
commodities:
|
||||||
|
- name: NPS_HDFC_E # commodity name
|
||||||
|
type: nps # type
|
||||||
|
code: SM008002 # nps fund scheme code
|
||||||
|
```
|
||||||
|
|
||||||
|
The example config above links NPS fund commodity with their
|
||||||
|
respective NPS fund scheme code. The scheme code can be found using
|
||||||
|
the *search* command.
|
||||||
|
|
||||||
|
```
|
||||||
|
❯ paisa search nps
|
||||||
|
INFO Using config file: /home/john/finance/paisa.yaml
|
||||||
|
INFO Using cached results; pass '-u' to update the cache
|
||||||
|
✔ HDFC Pension Management Company Limited
|
||||||
|
✔ HDFC PENSION MANAGEMENT COMPANY LIMITED SCHEME C - TIER I
|
||||||
|
INFO NPS Fund Scheme Code: SM008002
|
||||||
|
```
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
**paisa** fetches the latest price of the commodities only when
|
**paisa** fetches the latest price of the commodities only when
|
||||||
|
|
|
@ -17,7 +17,7 @@ INFO Generating journal file: /home/john/finance/personal.ledger
|
||||||
❯ paisa update
|
❯ paisa update
|
||||||
INFO Using config file: /home/john/finance/paisa.yaml
|
INFO Using config file: /home/john/finance/paisa.yaml
|
||||||
INFO Syncing transactions from journal
|
INFO Syncing transactions from journal
|
||||||
INFO Fetching mutual fund history
|
INFO Fetching commodities price history
|
||||||
INFO Fetching commodity NIFTY
|
INFO Fetching commodity NIFTY
|
||||||
INFO Fetching commodity NIFTY_JR
|
INFO Fetching commodity NIFTY_JR
|
||||||
❯ paisa serve
|
❯ paisa serve
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
finance, specially tailored for Indians. It builds on top of the
|
finance, specially tailored for Indians. It builds on top of the
|
||||||
[ledger](https://www.ledger-cli.org/) double entry accounting tool.
|
[ledger](https://www.ledger-cli.org/) double entry accounting tool.
|
||||||
|
|
||||||
* Your financial data never leaves your system
|
* Your financial data never leaves your system.
|
||||||
* The journal and config information are stored in plain text files
|
* The journal and config information are stored in plain text files
|
||||||
that can be easily version controlled
|
that can be easily version controlled.
|
||||||
|
* Can track the latest market price of Mutual Funds and NPS Funds holdings.
|
||||||
|
|
|
@ -13,13 +13,13 @@ content
|
||||||
|
|
||||||
```go
|
```go
|
||||||
2022/01/01 Salary
|
2022/01/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 100,000 INR
|
Checking 100,000 INR
|
||||||
```
|
```
|
||||||
|
|
||||||
**ledger** follows the double-entry accounting system. In simple terms, it
|
**ledger** follows the double-entry accounting system. In simple terms, it
|
||||||
tracks the movement of money from debit account to credit
|
tracks the movement of money from debit account to credit
|
||||||
account. Here `Income:Salary` is the debit account and
|
account. Here `Income:Salary:Acme` is the debit account and
|
||||||
`Checking` is the credit account. The date at which the
|
`Checking` is the credit account. The date at which the
|
||||||
transaction took place and a description of the transaction is written
|
transaction took place and a description of the transaction is written
|
||||||
in the first line followed by the list of credit or debit
|
in the first line followed by the list of credit or debit
|
||||||
|
@ -30,15 +30,15 @@ accounts must be zero.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
2022/01/01 Salary
|
2022/01/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 100,000 INR
|
Checking 100,000 INR
|
||||||
|
|
||||||
2022/02/01 Salary
|
2022/02/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 100,000 INR
|
Checking 100,000 INR
|
||||||
|
|
||||||
2022/03/01 Salary
|
2022/03/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 100,000 INR
|
Checking 100,000 INR
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -55,17 +55,17 @@ we could represent it as follows
|
||||||
|
|
||||||
```go
|
```go
|
||||||
2022/01/01 Salary
|
2022/01/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 88,000 INR
|
Checking 88,000 INR
|
||||||
Asset:Debt:EPF 12,000 INR
|
Asset:Debt:EPF 12,000 INR
|
||||||
|
|
||||||
2022/02/01 Salary
|
2022/02/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 88,000 INR
|
Checking 88,000 INR
|
||||||
Asset:Debt:EPF 12,000 INR
|
Asset:Debt:EPF 12,000 INR
|
||||||
|
|
||||||
2022/03/01 Salary
|
2022/03/01 Salary
|
||||||
Income:Salary -100,000 INR
|
Income:Salary:Acme -100,000 INR
|
||||||
Checking 88,000 INR
|
Checking 88,000 INR
|
||||||
Asset:Debt:EPF 12,000 INR
|
Asset:Debt:EPF 12,000 INR
|
||||||
```
|
```
|
||||||
|
@ -144,10 +144,10 @@ commodities:
|
||||||
code: 120684
|
code: 120684
|
||||||
```
|
```
|
||||||
|
|
||||||
**paisa** can fetch the latest price of mutual fund commodity as of
|
**paisa** can fetch the latest price of mutual funds and nps funds as
|
||||||
today. For other types of commodities, the purchase/sell price of the
|
of today. For other types of commodities, the purchase/sell price of
|
||||||
last transaction would be considered the latest price. The code is the
|
the last transaction would be considered the latest price. The code is
|
||||||
scheme code of the fund. The *search* command can be used to find
|
the scheme code of the fund. The *search* command can be used to find
|
||||||
scheme code
|
scheme code
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -173,7 +173,7 @@ the serve command.
|
||||||
❯ paisa update
|
❯ paisa update
|
||||||
INFO Using config file: /home/john/finance/paisa.yaml
|
INFO Using config file: /home/john/finance/paisa.yaml
|
||||||
INFO Syncing transactions from journal
|
INFO Syncing transactions from journal
|
||||||
INFO Fetching mutual fund history
|
INFO Fetching commodities price history
|
||||||
INFO Fetching commodity NIFTY
|
INFO Fetching commodity NIFTY
|
||||||
INFO Fetching commodity NIFTY_JR
|
INFO Fetching commodity NIFTY_JR
|
||||||
❯ paisa serve
|
❯ paisa serve
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/ananthakumaran/paisa/internal/ledger"
|
"github.com/ananthakumaran/paisa/internal/ledger"
|
||||||
"github.com/ananthakumaran/paisa/internal/model/posting"
|
"github.com/ananthakumaran/paisa/internal/model/posting"
|
||||||
"github.com/ananthakumaran/paisa/internal/model/price"
|
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||||
"github.com/ananthakumaran/paisa/internal/scraper/mutualfund"
|
"github.com/ananthakumaran/paisa/internal/scraper/mutualfund"
|
||||||
|
"github.com/ananthakumaran/paisa/internal/scraper/nps"
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/logrusorgru/aurora"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -20,11 +19,11 @@ func Sync(db *gorm.DB) {
|
||||||
posting.UpsertAll(db, postings)
|
posting.UpsertAll(db, postings)
|
||||||
|
|
||||||
db.AutoMigrate(&price.Price{})
|
db.AutoMigrate(&price.Price{})
|
||||||
log.Info("Fetching mutual fund history")
|
log.Info("Fetching commodities price history")
|
||||||
type Commodity struct {
|
type Commodity struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Code int
|
Code string
|
||||||
}
|
}
|
||||||
|
|
||||||
var commodities []Commodity
|
var commodities []Commodity
|
||||||
|
@ -32,8 +31,17 @@ func Sync(db *gorm.DB) {
|
||||||
for _, commodity := range commodities {
|
for _, commodity := range commodities {
|
||||||
name := commodity.Name
|
name := commodity.Name
|
||||||
log.Info("Fetching commodity ", aurora.Bold(name))
|
log.Info("Fetching commodity ", aurora.Bold(name))
|
||||||
schemeCode := strconv.Itoa(commodity.Code)
|
schemeCode := commodity.Code
|
||||||
prices, err := mutualfund.GetNav(schemeCode, name)
|
var prices []*price.Price
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch commodity.Type {
|
||||||
|
case string(price.MutualFund):
|
||||||
|
prices, err = mutualfund.GetNav(schemeCode, name)
|
||||||
|
case string(price.NPS):
|
||||||
|
prices, err = nps.GetNav(schemeCode, name)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Scheme struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
PFMName string
|
||||||
|
SchemeID string
|
||||||
|
SchemeName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Scheme) TableName() string {
|
||||||
|
return "nps_schemes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func Count(db *gorm.DB) int64 {
|
||||||
|
var count int64
|
||||||
|
db.Model(&Scheme{}).Count(&count)
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpsertAll(db *gorm.DB, schemes []*Scheme) {
|
||||||
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
err := tx.Exec("DELETE FROM nps_schemes").Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, scheme := range schemes {
|
||||||
|
err := tx.Create(scheme).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPFMs(db *gorm.DB) []string {
|
||||||
|
var pfms []string
|
||||||
|
db.Model(&Scheme{}).Distinct().Pluck("PFMName", &pfms)
|
||||||
|
return pfms
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSchemeNames(db *gorm.DB, pfm string) []string {
|
||||||
|
var schemeNames []string
|
||||||
|
db.Model(&Scheme{}).Where("pfm_name = ?", pfm).Pluck("SchemeName", &schemeNames)
|
||||||
|
return schemeNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindScheme(db *gorm.DB, pfm string, schemeName string) Scheme {
|
||||||
|
var scheme Scheme
|
||||||
|
result := db.Where("pfm_name = ? and scheme_name = ?", pfm, schemeName).First(&scheme)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Fatal(result)
|
||||||
|
}
|
||||||
|
return scheme
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ type CommodityType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MutualFund CommodityType = "mutualfund"
|
MutualFund CommodityType = "mutualfund"
|
||||||
|
NPS CommodityType = "nps"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Price struct {
|
type Price struct {
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package nps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ananthakumaran/paisa/internal/model/price"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetNav(schemeCode string, commodityName string) ([]*price.Price, error) {
|
||||||
|
url := fmt.Sprintf("https://nps.purifiedbytes.com/api/schemes/%s/nav.json", schemeCode)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data struct {
|
||||||
|
Date string
|
||||||
|
Nav float64
|
||||||
|
}
|
||||||
|
type Result struct {
|
||||||
|
Data []Data
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Result
|
||||||
|
err = json.Unmarshal(respBytes, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var prices []*price.Price
|
||||||
|
for _, data := range result.Data {
|
||||||
|
date, err := time.Parse("2006-01-02", data.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
price := price.Price{Date: date, CommodityType: price.NPS, CommodityID: schemeCode, CommodityName: commodityName, Value: data.Nav}
|
||||||
|
prices = append(prices, &price)
|
||||||
|
}
|
||||||
|
return prices, nil
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package nps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/ananthakumaran/paisa/internal/model/nps/scheme"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"io/ioutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSchemes() ([]*scheme.Scheme, error) {
|
||||||
|
log.Info("Fetching NPS scheme list from Purified Bytes")
|
||||||
|
resp, err := http.Get("https://nps.purifiedbytes.com/api/schemes.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scheme struct {
|
||||||
|
Id string
|
||||||
|
Name string
|
||||||
|
PFMName string `json:"pfm_name"`
|
||||||
|
}
|
||||||
|
type Result struct {
|
||||||
|
Data []Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Result
|
||||||
|
err = json.Unmarshal(respBytes, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var schemes []*scheme.Scheme
|
||||||
|
for _, s := range result.Data {
|
||||||
|
scheme := scheme.Scheme{PFMName: s.PFMName, SchemeID: s.Id, SchemeName: s.Name}
|
||||||
|
schemes = append(schemes, &scheme)
|
||||||
|
|
||||||
|
}
|
||||||
|
return schemes, nil
|
||||||
|
}
|
|
@ -17,6 +17,8 @@ type Breakdown struct {
|
||||||
InvestmentAmount float64 `json:"investment_amount"`
|
InvestmentAmount float64 `json:"investment_amount"`
|
||||||
WithdrawalAmount float64 `json:"withdrawal_amount"`
|
WithdrawalAmount float64 `json:"withdrawal_amount"`
|
||||||
MarketAmount float64 `json:"market_amount"`
|
MarketAmount float64 `json:"market_amount"`
|
||||||
|
BalanceUnits float64 `json:"balance_units"`
|
||||||
|
LatestPrice float64 `json:"latest_price"`
|
||||||
XIRR float64 `json:"xirr"`
|
XIRR float64 `json:"xirr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,14 +40,15 @@ func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]Breakd
|
||||||
var parts []string
|
var parts []string
|
||||||
for _, part := range strings.Split(p.Account, ":") {
|
for _, part := range strings.Split(p.Account, ":") {
|
||||||
parts = append(parts, part)
|
parts = append(parts, part)
|
||||||
accounts[strings.Join(parts, ":")] = true
|
accounts[strings.Join(parts, ":")] = false
|
||||||
}
|
}
|
||||||
|
accounts[p.Account] = true
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]Breakdown)
|
result := make(map[string]Breakdown)
|
||||||
|
|
||||||
for group := range accounts {
|
for group, leaf := 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) })
|
||||||
investmentAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
|
investmentAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
|
||||||
if p.Amount < 0 || service.IsInterest(db, p) {
|
if p.Amount < 0 || service.IsInterest(db, p) {
|
||||||
|
@ -62,9 +65,18 @@ func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]Breakd
|
||||||
}
|
}
|
||||||
}, 0.0)
|
}, 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)
|
||||||
|
var balanceUnits float64
|
||||||
|
if leaf {
|
||||||
|
balanceUnits = lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 {
|
||||||
|
if p.Commodity != "INR" {
|
||||||
|
return acc + p.Quantity
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
xirr := service.XIRR(db, ps)
|
xirr := service.XIRR(db, ps)
|
||||||
breakdown := Breakdown{InvestmentAmount: investmentAmount, WithdrawalAmount: withdrawalAmount, MarketAmount: marketAmount, XIRR: xirr, Group: group}
|
breakdown := Breakdown{InvestmentAmount: investmentAmount, WithdrawalAmount: withdrawalAmount, MarketAmount: marketAmount, XIRR: xirr, Group: group, BalanceUnits: balanceUnits}
|
||||||
result[group] = breakdown
|
result[group] = breakdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,9 @@ function renderBreakdowns(breakdowns: Breakdown[]) {
|
||||||
)}</td>
|
)}</td>
|
||||||
<td class='has-text-right'>${formatCurrency(b.investment_amount)}</td>
|
<td class='has-text-right'>${formatCurrency(b.investment_amount)}</td>
|
||||||
<td class='has-text-right'>${formatCurrency(b.withdrawal_amount)}</td>
|
<td class='has-text-right'>${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'>${formatCurrency(b.market_amount)}</td>
|
<td class='has-text-right'>${formatCurrency(b.market_amount)}</td>
|
||||||
<td class='${changeClass} has-text-right'>${formatCurrency(gain)}</td>
|
<td class='${changeClass} has-text-right'>${formatCurrency(gain)}</td>
|
||||||
<td class='${changeClass} has-text-right'>${formatFloat(b.xirr)}</td>
|
<td class='${changeClass} has-text-right'>${formatFloat(b.xirr)}</td>
|
||||||
|
|
|
@ -35,6 +35,7 @@ export interface Breakdown {
|
||||||
group: string;
|
group: string;
|
||||||
investment_amount: number;
|
investment_amount: number;
|
||||||
withdrawal_amount: number;
|
withdrawal_amount: number;
|
||||||
|
balance_units: number;
|
||||||
market_amount: number;
|
market_amount: number;
|
||||||
xirr: number;
|
xirr: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -286,6 +286,7 @@
|
||||||
<th>Account</th>
|
<th>Account</th>
|
||||||
<th class='has-text-right'>Investment Amount</th>
|
<th class='has-text-right'>Investment Amount</th>
|
||||||
<th class='has-text-right'>Withdrawal Amount</th>
|
<th class='has-text-right'>Withdrawal Amount</th>
|
||||||
|
<th class='has-text-right'>Balance Units</th>
|
||||||
<th class='has-text-right'>Market Value</th>
|
<th class='has-text-right'>Market Value</th>
|
||||||
<th class='has-text-right'>Change</th>
|
<th class='has-text-right'>Change</th>
|
||||||
<th class='has-text-right'>XIRR</th>
|
<th class='has-text-right'>XIRR</th>
|
||||||
|
|
Loading…
Reference in New Issue