Skip to content

Commit

Permalink
Using a dedicated currency object instead of float (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
oxisto authored Dec 19, 2023
1 parent e1dfcb8 commit 24829d5
Show file tree
Hide file tree
Showing 37 changed files with 1,213 additions and 908 deletions.
14 changes: 7 additions & 7 deletions cli/commands/portfolio.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ func (l *ListPortfolioCmd) Run(s *cli.Session) error {
strings.Repeat("-", 15),
strings.Repeat("-", 15),
15, "Market Value",
15, fmt.Sprintf("%.02f €", snapshot.Msg.TotalMarketValue),
15, snapshot.Msg.TotalMarketValue.Pretty(),
15, "Performance",
15, fmt.Sprintf("%s € (%s %%)",
greenOrRed(snapshot.Msg.TotalProfitOrLoss),
greenOrRed(float64(snapshot.Msg.TotalProfitOrLoss.Value)),
greenOrRed(snapshot.Msg.TotalGains*100),
),
)
Expand Down Expand Up @@ -148,7 +148,7 @@ func (cmd *ShowPortfolioCmd) Run(s *cli.Session) error {
return nil
}

func greenOrRed(f float32) string {
func greenOrRed(f float64) string {
if f < 0 {
return color.RedString("%.02f", f)
} else {
Expand All @@ -160,7 +160,7 @@ 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"`
Amount float64 `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"`
Expand All @@ -175,9 +175,9 @@ func (cmd *CreateTransactionCmd) Run(s *cli.Session) error {
Type: eventTypeFrom(cmd.Type), // eventTypeFrom(cmd.Type)
Amount: cmd.Amount,
Time: timeOrNow(cmd.Time),
Price: cmd.Price,
Fees: cmd.Fees,
Taxes: cmd.Taxes,
Price: portfoliov1.Value(int32(cmd.Price * 100)),
Fees: portfoliov1.Value(int32(cmd.Fees * 100)),
Taxes: portfoliov1.Value(int32(cmd.Taxes * 100)),
},
})

Expand Down
56 changes: 29 additions & 27 deletions finance/calculation.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,24 @@ import (
// list. We basically need to copy the values from the original transaction,
// since we need to modify it.
type fifoTx struct {
// amount of shares in this transaction
amount float32
// value contains the net value of this transaction, i.e., without taxes and fees
value float32
// fees contain any fees associated to this transaction
fees float32
// ppu is the price per unit (amount)
ppu float32
amount float64 // amount of shares in this transaction
value *portfoliov1.Currency // value contains the net value of this transaction, i.e., without taxes and fees
fees *portfoliov1.Currency // fees contain any fees associated to this transaction
ppu *portfoliov1.Currency // ppu is the price per unit (amount)
}

type calculation struct {
Amount float32
Fees float32
Taxes float32
Amount float64
Fees *portfoliov1.Currency
Taxes *portfoliov1.Currency

fifo []*fifoTx
}

func NewCalculation(txs []*portfoliov1.PortfolioEvent) *calculation {
var c calculation
c.Fees = portfoliov1.Zero()
c.Taxes = portfoliov1.Zero()

for _, tx := range txs {
c.Apply(tx)
Expand All @@ -62,7 +60,7 @@ func (c *calculation) Apply(tx *portfoliov1.PortfolioEvent) {
case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY:
// Increase the amount of shares and the fees by the value stored in the
// transaction
c.Fees += tx.Fees
c.Fees.PlusAssign(tx.Fees)
c.Amount += tx.Amount

// Add the transaction to the FIFO list. We need to have a list because
Expand All @@ -72,20 +70,20 @@ func (c *calculation) Apply(tx *portfoliov1.PortfolioEvent) {
c.fifo = append(c.fifo, &fifoTx{
amount: tx.Amount,
ppu: tx.Price,
value: tx.Price * float32(tx.Amount),
value: portfoliov1.Times(tx.Price, tx.Amount),
fees: tx.Fees,
})
case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_DELIVERY_OUTBOUND:
fallthrough
case portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL:
var (
sold float32
sold float64
)

// Increase the fees and taxes by the value stored in the
// transaction
c.Fees += tx.Fees
c.Taxes += tx.Taxes
c.Fees.PlusAssign(tx.Fees)
c.Taxes.PlusAssign(tx.Taxes)

// Store the amount of shares sold in this variable, since we later need
// to decrease it while looping through the FIFO list
Expand Down Expand Up @@ -115,44 +113,48 @@ func (c *calculation) Apply(tx *portfoliov1.PortfolioEvent) {

// Reduce the number of shares in this entry by the sold amount (but
// max it to the item's amount).
n := float32(math.Min(float64(sold), float64(item.amount)))
n := math.Min(float64(sold), float64(item.amount))
item.amount -= n

// Adjust the value with the new amount
item.value = item.ppu * float32(item.amount)
item.value = portfoliov1.Times(item.ppu, item.amount)

// If no shares are left in this FIFO transaction, also remove the
// fees, because they are now associated to the sale and not part of
// the price calculation anymore.
if item.amount <= 0 {
item.fees = 0
item.fees = portfoliov1.Zero()
}

sold -= n
}
}
}

func (c *calculation) NetValue() (f float32) {
func (c *calculation) NetValue() (f *portfoliov1.Currency) {
f = portfoliov1.Zero()

for _, item := range c.fifo {
f += item.value
f.PlusAssign(item.value)
}

return
}

func (c *calculation) GrossValue() (f float32) {
func (c *calculation) GrossValue() (f *portfoliov1.Currency) {
f = portfoliov1.Zero()

for _, item := range c.fifo {
f += (item.value + item.fees)
f.PlusAssign(portfoliov1.Plus(item.value, item.fees))
}

return
}

func (c *calculation) NetPrice() (f float32) {
return c.NetValue() / float32(c.Amount)
func (c *calculation) NetPrice() (f *portfoliov1.Currency) {
return portfoliov1.Divide(c.NetValue(), c.Amount)
}

func (c *calculation) GrossPrice() (f float32) {
return c.GrossValue() / float32(c.Amount)
func (c *calculation) GrossPrice() (f *portfoliov1.Currency) {
return portfoliov1.Divide(c.GrossValue(), c.Amount)
}
38 changes: 19 additions & 19 deletions finance/calculation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,55 +40,55 @@ func TestNewCalculation(t *testing.T) {
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY,
Amount: 5,
Price: 181.10,
Fees: 7.16,
Price: portfoliov1.Value(18110),
Fees: portfoliov1.Value(716),
},
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL,
Amount: 2,
Price: 304.30,
Fees: 6.42,
Taxes: 16.32,
Price: portfoliov1.Value(30430),
Fees: portfoliov1.Value(642),
Taxes: portfoliov1.Value(1632),
},
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY,
Amount: 5,
Price: 290,
Fees: 8.53,
Price: portfoliov1.Value(29000),
Fees: portfoliov1.Value(853),
},
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_SELL,
Amount: 3,
Price: 220,
Fees: 8.45,
Price: portfoliov1.Value(22000),
Fees: portfoliov1.Value(845),
},
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY,
Amount: 5,
Price: 203.30,
Fees: 7.44,
Price: portfoliov1.Value(20330),
Fees: portfoliov1.Value(744),
},
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY,
Amount: 5,
Price: 196.45,
Fees: 7.36,
Price: portfoliov1.Value(19645),
Fees: portfoliov1.Value(736),
},
{
Type: portfoliov1.PortfolioEventType_PORTFOLIO_EVENT_TYPE_BUY,
Amount: 10,
Price: 146.55,
Fees: 8.56,
Price: portfoliov1.Value(14655),
Fees: portfoliov1.Value(856),
},
},
},
want: func(t *testing.T, c *calculation) bool {
return true &&
assert.Equals(t, 25, c.Amount) &&
assert.Equals(t, 4914.25, c.NetValue()) &&
assert.Equals(t, 4946.14, c.GrossValue()) &&
assert.Equals(t, 196.57, c.NetPrice()) &&
assert.Equals(t, 197.84561, c.GrossPrice())
assert.Equals(t, 491425, int(c.NetValue().Value)) &&
assert.Equals(t, 494614, int(c.GrossValue().Value)) &&
assert.Equals(t, 19657, int(c.NetPrice().Value)) &&
assert.Equals(t, 19785, int(c.GrossPrice().Value))
},
},
}
Expand Down
Loading

0 comments on commit 24829d5

Please sign in to comment.