import z from 'zod'; import { Widget } from '../types'; import YahooFinance from 'yahoo-finance2'; import formatChatHistoryAsString from '@/lib/utils/formatHistory'; const yf = new YahooFinance({ suppressNotices: ['yahooSurvey'], }); const schema = z.object({ name: z .string() .describe( "The stock name for example Nvidia, Google, Apple, Microsoft etc. You can also return ticker if you're aware of it otherwise just use the name.", ), comparisonNames: z .array(z.string()) .max(3) .describe( "Optional array of up to 3 stock names to compare against the base name (e.g., ['Microsoft', 'GOOGL', 'Meta']). Charts will show percentage change comparison.", ), notPresent: z .boolean() .describe('Whether there is no need for the stock widget.'), }); const systemPrompt = ` You are a stock ticker/name extractor. You will receive a user follow up and a conversation history. Your task is to determine if the user is asking about stock information and extract the stock name(s) they want data for. - If the user is asking about a stock, extract the primary stock name or ticker. - If the user wants to compare stocks, extract up to 3 comparison stock names in comparisonNames. - You can use either stock names (e.g., "Nvidia", "Apple") or tickers (e.g., "NVDA", "AAPL"). - If you cannot determine a valid stock or the query is not stock-related, set notPresent to true. - If no comparison is needed, set comparisonNames to an empty array. You must respond in the following JSON format without any extra text, explanations or filler sentences: { "name": string, "comparisonNames": string[], "notPresent": boolean } `; const stockWidget: Widget = { type: 'stockWidget', shouldExecute: (classification) => classification.classification.showStockWidget, execute: async (input) => { const output = await input.llm.generateObject({ messages: [ { role: 'system', content: systemPrompt, }, { role: 'user', content: `\n${formatChatHistoryAsString(input.chatHistory)}\n\n\n${input.followUp}\n`, }, ], schema, }); if (output.notPresent) { return; } const params = output; try { const name = params.name; const findings = await yf.search(name); if (findings.quotes.length === 0) throw new Error(`Failed to find quote for name/symbol: ${name}`); const ticker = findings.quotes[0].symbol as string; const quote: any = await yf.quote(ticker); const chartPromises = { '1D': yf .chart(ticker, { period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), period2: new Date(), interval: '5m', }) .catch(() => null), '5D': yf .chart(ticker, { period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), period2: new Date(), interval: '15m', }) .catch(() => null), '1M': yf .chart(ticker, { period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), '3M': yf .chart(ticker, { period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), '6M': yf .chart(ticker, { period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), '1Y': yf .chart(ticker, { period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), MAX: yf .chart(ticker, { period1: new Date(Date.now() - 10 * 365 * 24 * 60 * 60 * 1000), interval: '1wk', }) .catch(() => null), }; const charts = await Promise.all([ chartPromises['1D'], chartPromises['5D'], chartPromises['1M'], chartPromises['3M'], chartPromises['6M'], chartPromises['1Y'], chartPromises['MAX'], ]); const [chart1D, chart5D, chart1M, chart3M, chart6M, chart1Y, chartMAX] = charts; if (!quote) { throw new Error(`No data found for ticker: ${ticker}`); } let comparisonData: any = null; if (params.comparisonNames.length > 0) { const comparisonPromises = params.comparisonNames .slice(0, 3) .map(async (compName) => { try { const compFindings = await yf.search(compName); if (compFindings.quotes.length === 0) return null; const compTicker = compFindings.quotes[0].symbol as string; const compQuote = await yf.quote(compTicker); const compCharts = await Promise.all([ yf .chart(compTicker, { period1: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), period2: new Date(), interval: '5m', }) .catch(() => null), yf .chart(compTicker, { period1: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000), period2: new Date(), interval: '15m', }) .catch(() => null), yf .chart(compTicker, { period1: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), yf .chart(compTicker, { period1: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), yf .chart(compTicker, { period1: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), yf .chart(compTicker, { period1: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000), interval: '1d', }) .catch(() => null), yf .chart(compTicker, { period1: new Date( Date.now() - 10 * 365 * 24 * 60 * 60 * 1000, ), interval: '1wk', }) .catch(() => null), ]); return { ticker: compTicker, name: compQuote.shortName || compTicker, charts: compCharts, }; } catch (error) { console.error( `Failed to fetch comparison ticker ${compName}:`, error, ); return null; } }); const compResults = await Promise.all(comparisonPromises); comparisonData = compResults.filter((r) => r !== null); } const stockData = { symbol: quote.symbol, shortName: quote.shortName || quote.longName || ticker, longName: quote.longName, exchange: quote.fullExchangeName || quote.exchange, currency: quote.currency, quoteType: quote.quoteType, marketState: quote.marketState, regularMarketTime: quote.regularMarketTime, postMarketTime: quote.postMarketTime, preMarketTime: quote.preMarketTime, regularMarketPrice: quote.regularMarketPrice, regularMarketChange: quote.regularMarketChange, regularMarketChangePercent: quote.regularMarketChangePercent, regularMarketPreviousClose: quote.regularMarketPreviousClose, regularMarketOpen: quote.regularMarketOpen, regularMarketDayHigh: quote.regularMarketDayHigh, regularMarketDayLow: quote.regularMarketDayLow, postMarketPrice: quote.postMarketPrice, postMarketChange: quote.postMarketChange, postMarketChangePercent: quote.postMarketChangePercent, preMarketPrice: quote.preMarketPrice, preMarketChange: quote.preMarketChange, preMarketChangePercent: quote.preMarketChangePercent, regularMarketVolume: quote.regularMarketVolume, averageDailyVolume3Month: quote.averageDailyVolume3Month, averageDailyVolume10Day: quote.averageDailyVolume10Day, bid: quote.bid, bidSize: quote.bidSize, ask: quote.ask, askSize: quote.askSize, fiftyTwoWeekLow: quote.fiftyTwoWeekLow, fiftyTwoWeekHigh: quote.fiftyTwoWeekHigh, fiftyTwoWeekChange: quote.fiftyTwoWeekChange, fiftyTwoWeekChangePercent: quote.fiftyTwoWeekChangePercent, marketCap: quote.marketCap, trailingPE: quote.trailingPE, forwardPE: quote.forwardPE, priceToBook: quote.priceToBook, bookValue: quote.bookValue, earningsPerShare: quote.epsTrailingTwelveMonths, epsForward: quote.epsForward, dividendRate: quote.dividendRate, dividendYield: quote.dividendYield, exDividendDate: quote.exDividendDate, trailingAnnualDividendRate: quote.trailingAnnualDividendRate, trailingAnnualDividendYield: quote.trailingAnnualDividendYield, beta: quote.beta, fiftyDayAverage: quote.fiftyDayAverage, fiftyDayAverageChange: quote.fiftyDayAverageChange, fiftyDayAverageChangePercent: quote.fiftyDayAverageChangePercent, twoHundredDayAverage: quote.twoHundredDayAverage, twoHundredDayAverageChange: quote.twoHundredDayAverageChange, twoHundredDayAverageChangePercent: quote.twoHundredDayAverageChangePercent, sector: quote.sector, industry: quote.industry, website: quote.website, chartData: { '1D': chart1D ? { timestamps: chart1D.quotes.map((q: any) => q.date.getTime()), prices: chart1D.quotes.map((q: any) => q.close), } : null, '5D': chart5D ? { timestamps: chart5D.quotes.map((q: any) => q.date.getTime()), prices: chart5D.quotes.map((q: any) => q.close), } : null, '1M': chart1M ? { timestamps: chart1M.quotes.map((q: any) => q.date.getTime()), prices: chart1M.quotes.map((q: any) => q.close), } : null, '3M': chart3M ? { timestamps: chart3M.quotes.map((q: any) => q.date.getTime()), prices: chart3M.quotes.map((q: any) => q.close), } : null, '6M': chart6M ? { timestamps: chart6M.quotes.map((q: any) => q.date.getTime()), prices: chart6M.quotes.map((q: any) => q.close), } : null, '1Y': chart1Y ? { timestamps: chart1Y.quotes.map((q: any) => q.date.getTime()), prices: chart1Y.quotes.map((q: any) => q.close), } : null, MAX: chartMAX ? { timestamps: chartMAX.quotes.map((q: any) => q.date.getTime()), prices: chartMAX.quotes.map((q: any) => q.close), } : null, }, comparisonData: comparisonData ? comparisonData.map((comp: any) => ({ ticker: comp.ticker, name: comp.name, chartData: { '1D': comp.charts[0] ? { timestamps: comp.charts[0].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[0].quotes.map((q: any) => q.close), } : null, '5D': comp.charts[1] ? { timestamps: comp.charts[1].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[1].quotes.map((q: any) => q.close), } : null, '1M': comp.charts[2] ? { timestamps: comp.charts[2].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[2].quotes.map((q: any) => q.close), } : null, '3M': comp.charts[3] ? { timestamps: comp.charts[3].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[3].quotes.map((q: any) => q.close), } : null, '6M': comp.charts[4] ? { timestamps: comp.charts[4].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[4].quotes.map((q: any) => q.close), } : null, '1Y': comp.charts[5] ? { timestamps: comp.charts[5].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[5].quotes.map((q: any) => q.close), } : null, MAX: comp.charts[6] ? { timestamps: comp.charts[6].quotes.map((q: any) => q.date.getTime(), ), prices: comp.charts[6].quotes.map((q: any) => q.close), } : null, }, })) : null, }; return { type: 'stock', llmContext: `Current price of ${stockData.shortName} (${stockData.symbol}) is ${stockData.regularMarketPrice} ${stockData.currency}. Other details: ${JSON.stringify( { marketState: stockData.marketState, regularMarketChange: stockData.regularMarketChange, regularMarketChangePercent: stockData.regularMarketChangePercent, marketCap: stockData.marketCap, peRatio: stockData.trailingPE, dividendYield: stockData.dividendYield, }, )}`, data: stockData, }; } catch (error: any) { return { type: 'stock', llmContext: 'Failed to fetch stock data.', data: { error: `Error fetching stock data: ${error.message || error}`, ticker: params.name, }, }; } }, }; export default stockWidget;