diff --git a/docs/assets/widget_stocks_demo_yahoo.png b/docs/assets/widget_stocks_demo_yahoo.png
new file mode 100644
index 00000000..0bf47c86
Binary files /dev/null and b/docs/assets/widget_stocks_demo_yahoo.png differ
diff --git a/docs/widgets/info/stocks.md b/docs/widgets/info/stocks.md
index 548bedb4..9b04fcdd 100644
--- a/docs/widgets/info/stocks.md
+++ b/docs/widgets/info/stocks.md
@@ -9,7 +9,10 @@ The Stocks Information Widget allows you to include basic stock market data in
your Homepage header. The widget includes the current price of a stock, and the
change in price for the day.
-Finnhub.io is currently the only supported provider for the stocks widget.
+
+#### Finnhub.io
+
+Finnhub.io free API only supports US stocks.
You can sign up for a free api key at [finnhub.io](https://finnhub.io).
You are encouraged to read finnhub.io's
[terms of service/privacy policy](https://finnhub.io/terms-of-service) before
@@ -46,3 +49,37 @@ The information widget allows for up to 8 items in the watchlist.
The above configuration would result in something like this:

+
+#### Yahoo Finance
+Yahoo Finance is a free provider and doesn't require any API key or authentication.
+The API is not officially supported by Yahoo, so it may be subject to change or may be unreliable (e.g. rate limited).
+Some data may be delayed for up to 15 minutes depending on the stock exchange.
+
+You may use the quote lookup tool on the [Yahoo Finance website](https://finance.yahoo.com/) to find the symbol you are interested in.
+
+Generally the following rules apply:
+ - US stocks: `TICKER`
+ - International stocks: `TICKER.STOCKEXCHANGE`
+ - Indices: `^INDEX`
+ - Forex: `TICKER=X`
+ - Cryptocurrencies: `TICKER-CURRENCY`
+ - Commodities/futures: `TICKER=F`
+ - Options: `TICKERDATESTRIKE` **(not recommended due to the excessive length)**
+
+```yaml
+- stocks:
+ provider: yahoofinance
+ color: true # optional, defaults to true
+ cache: 1 # optional, default caches results for 1 minute
+ watchlist:
+ - AAPL
+ - LLOY.L
+ - ^STOXX
+ - JPY=X
+ - BTC-EUR
+ - GC=F
+ - NQ=F
+```
+The above configuration would result in something like this:
+
+
\ No newline at end of file
diff --git a/docs/widgets/services/stocks.md b/docs/widgets/services/stocks.md
index 5d64c9ac..2b7bbf92 100644
--- a/docs/widgets/services/stocks.md
+++ b/docs/widgets/services/stocks.md
@@ -5,13 +5,25 @@ description: Stocks Service Widget Configuration
_(Find the Stocks information widget [here](../info/stocks.md))_
-The widget includes:
+The widget supports:
-- US stock market status
+- US stock market status (finnhub only)
- Current price of provided stock symbol
-- Change in price of stock symbol for the day.
+- Change in price of stock symbol for the day
+
+Additionally, using Yahoo Finance as a provider, the widget additionally supports:
+
+- International stocks
+- Indices
+- Forex
+- Cryptocurrencies
+- Commodities
+- Futures
+- Options
+
+
+#### Finnhub.io
-Finnhub.io is currently the only supported provider for the stocks widget.
You can sign up for a free api key at [finnhub.io](https://finnhub.io).
You are encouraged to read finnhub.io's
[terms of service/privacy policy](https://finnhub.io/terms-of-service) before
@@ -48,3 +60,34 @@ widget:
- AMZN
- BRK.B
```
+
+#### Yahoo Finance
+Yahoo Finance is a free provider and doesn't require any API key or authentication.
+The API is not officially supported by Yahoo, so it may be subject to change or may be unreliable (e.g. rate limited).
+Some data may be delayed for up to 15 minutes depending on the stock exchange.
+
+You may use the quote lookup tool on the [Yahoo Finance website](https://finance.yahoo.com/) to find the symbol you are interested in.
+
+Generally the following rules apply:
+ - US stocks: `TICKER`
+ - International stocks: `TICKER.STOCKEXCHANGE`
+ - Indices: `^INDEX`
+ - Forex: `TICKER=X`
+ - Cryptocurrencies: `TICKER-CURRENCY`
+ - Commodities/futures: `TICKER=F`
+ - Options: `TICKERDATESTRIKE`
+
+```yaml
+widget:
+ type: stocks
+ provider: yahoofinance
+ watchlist:
+ - AAPL
+ - LLOY.L
+ - ^STOXX
+ - JPY=X
+ - BTC-EUR
+ - GC=F
+ - NQ=F
+ - NVDA270115C00250000
+```
diff --git a/src/components/widgets/stocks/stocks.jsx b/src/components/widgets/stocks/stocks.jsx
index 8c2c03fd..d4ca7d23 100644
--- a/src/components/widgets/stocks/stocks.jsx
+++ b/src/components/widgets/stocks/stocks.jsx
@@ -62,10 +62,10 @@ export default function Widget({ options }) {
>
{stock.currentPrice !== null
? t("common.number", {
- value: stock.currentPrice,
- style: "currency",
- currency: "USD",
- })
+ value: stock.currentPrice,
+ style: "currency",
+ currency: stock.currency || "USD",
+ })
: t("widget.api_error")}
) : (
diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index 90280c3d..08f70335 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -34,14 +34,21 @@ export default async function handler(req, res) {
// map opaque endpoints to their actual endpoint
if (widget?.mappings) {
- const mapping = widget?.mappings?.[req.query.endpoint];
+ let mapping = widget?.mappings?.[req.query.endpoint];
+ // The none mapping is used to bypass the mapping - the endpoint is mapped to itself
+ if (!mapping && widget?.mappings?.none) {
+ mapping = {
+ endpoint: req.query.endpoint,
+ };
+ }
+
const mappingParams = mapping?.params;
const optionalParams = mapping?.optionalParams;
const map = mapping?.map;
const endpoint = mapping?.endpoint;
const endpointProxy = mapping?.proxyHandler || serviceProxyHandler;
- if (mapping.method && mapping.method !== req.method) {
+ if (mapping?.method && mapping.method !== req.method) {
logger.debug("Unsupported method: %s", req.method);
return res.status(403).json({ error: "Unsupported method" });
}
diff --git a/src/pages/api/widgets/stocks.js b/src/pages/api/widgets/stocks.js
index d80842e1..ee23d95c 100644
--- a/src/pages/api/widgets/stocks.js
+++ b/src/pages/api/widgets/stocks.js
@@ -28,19 +28,22 @@ export default async function handler(req, res) {
return res.status(400).json({ error: "Missing provider" });
}
- if (provider !== "finnhub") {
+ if (provider !== "finnhub" && provider !== "yahoofinance") {
return res.status(400).json({ error: "Invalid provider" });
}
const providersInConfig = getSettings()?.providers;
+ // Not all providers require an API key
let apiKey;
- Object.entries(providersInConfig).forEach(([key, val]) => {
- if (key === provider) apiKey = val;
- });
+ if (provider === "finnhub") {
+ Object.entries(providersInConfig).forEach(([key, val]) => {
+ if (key === provider) apiKey = val;
+ });
- if (typeof apiKey === "undefined") {
- return res.status(400).json({ error: "Missing or invalid API Key for provider" });
+ if (typeof apiKey === "undefined") {
+ return res.status(400).json({ error: "Missing or invalid API Key for provider" });
+ }
}
if (provider === "finnhub") {
@@ -72,5 +75,35 @@ export default async function handler(req, res) {
});
}
+ if (provider === "yahoofinance") {
+ const results = await Promise.all(
+ watchlistArr.map(async (ticker) => {
+ if (!ticker) {
+ return { ticker: null, currentPrice: null, percentChange: null };
+ }
+
+ // Use the chart endpoint with minimal data points
+ const apiUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${ticker}?period=1d&interval=1d`;
+ const { chart } = await cachedFetch(apiUrl, cache || 1);
+
+ // Symbol not found
+ if (chart.result === null) {
+ return { ticker, currentPrice: null, percentChange: null };
+ }
+
+ const price = chart.result[0].meta.regularMarketPrice
+ const previousClose = chart.result[0].meta.chartPreviousClose
+ const change = previousClose ? ((price - previousClose) / previousClose) * 100 : 0.0
+
+ // Rounding percentage, but we want it back to a number for comparison
+ return { ticker, currentPrice: price.toFixed(2), percentChange: change.toFixed(2), currency: chart.result[0].meta.currency };
+ }),
+ );
+
+ return res.send({
+ stocks: results,
+ });
+ }
+
return res.status(400).json({ error: "Invalid configuration" });
}
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 63dfb608..43ad62c6 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -473,6 +473,7 @@ export function cleanServiceGroups(groups) {
// stocks
watchlist,
showUSMarketStatus,
+ provider,
// truenas
enablePools,
@@ -630,6 +631,7 @@ export function cleanServiceGroups(groups) {
if (type === "stocks") {
if (watchlist) cleanedService.widget.watchlist = watchlist;
if (showUSMarketStatus) cleanedService.widget.showUSMarketStatus = showUSMarketStatus;
+ if (provider) cleanedService.widget.provider = provider;
}
if (type === "wgeasy") {
if (threshold !== undefined) cleanedService.widget.threshold = parseInt(threshold, 10);
diff --git a/src/widgets/stocks/component.jsx b/src/widgets/stocks/component.jsx
index 844365cb..d1d2790a 100644
--- a/src/widgets/stocks/component.jsx
+++ b/src/widgets/stocks/component.jsx
@@ -46,7 +46,15 @@ function StockItem({ service, ticker }) {
const { t } = useTranslation();
const { widget } = service;
- const { data, error } = useWidgetAPI(widget, "quote", { symbol: ticker });
+ let endpoint;
+ let queryParams;
+ if (widget.provider === "finnhub") {
+ endpoint = "quote";
+ queryParams = { symbol: ticker };
+ } else if (widget.provider === "yahoofinance") {
+ endpoint = ticker;
+ }
+ const { data, error } = useWidgetAPI(widget, endpoint, queryParams);
if (error || data?.error) {
return