Skip to content

Commit

Permalink
Displaying cash in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
oxisto committed Dec 20, 2023
1 parent b6727fb commit 2e0e0f5
Show file tree
Hide file tree
Showing 18 changed files with 459 additions and 198 deletions.
93 changes: 93 additions & 0 deletions gen/currency.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 Christian Banse
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file is part of The Money Gopher.
package portfoliov1

import (
"fmt"
"math"
)

func Zero() *Currency {
// TODO(oxisto): Somehow make it possible to change default currency
return &Currency{Symbol: "EUR"}
}

func Value(v int32) *Currency {
// TODO(oxisto): Somehow make it possible to change default currency
return &Currency{Symbol: "EUR", Value: v}
}

func (c *Currency) PlusAssign(o *Currency) {
if o != nil {
c.Value += o.Value
}
}

func (c *Currency) MinusAssign(o *Currency) {
if o != nil {
c.Value -= o.Value
}
}

func Plus(a *Currency, b *Currency) *Currency {
return &Currency{
Value: a.Value + b.Value,
Symbol: a.Symbol,
}
}

func (a *Currency) Plus(b *Currency) *Currency {
if b == nil {
return &Currency{
Value: a.Value,
Symbol: a.Symbol,
}
}

return &Currency{
Value: a.Value + b.Value,
Symbol: a.Symbol,
}
}

func Minus(a *Currency, b *Currency) *Currency {
return &Currency{
Value: a.Value - b.Value,
Symbol: a.Symbol,
}
}

func Divide(a *Currency, b float64) *Currency {
return &Currency{
Value: int32(math.Round((float64(a.Value) / b))),
Symbol: a.Symbol,
}
}

func Times(a *Currency, b float64) *Currency {
return &Currency{
Value: int32(math.Round((float64(a.Value) * b))),
Symbol: a.Symbol,
}
}

func (c *Currency) Pretty() string {
return fmt.Sprintf("%.0f %s", float32(c.Value)/100, c.Symbol)
}

func (c *Currency) IsZero() bool {
return c == nil || c.Value == 0
}
162 changes: 90 additions & 72 deletions gen/mgo.pb.go

Large diffs are not rendered by default.

70 changes: 0 additions & 70 deletions gen/portfolio.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
package portfoliov1

import (
"fmt"
"hash/fnv"
"log/slog"
"math"
"strconv"
"time"
)
Expand Down Expand Up @@ -89,71 +87,3 @@ func (ls *ListedSecurity) LogValue() slog.Value {
slog.String("ticker", ls.Ticker),
)
}

func Zero() *Currency {
// TODO(oxisto): Somehow make it possible to change default currency
return &Currency{Symbol: "EUR"}
}

func Value(v int32) *Currency {
// TODO(oxisto): Somehow make it possible to change default currency
return &Currency{Symbol: "EUR", Value: v}
}

func (c *Currency) PlusAssign(o *Currency) {
if o != nil {
c.Value += o.Value
}
}

func (c *Currency) MinusAssign(o *Currency) {
if o != nil {
c.Value -= o.Value
}
}

func Plus(a *Currency, b *Currency) *Currency {
return &Currency{
Value: a.Value + b.Value,
Symbol: a.Symbol,
}
}

func (a *Currency) Plus(b *Currency) *Currency {
if b == nil {
return &Currency{
Value: a.Value,
Symbol: a.Symbol,
}
}

return &Currency{
Value: a.Value + b.Value,
Symbol: a.Symbol,
}
}

func Minus(a *Currency, b *Currency) *Currency {
return &Currency{
Value: a.Value - b.Value,
Symbol: a.Symbol,
}
}

func Divide(a *Currency, b float64) *Currency {
return &Currency{
Value: int32(math.Round((float64(a.Value) / b))),
Symbol: a.Symbol,
}
}

func Times(a *Currency, b float64) *Currency {
return &Currency{
Value: int32(math.Round((float64(a.Value) * b))),
Symbol: a.Symbol,
}
}

func (c *Currency) Pretty() string {
return fmt.Sprintf("%.0f %s", float32(c.Value)/100, c.Symbol)
}
14 changes: 10 additions & 4 deletions mgo.proto
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,27 @@ message PortfolioSnapshot {
// snapshot.
optional google.protobuf.Timestamp first_transaction_time = 3;

// TotalPurchaseValue contains the total purchase value of all positions
// TotalPurchaseValue contains the total purchase value of all asset positions
Currency total_purchase_value = 10;

// TotalMarketValue contains the total market value of all positions
// TotalMarketValue contains the total market value of all asset positions
Currency total_market_value = 11;

// TotalProfitOrLoss contains the total absolute amount of profit or loss in
// this snapshot.
// this snapshot, based on asset value.
Currency total_profit_or_loss = 20;

// TotalGains contains the total relative amount of profit or loss in this
// snapshot.
// snapshot, based on asset value.
double total_gains = 21;

// Cash contains the current amount of cash in the portfolio's bank
// account(s).
Currency cash = 22;

// TotalPortfolioValue contains the amount of cash plus the total market value
// of all assets.
Currency total_portfolio_value = 23;
}

message PortfolioPosition {
Expand Down
11 changes: 10 additions & 1 deletion service/portfolio/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,17 @@ func (svc *service) GetPortfolioSnapshot(ctx context.Context, req *connect.Reque

c := finance.NewCalculation(txs)

if name == "cash" {
// Add deposited/withdrawn cash directly
snap.Cash.PlusAssign(c.Cash)
continue
}

if c.Amount == 0 {
continue
}

// Add to the cash
// Also add cash that is part of a securities' transaction (e.g., sell/buy)
snap.Cash.PlusAssign(c.Cash)

pos := &portfoliov1.PortfolioPosition{
Expand Down Expand Up @@ -124,6 +130,9 @@ func (svc *service) GetPortfolioSnapshot(ctx context.Context, req *connect.Reque
// Calculate total gains
snap.TotalGains = float64(portfoliov1.Minus(snap.TotalMarketValue, snap.TotalPurchaseValue).Value) / float64(snap.TotalPurchaseValue.Value)

// Calculate total portfolio value
snap.TotalPortfolioValue = snap.TotalMarketValue.Plus(snap.Cash)

return connect.NewResponse(snap), nil
}

Expand Down
30 changes: 29 additions & 1 deletion service/portfolio/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package portfolio
import (
"bytes"
"context"
"errors"
"log/slog"

portfoliov1 "github.com/oxisto/money-gopher/gen"
Expand All @@ -33,9 +34,36 @@ var portfolioEventSetter = func(obj *portfoliov1.PortfolioEvent) *portfoliov1.Po
return obj
}

var (
ErrMissingSecurityName = errors.New("the specified transaction type requires a security name")
ErrMissingPrice = errors.New("a transaction requires a price")
ErrMissingAmount = errors.New("the specified transaction type requires an amount")
)

func (svc *service) CreatePortfolioTransaction(ctx context.Context, req *connect.Request[portfoliov1.CreatePortfolioTransactionRequest]) (res *connect.Response[portfoliov1.PortfolioEvent], err error) {
var (
tx *portfoliov1.PortfolioEvent = req.Msg.Transaction
)

// Do some basic validation depending on the type
switch tx.Type {
case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL:
fallthrough
case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY:
if tx.SecurityName == "" {
return nil, connect.NewError(connect.CodeInvalidArgument, ErrMissingSecurityName)
} else if tx.Amount == 0 {
return nil, connect.NewError(connect.CodeInvalidArgument, ErrMissingAmount)
}
}

// We always need a price
if tx.Price.IsZero() {
return nil, connect.NewError(connect.CodeInvalidArgument, ErrMissingPrice)
}

// Create a unique name for the transaction
req.Msg.Transaction.MakeUniqueName()
tx.MakeUniqueName()

slog.Info(
"Creating transaction",
Expand Down
1 change: 0 additions & 1 deletion ui/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
4 changes: 2 additions & 2 deletions ui/src/lib/components/PortfolioBreadcrumb.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@
class="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
aria-hidden="true"
/>
{Object.values(snapshot.positions).length} Positions
{Object.values(snapshot.positions).length} Position(s)
</div>
<div class="mt-2 flex items-center text-sm text-gray-500">
<Icon
src={CurrencyEuro}
class="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
aria-hidden="true"
/>
{currency(snapshot.totalMarketValue, 'EUR')}
{currency(snapshot.totalPortfolioValue)}
</div>
<div class="mt-2 flex items-center text-sm text-gray-500">
<Icon
Expand Down
41 changes: 38 additions & 3 deletions ui/src/lib/components/PortfolioPositionsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@
}
</script>

{{ asc }}
{{ sortBy }}
<div class="-mx-4 mt-8 sm:-mx-0">
<table class="min-w-full divide-y divide-gray-300">
<thead>
Expand Down Expand Up @@ -120,7 +118,8 @@
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Total</th
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
>Total Assets</th
>
<th
scope="col"
Expand Down Expand Up @@ -165,6 +164,42 @@
</div>
</th>
</tr>
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
>
Cash Value
</th>
<th></th>
<th></th>
<th
scope="col"
class="{(snapshot.cash?.value ?? 0) < 0
? 'text-red-500'
: ''} px-3 py-3.5 text-right text-sm font-semibold text-gray-900 lg:table-cell"
>
{currency(snapshot.cash)}
</th>
</tr>
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
>
Total Portfolio Value
</th>
<th></th>
<th></th>
<th
scope="col"
class="{(snapshot.cash?.value ?? 0) < 0
? 'text-red-500'
: ''} px-3 py-3.5 text-right text-sm font-semibold text-gray-900 lg:table-cell"
>
{currency(snapshot.totalPortfolioValue)}
</th>
</tr>
</tfoot>
</table>
</div>
Loading

0 comments on commit 2e0e0f5

Please sign in to comment.