From bf0e934f4d4ea5369be758caedc447e9d370f3a0 Mon Sep 17 00:00:00 2001 From: Ani Channarasappa Date: Sat, 30 Jan 2021 21:44:18 -0500 Subject: [PATCH] feat: Added option to view fundamentals of a symbol --- README.md | 1 + cmd/root.go | 30 ++++----- internal/cli/cli.go | 26 ++++---- internal/quote/quote.go | 2 + internal/ui/component/watchlist/watchlist.go | 61 ++++++++++++------- .../ui/component/watchlist/watchlist_test.go | 55 +++++++++++++++++ internal/ui/ui.go | 2 +- 7 files changed, 126 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 7bcf48d..f849c05 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ ticker -w NET,AAPL,TSLA |-i|--interval|`5`|Refresh interval in seconds| |-w|--watchlist||comma separated list of symbols to watch| |-e|--extra-info-exchange||display currency, exchange name, and quote delay for each quote | +|-q|--extra-info-fundamentals||display open price, high, low, and volume for each quote | | |--compact||compact layout without separators between each quote| ## Configuration diff --git a/cmd/root.go b/cmd/root.go index 8d25e0b..83d7dc2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,26 +15,26 @@ import ( ) var ( - configPath string - config cli.Config - watchlist string - refreshInterval int - compact bool - extraInfoExchange bool - extraInfoQuote bool - rootCmd = &cobra.Command{ + configPath string + config cli.Config + watchlist string + refreshInterval int + compact bool + extraInfoExchange bool + extraInfoFundamentals bool + rootCmd = &cobra.Command{ Use: "ticker", Short: "Terminal stock ticker and stock gain/loss tracker", Args: cli.Validate( &config, afero.NewOsFs(), cli.Options{ - ConfigPath: &configPath, - RefreshInterval: &refreshInterval, - Watchlist: &watchlist, - Compact: &compact, - ExtraInfoExchange: &extraInfoExchange, - ExtraInfoQuote: &extraInfoQuote, + ConfigPath: &configPath, + RefreshInterval: &refreshInterval, + Watchlist: &watchlist, + Compact: &compact, + ExtraInfoExchange: &extraInfoExchange, + ExtraInfoFundamentals: &extraInfoFundamentals, }, ), Run: cli.Run(ui.Start(&config)), @@ -55,7 +55,7 @@ func init() { rootCmd.Flags().IntVarP(&refreshInterval, "interval", "i", 0, "refresh interval in seconds") rootCmd.Flags().BoolVar(&compact, "compact", false, "compact layout without separators between each quote") rootCmd.Flags().BoolVar(&extraInfoExchange, "extra-info-exchange", false, "display currency, exchange name, and quote delay for each quote") - rootCmd.Flags().BoolVar(&extraInfoQuote, "extra-info-quote", false, "display open price, high, low, and volume for each quote") + rootCmd.Flags().BoolVar(&extraInfoFundamentals, "extra-info-fundamentals", false, "display open price, high, low, and volume for each quote") } func initConfig() { diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 88f3cd3..0aea277 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -12,21 +12,21 @@ import ( ) type Config struct { - RefreshInterval int `yaml:"interval"` - Watchlist []string `yaml:"watchlist"` - Lots []position.Lot `yaml:"lots"` - Compact bool `yaml:"compact"` - ExtraInfoExchange bool `yaml:"extra-info-exchange"` - ExtraInfoQuote bool `yaml:"extra-info-quote"` + RefreshInterval int `yaml:"interval"` + Watchlist []string `yaml:"watchlist"` + Lots []position.Lot `yaml:"lots"` + Compact bool `yaml:"compact"` + ExtraInfoExchange bool `yaml:"extra-info-exchange"` + ExtraInfoFundamentals bool `yaml:"extra-info-fundamentals"` } type Options struct { - ConfigPath *string - RefreshInterval *int - Watchlist *string - Compact *bool - ExtraInfoExchange *bool - ExtraInfoQuote *bool + ConfigPath *string + RefreshInterval *int + Watchlist *string + Compact *bool + ExtraInfoExchange *bool + ExtraInfoFundamentals *bool } func Run(uiStartFn func() error) func(*cobra.Command, []string) { @@ -83,7 +83,7 @@ func read(fs afero.Fs, options Options, configFile *Config) (Config, error) { config.RefreshInterval = getRefreshInterval(*options.RefreshInterval, configFile.RefreshInterval) config.Compact = getBoolOption(*options.Compact, configFile.Compact) config.ExtraInfoExchange = getBoolOption(*options.ExtraInfoExchange, configFile.ExtraInfoExchange) - config.ExtraInfoQuote = getBoolOption(*options.ExtraInfoQuote, configFile.ExtraInfoQuote) + config.ExtraInfoFundamentals = getBoolOption(*options.ExtraInfoFundamentals, configFile.ExtraInfoFundamentals) return config, err diff --git a/internal/quote/quote.go b/internal/quote/quote.go index bf8a59d..33ff45c 100644 --- a/internal/quote/quote.go +++ b/internal/quote/quote.go @@ -18,6 +18,8 @@ type ResponseQuote struct { RegularMarketChangePercent float64 `json:"regularMarketChangePercent"` RegularMarketPrice float64 `json:"regularMarketPrice"` RegularMarketPreviousClose float64 `json:"regularMarketPreviousClose"` + RegularMarketOpen float64 `json:"regularMarketOpen"` + RegularMarketDayRange string `json:"regularMarketDayRange"` PostMarketChange float64 `json:"postMarketChange"` PostMarketChangePercent float64 `json:"postMarketChangePercent"` PostMarketPrice float64 `json:"postMarketPrice"` diff --git a/internal/ui/component/watchlist/watchlist.go b/internal/ui/component/watchlist/watchlist.go index b9a37f3..332bad6 100644 --- a/internal/ui/component/watchlist/watchlist.go +++ b/internal/ui/component/watchlist/watchlist.go @@ -31,21 +31,21 @@ const ( ) type Model struct { - Width int - Quotes []quote.Quote - Positions map[string]position.Position - Compact bool - ExtraInfoExchange bool - ExtraInfoQuote bool + Width int + Quotes []quote.Quote + Positions map[string]position.Position + Compact bool + ExtraInfoExchange bool + ExtraInfoFundamentals bool } // NewModel returns a model with default values. -func NewModel(compact bool, extraInfoExchange bool, extraInfoQuote bool) Model { +func NewModel(compact bool, extraInfoExchange bool, extraInfoFundamentals bool) Model { return Model{ - Width: 80, - Compact: compact, - ExtraInfoExchange: extraInfoExchange, - ExtraInfoQuote: extraInfoQuote, + Width: 80, + Compact: compact, + ExtraInfoExchange: extraInfoExchange, + ExtraInfoFundamentals: extraInfoFundamentals, } } @@ -63,7 +63,7 @@ func (m Model) View() string { strings.Join( []string{ item(quote, m.Positions[quote.Symbol], m.Width), - // extraInfoQuote(m.ExtraInfoQuote, quote, m.Width), + extraInfoFundamentals(m.ExtraInfoFundamentals, quote, m.Width), extraInfoExchange(m.ExtraInfoExchange, quote, m.Width), }, "", @@ -136,10 +136,6 @@ func extraInfoExchange(show bool, q quote.Quote, width int) string { } return "\n" + Line( width, - Cell{ - Text: "", - Align: RightAlign, - }, Cell{ Text: tagText(q.ExchangeName) + " " + tagText(exchangeDelayText(q.ExchangeDelay)) + " " + tagText(q.Currency), Align: RightAlign, @@ -152,12 +148,33 @@ func extraInfoExchange(show bool, q quote.Quote, width int) string { ) } -// func extraInfoQuote(show bool, q quote.Quote, width int) string { -// if !show { -// return "" -// } -// return "" -// } +func extraInfoFundamentals(show bool, q quote.Quote, width int) string { + if !show { + return "" + } + + return "\n" + Line( + width, + Cell{ + Width: 25, + Text: styleNeutralFaded("Prev Close: ") + styleNeutral(ConvertFloatToString(q.RegularMarketPreviousClose)), + }, + Cell{ + Width: 20, + Text: styleNeutralFaded("Open: ") + styleNeutral(ConvertFloatToString(q.RegularMarketOpen)), + }, + Cell{ + Text: dayRangeText(q.RegularMarketDayRange), + }, + ) +} + +func dayRangeText(dayRange string) string { + if len(dayRange) <= 0 { + return "" + } + return styleNeutralFaded("Day Range: ") + styleNeutral(dayRange) +} func exchangeDelayText(delay float64) string { if delay <= 0 { diff --git a/internal/ui/component/watchlist/watchlist_test.go b/internal/ui/component/watchlist/watchlist_test.go index 30c3272..1c50ca7 100644 --- a/internal/ui/component/watchlist/watchlist_test.go +++ b/internal/ui/component/watchlist/watchlist_test.go @@ -325,6 +325,61 @@ var _ = Describe("Watchlist", func() { }) }) + When("the option for extra fundamental information is set", func() { + It("should render extra fundamental information", func() { + m := NewModel(true, false, true) + m.Quotes = []Quote{ + { + ResponseQuote: ResponseQuote{ + Symbol: "BTC-USD", + ShortName: "Bitcoin", + RegularMarketPreviousClose: 10000.0, + RegularMarketOpen: 10000.0, + RegularMarketDayRange: "10000 - 10000", + }, + Price: 50000.0, + Change: 10000.0, + ChangePercent: 20.0, + IsActive: true, + IsRegularTradingSession: true, + }, + } + expected := strings.Join([]string{ + "BTC-USD ⦿ 50000.00", + "Bitcoin ↑ 10000.00 (20.00%)", + "Prev Close: 10000.00 Open: 10000.00 Day Range: 10000 - 10000 ", + }, "\n") + Expect(removeFormatting(m.View())).To(Equal(expected)) + }) + + When("there is no day range", func() { + It("should not render the day range field", func() { + m := NewModel(true, false, true) + m.Quotes = []Quote{ + { + ResponseQuote: ResponseQuote{ + Symbol: "BTC-USD", + ShortName: "Bitcoin", + RegularMarketPreviousClose: 10000.0, + RegularMarketOpen: 10000.0, + }, + Price: 50000.0, + Change: 10000.0, + ChangePercent: 20.0, + IsActive: true, + IsRegularTradingSession: true, + }, + } + expected := strings.Join([]string{ + "BTC-USD ⦿ 50000.00", + "Bitcoin ↑ 10000.00 (20.00%)", + "Prev Close: 10000.00 Open: 10000.00 ", + }, "\n") + Expect(removeFormatting(m.View())).To(Equal(expected)) + }) + }) + }) + When("no quotes are set", func() { It("should render an empty watchlist", func() { m := NewModel(false, false, false) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 9a36d45..86aaf61 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -59,7 +59,7 @@ func NewModel(config cli.Config, client *resty.Client) Model { requestInterval: 3, getQuotes: quote.GetQuotes(*client, symbols), getPositions: position.GetPositions(aggregatedLots), - watchlist: watchlist.NewModel(config.Compact, config.ExtraInfoExchange, config.ExtraInfoQuote), + watchlist: watchlist.NewModel(config.Compact, config.ExtraInfoExchange, config.ExtraInfoFundamentals), } }