mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
Add Yahoo Finance module (#1066)
* Move finnhub to a stocks folder
As I am preparing an other stocks data provider, let's move `finnhub` to
a stocks folder that will host the others providers.
* Use go-pretty v6
Will be used by the new stock provider module, so let's just upgrade
this one to reduce the number of dependencies.
* Add Yahoo Finance module
Yahoo Finance provides an API for which `piquette/finance-go` is a
powerful client. This new module leverages this module to integrate all
indices provided by Yahoo Finance (international stocks, crypto,
options, currencies...)
Sample config:
```yaml
yfinance:
title: "Stocks 🚀"
symbols:
- "MSFT"
- "GC=F"
- "ORA.PA"
sort: true
enabled: true
refreshInterval: 60
position:
top: 1
left: 0
height: 1
width: 1
```
This commit is contained in:
80
modules/stocks/finnhub/client.go
Normal file
80
modules/stocks/finnhub/client.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package finnhub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Client ..
|
||||
type Client struct {
|
||||
symbols []string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// NewClient ..
|
||||
func NewClient(symbols []string, apiKey string) *Client {
|
||||
client := Client{
|
||||
symbols: symbols,
|
||||
apiKey: apiKey,
|
||||
}
|
||||
|
||||
return &client
|
||||
}
|
||||
|
||||
// Getquote ..
|
||||
func (client *Client) Getquote() ([]Quote, error) {
|
||||
quotes := []Quote{}
|
||||
|
||||
for _, s := range client.symbols {
|
||||
resp, err := client.finnhubRequest(s)
|
||||
if err != nil {
|
||||
return quotes, err
|
||||
}
|
||||
|
||||
var quote Quote
|
||||
quote.Stock = s
|
||||
err = json.NewDecoder(resp.Body).Decode("e)
|
||||
if err != nil {
|
||||
return quotes, err
|
||||
}
|
||||
quotes = append(quotes, quote)
|
||||
}
|
||||
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
var (
|
||||
finnhubURL = &url.URL{Scheme: "https", Host: "finnhub.io", Path: "/api/v1/quote"}
|
||||
)
|
||||
|
||||
func (client *Client) finnhubRequest(symbol string) (*http.Response, error) {
|
||||
params := url.Values{}
|
||||
params.Add("symbol", symbol)
|
||||
params.Add("token", client.apiKey)
|
||||
|
||||
url := finnhubURL.ResolveReference(&url.URL{RawQuery: params.Encode()})
|
||||
|
||||
req, err := http.NewRequest("GET", url.String(), nil)
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return nil, fmt.Errorf(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
12
modules/stocks/finnhub/quote.go
Normal file
12
modules/stocks/finnhub/quote.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package finnhub
|
||||
|
||||
type Quote struct {
|
||||
C float64 `json:"c"`
|
||||
H float64 `json:"h"`
|
||||
L float64 `json:"l"`
|
||||
O float64 `json:"o"`
|
||||
Pc float64 `json:"pc"`
|
||||
T int `json:"t"`
|
||||
|
||||
Stock string
|
||||
}
|
||||
37
modules/stocks/finnhub/settings.go
Normal file
37
modules/stocks/finnhub/settings.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package finnhub
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/olebedev/config"
|
||||
"github.com/wtfutil/wtf/cfg"
|
||||
"github.com/wtfutil/wtf/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFocusable = true
|
||||
defaultTitle = "📈 Stocks Price"
|
||||
)
|
||||
|
||||
// Settings defines the configuration properties for this module
|
||||
type Settings struct {
|
||||
*cfg.Common
|
||||
|
||||
apiKey string `help:"Your finnhub API token."`
|
||||
symbols []string `help:"An array of stocks symbols (i.e. AAPL, MSFT)"`
|
||||
}
|
||||
|
||||
// NewSettingsFromYAML creates a new settings instance from a YAML config block
|
||||
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
|
||||
|
||||
settings := Settings{
|
||||
Common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
|
||||
|
||||
apiKey: ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_FINNHUB_API_KEY"))),
|
||||
symbols: utils.ToStrs(ymlConfig.UList("symbols")),
|
||||
}
|
||||
|
||||
cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load()
|
||||
|
||||
return &settings
|
||||
}
|
||||
61
modules/stocks/finnhub/widget.go
Normal file
61
modules/stocks/finnhub/widget.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package finnhub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/view"
|
||||
)
|
||||
|
||||
// Widget ..
|
||||
type Widget struct {
|
||||
view.TextWidget
|
||||
*Client
|
||||
|
||||
settings *Settings
|
||||
}
|
||||
|
||||
// NewWidget ..
|
||||
func NewWidget(tviewApp *tview.Application, settings *Settings) *Widget {
|
||||
widget := Widget{
|
||||
Client: NewClient(settings.symbols, settings.apiKey),
|
||||
TextWidget: view.NewTextWidget(tviewApp, nil, settings.Common),
|
||||
|
||||
settings: settings,
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) Refresh() {
|
||||
if widget.Disabled() {
|
||||
return
|
||||
}
|
||||
|
||||
widget.Redraw(widget.content)
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) content() (string, string, bool) {
|
||||
quotes, err := widget.Client.Getquote()
|
||||
|
||||
title := widget.CommonSettings().Title
|
||||
t := table.NewWriter()
|
||||
t.AppendHeader(table.Row{"#", "Stock", "Current Price", "Open Price", "Change"})
|
||||
wrap := false
|
||||
if err != nil {
|
||||
wrap = true
|
||||
} else {
|
||||
for idx, q := range quotes {
|
||||
t.AppendRows([]table.Row{
|
||||
{idx, q.Stock, q.C, q.O, fmt.Sprintf("%.4f", (q.C-q.O)/q.C)},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return title, t.Render(), wrap
|
||||
}
|
||||
45
modules/stocks/yfinance/settings.go
Normal file
45
modules/stocks/yfinance/settings.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package yfinance
|
||||
|
||||
import (
|
||||
"github.com/olebedev/config"
|
||||
"github.com/wtfutil/wtf/cfg"
|
||||
"github.com/wtfutil/wtf/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFocusable = false
|
||||
defaultTitle = "Yahoo Finance"
|
||||
)
|
||||
|
||||
type colors struct {
|
||||
bigup string
|
||||
up string
|
||||
drop string
|
||||
bigdrop string
|
||||
}
|
||||
|
||||
// Settings defines the configuration properties for this module
|
||||
type Settings struct {
|
||||
common *cfg.Common
|
||||
|
||||
colors colors
|
||||
sort bool
|
||||
symbols []string `help:"An array of Yahoo Finance symbols (for example: DOCN, GME, GC=F)"`
|
||||
}
|
||||
|
||||
// NewSettingsFromYAML creates a new settings instance from a YAML config block
|
||||
func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings {
|
||||
settings := Settings{
|
||||
common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig),
|
||||
// RefreshInterval: ,
|
||||
}
|
||||
|
||||
settings.common.RefreshInterval = ymlConfig.UInt("refreshInterval", 60)
|
||||
settings.colors.bigup = ymlConfig.UString("colors.bigup", "greenyellow")
|
||||
settings.colors.up = ymlConfig.UString("colors.up", "green")
|
||||
settings.colors.drop = ymlConfig.UString("colors.drop", "firebrick")
|
||||
settings.colors.bigdrop = ymlConfig.UString("colors.bigdrop", "red")
|
||||
settings.sort = ymlConfig.UBool("sort", false)
|
||||
settings.symbols = utils.ToStrs(ymlConfig.UList("symbols"))
|
||||
return &settings
|
||||
}
|
||||
74
modules/stocks/yfinance/widget.go
Normal file
74
modules/stocks/yfinance/widget.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package yfinance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/wtfutil/wtf/view"
|
||||
)
|
||||
|
||||
// Widget is the container for your module's data
|
||||
type Widget struct {
|
||||
view.TextWidget
|
||||
|
||||
settings *Settings
|
||||
}
|
||||
|
||||
// NewWidget creates and returns an instance of Widget
|
||||
func NewWidget(tviewApp *tview.Application, settings *Settings) *Widget {
|
||||
widget := Widget{
|
||||
TextWidget: view.NewTextWidget(tviewApp, nil, settings.common),
|
||||
|
||||
settings: settings,
|
||||
}
|
||||
|
||||
return &widget
|
||||
}
|
||||
|
||||
/* -------------------- Exported Functions -------------------- */
|
||||
|
||||
// Refresh updates the onscreen contents of the widget
|
||||
func (widget *Widget) Refresh() {
|
||||
|
||||
// The last call should always be to the display function
|
||||
widget.display()
|
||||
}
|
||||
|
||||
/* -------------------- Unexported Functions -------------------- */
|
||||
|
||||
func (widget *Widget) content() string {
|
||||
yquotes := quotes(widget.settings.symbols)
|
||||
|
||||
colors := map[string]string{
|
||||
"bigup": widget.settings.colors.bigup,
|
||||
"up": widget.settings.colors.up,
|
||||
"drop": widget.settings.colors.drop,
|
||||
"bigdrop": widget.settings.colors.bigdrop,
|
||||
}
|
||||
|
||||
if widget.settings.sort {
|
||||
sort.SliceStable(yquotes, func(i, j int) bool { return yquotes[i].MarketChangePct > yquotes[j].MarketChangePct })
|
||||
}
|
||||
|
||||
t := table.NewWriter()
|
||||
t.SetStyle(tableStyle())
|
||||
for _, yq := range yquotes {
|
||||
t.AppendRow([]interface{}{
|
||||
GetMarketIcon(yq.MarketState),
|
||||
yq.Symbol,
|
||||
fmt.Sprintf("%8.2f %s", yq.MarketPrice, yq.Currency),
|
||||
GetTrendIcon(yq.Trend),
|
||||
fmt.Sprintf("[%s]%+6.2f (%+5.2f%%)", colors[yq.Trend], yq.MarketChange, yq.MarketChangePct),
|
||||
})
|
||||
}
|
||||
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
func (widget *Widget) display() {
|
||||
widget.Redraw(func() (string, string, bool) {
|
||||
return widget.CommonSettings().Title, widget.content(), false
|
||||
})
|
||||
}
|
||||
145
modules/stocks/yfinance/yquote.go
Normal file
145
modules/stocks/yfinance/yquote.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package yfinance
|
||||
|
||||
import (
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/piquette/finance-go/quote"
|
||||
)
|
||||
|
||||
type MarketState string
|
||||
|
||||
type yquote struct {
|
||||
Trend string // can be bigup (>3%), up, drop or bigdrop (<3%)
|
||||
Symbol string
|
||||
Currency string
|
||||
MarketState string
|
||||
MarketPrice float64
|
||||
MarketChange float64
|
||||
MarketChangePct float64
|
||||
}
|
||||
|
||||
func tableStyle() table.Style {
|
||||
return table.Style{
|
||||
Name: "yfinance",
|
||||
Box: table.BoxStyle{
|
||||
BottomLeft: "",
|
||||
BottomRight: "",
|
||||
BottomSeparator: "",
|
||||
Left: "",
|
||||
LeftSeparator: "",
|
||||
MiddleHorizontal: " ",
|
||||
MiddleSeparator: "",
|
||||
MiddleVertical: "",
|
||||
PaddingLeft: " ",
|
||||
PaddingRight: "",
|
||||
Right: "",
|
||||
RightSeparator: "",
|
||||
TopLeft: "",
|
||||
TopRight: "",
|
||||
TopSeparator: "",
|
||||
UnfinishedRow: "",
|
||||
},
|
||||
Color: table.ColorOptions{
|
||||
Footer: text.Colors{},
|
||||
Header: text.Colors{},
|
||||
Row: text.Colors{},
|
||||
RowAlternate: text.Colors{},
|
||||
},
|
||||
Format: table.FormatOptions{
|
||||
Footer: text.FormatUpper,
|
||||
Header: text.FormatUpper,
|
||||
Row: text.FormatDefault,
|
||||
},
|
||||
Options: table.Options{
|
||||
DrawBorder: false,
|
||||
SeparateColumns: false,
|
||||
SeparateFooter: false,
|
||||
SeparateHeader: false,
|
||||
SeparateRows: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func quotes(symbols []string) []yquote {
|
||||
var yquotes []yquote
|
||||
for _, symbol := range symbols {
|
||||
var yq yquote
|
||||
|
||||
var MarketPrice float64
|
||||
var MarketChange float64
|
||||
var MarketChangePct float64
|
||||
|
||||
q, err := quote.Get(symbol)
|
||||
if q == nil || err != nil {
|
||||
yq = yquote{
|
||||
Symbol: symbol,
|
||||
Trend: "?",
|
||||
MarketState: "?",
|
||||
}
|
||||
} else {
|
||||
if q.MarketState == "PRE" {
|
||||
MarketPrice = q.PreMarketPrice
|
||||
MarketChange = q.PreMarketChange
|
||||
MarketChangePct = q.PreMarketChangePercent
|
||||
|
||||
} else if q.MarketState == "POST" {
|
||||
MarketPrice = q.PostMarketPrice
|
||||
MarketChange = q.PostMarketChange
|
||||
MarketChangePct = q.PostMarketChangePercent
|
||||
} else {
|
||||
MarketPrice = q.RegularMarketPrice
|
||||
MarketChange = q.RegularMarketChange
|
||||
MarketChangePct = q.RegularMarketChangePercent
|
||||
}
|
||||
yq = yquote{
|
||||
Symbol: q.Symbol,
|
||||
Currency: q.CurrencyID,
|
||||
Trend: GetTrend(MarketChangePct),
|
||||
MarketState: string(q.MarketState),
|
||||
MarketPrice: MarketPrice,
|
||||
MarketChange: MarketChange,
|
||||
MarketChangePct: MarketChangePct,
|
||||
}
|
||||
}
|
||||
yquotes = append(yquotes, yq)
|
||||
}
|
||||
return yquotes
|
||||
}
|
||||
|
||||
func GetMarketIcon(state string) string {
|
||||
states := map[string]string{
|
||||
"PRE": "⏭",
|
||||
"REGULAR": "▶",
|
||||
"POST": "⏮",
|
||||
"?": "?",
|
||||
}
|
||||
if icon, ok := states[state]; ok {
|
||||
return icon
|
||||
} else {
|
||||
return "⏹"
|
||||
}
|
||||
}
|
||||
|
||||
func GetTrendIcon(trend string) string {
|
||||
icons := map[string]string{
|
||||
"bigup": "⬆️ ",
|
||||
"up": "↗️ ",
|
||||
"drop": "↘️ ",
|
||||
"bigdrop": "⬇️ ",
|
||||
}
|
||||
return icons[trend]
|
||||
}
|
||||
|
||||
func GetTrend(pct float64) string {
|
||||
var trend string
|
||||
if pct > 3 {
|
||||
trend = "bigup"
|
||||
} else if pct > 0 {
|
||||
trend = "up"
|
||||
} else if pct > -3 {
|
||||
trend = "drop"
|
||||
} else {
|
||||
trend = "bigdrop"
|
||||
}
|
||||
return trend
|
||||
}
|
||||
Reference in New Issue
Block a user