diff --git a/internal/currency/currency.go b/internal/currency/currency.go index 9c6410c..53a5ab4 100644 --- a/internal/currency/currency.go +++ b/internal/currency/currency.go @@ -103,3 +103,10 @@ func GetCurrencyRates(client resty.Client, symbols []string, targetCurrency stri return getCurrencyRatesFromCurrencyPairSymbols(client, currencyPairSymbols) } + +func GetCurrencyRateFromContext(ctx c.Context, fromCurrency string) (float64, string) { + if currencyRate, ok := ctx.Reference.CurrencyRates[fromCurrency]; ok { + return currencyRate.Rate, currencyRate.ToCurrency + } + return 1.0, fromCurrency +} diff --git a/internal/currency/currency_test.go b/internal/currency/currency_test.go index 9c55da5..bb1b7b8 100644 --- a/internal/currency/currency_test.go +++ b/internal/currency/currency_test.go @@ -53,4 +53,52 @@ var _ = Describe("Currency", func() { }) }) }) + + Describe("GetCurrencyRateFromContext", func() { + It("should return identity currency information when a rate is not found in reference data", func() { + inputCtx := c.Context{ + Reference: c.Reference{ + CurrencyRates: c.CurrencyRates{ + "USD": c.CurrencyRate{ + FromCurrency: "USD", + ToCurrency: "EUR", + Rate: 4, + }, + "GBP": c.CurrencyRate{ + FromCurrency: "GBP", + ToCurrency: "EUR", + Rate: 2, + }, + }, + }, + } + outputRate, outputCurrencyCode := GetCurrencyRateFromContext(inputCtx, "EUR") + Expect(outputRate).To(Equal(1.0)) + Expect(outputCurrencyCode).To(Equal("EUR")) + }) + }) + + When("there is a matching currency in reference data", func() { + It("should return information needed to convert currency", func() { + inputCtx := c.Context{ + Reference: c.Reference{ + CurrencyRates: c.CurrencyRates{ + "USD": c.CurrencyRate{ + FromCurrency: "USD", + ToCurrency: "EUR", + Rate: 1.25, + }, + "GBP": c.CurrencyRate{ + FromCurrency: "GBP", + ToCurrency: "EUR", + Rate: 2, + }, + }, + }, + } + outputRate, outputCurrencyCode := GetCurrencyRateFromContext(inputCtx, "USD") + Expect(outputRate).To(Equal(1.25)) + Expect(outputCurrencyCode).To(Equal("EUR")) + }) + }) }) diff --git a/internal/position/position.go b/internal/position/position.go index b072418..1c77072 100644 --- a/internal/position/position.go +++ b/internal/position/position.go @@ -2,6 +2,7 @@ package position import ( c "github.com/achannarasappa/ticker/internal/common" + "github.com/achannarasappa/ticker/internal/currency" . "github.com/achannarasappa/ticker/internal/quote" "github.com/novalagung/gubrak/v2" @@ -15,6 +16,7 @@ type Position struct { TotalChange float64 TotalChangePercent float64 Currency string + CurrencyConverted string } type PositionSummary struct { @@ -79,24 +81,24 @@ func GetSymbols(symbols []string, aggregatedLots map[string]AggregatedLot) []str } -func GetPositions(aggregatedLots map[string]AggregatedLot) func([]Quote) map[string]Position { +func GetPositions(ctx c.Context, aggregatedLots map[string]AggregatedLot) func([]Quote) map[string]Position { return func(quotes []Quote) map[string]Position { positions := gubrak. From(quotes). Reduce(func(acc []Position, quote Quote) []Position { if aggLot, ok := aggregatedLots[quote.Symbol]; ok { - dayChange := quote.Change * aggLot.Quantity - totalChange := (quote.Price * aggLot.Quantity) - aggLot.Cost - valuePreviousClose := quote.RegularMarketPreviousClose * aggLot.Quantity + currencyRate, currencyCode := currency.GetCurrencyRateFromContext(ctx, quote.Currency) + totalChange := (quote.Price * aggLot.Quantity) - (aggLot.Cost * currencyRate) return append(acc, Position{ AggregatedLot: aggLot, Value: quote.Price * aggLot.Quantity, - DayChange: dayChange, - DayChangePercent: (dayChange / valuePreviousClose) * 100, + DayChange: quote.Change * aggLot.Quantity, + DayChangePercent: quote.ChangePercent, TotalChange: totalChange, - TotalChangePercent: (totalChange / aggLot.Cost) * 100, + TotalChangePercent: (totalChange / (aggLot.Cost * currencyRate)) * 100, Currency: quote.Currency, + CurrencyConverted: currencyCode, }) } return acc @@ -114,15 +116,13 @@ func GetPositionSummary(ctx c.Context, positions map[string]Position) PositionSu positionValueCost := gubrak.From(positions). Reduce(func(acc PositionSummary, position Position, key string) PositionSummary { - if currencyRate, ok := ctx.Reference.CurrencyRates[position.Currency]; ok { - acc.Value += (position.Value * currencyRate.Rate) - acc.Cost += (position.Cost * currencyRate.Rate) - acc.DayChange += (position.DayChange * currencyRate.Rate) - return acc + currencyRate := 1.0 + if ctx.Config.Currency == "" { + currencyRate, _ = currency.GetCurrencyRateFromContext(ctx, position.Currency) } - acc.Value += position.Value - acc.Cost += position.Cost - acc.DayChange += position.DayChange + acc.Value += (position.Value * currencyRate) + acc.Cost += (position.Cost * currencyRate) + acc.DayChange += (position.DayChange * currencyRate) return acc }, PositionSummary{}). Result() diff --git a/internal/position/position_test.go b/internal/position/position_test.go index ec3997e..0fbedd3 100644 --- a/internal/position/position_test.go +++ b/internal/position/position_test.go @@ -77,7 +77,8 @@ var _ = Describe("Position", func() { Change: 0.0, }, } - output := GetPositions(inputAggregatedLots)(inputQuotes) + inputCtx := c.Context{} + output := GetPositions(inputCtx, inputAggregatedLots)(inputQuotes) expected := map[string]Position{ "ARKW": { AggregatedLot: AggregatedLot{ @@ -87,7 +88,6 @@ var _ = Describe("Position", func() { }, Value: 8000, DayChange: 2000, - DayChangePercent: 50, TotalChange: 4000, TotalChangePercent: 100, }, diff --git a/internal/quote/quote.go b/internal/quote/quote.go index c6c723f..6cbace1 100644 --- a/internal/quote/quote.go +++ b/internal/quote/quote.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + c "github.com/achannarasappa/ticker/internal/common" + "github.com/achannarasappa/ticker/internal/currency" "github.com/go-resty/resty/v2" ) @@ -35,6 +37,7 @@ type Quote struct { ChangePercent float64 IsActive bool IsRegularTradingSession bool + CurrencyConverted string } type Response struct { @@ -44,96 +47,105 @@ type Response struct { } `json:"quoteResponse"` } -func transformResponseQuote(responseQuote ResponseQuote) Quote { +func transformResponseQuote(ctx c.Context, responseQuote ResponseQuote) Quote { + + currencyRate, currencyCode := currency.GetCurrencyRateFromContext(ctx, responseQuote.Currency) if responseQuote.MarketState == "REGULAR" { return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.RegularMarketPrice, - Change: responseQuote.RegularMarketChange, + Price: responseQuote.RegularMarketPrice * currencyRate, + Change: (responseQuote.RegularMarketChange) * currencyRate, ChangePercent: responseQuote.RegularMarketChangePercent, IsActive: true, IsRegularTradingSession: true, + CurrencyConverted: currencyCode, } } if responseQuote.MarketState == "POST" && responseQuote.PostMarketPrice == 0.0 { return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.RegularMarketPrice, - Change: responseQuote.RegularMarketChange, + Price: responseQuote.RegularMarketPrice * currencyRate, + Change: (responseQuote.RegularMarketChange) * currencyRate, ChangePercent: responseQuote.RegularMarketChangePercent, IsActive: true, IsRegularTradingSession: false, + CurrencyConverted: currencyCode, } } if responseQuote.MarketState == "PRE" && responseQuote.PreMarketPrice == 0.0 { return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.RegularMarketPrice, - Change: responseQuote.RegularMarketChange, + Price: responseQuote.RegularMarketPrice * currencyRate, + Change: (responseQuote.RegularMarketChange) * currencyRate, ChangePercent: responseQuote.RegularMarketChangePercent, IsActive: false, IsRegularTradingSession: false, + CurrencyConverted: currencyCode, } } if responseQuote.MarketState == "POST" { return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.PostMarketPrice, - Change: responseQuote.PostMarketChange + responseQuote.RegularMarketChange, + Price: responseQuote.PostMarketPrice * currencyRate, + Change: (responseQuote.PostMarketChange + responseQuote.RegularMarketChange) * currencyRate, ChangePercent: responseQuote.PostMarketChangePercent + responseQuote.RegularMarketChangePercent, IsActive: true, IsRegularTradingSession: false, + CurrencyConverted: currencyCode, } } if responseQuote.MarketState == "PRE" { return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.PreMarketPrice, - Change: responseQuote.PreMarketChange, + Price: responseQuote.PreMarketPrice * currencyRate, + Change: (responseQuote.PreMarketChange) * currencyRate, ChangePercent: responseQuote.PreMarketChangePercent, IsActive: true, IsRegularTradingSession: false, + CurrencyConverted: currencyCode, } } if responseQuote.PostMarketPrice != 0.0 { return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.PostMarketPrice, - Change: responseQuote.PostMarketChange + responseQuote.RegularMarketChange, + Price: responseQuote.PostMarketPrice * currencyRate, + Change: (responseQuote.PostMarketChange + responseQuote.RegularMarketChange) * currencyRate, ChangePercent: responseQuote.PostMarketChangePercent + responseQuote.RegularMarketChangePercent, IsActive: false, IsRegularTradingSession: false, + CurrencyConverted: currencyCode, } } return Quote{ ResponseQuote: responseQuote, - Price: responseQuote.RegularMarketPrice, - Change: responseQuote.RegularMarketChange, + Price: responseQuote.RegularMarketPrice * currencyRate, + Change: (responseQuote.RegularMarketChange) * currencyRate, ChangePercent: responseQuote.RegularMarketChangePercent, IsActive: false, IsRegularTradingSession: false, + CurrencyConverted: currencyCode, } } -func transformResponseQuotes(responseQuotes []ResponseQuote) []Quote { +func transformResponseQuotes(ctx c.Context, responseQuotes []ResponseQuote) []Quote { quotes := make([]Quote, 0) for _, responseQuote := range responseQuotes { - quotes = append(quotes, transformResponseQuote(responseQuote)) + quotes = append(quotes, transformResponseQuote(ctx, responseQuote)) } return quotes } -func GetQuotes(client resty.Client, symbols []string) func() []Quote { +func GetQuotes(ctx c.Context, client resty.Client, symbols []string) func() []Quote { return func() []Quote { symbolsString := strings.Join(symbols, ",") url := fmt.Sprintf("https://query1.finance.yahoo.com/v7/finance/quote?lang=en-US®ion=US&corsDomain=finance.yahoo.com&symbols=%s", symbolsString) @@ -141,6 +153,6 @@ func GetQuotes(client resty.Client, symbols []string) func() []Quote { SetResult(Response{}). Get(url) - return transformResponseQuotes((res.Result().(*Response)).QuoteResponse.Quotes) + return transformResponseQuotes(ctx, (res.Result().(*Response)).QuoteResponse.Quotes) } } diff --git a/internal/quote/quote_test.go b/internal/quote/quote_test.go index 557ad5f..b9c1b27 100644 --- a/internal/quote/quote_test.go +++ b/internal/quote/quote_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + c "github.com/achannarasappa/ticker/internal/common" . "github.com/achannarasappa/ticker/internal/quote" ) @@ -37,7 +38,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() expected := []Quote{ { ResponseQuote: ResponseQuote{ @@ -88,7 +90,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() expected := []Quote{ { ResponseQuote: ResponseQuote{ @@ -139,7 +142,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() Expect(output[0].Price).To(Equal(84.98)) Expect(output[0].Change).To(Equal(3.0800018)) Expect(output[0].ChangePercent).To(Equal(3.7606857)) @@ -178,7 +182,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() expected := []Quote{ { ResponseQuote: ResponseQuote{ @@ -229,7 +234,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() expectedPrice := 84.98 expectedChange := 3.0800018 expectedChangePercent := 3.7606857 @@ -266,7 +272,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() Expect(output[0].Price).To(Equal(84.98)) Expect(output[0].Change).To(Equal(3.0800018)) Expect(output[0].ChangePercent).To(Equal(3.7606857)) @@ -303,7 +310,8 @@ var _ = Describe("Quote", func() { return resp, nil }) - output := GetQuotes(*client, []string{"NET"})() + inputCtx := c.Context{} + output := GetQuotes(inputCtx, *client, []string{"NET"})() Expect(output[0].Price).To(Equal(86.02)) Expect(output[0].Change).To(Equal(4.1199951)) Expect(output[0].ChangePercent).To(Equal(4.9844951)) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 4b6e302..cbc032a 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -64,8 +64,8 @@ func NewModel(dep c.Dependencies, ctx c.Context) Model { headerHeight: getVerticalMargin(ctx.Config), ready: false, requestInterval: ctx.Config.RefreshInterval, - getQuotes: quote.GetQuotes(*dep.HttpClient, symbols), - getPositions: position.GetPositions(aggregatedLots), + getQuotes: quote.GetQuotes(ctx, *dep.HttpClient, symbols), + getPositions: position.GetPositions(ctx, aggregatedLots), watchlist: watchlist.NewModel(ctx), summary: summary.NewModel(), }