Rename Stocks to Markets

Also fix bug that would remove markets if a network request failed
and not show them again until Glance was restarted
This commit is contained in:
Svilen Markov 2024-05-28 18:01:26 +01:00
parent d2f5dbbc26
commit 1bebb88d0e
9 changed files with 74 additions and 68 deletions

View File

@ -20,7 +20,7 @@
- [Bookmarks](#bookmarks)
- [Calendar](#calendar)
- [Clock](#clock)
- [Stocks](#stocks)
- [Markets](#markets)
- [Twitch Channels](#twitch-channels)
- [Twitch Top Games](#twitch-top-games)
- [iframe](#iframe)
@ -80,8 +80,8 @@ pages:
- type: weather
location: London, United Kingdom
- type: stocks
stocks:
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
@ -1146,14 +1146,14 @@ Preview:
>
> There is currently no customizability available for the calendar. Extra features will be added in the future.
### Stocks
Display a list of stocks, their current value, change for the day and a small 21d chart. Data is taken from Yahoo Finance.
### Markets
Display a list of markets, their current value, change for the day and a small 21d chart. Data is taken from Yahoo Finance.
Example:
```yaml
- type: stocks
stocks:
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
@ -1168,21 +1168,21 @@ Example:
Preview:
![](images/stocks-widget-preview.png)
![](images/markets-widget-preview.png)
#### Properties
| Name | Type | Required |
| ---- | ---- | -------- |
| stocks | array | yes |
| markets | array | yes |
| sort-by | string | no |
| style | string | no |
##### `stocks`
An array of stocks for which to display information about.
##### `markets`
An array of markets for which to display information about.
##### `sort-by`
By default the stocks are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `absolute-change` for descending order based on the stock's absolute price change.
By default the markets are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `absolute-change` for descending order based on the stock's absolute price change.
##### `style`
To make the widget scale appropriately in a `full` size column, set the style to the experimental `dynamic-columns-experimental` option.

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -622,16 +622,16 @@ kbd:active {
color: var(--color-text-highlight);
}
.stock-chart {
.market-chart {
margin-left: auto;
width: 6.5rem;
}
.stock-chart svg {
.market-chart svg {
width: 100%;
}
.stock-values {
.market-values {
min-width: 8rem;
}

View File

@ -26,7 +26,7 @@ var (
ChangeDetectionTemplate = compileTemplate("change-detection.html", "widget-base.html")
VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html")
VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
MarketsTemplate = compileTemplate("markets.html", "widget-base.html")
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
RSSDetailedListTemplate = compileTemplate("rss-detailed-list.html", "widget-base.html")
RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html")

View File

@ -3,31 +3,31 @@
{{ define "widget-content" }}
{{ if ne .Style "dynamic-columns-experimental" }}
<ul class="list list-gap-20 list-with-separator">
{{ range .Stocks }}
{{ range .Markets }}
<li class="flex items-center gap-15">
{{ template "stock" . }}
{{ template "market" . }}
</li>
{{ end }}
</ul>
{{ else }}
<div class="dynamic-columns">
{{ range .Stocks }}
{{ range .Markets }}
<div class="flex items-center gap-15">
{{ template "stock" . }}
{{ template "market" . }}
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ define "stock" }}
{{ define "market" }}
<div class="min-width-0">
<a{{ if ne "" .SymbolLink }} href="{{ .SymbolLink }}" target="_blank" rel="noreferrer"{{ end }} class="color-highlight size-h3 block text-truncate">{{ .Symbol }}</a>
<div class="text-truncate">{{ .Name }}</div>
</div>
<a class="stock-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
<a class="market-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
<svg class="market-chart shrink-0" viewBox="0 0 100 50">
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
</svg>
</a>

View File

@ -85,20 +85,24 @@ var currencyToSymbol = map[string]string{
"PHP": "₱",
}
type Stock struct {
Name string `yaml:"name"`
Symbol string `yaml:"symbol"`
ChartLink string `yaml:"chart-link"`
SymbolLink string `yaml:"symbol-link"`
type MarketRequest struct {
Name string `yaml:"name"`
Symbol string `yaml:"symbol"`
ChartLink string `yaml:"chart-link"`
SymbolLink string `yaml:"symbol-link"`
}
type Market struct {
MarketRequest
Currency string `yaml:"-"`
Price float64 `yaml:"-"`
PercentChange float64 `yaml:"-"`
SvgChartPoints string `yaml:"-"`
}
type Stocks []Stock
type Markets []Market
func (t Stocks) SortByAbsChange() {
func (t Markets) SortByAbsChange() {
sort.Slice(t, func(i, j int) bool {
return math.Abs(t[i].PercentChange) > math.Abs(t[j].PercentChange)
})

View File

@ -6,7 +6,7 @@ import (
"net/http"
)
type stockResponseJson struct {
type marketResponseJson struct {
Chart struct {
Result []struct {
Meta struct {
@ -25,30 +25,30 @@ type stockResponseJson struct {
}
// TODO: allow changing chart time frame
const stockChartDays = 21
const marketChartDays = 21
func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
requests := make([]*http.Request, 0, len(stockRequests))
func FetchMarketsDataFromYahoo(marketRequests []MarketRequest) (Markets, error) {
requests := make([]*http.Request, 0, len(marketRequests))
for i := range stockRequests {
request, _ := http.NewRequest("GET", fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?range=1mo&interval=1d", stockRequests[i].Symbol), nil)
for i := range marketRequests {
request, _ := http.NewRequest("GET", fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?range=1mo&interval=1d", marketRequests[i].Symbol), nil)
requests = append(requests, request)
}
job := newJob(decodeJsonFromRequestTask[stockResponseJson](defaultClient), requests)
job := newJob(decodeJsonFromRequestTask[marketResponseJson](defaultClient), requests)
responses, errs, err := workerPoolDo(job)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrNoContent, err)
}
stocks := make(Stocks, 0, len(responses))
markets := make(Markets, 0, len(responses))
var failed int
for i := range responses {
if errs[i] != nil {
failed++
slog.Error("Failed to fetch stock data", "symbol", stockRequests[i].Symbol, "error", errs[i])
slog.Error("Failed to fetch market data", "symbol", marketRequests[i].Symbol, "error", errs[i])
continue
}
@ -56,14 +56,14 @@ func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
if len(response.Chart.Result) == 0 {
failed++
slog.Error("Stock response contains no data", "symbol", stockRequests[i].Symbol)
slog.Error("Market response contains no data", "symbol", marketRequests[i].Symbol)
continue
}
prices := response.Chart.Result[0].Indicators.Quote[0].Close
if len(prices) > stockChartDays {
prices = prices[len(prices)-stockChartDays:]
if len(prices) > marketChartDays {
prices = prices[len(prices)-marketChartDays:]
}
previous := response.Chart.Result[0].Meta.RegularMarketPrice
@ -80,13 +80,10 @@ func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
currency = response.Chart.Result[0].Meta.Currency
}
stocks = append(stocks, Stock{
Name: stockRequests[i].Name,
Symbol: response.Chart.Result[0].Meta.Symbol,
SymbolLink: stockRequests[i].SymbolLink,
ChartLink: stockRequests[i].ChartLink,
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
Currency: currency,
markets = append(markets, Market{
MarketRequest: marketRequests[i],
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
Currency: currency,
PercentChange: percentChange(
response.Chart.Result[0].Meta.RegularMarketPrice,
previous,
@ -95,13 +92,13 @@ func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
})
}
if len(stocks) == 0 {
if len(markets) == 0 {
return nil, ErrNoContent
}
if failed > 0 {
return stocks, fmt.Errorf("%w: could not fetch data for %d stock(s)", ErrPartialContent, failed)
return markets, fmt.Errorf("%w: could not fetch data for %d market(s)", ErrPartialContent, failed)
}
return stocks, nil
return markets, nil
}

View File

@ -9,34 +9,39 @@ import (
"github.com/glanceapp/glance/internal/feed"
)
// TODO: rename to Markets at some point
type Stocks struct {
widgetBase `yaml:",inline"`
Stocks feed.Stocks `yaml:"stocks"`
Sort string `yaml:"sort-by"`
Style string `yaml:"style"`
type Markets struct {
widgetBase `yaml:",inline"`
StocksRequests []feed.MarketRequest `yaml:"stocks"`
MarketRequests []feed.MarketRequest `yaml:"markets"`
Sort string `yaml:"sort-by"`
Style string `yaml:"style"`
Markets feed.Markets `yaml:"-"`
}
func (widget *Stocks) Initialize() error {
widget.withTitle("Stocks").withCacheDuration(time.Hour)
func (widget *Markets) Initialize() error {
widget.withTitle("Markets").withCacheDuration(time.Hour)
if len(widget.MarketRequests) == 0 {
widget.MarketRequests = widget.StocksRequests
}
return nil
}
func (widget *Stocks) Update(ctx context.Context) {
stocks, err := feed.FetchStocksDataFromYahoo(widget.Stocks)
func (widget *Markets) Update(ctx context.Context) {
markets, err := feed.FetchMarketsDataFromYahoo(widget.MarketRequests)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
if widget.Sort == "absolute-change" {
stocks.SortByAbsChange()
markets.SortByAbsChange()
}
widget.Stocks = stocks
widget.Markets = markets
}
func (widget *Stocks) Render() template.HTML {
return widget.render(widget, assets.StocksTemplate)
func (widget *Markets) Render() template.HTML {
return widget.render(widget, assets.MarketsTemplate)
}

View File

@ -33,8 +33,8 @@ func New(widgetType string) (Widget, error) {
return &Releases{}, nil
case "videos":
return &Videos{}, nil
case "stocks":
return &Stocks{}, nil
case "markets", "stocks":
return &Markets{}, nil
case "reddit":
return &Reddit{}, nil
case "rss":