src / yahooFinance.ts
/**
* Yahoo Finance API Client
*
* This module provides a TypeScript wrapper around the yahoo-finance2 library (v4),
* implementing all 9 tools from the original Python MCP server:
* https://github.com/Alex2Yang97/yahoo-finance-mcp
*
* Uses yahoo-finance2 v4: https://github.com/gadicc/yahoo-finance2
*/
import YahooFinance from "yahoo-finance2";
// ─── Singleton Instance ────────────────────────────────────────────────────────
const yahooFinance = new YahooFinance();
// ─── Type Definitions ──────────────────────────────────────────────────────────
export type Period =
| "1d" | "5d" | "1mo" | "3mo" | "6mo"
| "1y" | "2y" | "5y" | "10y" | "ytd" | "max";
export type Interval =
| "1m" | "2m" | "5m" | "15m" | "30m" | "60m" | "90m"
| "1h" | "1d" | "5d" | "1wk" | "1mo" | "3mo";
export type FinancialType =
| "income_stmt"
| "quarterly_income_stmt"
| "balance_sheet"
| "quarterly_balance_sheet"
| "cashflow"
| "quarterly_cashflow";
export type HolderType =
| "major_holders"
| "institutional_holders"
| "mutualfund_holders"
| "insider_transactions"
| "insider_purchases"
| "insider_roster_holders";
export type RecommendationType = "recommendations" | "upgrades_downgrades";
export type OptionType = "calls" | "puts";
// ─── Helper Functions ──────────────────────────────────────────────────────────
/**
* Convert a yfinance-style period string to a Date object (period1 start date).
* The chart module in yahoo-finance2 uses period1/period2 (Date) instead of
* yfinance's "period" string parameter.
*/
function periodToDateRange(period: Period): { period1: Date; period2: Date } {
const now = new Date();
const period2 = now;
let period1: Date;
switch (period) {
case "1d":
period1 = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000);
break;
case "5d":
period1 = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000);
break;
case "1mo":
period1 = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
break;
case "3mo":
period1 = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
break;
case "6mo":
period1 = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
break;
case "1y":
period1 = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
break;
case "2y":
period1 = new Date(now.getFullYear() - 2, now.getMonth(), now.getDate());
break;
case "5y":
period1 = new Date(now.getFullYear() - 5, now.getMonth(), now.getDate());
break;
case "10y":
period1 = new Date(now.getFullYear() - 10, now.getMonth(), now.getDate());
break;
case "ytd":
period1 = new Date(now.getFullYear(), 0, 1);
break;
case "max":
period1 = new Date(1970, 0, 1);
break;
default:
period1 = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
}
return { period1, period2 };
}
/**
* Safely serialize a value, converting Date objects to ISO strings
* and handling null/undefined.
*/
function safeSerialize(value: unknown): unknown {
if (value === null || value === undefined) return null;
if (value instanceof Date) return value.toISOString();
if (typeof value === "object" && !Array.isArray(value)) {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
result[k] = safeSerialize(v);
}
return result;
}
if (Array.isArray(value)) {
return value.map(safeSerialize);
}
return value;
}
/**
* Validate that a ticker symbol exists by attempting a basic quote lookup.
* Returns an error message string if invalid, or null if valid.
*/
async function validateTicker(ticker: string): Promise<string | null> {
try {
const quote = await yahooFinance.quote(ticker);
if (!quote || !quote.symbol) {
return `Company ticker ${ticker} not found.`;
}
return null;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("No data found") || msg.includes("not found")) {
return `Company ticker ${ticker} not found.`;
}
return `Error validating ticker ${ticker}: ${msg}`;
}
}
// ─── Tool 1: Get Historical Stock Prices ───────────────────────────────────────
/**
* Get historical OHLCV data for a given ticker symbol.
* Maps to: yfinance Ticker.history(period, interval)
* Uses: yahooFinance.chart() with period1/period2 conversion
*/
export async function getHistoricalStockPrices(
ticker: string,
period: Period = "1mo",
interval: Interval = "1d"
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const { period1, period2 } = periodToDateRange(period);
const result = await yahooFinance.chart(ticker, {
period1,
period2,
interval,
return: "array",
});
if (!result.quotes || result.quotes.length === 0) {
return JSON.stringify([]);
}
// Transform to match yfinance output format:
// Date, Open, High, Low, Close, Volume, Adj Close
const records = result.quotes.map((q) => ({
Date: q.date instanceof Date ? q.date.toISOString() : q.date,
Open: q.open,
High: q.high,
Low: q.low,
Close: q.close,
Volume: q.volume,
"Adj Close": q.adjclose,
}));
return JSON.stringify(records);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting historical stock prices for ${ticker}: ${msg}`;
}
}
// ─── Tool 2: Get Stock Info ────────────────────────────────────────────────────
/**
* Get comprehensive stock information for a given ticker symbol.
* Maps to: yfinance Ticker.info
* Uses: yahooFinance.quoteSummary() with modules: "all"
*/
export async function getStockInfo(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: "all",
});
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting stock information for ${ticker}: ${msg}`;
}
}
// ─── Tool 3: Get Yahoo Finance News ───────────────────────────────────────────
/**
* Get news articles for a given ticker symbol.
* Maps to: yfinance Ticker.news (filtered by contentType === "STORY")
* Uses: yahooFinance.search() with newsCount: 10
*/
export async function getYahooFinanceNews(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const searchResult = await yahooFinance.search(ticker, {
newsCount: 10,
quotesCount: 0,
});
if (!searchResult.news || searchResult.news.length === 0) {
return `No news found for company that searched with ${ticker} ticker.`;
}
const newsList = searchResult.news.map((article) => {
const title = article.title || "";
const publisher = article.publisher || "";
const link = article.link || "";
const publishTime = article.providerPublishTime
? new Date(article.providerPublishTime).toISOString()
: "";
const relatedTickers = article.relatedTickers?.join(", ") || "";
return [
`Title: ${title}`,
`Publisher: ${publisher}`,
`Published: ${publishTime}`,
`Related Tickers: ${relatedTickers}`,
`URL: ${link}`,
].join("\n");
});
return newsList.join("\n\n");
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting news for ${ticker}: ${msg}`;
}
}
// ─── Tool 4: Get Stock Actions (Dividends & Splits) ───────────────────────────
/**
* Get stock dividends and stock splits for a given ticker symbol.
* Maps to: yfinance Ticker.actions
* Uses: yahooFinance.chart() with events: "div|split"
*/
export async function getStockActions(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
// Use a wide date range to get all historical actions
const result = await yahooFinance.chart(ticker, {
period1: new Date(1970, 0, 1),
period2: new Date(),
interval: "1d",
events: "div|split",
return: "array",
});
const actions: Array<Record<string, unknown>> = [];
// Process dividends
if (result.events?.dividends) {
for (const div of result.events.dividends) {
actions.push({
Date: div.date instanceof Date ? div.date.toISOString() : div.date,
Type: "Dividend",
Amount: div.amount,
});
}
}
// Process splits
if (result.events?.splits) {
for (const split of result.events.splits) {
actions.push({
Date: split.date instanceof Date ? split.date.toISOString() : split.date,
Type: "Split",
Numerator: split.numerator,
Denominator: split.denominator,
SplitRatio: split.splitRatio,
});
}
}
// Sort by date
actions.sort((a, b) => {
const dateA = new Date(a.Date as string).getTime();
const dateB = new Date(b.Date as string).getTime();
return dateA - dateB;
});
return JSON.stringify(actions);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting stock actions for ${ticker}: ${msg}`;
}
}
// ─── Tool 5: Get Financial Statement ──────────────────────────────────────────
/**
* Get financial statements for a given ticker symbol.
* Maps to: yfinance Ticker.income_stmt / balance_sheet / cashflow (and quarterly variants)
* Uses: yahooFinance.fundamentalsTimeSeries()
*
* Note: Since Nov 2024, quoteSummary's incomeStatementHistory*, balanceSheetHistory*,
* cashflowStatementHistory* modules provide almost no data. We use fundamentalsTimeSeries
* instead as recommended by the yahoo-finance2 docs.
*/
export async function getFinancialStatement(
ticker: string,
financialType: FinancialType
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
let type: string;
let module: string;
switch (financialType) {
case "income_stmt":
type = "annual";
module = "financials";
break;
case "quarterly_income_stmt":
type = "quarterly";
module = "financials";
break;
case "balance_sheet":
type = "annual";
module = "balance-sheet";
break;
case "quarterly_balance_sheet":
type = "quarterly";
module = "balance-sheet";
break;
case "cashflow":
type = "annual";
module = "cash-flow";
break;
case "quarterly_cashflow":
type = "quarterly";
module = "cash-flow";
break;
default:
return `Error: invalid financial type ${financialType}.`;
}
const period1 = new Date();
period1.setFullYear(period1.getFullYear() - 10);
const result = await yahooFinance.fundamentalsTimeSeries(ticker, {
period1,
type,
module,
});
if (!result || result.length === 0) {
return JSON.stringify([]);
}
// ✅ 修正:移除參數型別註解,改用 as 斷言
const records = result.map((entry) => {
const record: Record<string, unknown> = {};
const entryObj = entry as Record<string, unknown>;
for (const [key, value] of Object.entries(entryObj)) {
if (key === "date") {
record["date"] = value instanceof Date
? (value as Date).toISOString().split("T")[0]
: value;
} else if (key === "periodType" || key === "TYPE") {
continue;
} else {
record[key] = value === null || value === undefined ? null : value;
}
}
return record;
});
return JSON.stringify(records);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting financial statement for ${ticker}: ${msg}`;
}
}
// ─── Tool 6: Get Holder Info ──────────────────────────────────────────────────
/**
* Get holder information for a given ticker symbol.
* Maps to: yfinance Ticker.major_holders / institutional_holders / etc.
* Uses: yahooFinance.quoteSummary() with specific modules
*/
export async function getHolderInfo(
ticker: string,
holderType: HolderType
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
// Map holder_type to quoteSummary modules
let modules: string[];
switch (holderType) {
case "major_holders":
modules = ["majorHoldersBreakdown"];
break;
case "institutional_holders":
modules = ["institutionOwnership"];
break;
case "mutualfund_holders":
modules = ["fundOwnership"];
break;
case "insider_transactions":
modules = ["insiderTransactions"];
break;
case "insider_purchases":
modules = ["netSharePurchaseActivity"];
break;
case "insider_roster_holders":
modules = ["insiderHolders"];
break;
default:
return `Error: invalid holder type ${holderType}. Please use one of the following: major_holders, institutional_holders, mutualfund_holders, insider_transactions, insider_purchases, insider_roster_holders.`;
}
const result = await yahooFinance.quoteSummary(ticker, {
modules: modules as any,
});
// Extract the relevant module data
let data: unknown;
switch (holderType) {
case "major_holders":
data = result.majorHoldersBreakdown;
break;
case "institutional_holders":
data = result.institutionOwnership;
break;
case "mutualfund_holders":
data = result.fundOwnership;
break;
case "insider_transactions":
data = result.insiderTransactions;
break;
case "insider_purchases":
data = result.netSharePurchaseActivity;
break;
case "insider_roster_holders":
data = result.insiderHolders;
break;
}
if (!data) {
return JSON.stringify({ message: `No ${holderType} data available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(data));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting holder info for ${ticker}: ${msg}`;
}
}
// ─── Tool 7: Get Option Expiration Dates ──────────────────────────────────────
/**
* Fetch the available options expiration dates for a given ticker symbol.
* Maps to: yfinance Ticker.options
* Uses: yahooFinance.options() and extract expirationDates
*/
export async function getOptionExpirationDates(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.options(ticker);
if (!result.expirationDates || result.expirationDates.length === 0) {
return JSON.stringify([]);
}
// Convert Date objects to YYYY-MM-DD strings to match yfinance format
const dates = result.expirationDates.map((d: Date) => {
if (d instanceof Date) {
return d.toISOString().split("T")[0];
}
return String(d);
});
return JSON.stringify(dates);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting option expiration dates for ${ticker}: ${msg}`;
}
}
// ─── Tool 8: Get Option Chain ─────────────────────────────────────────────────
/**
* Fetch the option chain for a given ticker symbol, expiration date, and option type.
* Maps to: yfinance Ticker.option_chain(date).calls / .puts
* Uses: yahooFinance.options() with date parameter
*/
export async function getOptionChain(
ticker: string,
expirationDate: string,
optionType: OptionType
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
if (optionType !== "calls" && optionType !== "puts") {
return "Error: Invalid option type. Please use 'calls' or 'puts'.";
}
const allOptions = await yahooFinance.options(ticker);
const availableDates = (allOptions.expirationDates || []).map((d: Date) =>
d instanceof Date ? d.toISOString().split("T")[0] : String(d)
);
if (!availableDates.includes(expirationDate)) {
return `Error: No options available for the date ${expirationDate}.`;
}
const result = await yahooFinance.options(ticker, {
date: new Date(expirationDate + "T00:00:00Z"),
});
if (!result.options || result.options.length === 0) {
return JSON.stringify([]);
}
// ✅ 修正:加入 undefined 檢查
const optionData = result.options[0];
if (!optionData) {
return JSON.stringify([]);
}
const chain = optionType === "calls" ? optionData.calls : optionData.puts;
if (!chain || chain.length === 0) {
return JSON.stringify([]);
}
const records = chain.map((option: Record<string, unknown>) => {
const record: Record<string, unknown> = {};
for (const [key, value] of Object.entries(option)) {
if (value instanceof Date) {
record[key] = value.toISOString();
} else {
record[key] = value;
}
}
return record;
});
return JSON.stringify(records);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting option chain for ${ticker}: ${msg}`;
}
}
// ─── Tool 9: Get Recommendations ──────────────────────────────────────────────
/**
* Get analyst recommendations or upgrades/downgrades for a given ticker symbol.
* Maps to: yfinance Ticker.recommendations / Ticker.upgrades_downgrades
* Uses: yahooFinance.quoteSummary() with recommendationTrend / upgradeDowngradeHistory
*/
export async function getRecommendations(
ticker: string,
recommendationType: RecommendationType,
monthsBack: number = 12
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
if (recommendationType === "recommendations") {
// Get recommendation trend (buy/hold/sell counts by period)
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["recommendationTrend"],
});
if (!result.recommendationTrend?.trend) {
return JSON.stringify({ message: `No recommendations data available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.recommendationTrend.trend));
} else if (recommendationType === "upgrades_downgrades") {
// Get upgrade/downgrade history
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["upgradeDowngradeHistory"],
});
if (!result.upgradeDowngradeHistory?.history) {
return JSON.stringify({ message: `No upgrades/downgrades data available for ${ticker}.` });
}
// Filter by monthsBack (matching yfinance behavior)
const cutoffDate = new Date();
cutoffDate.setMonth(cutoffDate.getMonth() - monthsBack);
const filtered = result.upgradeDowngradeHistory.history.filter(
(item) => {
const gradeDate = item.epochGradeDate;
if (gradeDate instanceof Date) {
return gradeDate >= cutoffDate;
}
return true;
}
);
// Sort by date descending (most recent first)
filtered.sort((a, b) => {
const dateA = a.epochGradeDate instanceof Date ? a.epochGradeDate.getTime() : 0;
const dateB = b.epochGradeDate instanceof Date ? b.epochGradeDate.getTime() : 0;
return dateB - dateA;
});
// Get the first occurrence (most recent) for each firm
// This matches the yfinance behavior: drop_duplicates(subset=["Firm"])
const seenFirms = new Set<string>();
const latestByFirm = filtered.filter((item) => {
const firm = item.firm;
if (seenFirms.has(firm)) return false;
seenFirms.add(firm);
return true;
});
return JSON.stringify(safeSerialize(latestByFirm));
} else {
return `Error: invalid recommendation type ${recommendationType}. Please use one of the following: recommendations, upgrades_downgrades.`;
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting recommendations for ${ticker}: ${msg}`;
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// 以下為新增工具(Tool 10 ~ Tool 23)
// ═══════════════════════════════════════════════════════════════════════════════
// ─── Tool 10: Get Quote (Real-time) ───────────────────────────────────────────
/**
* Get real-time quote data for a stock ticker.
* Uses: yahooFinance.quote()
*/
export async function getQuote(ticker: string): Promise<string> {
try {
const result = await yahooFinance.quote(ticker);
if (!result) {
return `No quote data found for ${ticker}.`;
}
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting quote for ${ticker}: ${msg}`;
}
}
// ─── Tool 11: Get Earnings ────────────────────────────────────────────────────
/**
* Get earnings data including history, trend, and estimates.
* Uses: yahooFinance.quoteSummary() with earnings modules
*/
export async function getEarnings(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["earnings", "earningsHistory", "earningsTrend"],
});
const data: Record<string, unknown> = {};
if (result.earnings) {
data.earnings = safeSerialize(result.earnings);
}
if (result.earningsHistory) {
data.earningsHistory = safeSerialize(result.earningsHistory);
}
if (result.earningsTrend) {
data.earningsTrend = safeSerialize(result.earningsTrend);
}
if (Object.keys(data).length === 0) {
return JSON.stringify({ message: `No earnings data available for ${ticker}.` });
}
return JSON.stringify(data);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting earnings for ${ticker}: ${msg}`;
}
}
// ─── Tool 12: Get Calendar Events ─────────────────────────────────────────────
/**
* Get upcoming calendar events (earnings dates, ex-dividend dates).
* Uses: yahooFinance.quoteSummary() with calendarEvents module
*/
export async function getCalendarEvents(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["calendarEvents"],
});
if (!result.calendarEvents) {
return JSON.stringify({ message: `No calendar events available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.calendarEvents));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting calendar events for ${ticker}: ${msg}`;
}
}
// ─── Tool 13: Get ESG Scores ──────────────────────────────────────────────────
/**
* Get ESG (Environmental, Social, Governance) sustainability scores.
* Uses: yahooFinance.quoteSummary() with esgScores module
*/
export async function getEsgScores(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["esgScores" as any],
});
const esgData = (result as any).esgScores;
if (!esgData) {
return JSON.stringify({ message: `No ESG scores available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(esgData));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting ESG scores for ${ticker}: ${msg}`;
}
}
// ─── Tool 14: Get SEC Filings ─────────────────────────────────────────────────
/**
* Get SEC regulatory filings for a company.
* Uses: yahooFinance.quoteSummary() with secFilings module
*/
export async function getSecFilings(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["secFilings"],
});
if (!result.secFilings || !result.secFilings.filings || result.secFilings.filings.length === 0) {
return JSON.stringify({ message: `No SEC filings available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.secFilings.filings));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting SEC filings for ${ticker}: ${msg}`;
}
}
// ─── Tool 15: Get Key Statistics ──────────────────────────────────────────────
/**
* Get key financial statistics and ratios (P/E, P/B, beta, margins, etc.)
* Uses: yahooFinance.quoteSummary() with defaultKeyStatistics module
*/
export async function getKeyStatistics(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["defaultKeyStatistics"],
});
if (!result.defaultKeyStatistics) {
return JSON.stringify({ message: `No key statistics available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.defaultKeyStatistics));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting key statistics for ${ticker}: ${msg}`;
}
}
// ─── Tool 16: Get Financial Data ──────────────────────────────────────────────
/**
* Get detailed financial data (revenue, margins, cash flow, balance sheet metrics).
* Uses: yahooFinance.quoteSummary() with financialData module
*/
export async function getFinancialData(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["financialData"],
});
if (!result.financialData) {
return JSON.stringify({ message: `No financial data available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.financialData));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting financial data for ${ticker}: ${msg}`;
}
}
// ─── Tool 17: Get Insights ────────────────────────────────────────────────────
/**
* Get company insights including analyst sentiment and key metrics.
* Uses: yahooFinance.insights()
*/
export async function getInsights(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.insights(ticker);
if (!result) {
return JSON.stringify({ message: `No insights available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting insights for ${ticker}: ${msg}`;
}
}
// ─── Tool 18: Get Trending Symbols ────────────────────────────────────────────
/**
* Get currently trending symbols on Yahoo Finance.
* Uses: yahooFinance.trendingSymbols()
*/
export async function getTrendingSymbols(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.trendingSymbols("US", { count });
if (!result || result.length === 0) {
return JSON.stringify({ message: "No trending symbols available." });
}
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting trending symbols: ${msg}`;
}
}
// ─── Tool 19: Get Daily Gainers ───────────────────────────────────────────────
/**
* Get top daily gainers.
* Uses: yahooFinance.screener("day_gainers")
*/
export async function getDailyGainers(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.screener("day_gainers");
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: "No daily gainers data available." });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting daily gainers: ${msg}`;
}
}
// ─── Tool 20: Get Daily Losers ────────────────────────────────────────────────
/**
* Get top daily losers.
* Uses: yahooFinance.screener("day_losers")
*/
export async function getDailyLosers(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.screener("day_losers");
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: "No daily losers data available." });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting daily losers: ${msg}`;
}
}
// ─── Tool 21: Get Most Actives ────────────────────────────────────────────────
/**
* Get most actively traded stocks.
* Uses: yahooFinance.screener("most_actives")
*/
export async function getMostActives(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.screener("most_actives");
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: "No most actives data available." });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting most actives: ${msg}`;
}
}
// ─── Tool 22: Get Screener ────────────────────────────────────────────────────
/**
* Run a stock screener with a predefined query.
* Uses: yahooFinance.screener(query)
*
* Available predefined queries:
* - day_gainers, day_losers, most_actives
* - growth_technology_stocks, portfolio_anchors
* - small_cap_gainers, undervalued_growth_stocks
* - undervalued_large_caps, aggressive_small_caps
* - conservative_foreign_funds, high_yield_bond
* - solid_large_growth_funds, solid_midcap_growth_funds
* - top_mutual_funds
*/
export type ScreenerQuery =
| "day_gainers"
| "day_losers"
| "most_actives"
| "growth_technology_stocks"
| "portfolio_anchors"
| "small_cap_gainers"
| "undervalued_growth_stocks"
| "undervalued_large_caps"
| "aggressive_small_caps"
| "conservative_foreign_funds"
| "high_yield_bond"
| "solid_large_growth_funds"
| "solid_midcap_growth_funds"
| "top_mutual_funds";
export async function getScreener(
query: ScreenerQuery,
count: number = 20
): Promise<string> {
try {
const result = await yahooFinance.screener(query);
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: `No results for screener query: ${query}.` });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: running screener with query "${query}": ${msg}`;
}
}
// ─── Tool 23: Get Search ──────────────────────────────────────────────────────
/**
* General search across Yahoo Finance (quotes, news, etc.)
* Uses: yahooFinance.search()
*/
export async function getSearch(
query: string,
quotesCount: number = 5,
newsCount: number = 5
): Promise<string> {
try {
const result = await yahooFinance.search(query, {
quotesCount,
newsCount,
});
const data: Record<string, unknown> = {};
if (result.quotes && result.quotes.length > 0) {
data.quotes = result.quotes.map((q) => ({
symbol: q.symbol,
name: q.shortname || q.longname || "",
exchange: q.exchange || "",
type: q.quoteType || "",
score: q.score,
}));
}
if (result.news && result.news.length > 0) {
data.news = result.news.map((n) => ({
title: n.title,
publisher: n.publisher,
link: n.link,
publishTime: n.providerPublishTime
? new Date(n.providerPublishTime).toISOString()
: null,
relatedTickers: n.relatedTickers || [],
}));
}
if (Object.keys(data).length === 0) {
return `No results found for search query: "${query}".`;
}
return JSON.stringify(data);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: searching for "${query}": ${msg}`;
}
}
src / yahooFinance.ts
/**
* Yahoo Finance API Client
*
* This module provides a TypeScript wrapper around the yahoo-finance2 library (v4),
* implementing all 9 tools from the original Python MCP server:
* https://github.com/Alex2Yang97/yahoo-finance-mcp
*
* Uses yahoo-finance2 v4: https://github.com/gadicc/yahoo-finance2
*/
import YahooFinance from "yahoo-finance2";
// ─── Singleton Instance ────────────────────────────────────────────────────────
const yahooFinance = new YahooFinance();
// ─── Type Definitions ──────────────────────────────────────────────────────────
export type Period =
| "1d" | "5d" | "1mo" | "3mo" | "6mo"
| "1y" | "2y" | "5y" | "10y" | "ytd" | "max";
export type Interval =
| "1m" | "2m" | "5m" | "15m" | "30m" | "60m" | "90m"
| "1h" | "1d" | "5d" | "1wk" | "1mo" | "3mo";
export type FinancialType =
| "income_stmt"
| "quarterly_income_stmt"
| "balance_sheet"
| "quarterly_balance_sheet"
| "cashflow"
| "quarterly_cashflow";
export type HolderType =
| "major_holders"
| "institutional_holders"
| "mutualfund_holders"
| "insider_transactions"
| "insider_purchases"
| "insider_roster_holders";
export type RecommendationType = "recommendations" | "upgrades_downgrades";
export type OptionType = "calls" | "puts";
// ─── Helper Functions ──────────────────────────────────────────────────────────
/**
* Convert a yfinance-style period string to a Date object (period1 start date).
* The chart module in yahoo-finance2 uses period1/period2 (Date) instead of
* yfinance's "period" string parameter.
*/
function periodToDateRange(period: Period): { period1: Date; period2: Date } {
const now = new Date();
const period2 = now;
let period1: Date;
switch (period) {
case "1d":
period1 = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000);
break;
case "5d":
period1 = new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000);
break;
case "1mo":
period1 = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
break;
case "3mo":
period1 = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
break;
case "6mo":
period1 = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
break;
case "1y":
period1 = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
break;
case "2y":
period1 = new Date(now.getFullYear() - 2, now.getMonth(), now.getDate());
break;
case "5y":
period1 = new Date(now.getFullYear() - 5, now.getMonth(), now.getDate());
break;
case "10y":
period1 = new Date(now.getFullYear() - 10, now.getMonth(), now.getDate());
break;
case "ytd":
period1 = new Date(now.getFullYear(), 0, 1);
break;
case "max":
period1 = new Date(1970, 0, 1);
break;
default:
period1 = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
}
return { period1, period2 };
}
/**
* Safely serialize a value, converting Date objects to ISO strings
* and handling null/undefined.
*/
function safeSerialize(value: unknown): unknown {
if (value === null || value === undefined) return null;
if (value instanceof Date) return value.toISOString();
if (typeof value === "object" && !Array.isArray(value)) {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
result[k] = safeSerialize(v);
}
return result;
}
if (Array.isArray(value)) {
return value.map(safeSerialize);
}
return value;
}
/**
* Validate that a ticker symbol exists by attempting a basic quote lookup.
* Returns an error message string if invalid, or null if valid.
*/
async function validateTicker(ticker: string): Promise<string | null> {
try {
const quote = await yahooFinance.quote(ticker);
if (!quote || !quote.symbol) {
return `Company ticker ${ticker} not found.`;
}
return null;
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("No data found") || msg.includes("not found")) {
return `Company ticker ${ticker} not found.`;
}
return `Error validating ticker ${ticker}: ${msg}`;
}
}
// ─── Tool 1: Get Historical Stock Prices ───────────────────────────────────────
/**
* Get historical OHLCV data for a given ticker symbol.
* Maps to: yfinance Ticker.history(period, interval)
* Uses: yahooFinance.chart() with period1/period2 conversion
*/
export async function getHistoricalStockPrices(
ticker: string,
period: Period = "1mo",
interval: Interval = "1d"
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const { period1, period2 } = periodToDateRange(period);
const result = await yahooFinance.chart(ticker, {
period1,
period2,
interval,
return: "array",
});
if (!result.quotes || result.quotes.length === 0) {
return JSON.stringify([]);
}
// Transform to match yfinance output format:
// Date, Open, High, Low, Close, Volume, Adj Close
const records = result.quotes.map((q) => ({
Date: q.date instanceof Date ? q.date.toISOString() : q.date,
Open: q.open,
High: q.high,
Low: q.low,
Close: q.close,
Volume: q.volume,
"Adj Close": q.adjclose,
}));
return JSON.stringify(records);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting historical stock prices for ${ticker}: ${msg}`;
}
}
// ─── Tool 2: Get Stock Info ────────────────────────────────────────────────────
/**
* Get comprehensive stock information for a given ticker symbol.
* Maps to: yfinance Ticker.info
* Uses: yahooFinance.quoteSummary() with modules: "all"
*/
export async function getStockInfo(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: "all",
});
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting stock information for ${ticker}: ${msg}`;
}
}
// ─── Tool 3: Get Yahoo Finance News ───────────────────────────────────────────
/**
* Get news articles for a given ticker symbol.
* Maps to: yfinance Ticker.news (filtered by contentType === "STORY")
* Uses: yahooFinance.search() with newsCount: 10
*/
export async function getYahooFinanceNews(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const searchResult = await yahooFinance.search(ticker, {
newsCount: 10,
quotesCount: 0,
});
if (!searchResult.news || searchResult.news.length === 0) {
return `No news found for company that searched with ${ticker} ticker.`;
}
const newsList = searchResult.news.map((article) => {
const title = article.title || "";
const publisher = article.publisher || "";
const link = article.link || "";
const publishTime = article.providerPublishTime
? new Date(article.providerPublishTime).toISOString()
: "";
const relatedTickers = article.relatedTickers?.join(", ") || "";
return [
`Title: ${title}`,
`Publisher: ${publisher}`,
`Published: ${publishTime}`,
`Related Tickers: ${relatedTickers}`,
`URL: ${link}`,
].join("\n");
});
return newsList.join("\n\n");
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting news for ${ticker}: ${msg}`;
}
}
// ─── Tool 4: Get Stock Actions (Dividends & Splits) ───────────────────────────
/**
* Get stock dividends and stock splits for a given ticker symbol.
* Maps to: yfinance Ticker.actions
* Uses: yahooFinance.chart() with events: "div|split"
*/
export async function getStockActions(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
// Use a wide date range to get all historical actions
const result = await yahooFinance.chart(ticker, {
period1: new Date(1970, 0, 1),
period2: new Date(),
interval: "1d",
events: "div|split",
return: "array",
});
const actions: Array<Record<string, unknown>> = [];
// Process dividends
if (result.events?.dividends) {
for (const div of result.events.dividends) {
actions.push({
Date: div.date instanceof Date ? div.date.toISOString() : div.date,
Type: "Dividend",
Amount: div.amount,
});
}
}
// Process splits
if (result.events?.splits) {
for (const split of result.events.splits) {
actions.push({
Date: split.date instanceof Date ? split.date.toISOString() : split.date,
Type: "Split",
Numerator: split.numerator,
Denominator: split.denominator,
SplitRatio: split.splitRatio,
});
}
}
// Sort by date
actions.sort((a, b) => {
const dateA = new Date(a.Date as string).getTime();
const dateB = new Date(b.Date as string).getTime();
return dateA - dateB;
});
return JSON.stringify(actions);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting stock actions for ${ticker}: ${msg}`;
}
}
// ─── Tool 5: Get Financial Statement ──────────────────────────────────────────
/**
* Get financial statements for a given ticker symbol.
* Maps to: yfinance Ticker.income_stmt / balance_sheet / cashflow (and quarterly variants)
* Uses: yahooFinance.fundamentalsTimeSeries()
*
* Note: Since Nov 2024, quoteSummary's incomeStatementHistory*, balanceSheetHistory*,
* cashflowStatementHistory* modules provide almost no data. We use fundamentalsTimeSeries
* instead as recommended by the yahoo-finance2 docs.
*/
export async function getFinancialStatement(
ticker: string,
financialType: FinancialType
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
let type: string;
let module: string;
switch (financialType) {
case "income_stmt":
type = "annual";
module = "financials";
break;
case "quarterly_income_stmt":
type = "quarterly";
module = "financials";
break;
case "balance_sheet":
type = "annual";
module = "balance-sheet";
break;
case "quarterly_balance_sheet":
type = "quarterly";
module = "balance-sheet";
break;
case "cashflow":
type = "annual";
module = "cash-flow";
break;
case "quarterly_cashflow":
type = "quarterly";
module = "cash-flow";
break;
default:
return `Error: invalid financial type ${financialType}.`;
}
const period1 = new Date();
period1.setFullYear(period1.getFullYear() - 10);
const result = await yahooFinance.fundamentalsTimeSeries(ticker, {
period1,
type,
module,
});
if (!result || result.length === 0) {
return JSON.stringify([]);
}
// ✅ 修正:移除參數型別註解,改用 as 斷言
const records = result.map((entry) => {
const record: Record<string, unknown> = {};
const entryObj = entry as Record<string, unknown>;
for (const [key, value] of Object.entries(entryObj)) {
if (key === "date") {
record["date"] = value instanceof Date
? (value as Date).toISOString().split("T")[0]
: value;
} else if (key === "periodType" || key === "TYPE") {
continue;
} else {
record[key] = value === null || value === undefined ? null : value;
}
}
return record;
});
return JSON.stringify(records);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting financial statement for ${ticker}: ${msg}`;
}
}
// ─── Tool 6: Get Holder Info ──────────────────────────────────────────────────
/**
* Get holder information for a given ticker symbol.
* Maps to: yfinance Ticker.major_holders / institutional_holders / etc.
* Uses: yahooFinance.quoteSummary() with specific modules
*/
export async function getHolderInfo(
ticker: string,
holderType: HolderType
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
// Map holder_type to quoteSummary modules
let modules: string[];
switch (holderType) {
case "major_holders":
modules = ["majorHoldersBreakdown"];
break;
case "institutional_holders":
modules = ["institutionOwnership"];
break;
case "mutualfund_holders":
modules = ["fundOwnership"];
break;
case "insider_transactions":
modules = ["insiderTransactions"];
break;
case "insider_purchases":
modules = ["netSharePurchaseActivity"];
break;
case "insider_roster_holders":
modules = ["insiderHolders"];
break;
default:
return `Error: invalid holder type ${holderType}. Please use one of the following: major_holders, institutional_holders, mutualfund_holders, insider_transactions, insider_purchases, insider_roster_holders.`;
}
const result = await yahooFinance.quoteSummary(ticker, {
modules: modules as any,
});
// Extract the relevant module data
let data: unknown;
switch (holderType) {
case "major_holders":
data = result.majorHoldersBreakdown;
break;
case "institutional_holders":
data = result.institutionOwnership;
break;
case "mutualfund_holders":
data = result.fundOwnership;
break;
case "insider_transactions":
data = result.insiderTransactions;
break;
case "insider_purchases":
data = result.netSharePurchaseActivity;
break;
case "insider_roster_holders":
data = result.insiderHolders;
break;
}
if (!data) {
return JSON.stringify({ message: `No ${holderType} data available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(data));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting holder info for ${ticker}: ${msg}`;
}
}
// ─── Tool 7: Get Option Expiration Dates ──────────────────────────────────────
/**
* Fetch the available options expiration dates for a given ticker symbol.
* Maps to: yfinance Ticker.options
* Uses: yahooFinance.options() and extract expirationDates
*/
export async function getOptionExpirationDates(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.options(ticker);
if (!result.expirationDates || result.expirationDates.length === 0) {
return JSON.stringify([]);
}
// Convert Date objects to YYYY-MM-DD strings to match yfinance format
const dates = result.expirationDates.map((d: Date) => {
if (d instanceof Date) {
return d.toISOString().split("T")[0];
}
return String(d);
});
return JSON.stringify(dates);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting option expiration dates for ${ticker}: ${msg}`;
}
}
// ─── Tool 8: Get Option Chain ─────────────────────────────────────────────────
/**
* Fetch the option chain for a given ticker symbol, expiration date, and option type.
* Maps to: yfinance Ticker.option_chain(date).calls / .puts
* Uses: yahooFinance.options() with date parameter
*/
export async function getOptionChain(
ticker: string,
expirationDate: string,
optionType: OptionType
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
if (optionType !== "calls" && optionType !== "puts") {
return "Error: Invalid option type. Please use 'calls' or 'puts'.";
}
const allOptions = await yahooFinance.options(ticker);
const availableDates = (allOptions.expirationDates || []).map((d: Date) =>
d instanceof Date ? d.toISOString().split("T")[0] : String(d)
);
if (!availableDates.includes(expirationDate)) {
return `Error: No options available for the date ${expirationDate}.`;
}
const result = await yahooFinance.options(ticker, {
date: new Date(expirationDate + "T00:00:00Z"),
});
if (!result.options || result.options.length === 0) {
return JSON.stringify([]);
}
// ✅ 修正:加入 undefined 檢查
const optionData = result.options[0];
if (!optionData) {
return JSON.stringify([]);
}
const chain = optionType === "calls" ? optionData.calls : optionData.puts;
if (!chain || chain.length === 0) {
return JSON.stringify([]);
}
const records = chain.map((option: Record<string, unknown>) => {
const record: Record<string, unknown> = {};
for (const [key, value] of Object.entries(option)) {
if (value instanceof Date) {
record[key] = value.toISOString();
} else {
record[key] = value;
}
}
return record;
});
return JSON.stringify(records);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting option chain for ${ticker}: ${msg}`;
}
}
// ─── Tool 9: Get Recommendations ──────────────────────────────────────────────
/**
* Get analyst recommendations or upgrades/downgrades for a given ticker symbol.
* Maps to: yfinance Ticker.recommendations / Ticker.upgrades_downgrades
* Uses: yahooFinance.quoteSummary() with recommendationTrend / upgradeDowngradeHistory
*/
export async function getRecommendations(
ticker: string,
recommendationType: RecommendationType,
monthsBack: number = 12
): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
if (recommendationType === "recommendations") {
// Get recommendation trend (buy/hold/sell counts by period)
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["recommendationTrend"],
});
if (!result.recommendationTrend?.trend) {
return JSON.stringify({ message: `No recommendations data available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.recommendationTrend.trend));
} else if (recommendationType === "upgrades_downgrades") {
// Get upgrade/downgrade history
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["upgradeDowngradeHistory"],
});
if (!result.upgradeDowngradeHistory?.history) {
return JSON.stringify({ message: `No upgrades/downgrades data available for ${ticker}.` });
}
// Filter by monthsBack (matching yfinance behavior)
const cutoffDate = new Date();
cutoffDate.setMonth(cutoffDate.getMonth() - monthsBack);
const filtered = result.upgradeDowngradeHistory.history.filter(
(item) => {
const gradeDate = item.epochGradeDate;
if (gradeDate instanceof Date) {
return gradeDate >= cutoffDate;
}
return true;
}
);
// Sort by date descending (most recent first)
filtered.sort((a, b) => {
const dateA = a.epochGradeDate instanceof Date ? a.epochGradeDate.getTime() : 0;
const dateB = b.epochGradeDate instanceof Date ? b.epochGradeDate.getTime() : 0;
return dateB - dateA;
});
// Get the first occurrence (most recent) for each firm
// This matches the yfinance behavior: drop_duplicates(subset=["Firm"])
const seenFirms = new Set<string>();
const latestByFirm = filtered.filter((item) => {
const firm = item.firm;
if (seenFirms.has(firm)) return false;
seenFirms.add(firm);
return true;
});
return JSON.stringify(safeSerialize(latestByFirm));
} else {
return `Error: invalid recommendation type ${recommendationType}. Please use one of the following: recommendations, upgrades_downgrades.`;
}
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting recommendations for ${ticker}: ${msg}`;
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// 以下為新增工具(Tool 10 ~ Tool 23)
// ═══════════════════════════════════════════════════════════════════════════════
// ─── Tool 10: Get Quote (Real-time) ───────────────────────────────────────────
/**
* Get real-time quote data for a stock ticker.
* Uses: yahooFinance.quote()
*/
export async function getQuote(ticker: string): Promise<string> {
try {
const result = await yahooFinance.quote(ticker);
if (!result) {
return `No quote data found for ${ticker}.`;
}
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting quote for ${ticker}: ${msg}`;
}
}
// ─── Tool 11: Get Earnings ────────────────────────────────────────────────────
/**
* Get earnings data including history, trend, and estimates.
* Uses: yahooFinance.quoteSummary() with earnings modules
*/
export async function getEarnings(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["earnings", "earningsHistory", "earningsTrend"],
});
const data: Record<string, unknown> = {};
if (result.earnings) {
data.earnings = safeSerialize(result.earnings);
}
if (result.earningsHistory) {
data.earningsHistory = safeSerialize(result.earningsHistory);
}
if (result.earningsTrend) {
data.earningsTrend = safeSerialize(result.earningsTrend);
}
if (Object.keys(data).length === 0) {
return JSON.stringify({ message: `No earnings data available for ${ticker}.` });
}
return JSON.stringify(data);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting earnings for ${ticker}: ${msg}`;
}
}
// ─── Tool 12: Get Calendar Events ─────────────────────────────────────────────
/**
* Get upcoming calendar events (earnings dates, ex-dividend dates).
* Uses: yahooFinance.quoteSummary() with calendarEvents module
*/
export async function getCalendarEvents(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["calendarEvents"],
});
if (!result.calendarEvents) {
return JSON.stringify({ message: `No calendar events available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.calendarEvents));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting calendar events for ${ticker}: ${msg}`;
}
}
// ─── Tool 13: Get ESG Scores ──────────────────────────────────────────────────
/**
* Get ESG (Environmental, Social, Governance) sustainability scores.
* Uses: yahooFinance.quoteSummary() with esgScores module
*/
export async function getEsgScores(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["esgScores" as any],
});
const esgData = (result as any).esgScores;
if (!esgData) {
return JSON.stringify({ message: `No ESG scores available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(esgData));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting ESG scores for ${ticker}: ${msg}`;
}
}
// ─── Tool 14: Get SEC Filings ─────────────────────────────────────────────────
/**
* Get SEC regulatory filings for a company.
* Uses: yahooFinance.quoteSummary() with secFilings module
*/
export async function getSecFilings(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["secFilings"],
});
if (!result.secFilings || !result.secFilings.filings || result.secFilings.filings.length === 0) {
return JSON.stringify({ message: `No SEC filings available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.secFilings.filings));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting SEC filings for ${ticker}: ${msg}`;
}
}
// ─── Tool 15: Get Key Statistics ──────────────────────────────────────────────
/**
* Get key financial statistics and ratios (P/E, P/B, beta, margins, etc.)
* Uses: yahooFinance.quoteSummary() with defaultKeyStatistics module
*/
export async function getKeyStatistics(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["defaultKeyStatistics"],
});
if (!result.defaultKeyStatistics) {
return JSON.stringify({ message: `No key statistics available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.defaultKeyStatistics));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting key statistics for ${ticker}: ${msg}`;
}
}
// ─── Tool 16: Get Financial Data ──────────────────────────────────────────────
/**
* Get detailed financial data (revenue, margins, cash flow, balance sheet metrics).
* Uses: yahooFinance.quoteSummary() with financialData module
*/
export async function getFinancialData(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.quoteSummary(ticker, {
modules: ["financialData"],
});
if (!result.financialData) {
return JSON.stringify({ message: `No financial data available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result.financialData));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting financial data for ${ticker}: ${msg}`;
}
}
// ─── Tool 17: Get Insights ────────────────────────────────────────────────────
/**
* Get company insights including analyst sentiment and key metrics.
* Uses: yahooFinance.insights()
*/
export async function getInsights(ticker: string): Promise<string> {
const validationError = await validateTicker(ticker);
if (validationError) return validationError;
try {
const result = await yahooFinance.insights(ticker);
if (!result) {
return JSON.stringify({ message: `No insights available for ${ticker}.` });
}
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting insights for ${ticker}: ${msg}`;
}
}
// ─── Tool 18: Get Trending Symbols ────────────────────────────────────────────
/**
* Get currently trending symbols on Yahoo Finance.
* Uses: yahooFinance.trendingSymbols()
*/
export async function getTrendingSymbols(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.trendingSymbols("US", { count });
if (!result || result.length === 0) {
return JSON.stringify({ message: "No trending symbols available." });
}
return JSON.stringify(safeSerialize(result));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting trending symbols: ${msg}`;
}
}
// ─── Tool 19: Get Daily Gainers ───────────────────────────────────────────────
/**
* Get top daily gainers.
* Uses: yahooFinance.screener("day_gainers")
*/
export async function getDailyGainers(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.screener("day_gainers");
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: "No daily gainers data available." });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting daily gainers: ${msg}`;
}
}
// ─── Tool 20: Get Daily Losers ────────────────────────────────────────────────
/**
* Get top daily losers.
* Uses: yahooFinance.screener("day_losers")
*/
export async function getDailyLosers(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.screener("day_losers");
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: "No daily losers data available." });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting daily losers: ${msg}`;
}
}
// ─── Tool 21: Get Most Actives ────────────────────────────────────────────────
/**
* Get most actively traded stocks.
* Uses: yahooFinance.screener("most_actives")
*/
export async function getMostActives(count: number = 10): Promise<string> {
try {
const result = await yahooFinance.screener("most_actives");
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: "No most actives data available." });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: getting most actives: ${msg}`;
}
}
// ─── Tool 22: Get Screener ────────────────────────────────────────────────────
/**
* Run a stock screener with a predefined query.
* Uses: yahooFinance.screener(query)
*
* Available predefined queries:
* - day_gainers, day_losers, most_actives
* - growth_technology_stocks, portfolio_anchors
* - small_cap_gainers, undervalued_growth_stocks
* - undervalued_large_caps, aggressive_small_caps
* - conservative_foreign_funds, high_yield_bond
* - solid_large_growth_funds, solid_midcap_growth_funds
* - top_mutual_funds
*/
export type ScreenerQuery =
| "day_gainers"
| "day_losers"
| "most_actives"
| "growth_technology_stocks"
| "portfolio_anchors"
| "small_cap_gainers"
| "undervalued_growth_stocks"
| "undervalued_large_caps"
| "aggressive_small_caps"
| "conservative_foreign_funds"
| "high_yield_bond"
| "solid_large_growth_funds"
| "solid_midcap_growth_funds"
| "top_mutual_funds";
export async function getScreener(
query: ScreenerQuery,
count: number = 20
): Promise<string> {
try {
const result = await yahooFinance.screener(query);
if (!result || !result.quotes || result.quotes.length === 0) {
return JSON.stringify({ message: `No results for screener query: ${query}.` });
}
const limited = result.quotes.slice(0, count);
return JSON.stringify(safeSerialize(limited));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: running screener with query "${query}": ${msg}`;
}
}
// ─── Tool 23: Get Search ──────────────────────────────────────────────────────
/**
* General search across Yahoo Finance (quotes, news, etc.)
* Uses: yahooFinance.search()
*/
export async function getSearch(
query: string,
quotesCount: number = 5,
newsCount: number = 5
): Promise<string> {
try {
const result = await yahooFinance.search(query, {
quotesCount,
newsCount,
});
const data: Record<string, unknown> = {};
if (result.quotes && result.quotes.length > 0) {
data.quotes = result.quotes.map((q) => ({
symbol: q.symbol,
name: q.shortname || q.longname || "",
exchange: q.exchange || "",
type: q.quoteType || "",
score: q.score,
}));
}
if (result.news && result.news.length > 0) {
data.news = result.news.map((n) => ({
title: n.title,
publisher: n.publisher,
link: n.link,
publishTime: n.providerPublishTime
? new Date(n.providerPublishTime).toISOString()
: null,
relatedTickers: n.relatedTickers || [],
}));
}
if (Object.keys(data).length === 0) {
return `No results found for search query: "${query}".`;
}
return JSON.stringify(data);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
return `Error: searching for "${query}": ${msg}`;
}
}