Files
homarr/packages/request-handler/src/stock-price.ts
2025-11-13 22:07:23 +01:00

67 lines
1.9 KiB
TypeScript

import dayjs from "dayjs";
import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const fetchStockPriceHandler = createCachedWidgetRequestHandler({
queryKey: "fetchStockPriceResult",
widgetKind: "stockPrice",
async requestAsync(input: { stock: string; timeRange: string; timeInterval: string }) {
const response = await fetchWithTimeout(
`https://query1.finance.yahoo.com/v8/finance/chart/${input.stock}?range=${input.timeRange}&interval=${input.timeInterval}`,
);
const data = dataSchema.parse(await response.json());
if ("error" in data) {
throw new Error(data.error.description);
}
if (data.chart.result.length !== 1) {
throw new Error("Received multiple results");
}
const firstResult = data.chart.result[0];
if (!firstResult) {
throw new Error("Received invalid data");
}
return {
priceHistory:
firstResult.indicators.quote[0]?.close.filter(
// Filter out null values from price arrays (Yahoo Finance returns null for missing data points)
(value) => value !== null && value !== undefined,
) ?? [],
symbol: firstResult.meta.symbol,
shortName: firstResult.meta.shortName,
};
},
cacheDuration: dayjs.duration(5, "minutes"),
});
const dataSchema = z
.object({
error: z.object({
description: z.string(),
}),
})
.or(
z.object({
chart: z.object({
result: z.array(
z.object({
indicators: z.object({
quote: z.array(
z.object({
close: z.array(z.number().nullish()),
}),
),
}),
meta: z.object({
symbol: z.string(),
shortName: z.string(),
}),
}),
),
}),
}),
);