309 lines
11 KiB
Python
309 lines
11 KiB
Python
"""Finnhub fundamental data functions.
|
|
|
|
Provides company profiles, financial statements, and key financial metrics
|
|
using the Finnhub REST API. Output formats mirror the Alpha Vantage
|
|
equivalents where possible for consistent agent-facing data.
|
|
"""
|
|
|
|
from typing import Literal
|
|
|
|
from .finnhub_common import (
|
|
FinnhubError,
|
|
ThirdPartyParseError,
|
|
_make_api_request,
|
|
_now_str,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Type aliases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
StatementType = Literal["balance_sheet", "income_statement", "cash_flow"]
|
|
Frequency = Literal["annual", "quarterly"]
|
|
|
|
# Mapping from our canonical statement_type names to Finnhub's "statement" param
|
|
_STATEMENT_MAP: dict[str, str] = {
|
|
"balance_sheet": "bs",
|
|
"income_statement": "ic",
|
|
"cash_flow": "cf",
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def get_company_profile(symbol: str) -> str:
|
|
"""Fetch company profile and overview via Finnhub /stock/profile2.
|
|
|
|
Returns a formatted text block with key company metadata including name,
|
|
industry, sector, market cap, and shares outstanding — mirroring the
|
|
information returned by Alpha Vantage OVERVIEW.
|
|
|
|
Args:
|
|
symbol: Equity ticker symbol (e.g. "AAPL").
|
|
|
|
Returns:
|
|
Formatted multi-line string with company profile fields.
|
|
|
|
Raises:
|
|
FinnhubError: When the API returns an error or the symbol is invalid.
|
|
ThirdPartyParseError: When the response cannot be parsed.
|
|
"""
|
|
data = _make_api_request("stock/profile2", {"symbol": symbol})
|
|
|
|
if not data:
|
|
raise FinnhubError(
|
|
f"Empty profile response for symbol={symbol}. "
|
|
"Symbol may be invalid or not covered."
|
|
)
|
|
|
|
name = data.get("name", "N/A")
|
|
ticker = data.get("ticker", symbol)
|
|
exchange = data.get("exchange", "N/A")
|
|
ipo_date = data.get("ipo", "N/A")
|
|
industry = data.get("finnhubIndustry", "N/A")
|
|
# Finnhub does not return a top-level sector — the industry string is the
|
|
# finest granularity available in the free profile endpoint.
|
|
market_cap = data.get("marketCapitalization", None)
|
|
shares_outstanding = data.get("shareOutstanding", None)
|
|
currency = data.get("currency", "USD")
|
|
country = data.get("country", "N/A")
|
|
website = data.get("weburl", "N/A")
|
|
logo = data.get("logo", "N/A")
|
|
phone = data.get("phone", "N/A")
|
|
|
|
# Format market cap in billions for readability
|
|
if market_cap is not None:
|
|
try:
|
|
market_cap_str = f"${float(market_cap):,.2f}M"
|
|
except (ValueError, TypeError):
|
|
market_cap_str = str(market_cap)
|
|
else:
|
|
market_cap_str = "N/A"
|
|
|
|
if shares_outstanding is not None:
|
|
try:
|
|
shares_str = f"{float(shares_outstanding):,.2f}M"
|
|
except (ValueError, TypeError):
|
|
shares_str = str(shares_outstanding)
|
|
else:
|
|
shares_str = "N/A"
|
|
|
|
lines: list[str] = [
|
|
f"# Company Profile: {name} ({ticker}) — Finnhub",
|
|
f"# Data retrieved on: {_now_str()}",
|
|
"",
|
|
f"Name: {name}",
|
|
f"Symbol: {ticker}",
|
|
f"Exchange: {exchange}",
|
|
f"Country: {country}",
|
|
f"Currency: {currency}",
|
|
f"Industry: {industry}",
|
|
f"IPO Date: {ipo_date}",
|
|
f"Market Cap: {market_cap_str}",
|
|
f"Shares Outstanding: {shares_str}",
|
|
f"Website: {website}",
|
|
f"Phone: {phone}",
|
|
f"Logo: {logo}",
|
|
]
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def get_financial_statements(
|
|
symbol: str,
|
|
statement_type: StatementType = "income_statement",
|
|
freq: Frequency = "quarterly",
|
|
) -> str:
|
|
"""Fetch financial statement data via Finnhub /financials-reported.
|
|
|
|
Returns a structured text representation of the most recent reported
|
|
financial data. Mirrors the pattern of the Alpha Vantage INCOME_STATEMENT,
|
|
BALANCE_SHEET, and CASH_FLOW endpoints.
|
|
|
|
Args:
|
|
symbol: Equity ticker symbol (e.g. "AAPL").
|
|
statement_type: One of ``'balance_sheet'``, ``'income_statement'``,
|
|
or ``'cash_flow'``.
|
|
freq: Reporting frequency — ``'annual'`` or ``'quarterly'``.
|
|
|
|
Returns:
|
|
Formatted multi-line string with the financial statement data.
|
|
|
|
Raises:
|
|
ValueError: When an unsupported ``statement_type`` is provided.
|
|
FinnhubError: On API-level errors or missing data.
|
|
ThirdPartyParseError: When the response cannot be parsed.
|
|
"""
|
|
if statement_type not in _STATEMENT_MAP:
|
|
raise ValueError(
|
|
f"Invalid statement_type '{statement_type}'. "
|
|
f"Must be one of: {list(_STATEMENT_MAP.keys())}"
|
|
)
|
|
|
|
finnhub_statement = _STATEMENT_MAP[statement_type]
|
|
# Finnhub uses "annual" / "quarterly" directly
|
|
params = {
|
|
"symbol": symbol,
|
|
"freq": freq,
|
|
}
|
|
|
|
data = _make_api_request("financials-reported", params)
|
|
|
|
reports: list[dict] = data.get("data", [])
|
|
if not reports:
|
|
raise FinnhubError(
|
|
f"No financial reports returned for symbol={symbol}, "
|
|
f"statement_type={statement_type}, freq={freq}"
|
|
)
|
|
|
|
# Use the most recent report
|
|
latest_report = reports[0]
|
|
period = latest_report.get("period", "N/A")
|
|
year = latest_report.get("year", "N/A")
|
|
quarter = latest_report.get("quarter", "")
|
|
filing_date = latest_report.get("filedDate", "N/A")
|
|
accepted_date = latest_report.get("acceptedDate", "N/A")
|
|
form = latest_report.get("form", "N/A")
|
|
cik = latest_report.get("cik", "N/A")
|
|
|
|
# The 'report' sub-dict holds the three statement types under keys "bs", "ic", "cf"
|
|
report_data: dict = latest_report.get("report", {})
|
|
statement_rows: list[dict] = report_data.get(finnhub_statement, [])
|
|
|
|
period_label = f"Q{quarter} {year}" if quarter else str(year)
|
|
header = (
|
|
f"# {statement_type.replace('_', ' ').title()} — {symbol} "
|
|
f"({period_label}, {freq.title()}) — Finnhub\n"
|
|
f"# Data retrieved on: {_now_str()}\n"
|
|
f"# Filing: {form} | Filed: {filing_date} | Accepted: {accepted_date}\n"
|
|
f"# CIK: {cik} | Period: {period}\n\n"
|
|
)
|
|
|
|
if not statement_rows:
|
|
return header + "_No line items found in this report._\n"
|
|
|
|
lines: list[str] = [header]
|
|
lines.append(f"{'Concept':<50} {'Unit':<10} {'Value':>20}")
|
|
lines.append("-" * 82)
|
|
|
|
for row in statement_rows:
|
|
concept = row.get("concept", "N/A")
|
|
label = row.get("label", concept)
|
|
unit = row.get("unit", "USD")
|
|
value = row.get("value", None)
|
|
|
|
if value is None:
|
|
value_str = "N/A"
|
|
else:
|
|
try:
|
|
value_str = f"{float(value):>20,.0f}"
|
|
except (ValueError, TypeError):
|
|
value_str = str(value)
|
|
|
|
# Truncate long labels to keep alignment readable
|
|
display_label = label[:49] if len(label) > 49 else label
|
|
lines.append(f"{display_label:<50} {unit:<10} {value_str}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def get_basic_financials(symbol: str) -> str:
|
|
"""Fetch key financial ratios and metrics via Finnhub /stock/metric.
|
|
|
|
Returns a formatted text block with P/E, P/B, ROE, debt/equity, 52-week
|
|
range, and other standard financial metrics — mirroring the kind of data
|
|
returned by Alpha Vantage OVERVIEW for ratio-focused consumers.
|
|
|
|
Args:
|
|
symbol: Equity ticker symbol (e.g. "AAPL").
|
|
|
|
Returns:
|
|
Formatted multi-line string with key financial metrics.
|
|
|
|
Raises:
|
|
FinnhubError: On API-level errors or missing data.
|
|
ThirdPartyParseError: When the response cannot be parsed.
|
|
"""
|
|
data = _make_api_request("stock/metric", {"symbol": symbol, "metric": "all"})
|
|
|
|
metric: dict = data.get("metric", {})
|
|
if not metric:
|
|
raise FinnhubError(
|
|
f"No metric data returned for symbol={symbol}. "
|
|
"Symbol may be invalid or not covered on the free tier."
|
|
)
|
|
|
|
series: dict = data.get("series", {})
|
|
|
|
def _fmt(key: str, prefix: str = "", suffix: str = "") -> str:
|
|
"""Format a metric value with optional prefix/suffix."""
|
|
val = metric.get(key)
|
|
if val is None:
|
|
return "N/A"
|
|
try:
|
|
return f"{prefix}{float(val):,.4f}{suffix}"
|
|
except (ValueError, TypeError):
|
|
return str(val)
|
|
|
|
def _fmt_int(key: str, prefix: str = "", suffix: str = "") -> str:
|
|
"""Format a metric value as an integer."""
|
|
val = metric.get(key)
|
|
if val is None:
|
|
return "N/A"
|
|
try:
|
|
return f"{prefix}{int(float(val)):,}{suffix}"
|
|
except (ValueError, TypeError):
|
|
return str(val)
|
|
|
|
lines: list[str] = [
|
|
f"# Key Financial Metrics: {symbol} — Finnhub",
|
|
f"# Data retrieved on: {_now_str()}",
|
|
"",
|
|
"## Valuation",
|
|
f" P/E (TTM): {_fmt('peTTM')}",
|
|
f" P/E (Annual): {_fmt('peAnnual')}",
|
|
f" P/B (Quarterly): {_fmt('pbQuarterly')}",
|
|
f" P/B (Annual): {_fmt('pbAnnual')}",
|
|
f" P/S (TTM): {_fmt('psTTM')}",
|
|
f" P/CF (TTM): {_fmt('pcfShareTTM')}",
|
|
f" EV/EBITDA (TTM): {_fmt('evEbitdaTTM')}",
|
|
"",
|
|
"## Price Range",
|
|
f" 52-Week High: {_fmt('52WeekHigh', prefix='$')}",
|
|
f" 52-Week Low: {_fmt('52WeekLow', prefix='$')}",
|
|
f" 52-Week Return: {_fmt('52WeekPriceReturnDaily', suffix='%')}",
|
|
f" Beta (5Y Monthly): {_fmt('beta')}",
|
|
"",
|
|
"## Profitability",
|
|
f" ROE (TTM): {_fmt('roeTTM', suffix='%')}",
|
|
f" ROA (TTM): {_fmt('roaTTM', suffix='%')}",
|
|
f" ROIC (TTM): {_fmt('roicTTM', suffix='%')}",
|
|
f" Gross Margin (TTM): {_fmt('grossMarginTTM', suffix='%')}",
|
|
f" Net Profit Margin (TTM): {_fmt('netProfitMarginTTM', suffix='%')}",
|
|
f" Operating Margin (TTM): {_fmt('operatingMarginTTM', suffix='%')}",
|
|
"",
|
|
"## Leverage",
|
|
f" Total Debt/Equity (Quarterly):{_fmt('totalDebt/totalEquityQuarterly')}",
|
|
f" Total Debt/Equity (Annual): {_fmt('totalDebt/totalEquityAnnual')}",
|
|
f" Current Ratio (Quarterly): {_fmt('currentRatioQuarterly')}",
|
|
f" Quick Ratio (Quarterly): {_fmt('quickRatioQuarterly')}",
|
|
"",
|
|
"## Growth",
|
|
f" EPS Growth (TTM YoY): {_fmt('epsGrowthTTMYoy', suffix='%')}",
|
|
f" Revenue Growth (TTM YoY): {_fmt('revenueGrowthTTMYoy', suffix='%')}",
|
|
f" Dividend Yield (TTM): {_fmt('dividendYieldIndicatedAnnual', suffix='%')}",
|
|
f" Payout Ratio (TTM): {_fmt('payoutRatioTTM', suffix='%')}",
|
|
"",
|
|
"## Per Share",
|
|
f" EPS (TTM): {_fmt('epsTTM', prefix='$')}",
|
|
f" EPS (Annual): {_fmt('epsAnnual', prefix='$')}",
|
|
f" Revenue Per Share (TTM): {_fmt('revenuePerShareTTM', prefix='$')}",
|
|
f" Free Cash Flow Per Share: {_fmt('fcfPerShareTTM', prefix='$')}",
|
|
f" Book Value Per Share (Qtr): {_fmt('bookValuePerShareQuarterly', prefix='$')}",
|
|
]
|
|
|
|
return "\n".join(lines)
|