Skip to content

Commit

Permalink
feat: added currency conversion for quotes and positions
Browse files Browse the repository at this point in the history
  • Loading branch information
achannarasappa committed Feb 14, 2021
1 parent ffae3f3 commit 81b07d7
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 45 deletions.
7 changes: 7 additions & 0 deletions internal/currency/currency.go
Expand Up @@ -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
}
48 changes: 48 additions & 0 deletions internal/currency/currency_test.go
Expand Up @@ -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"))
})
})
})
30 changes: 15 additions & 15 deletions internal/position/position.go
Expand Up @@ -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"
Expand All @@ -15,6 +16,7 @@ type Position struct {
TotalChange float64
TotalChangePercent float64
Currency string
CurrencyConverted string
}

type PositionSummary struct {
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions internal/position/position_test.go
Expand Up @@ -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{
Expand All @@ -87,7 +88,6 @@ var _ = Describe("Position", func() {
},
Value: 8000,
DayChange: 2000,
DayChangePercent: 50,
TotalChange: 4000,
TotalChangePercent: 100,
},
Expand Down
50 changes: 31 additions & 19 deletions internal/quote/quote.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -35,6 +37,7 @@ type Quote struct {
ChangePercent float64
IsActive bool
IsRegularTradingSession bool
CurrencyConverted string
}

type Response struct {
Expand All @@ -44,103 +47,112 @@ 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&region=US&corsDomain=finance.yahoo.com&symbols=%s", symbolsString)
res, _ := client.R().
SetResult(Response{}).
Get(url)

return transformResponseQuotes((res.Result().(*Response)).QuoteResponse.Quotes)
return transformResponseQuotes(ctx, (res.Result().(*Response)).QuoteResponse.Quotes)
}
}
22 changes: 15 additions & 7 deletions internal/quote/quote_test.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/ui.go
Expand Up @@ -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(),
}
Expand Down

0 comments on commit 81b07d7

Please sign in to comment.