diff --git a/gen/currency.go b/gen/currency.go new file mode 100644 index 00000000..3772e74f --- /dev/null +++ b/gen/currency.go @@ -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 +} diff --git a/gen/mgo.pb.go b/gen/mgo.pb.go index 134e4216..eeb0c359 100644 --- a/gen/mgo.pb.go +++ b/gen/mgo.pb.go @@ -1029,17 +1029,22 @@ type PortfolioSnapshot struct { // FirstTransactionTime is the time of the first transaction with the // snapshot. FirstTransactionTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=first_transaction_time,json=firstTransactionTime,proto3,oneof" json:"first_transaction_time,omitempty"` - // TotalPurchaseValue contains the total purchase value of all positions + // TotalPurchaseValue contains the total purchase value of all asset positions TotalPurchaseValue *Currency `protobuf:"bytes,10,opt,name=total_purchase_value,json=totalPurchaseValue,proto3" json:"total_purchase_value,omitempty"` - // TotalMarketValue contains the total market value of all positions + // TotalMarketValue contains the total market value of all asset positions TotalMarketValue *Currency `protobuf:"bytes,11,opt,name=total_market_value,json=totalMarketValue,proto3" json:"total_market_value,omitempty"` // TotalProfitOrLoss contains the total absolute amount of profit or loss in - // this snapshot. + // this snapshot, based on asset value. TotalProfitOrLoss *Currency `protobuf:"bytes,20,opt,name=total_profit_or_loss,json=totalProfitOrLoss,proto3" json:"total_profit_or_loss,omitempty"` // TotalGains contains the total relative amount of profit or loss in this - // snapshot. - TotalGains float64 `protobuf:"fixed64,21,opt,name=total_gains,json=totalGains,proto3" json:"total_gains,omitempty"` - Cash *Currency `protobuf:"bytes,22,opt,name=cash,proto3" json:"cash,omitempty"` + // snapshot, based on asset value. + TotalGains float64 `protobuf:"fixed64,21,opt,name=total_gains,json=totalGains,proto3" json:"total_gains,omitempty"` + // Cash contains the current amount of cash in the portfolio's bank + // account(s). + Cash *Currency `protobuf:"bytes,22,opt,name=cash,proto3" json:"cash,omitempty"` + // TotalPortfolioValue contains the amount of cash plus the total market value + // of all assets. + TotalPortfolioValue *Currency `protobuf:"bytes,23,opt,name=total_portfolio_value,json=totalPortfolioValue,proto3" json:"total_portfolio_value,omitempty"` } func (x *PortfolioSnapshot) Reset() { @@ -1130,6 +1135,13 @@ func (x *PortfolioSnapshot) GetCash() *Currency { return nil } +func (x *PortfolioSnapshot) GetTotalPortfolioValue() *Currency { + if x != nil { + return x.TotalPortfolioValue + } + return nil +} + type PortfolioPosition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2052,7 +2064,7 @@ var file_mgo_proto_rawDesc = []byte{ 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xa0, 0x05, 0x0a, 0x11, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, + 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xf0, 0x05, 0x0a, 0x11, 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, @@ -2086,7 +2098,12 @@ var file_mgo_proto_rawDesc = []byte{ 0x6f, 0x74, 0x61, 0x6c, 0x47, 0x61, 0x69, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x04, 0x63, 0x61, 0x73, 0x68, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x63, 0x79, 0x52, 0x04, 0x63, 0x61, 0x73, 0x68, 0x1a, 0x61, 0x0a, 0x0e, 0x50, 0x6f, 0x73, + 0x6e, 0x63, 0x79, 0x52, 0x04, 0x63, 0x61, 0x73, 0x68, 0x12, 0x4e, 0x0a, 0x15, 0x74, 0x6f, 0x74, + 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x67, 0x6f, 0x2e, 0x70, + 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x63, 0x79, 0x52, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x66, + 0x6f, 0x6c, 0x69, 0x6f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x61, 0x0a, 0x0e, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6d, @@ -2460,70 +2477,71 @@ var file_mgo_proto_depIdxs = []int32{ 1, // 15: mgo.portfolio.v1.PortfolioSnapshot.total_market_value:type_name -> mgo.portfolio.v1.Currency 1, // 16: mgo.portfolio.v1.PortfolioSnapshot.total_profit_or_loss:type_name -> mgo.portfolio.v1.Currency 1, // 17: mgo.portfolio.v1.PortfolioSnapshot.cash:type_name -> mgo.portfolio.v1.Currency - 22, // 18: mgo.portfolio.v1.PortfolioPosition.security:type_name -> mgo.portfolio.v1.Security - 1, // 19: mgo.portfolio.v1.PortfolioPosition.purchase_value:type_name -> mgo.portfolio.v1.Currency - 1, // 20: mgo.portfolio.v1.PortfolioPosition.purchase_price:type_name -> mgo.portfolio.v1.Currency - 1, // 21: mgo.portfolio.v1.PortfolioPosition.market_value:type_name -> mgo.portfolio.v1.Currency - 1, // 22: mgo.portfolio.v1.PortfolioPosition.market_price:type_name -> mgo.portfolio.v1.Currency - 1, // 23: mgo.portfolio.v1.PortfolioPosition.total_fees:type_name -> mgo.portfolio.v1.Currency - 1, // 24: mgo.portfolio.v1.PortfolioPosition.profit_or_loss:type_name -> mgo.portfolio.v1.Currency - 0, // 25: mgo.portfolio.v1.PortfolioEvent.type:type_name -> mgo.portfolio.v1.PortfolioEventType - 35, // 26: mgo.portfolio.v1.PortfolioEvent.time:type_name -> google.protobuf.Timestamp - 1, // 27: mgo.portfolio.v1.PortfolioEvent.price:type_name -> mgo.portfolio.v1.Currency - 1, // 28: mgo.portfolio.v1.PortfolioEvent.fees:type_name -> mgo.portfolio.v1.Currency - 1, // 29: mgo.portfolio.v1.PortfolioEvent.taxes:type_name -> mgo.portfolio.v1.Currency - 23, // 30: mgo.portfolio.v1.Security.listed_on:type_name -> mgo.portfolio.v1.ListedSecurity - 1, // 31: mgo.portfolio.v1.ListedSecurity.latest_quote:type_name -> mgo.portfolio.v1.Currency - 35, // 32: mgo.portfolio.v1.ListedSecurity.latest_quote_timestamp:type_name -> google.protobuf.Timestamp - 33, // 33: mgo.portfolio.v1.ListSecuritiesRequest.filter:type_name -> mgo.portfolio.v1.ListSecuritiesRequest.Filter - 22, // 34: mgo.portfolio.v1.ListSecuritiesResponse.securities:type_name -> mgo.portfolio.v1.Security - 22, // 35: mgo.portfolio.v1.CreateSecurityRequest.security:type_name -> mgo.portfolio.v1.Security - 22, // 36: mgo.portfolio.v1.UpdateSecurityRequest.security:type_name -> mgo.portfolio.v1.Security - 34, // 37: mgo.portfolio.v1.UpdateSecurityRequest.update_mask:type_name -> google.protobuf.FieldMask - 20, // 38: mgo.portfolio.v1.PortfolioSnapshot.PositionsEntry.value:type_name -> mgo.portfolio.v1.PortfolioPosition - 2, // 39: mgo.portfolio.v1.PortfolioService.CreatePortfolio:input_type -> mgo.portfolio.v1.CreatePortfolioRequest - 3, // 40: mgo.portfolio.v1.PortfolioService.ListPortfolios:input_type -> mgo.portfolio.v1.ListPortfoliosRequest - 5, // 41: mgo.portfolio.v1.PortfolioService.GetPortfolio:input_type -> mgo.portfolio.v1.GetPortfolioRequest - 6, // 42: mgo.portfolio.v1.PortfolioService.UpdatePortfolio:input_type -> mgo.portfolio.v1.UpdatePortfolioRequest - 7, // 43: mgo.portfolio.v1.PortfolioService.DeletePortfolio:input_type -> mgo.portfolio.v1.DeletePortfolioRequest - 8, // 44: mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot:input_type -> mgo.portfolio.v1.GetPortfolioSnapshotRequest - 9, // 45: mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction:input_type -> mgo.portfolio.v1.CreatePortfolioTransactionRequest - 10, // 46: mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction:input_type -> mgo.portfolio.v1.GetPortfolioTransactionRequest - 11, // 47: mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions:input_type -> mgo.portfolio.v1.ListPortfolioTransactionsRequest - 13, // 48: mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction:input_type -> mgo.portfolio.v1.UpdatePortfolioTransactionRequest - 14, // 49: mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction:input_type -> mgo.portfolio.v1.DeletePortfolioTransactionRequest - 15, // 50: mgo.portfolio.v1.PortfolioService.ImportTransactions:input_type -> mgo.portfolio.v1.ImportTransactionsRequest - 16, // 51: mgo.portfolio.v1.PortfolioService.CreateBankAccount:input_type -> mgo.portfolio.v1.CreateBankAccountRequest - 24, // 52: mgo.portfolio.v1.SecuritiesService.ListSecurities:input_type -> mgo.portfolio.v1.ListSecuritiesRequest - 26, // 53: mgo.portfolio.v1.SecuritiesService.GetSecurity:input_type -> mgo.portfolio.v1.GetSecurityRequest - 27, // 54: mgo.portfolio.v1.SecuritiesService.CreateSecurity:input_type -> mgo.portfolio.v1.CreateSecurityRequest - 28, // 55: mgo.portfolio.v1.SecuritiesService.UpdateSecurity:input_type -> mgo.portfolio.v1.UpdateSecurityRequest - 29, // 56: mgo.portfolio.v1.SecuritiesService.DeleteSecurity:input_type -> mgo.portfolio.v1.DeleteSecurityRequest - 30, // 57: mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate:input_type -> mgo.portfolio.v1.TriggerQuoteUpdateRequest - 17, // 58: mgo.portfolio.v1.PortfolioService.CreatePortfolio:output_type -> mgo.portfolio.v1.Portfolio - 4, // 59: mgo.portfolio.v1.PortfolioService.ListPortfolios:output_type -> mgo.portfolio.v1.ListPortfoliosResponse - 17, // 60: mgo.portfolio.v1.PortfolioService.GetPortfolio:output_type -> mgo.portfolio.v1.Portfolio - 17, // 61: mgo.portfolio.v1.PortfolioService.UpdatePortfolio:output_type -> mgo.portfolio.v1.Portfolio - 36, // 62: mgo.portfolio.v1.PortfolioService.DeletePortfolio:output_type -> google.protobuf.Empty - 19, // 63: mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot:output_type -> mgo.portfolio.v1.PortfolioSnapshot - 21, // 64: mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent - 21, // 65: mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent - 12, // 66: mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions:output_type -> mgo.portfolio.v1.ListPortfolioTransactionsResponse - 21, // 67: mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent - 36, // 68: mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction:output_type -> google.protobuf.Empty - 36, // 69: mgo.portfolio.v1.PortfolioService.ImportTransactions:output_type -> google.protobuf.Empty - 18, // 70: mgo.portfolio.v1.PortfolioService.CreateBankAccount:output_type -> mgo.portfolio.v1.BankAccount - 25, // 71: mgo.portfolio.v1.SecuritiesService.ListSecurities:output_type -> mgo.portfolio.v1.ListSecuritiesResponse - 22, // 72: mgo.portfolio.v1.SecuritiesService.GetSecurity:output_type -> mgo.portfolio.v1.Security - 22, // 73: mgo.portfolio.v1.SecuritiesService.CreateSecurity:output_type -> mgo.portfolio.v1.Security - 22, // 74: mgo.portfolio.v1.SecuritiesService.UpdateSecurity:output_type -> mgo.portfolio.v1.Security - 36, // 75: mgo.portfolio.v1.SecuritiesService.DeleteSecurity:output_type -> google.protobuf.Empty - 31, // 76: mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate:output_type -> mgo.portfolio.v1.TriggerQuoteUpdateResponse - 58, // [58:77] is the sub-list for method output_type - 39, // [39:58] is the sub-list for method input_type - 39, // [39:39] is the sub-list for extension type_name - 39, // [39:39] is the sub-list for extension extendee - 0, // [0:39] is the sub-list for field type_name + 1, // 18: mgo.portfolio.v1.PortfolioSnapshot.total_portfolio_value:type_name -> mgo.portfolio.v1.Currency + 22, // 19: mgo.portfolio.v1.PortfolioPosition.security:type_name -> mgo.portfolio.v1.Security + 1, // 20: mgo.portfolio.v1.PortfolioPosition.purchase_value:type_name -> mgo.portfolio.v1.Currency + 1, // 21: mgo.portfolio.v1.PortfolioPosition.purchase_price:type_name -> mgo.portfolio.v1.Currency + 1, // 22: mgo.portfolio.v1.PortfolioPosition.market_value:type_name -> mgo.portfolio.v1.Currency + 1, // 23: mgo.portfolio.v1.PortfolioPosition.market_price:type_name -> mgo.portfolio.v1.Currency + 1, // 24: mgo.portfolio.v1.PortfolioPosition.total_fees:type_name -> mgo.portfolio.v1.Currency + 1, // 25: mgo.portfolio.v1.PortfolioPosition.profit_or_loss:type_name -> mgo.portfolio.v1.Currency + 0, // 26: mgo.portfolio.v1.PortfolioEvent.type:type_name -> mgo.portfolio.v1.PortfolioEventType + 35, // 27: mgo.portfolio.v1.PortfolioEvent.time:type_name -> google.protobuf.Timestamp + 1, // 28: mgo.portfolio.v1.PortfolioEvent.price:type_name -> mgo.portfolio.v1.Currency + 1, // 29: mgo.portfolio.v1.PortfolioEvent.fees:type_name -> mgo.portfolio.v1.Currency + 1, // 30: mgo.portfolio.v1.PortfolioEvent.taxes:type_name -> mgo.portfolio.v1.Currency + 23, // 31: mgo.portfolio.v1.Security.listed_on:type_name -> mgo.portfolio.v1.ListedSecurity + 1, // 32: mgo.portfolio.v1.ListedSecurity.latest_quote:type_name -> mgo.portfolio.v1.Currency + 35, // 33: mgo.portfolio.v1.ListedSecurity.latest_quote_timestamp:type_name -> google.protobuf.Timestamp + 33, // 34: mgo.portfolio.v1.ListSecuritiesRequest.filter:type_name -> mgo.portfolio.v1.ListSecuritiesRequest.Filter + 22, // 35: mgo.portfolio.v1.ListSecuritiesResponse.securities:type_name -> mgo.portfolio.v1.Security + 22, // 36: mgo.portfolio.v1.CreateSecurityRequest.security:type_name -> mgo.portfolio.v1.Security + 22, // 37: mgo.portfolio.v1.UpdateSecurityRequest.security:type_name -> mgo.portfolio.v1.Security + 34, // 38: mgo.portfolio.v1.UpdateSecurityRequest.update_mask:type_name -> google.protobuf.FieldMask + 20, // 39: mgo.portfolio.v1.PortfolioSnapshot.PositionsEntry.value:type_name -> mgo.portfolio.v1.PortfolioPosition + 2, // 40: mgo.portfolio.v1.PortfolioService.CreatePortfolio:input_type -> mgo.portfolio.v1.CreatePortfolioRequest + 3, // 41: mgo.portfolio.v1.PortfolioService.ListPortfolios:input_type -> mgo.portfolio.v1.ListPortfoliosRequest + 5, // 42: mgo.portfolio.v1.PortfolioService.GetPortfolio:input_type -> mgo.portfolio.v1.GetPortfolioRequest + 6, // 43: mgo.portfolio.v1.PortfolioService.UpdatePortfolio:input_type -> mgo.portfolio.v1.UpdatePortfolioRequest + 7, // 44: mgo.portfolio.v1.PortfolioService.DeletePortfolio:input_type -> mgo.portfolio.v1.DeletePortfolioRequest + 8, // 45: mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot:input_type -> mgo.portfolio.v1.GetPortfolioSnapshotRequest + 9, // 46: mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction:input_type -> mgo.portfolio.v1.CreatePortfolioTransactionRequest + 10, // 47: mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction:input_type -> mgo.portfolio.v1.GetPortfolioTransactionRequest + 11, // 48: mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions:input_type -> mgo.portfolio.v1.ListPortfolioTransactionsRequest + 13, // 49: mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction:input_type -> mgo.portfolio.v1.UpdatePortfolioTransactionRequest + 14, // 50: mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction:input_type -> mgo.portfolio.v1.DeletePortfolioTransactionRequest + 15, // 51: mgo.portfolio.v1.PortfolioService.ImportTransactions:input_type -> mgo.portfolio.v1.ImportTransactionsRequest + 16, // 52: mgo.portfolio.v1.PortfolioService.CreateBankAccount:input_type -> mgo.portfolio.v1.CreateBankAccountRequest + 24, // 53: mgo.portfolio.v1.SecuritiesService.ListSecurities:input_type -> mgo.portfolio.v1.ListSecuritiesRequest + 26, // 54: mgo.portfolio.v1.SecuritiesService.GetSecurity:input_type -> mgo.portfolio.v1.GetSecurityRequest + 27, // 55: mgo.portfolio.v1.SecuritiesService.CreateSecurity:input_type -> mgo.portfolio.v1.CreateSecurityRequest + 28, // 56: mgo.portfolio.v1.SecuritiesService.UpdateSecurity:input_type -> mgo.portfolio.v1.UpdateSecurityRequest + 29, // 57: mgo.portfolio.v1.SecuritiesService.DeleteSecurity:input_type -> mgo.portfolio.v1.DeleteSecurityRequest + 30, // 58: mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate:input_type -> mgo.portfolio.v1.TriggerQuoteUpdateRequest + 17, // 59: mgo.portfolio.v1.PortfolioService.CreatePortfolio:output_type -> mgo.portfolio.v1.Portfolio + 4, // 60: mgo.portfolio.v1.PortfolioService.ListPortfolios:output_type -> mgo.portfolio.v1.ListPortfoliosResponse + 17, // 61: mgo.portfolio.v1.PortfolioService.GetPortfolio:output_type -> mgo.portfolio.v1.Portfolio + 17, // 62: mgo.portfolio.v1.PortfolioService.UpdatePortfolio:output_type -> mgo.portfolio.v1.Portfolio + 36, // 63: mgo.portfolio.v1.PortfolioService.DeletePortfolio:output_type -> google.protobuf.Empty + 19, // 64: mgo.portfolio.v1.PortfolioService.GetPortfolioSnapshot:output_type -> mgo.portfolio.v1.PortfolioSnapshot + 21, // 65: mgo.portfolio.v1.PortfolioService.CreatePortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent + 21, // 66: mgo.portfolio.v1.PortfolioService.GetPortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent + 12, // 67: mgo.portfolio.v1.PortfolioService.ListPortfolioTransactions:output_type -> mgo.portfolio.v1.ListPortfolioTransactionsResponse + 21, // 68: mgo.portfolio.v1.PortfolioService.UpdatePortfolioTransaction:output_type -> mgo.portfolio.v1.PortfolioEvent + 36, // 69: mgo.portfolio.v1.PortfolioService.DeletePortfolioTransaction:output_type -> google.protobuf.Empty + 36, // 70: mgo.portfolio.v1.PortfolioService.ImportTransactions:output_type -> google.protobuf.Empty + 18, // 71: mgo.portfolio.v1.PortfolioService.CreateBankAccount:output_type -> mgo.portfolio.v1.BankAccount + 25, // 72: mgo.portfolio.v1.SecuritiesService.ListSecurities:output_type -> mgo.portfolio.v1.ListSecuritiesResponse + 22, // 73: mgo.portfolio.v1.SecuritiesService.GetSecurity:output_type -> mgo.portfolio.v1.Security + 22, // 74: mgo.portfolio.v1.SecuritiesService.CreateSecurity:output_type -> mgo.portfolio.v1.Security + 22, // 75: mgo.portfolio.v1.SecuritiesService.UpdateSecurity:output_type -> mgo.portfolio.v1.Security + 36, // 76: mgo.portfolio.v1.SecuritiesService.DeleteSecurity:output_type -> google.protobuf.Empty + 31, // 77: mgo.portfolio.v1.SecuritiesService.TriggerSecurityQuoteUpdate:output_type -> mgo.portfolio.v1.TriggerQuoteUpdateResponse + 59, // [59:78] is the sub-list for method output_type + 40, // [40:59] is the sub-list for method input_type + 40, // [40:40] is the sub-list for extension type_name + 40, // [40:40] is the sub-list for extension extendee + 0, // [0:40] is the sub-list for field type_name } func init() { file_mgo_proto_init() } diff --git a/gen/portfolio.go b/gen/portfolio.go index 4bf96e3a..51da4595 100644 --- a/gen/portfolio.go +++ b/gen/portfolio.go @@ -16,10 +16,8 @@ package portfoliov1 import ( - "fmt" "hash/fnv" "log/slog" - "math" "strconv" "time" ) @@ -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) -} diff --git a/mgo.proto b/mgo.proto index 75d07c4c..47cfd231 100644 --- a/mgo.proto +++ b/mgo.proto @@ -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 { diff --git a/service/portfolio/snapshot.go b/service/portfolio/snapshot.go index d318a239..01ec83c4 100644 --- a/service/portfolio/snapshot.go +++ b/service/portfolio/snapshot.go @@ -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{ @@ -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 } diff --git a/service/portfolio/transactions.go b/service/portfolio/transactions.go index edd7710d..b3b77e41 100644 --- a/service/portfolio/transactions.go +++ b/service/portfolio/transactions.go @@ -19,6 +19,7 @@ package portfolio import ( "bytes" "context" + "errors" "log/slog" portfoliov1 "github.com/oxisto/money-gopher/gen" @@ -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", diff --git a/ui/.prettierrc b/ui/.prettierrc index c60a8c10..8bc6e864 100644 --- a/ui/.prettierrc +++ b/ui/.prettierrc @@ -4,6 +4,5 @@ "trailingComma": "none", "printWidth": 100, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "pluginSearchDirs": ["."], "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/ui/src/lib/components/PortfolioBreadcrumb.svelte b/ui/src/lib/components/PortfolioBreadcrumb.svelte index c49ee790..b01a6588 100644 --- a/ui/src/lib/components/PortfolioBreadcrumb.svelte +++ b/ui/src/lib/components/PortfolioBreadcrumb.svelte @@ -63,7 +63,7 @@ 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)
Total | Total Assets|||
---|---|---|---|
+ Cash Value + | ++ | + | + {currency(snapshot.cash)} + | +
+ Total Portfolio Value + | ++ | + | + {currency(snapshot.totalPortfolioValue)} + | +