Skip to content

Commit

Permalink
Implementing CLI command to create transaction
Browse files Browse the repository at this point in the history
This PR fixes #230 and also expands the README a little bit.
  • Loading branch information
oxisto committed Dec 10, 2023
1 parent 88757ab commit b26a51e
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 33 deletions.
73 changes: 70 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ The Money Gopher will help you to keep track of your investments.

Surely, there are a number of programs and services out there that already
manage your portfolio(s), why creating another one? Well there are several
reasons or rather requirements that I need. Note, that these might be very
specific to my use case, but maybe somebody else will appreciate them as well.
reasons or rather requirements that I have. Note, that these might be very
specific to my use-case, but maybe somebody else will appreciate them as well.

* 🏘️ I need to manage several portfolios for several distinct people, for
example my own and my children's. I want to keep these portfolios completely
Expand Down Expand Up @@ -44,13 +44,80 @@ creating this.

* 📞 I wanted to explore new ways of providing RPC-style APIs that are not based
on the arguably bloated gRPC framework. Therefore, I am exploring Buf's
[Connect](https://connect.build) framework in this project, which seems
[Connect](https://connectrpc.com) framework in this project, which seems
promising, even for browser-based interactions.
* 🔲 I am still on the spiritual search for a good UI framework, so this might
be a good chance to explore different options.
* 📈 I wanted to understand the math behind some of the used performance models,
such as time-weighted rate of return a little bit better.

# Usage

This project has currently three main components:
* A server component [`moneyd`](./cmd/moneyd), which manages the connection to
the database and offers a RPC-API using [Connect](https://connectrpc.com) to
manage portfolios and securities.
* A simple CLI [`mgo`](./cmd/mgo) which can be used to interact with
the API.
* An even simpler [web-based user interface](./ui/), based on
[SvelteKit](https://kit.svelte.dev).


## Starting `moneyd`

After checking out the source-code, the necessary Go binaries can be compiled
using `go build ./cmd/moneyd` and can be started using `./moneyd`. It will print
out some information, but logging definitely needs to improved.

On startup, an SQLite database named `money.db` will be created (or loaded) in
the same directory as the started binary. If the database is empty, a new
portfolio named `mybank/myportfolio` and one example security will be created.

As a simple check, one can simply interact with the RPC-API with a normal HTTP
client, for example to list all portfolios.
```zsh
curl \
--header 'Content-Type: application/json' \
--data '{}' \
http://localhost:8080/mgo.portfolio.v1.PortfolioService/ListPortfolios
```

This should print something like the following.

```json
{"portfolios":[{"name":"bank/myportfolio","displayName":"My Portfolio"}]}
```

## Using `mgo`

Alternatively, a simple CLI called `mgo` can be used. It is preferable to
install it for the current user using `go install ./cmd/mgo`. Afterwards, it can
for example used to display all portfolios with `mgo portfolio list`.

### Adding Transactions

To add transactions (buy, sell, etc.) to an existing portfolio, the command `mgo
portfolio transaction create` can be used. The following shows an example for
the security with the ISIN US0378331005.
```zsh
mgo portfolio transactions create US0378331005 --portfolio-name bank/myportfolio --amount 5 --price 120 --time="2022-01-01 10:00"
```

When successful, this should print something like the following.

```
Successfully created a buy transaction (1c12ac28dfbc5440) for security US09075V1026 in bank/myportfolio.
```

The unique identifier (also called 'name') of the transaction can be used in
other calls, e.g., to modify it.

### Available Commands and Shell Completion

For a detailed list of all available commands see `mgo --help`. The CLI also
supports (basic) shell completion. For details how to activate it, please see
`mgo completion`.

# When is it finished?

Since I am working on this in my spare time, it will probably take a while 😃.
2 changes: 2 additions & 0 deletions buf.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Generated by buf. DO NOT EDIT.
version: v1
122 changes: 95 additions & 27 deletions cli/commands/portfolio.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/http"
"os"
"strings"
"time"

"github.com/fatih/color"
kongcompletion "github.com/jotaen/kong-completion"
Expand All @@ -37,10 +38,13 @@ import (
)

type PortfolioCmd struct {
Create CreatePortfolioCmd `cmd:"" help:"Creates a new portfolio."`
List ListPortfolioCmd `cmd:"" help:"Lists all portfolios."`
Show ShowPortfolioCmd `cmd:"" help:"Shows details about one portfolio."`
ImportTransactions ImportTransactionsCmd `cmd:"" help:"Imports transactions from CSV."`
Create CreatePortfolioCmd `cmd:"" help:"Creates a new portfolio."`
List ListPortfolioCmd `cmd:"" help:"Lists all portfolios."`
Show ShowPortfolioCmd `cmd:"" help:"Shows details about one portfolio."`
Transactions struct {
Create CreateTransactionCmd `cmd:"" help:"Creates a transaction. Defaults to a \"buy\" transaction."`
Import ImportTransactionsCmd `cmd:"" help:"Imports transactions from CSV."`
} `cmd:"" help:"Subcommands supporting transactions within one portfolio"`
}

type ListPortfolioCmd struct{}
Expand Down Expand Up @@ -153,6 +157,74 @@ func greenOrRed(f float32) string {
}
}

type CreateTransactionCmd struct {
PortfolioName string `required:"" predictor:"portfolio" help:"The name of the portfolio where the transaction will be created in"`
SecurityName string `arg:"" predictor:"security" help:"The name of the security this transaction belongs to (its ISIN)"`
Type string `required:"" enum:"buy,sell,delivery-inbound,delivery-outbound,dividend" default:"buy"`
Amount float32 `required:"" help:"The amount of securities involved in the transaction"`
Price float32 `required:"" help:"The price without fees or taxes"`
Fees float32 `help:"Any fees that applied to the transaction"`
Taxes float32 `help:"Any taxes that applied to the transaction"`
Time time.Time `help:"The time of the transaction. Defaults to 'now'" format:"2006-01-02 15:04"`
}

func (cmd *CreateTransactionCmd) Run(s *cli.Session) error {
var req = connect.NewRequest(&portfoliov1.CreatePortfolioTransactionRequest{
Transaction: &portfoliov1.PortfolioEvent{
PortfolioName: cmd.PortfolioName,
SecurityName: cmd.SecurityName,
Type: eventTypeFrom(cmd.Type), // eventTypeFrom(cmd.Type)
Amount: cmd.Amount,
Time: timeOrNow(cmd.Time),
Price: cmd.Price,
Fees: cmd.Fees,
Taxes: cmd.Taxes,
},
})

client := portfoliov1connect.NewPortfolioServiceClient(
http.DefaultClient, "http://localhost:8080",
connect.WithHTTPGet(),
)
res, err := client.CreatePortfolioTransaction(context.Background(), req)
if err != nil {
return err
}

fmt.Printf("Successfully created a %s transaction (%s) for security %s in %s.\n",
color.CyanString(cmd.Type),
color.BlackString(res.Msg.Name),
color.BlueString(res.Msg.SecurityName),
color.BlueString(res.Msg.PortfolioName),
)

return nil
}

func eventTypeFrom(typ string) portfoliov1.PortfolioEventType {
if typ == "buy" {
return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY
} else if typ == "sell" {
return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL
} else if typ == "delivery-inbound" {
return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_INBOUND
} else if typ == "delivery-outbound" {
return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND
} else if typ == "dividend" {
return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DIVIDEND
}

return portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_UNSPECIFIED
}

func timeOrNow(t time.Time) *timestamppb.Timestamp {
if t.IsZero() {
return timestamppb.Now()
}

return timestamppb.New(t)
}

type ImportTransactionsCmd struct {
PortfolioName string `required:"" predictor:"portfolio"`
CsvFile string `arg:"" help:"The path to the CSV file to import"`
Expand Down Expand Up @@ -191,29 +263,25 @@ func (cmd *ImportTransactionsCmd) Run(s *cli.Session) error {
return nil
}

type PortfolioPredictor struct{}

func (p *PortfolioPredictor) Predict(complete.Args) (names []string) {
client := portfoliov1connect.NewPortfolioServiceClient(
http.DefaultClient, "http://localhost:8080",
connect.WithHTTPGet(),
)
res, err := client.ListPortfolios(
context.Background(),
connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}),
)
if err != nil {
return nil
}

for _, p := range res.Msg.Portfolios {
names = append(names, p.Name)
}

return
}

var PredictPortfolios = kongcompletion.WithPredictor(
"portfolio",
&PortfolioPredictor{},
complete.PredictFunc(func(complete.Args) (names []string) {
client := portfoliov1connect.NewPortfolioServiceClient(
http.DefaultClient, "http://localhost:8080",
connect.WithHTTPGet(),
)
res, err := client.ListPortfolios(
context.Background(),
connect.NewRequest(&portfoliov1.ListPortfoliosRequest{}),
)
if err != nil {
return nil
}

for _, p := range res.Msg.Portfolios {
names = append(names, p.Name)
}

return
}),
)
25 changes: 25 additions & 0 deletions cli/commands/securities.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import (
"net/http"

"connectrpc.com/connect"
kongcompletion "github.com/jotaen/kong-completion"
"github.com/oxisto/money-gopher/cli"
portfoliov1 "github.com/oxisto/money-gopher/gen"
"github.com/oxisto/money-gopher/gen/portfoliov1connect"
"github.com/posener/complete"
)

type SecurityCmd struct {
Expand Down Expand Up @@ -99,3 +101,26 @@ func (cmd *UpdateAllQuotesCmd) Run(s *cli.Session) error {

return err
}

var PredictSecurities = kongcompletion.WithPredictor(
"security",
complete.PredictFunc(func(complete.Args) (names []string) {
client := portfoliov1connect.NewSecuritiesServiceClient(
http.DefaultClient, "http://localhost:8080",
connect.WithHTTPGet(),
)
res, err := client.ListSecurities(
context.Background(),
connect.NewRequest(&portfoliov1.ListSecuritiesRequest{}),
)
if err != nil {
return nil
}

for _, p := range res.Msg.Securities {
names = append(names, p.Name)
}

return
}),
)
5 changes: 4 additions & 1 deletion cmd/mgo/mgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ func main() {
kong.UsageOnError(),
)

kongcompletion.Register(parser, commands.PredictPortfolios)
kongcompletion.Register(parser,
commands.PredictPortfolios,
commands.PredictSecurities,
)

// Proceed as normal after kongplete.Complete.
ctx, err := parser.Parse(os.Args[1:])
Expand Down
3 changes: 3 additions & 0 deletions money-gopher.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
"jotaen",
"kongcompletion",
"modernc",
"moneyd",
"moneygopher",
"mybank",
"myportfolio",
"oxisto",
"portfoliov",
"protobuf",
Expand Down
2 changes: 1 addition & 1 deletion ui/src/lib/gen/mgo_connect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @generated by protoc-gen-connect-es v1.1.3 with parameter "target=ts"
// @generated by protoc-gen-connect-es v1.1.4 with parameter "target=ts"
// @generated from file mgo.proto (package mgo.portfolio.v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
Expand Down
2 changes: 1 addition & 1 deletion ui/src/lib/gen/mgo_pb.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @generated by protoc-gen-es v1.4.2 with parameter "target=ts"
// @generated by protoc-gen-es v1.5.1 with parameter "target=ts"
// @generated from file mgo.proto (package mgo.portfolio.v1, syntax proto3)
/* eslint-disable */
// @ts-nocheck
Expand Down

0 comments on commit b26a51e

Please sign in to comment.