Elixir client for Yahoo! Finance.
v0.11 surface:
get_quote/1— single-symbol quote (carriesquote_typesince v0.9).get_quotes/1— batched quote fetch (up to 50 symbols per HTTP call; this function transparently batches larger lists).get_fx_rate/2— current FX rate between two ISO 4217 currency codes via Yahoo's<FROM><TO>=Xquote symbol.get_asset_profile/1— company profile (sector, industry, website, description) via thequoteSummaryendpoint'sassetProfilemodule (v0.3; website + description added in v0.6).get_financial_data/1— leverage figures (total debt, debt/equity, current & quick ratio, cash, EBITDA) via thequoteSummaryendpoint'sfinancialDatamodule (v0.5).get_dividend_history/2— per-payment dividend history via the chart endpoint'sevents=divstream (v0.3); the raw material for payment-schedule inference.search/2— free-text ticker/company autocomplete via thesearchendpoint (v0.4).get_news/2— recent news headlines via thesearchendpoint'snewsstream (v0.6).get_price_history/2— monthly closing prices via the chart endpoint (the price series beside the dividend stream) (v0.7).get_fund_profile/1— fund/ETF profile (expense ratio, AUM, category, family, inception, top holdings, sector weights) via thequoteSummaryendpoint'sfundProfile/defaultKeyStatistics/topHoldingsmodules;:not_foundfor single stocks (v0.9).get_option_chain/2— calls and puts for one symbol and expiry, plus every listed expiry and the underlying's spot price, via the options endpoint (v0.10).get_earnings_date/1— the next scheduled earnings report, and whether the date is confirmed or still Yahoo's estimate, via thequoteSummaryendpoint'scalendarEventsmodule (v0.11).
All paths go through YahooFinanceEx.Session to handle the cookie + CSRF
crumb auth dance, and through Req for HTTP — so tests can stub the
whole thing with Req.Test.
An in-memory cache layer is a planned follow-up.
Quickstart
{:ok, quote} = YahooFinanceEx.get_quote("AAPL")
{:ok, by_symbol} = YahooFinanceEx.get_quotes(["AAPL", "MSFT", "GOOG"])
by_symbol["AAPL"]
#=> {:ok, %YahooFinanceEx.Quote{symbol: "AAPL", ...}}
{:ok, rate} = YahooFinanceEx.get_fx_rate("EUR", "USD")
#=> {:ok, 1.08}Notes
Yahoo's API is unofficial. Endpoints, auth requirements, and response shapes can change without notice. Two auth strategies are tried in order before erroring; sessions live for 60 seconds before being re-fetched.
Summary
Types
Errors returned by the public functions.
An option chain for a single expiry, plus the symbol's context.
One option contract returned by get_option_chain/2.
Per-symbol result inside a batched get_quotes/1 response.
One match returned by search/2.
Functions
Fetches the company profile for a ticker via Yahoo's quoteSummary
endpoint (assetProfile module).
Fetches the per-payment dividend history for a ticker via the chart
endpoint's events=div stream.
Fetches the next scheduled earnings report for a ticker via the
quoteSummary endpoint's calendarEvents module.
Fetches key leverage / balance-sheet figures for a ticker via the
quoteSummary endpoint (financialData module).
Fetches fund/ETF profile data for a ticker via the quoteSummary endpoint
(fundProfile, defaultKeyStatistics, and topHoldings modules).
Fetches the current FX rate between two ISO 4217 currency codes — one
unit of from expressed in to.
Fetches recent news headlines for a ticker via Yahoo's
/v1/finance/search endpoint (its news stream).
Fetches the option chain for one symbol and one expiry.
Fetches the monthly closing-price history for a ticker via the chart endpoint (the price series alongside the dividend stream).
Fetches a single stock quote.
Fetches quotes for many symbols in one or more batched HTTP calls.
Searches Yahoo Finance for tickers matching a free-text query (a
ticker fragment or a company name) via the /v1/finance/search
autocomplete endpoint.
Types
@type error() :: {:auth_failed, term()} | {:http_status, non_neg_integer()} | {:transport, term()} | :not_found
Errors returned by the public functions.
@type option_chain() :: %{ symbol: String.t(), expiry: Date.t() | nil, expirations: [Date.t()], spot: float() | nil, currency: String.t() | nil, contracts: [option_contract()] }
An option chain for a single expiry, plus the symbol's context.
@type option_contract() :: %{ contract_symbol: String.t(), side: :call | :put, strike: float(), expiry: Date.t() | nil, bid: float() | nil, ask: float() | nil, last: float() | nil, volume: integer() | nil, open_interest: integer() | nil, implied_volatility: float() | nil, in_the_money: boolean() | nil, contract_size: pos_integer() | nil, currency: String.t() | nil, last_trade_at: DateTime.t() | nil }
One option contract returned by get_option_chain/2.
@type per_symbol_result() :: {:ok, YahooFinanceEx.Quote.t()} | {:error, :not_found}
Per-symbol result inside a batched get_quotes/1 response.
@type search_result() :: %{ symbol: String.t(), name: String.t(), exchange: String.t() | nil, type: String.t() | nil }
One match returned by search/2.
Functions
@spec get_asset_profile(String.t()) :: {:ok, %{ sector: String.t(), industry: String.t() | nil, website: String.t() | nil, description: String.t() | nil }} | {:error, error()}
Fetches the company profile for a ticker via Yahoo's quoteSummary
endpoint (assetProfile module).
Returns {:ok, %{sector:, industry:, website:, description:}} — industry,
website and description may be nil — or {:error, :not_found} for funds,
ETFs, and any symbol where Yahoo exposes no asset profile (a blank sector
counts as none — matching the Ruby client's behavior). description is
Yahoo's longBusinessSummary (English).
@spec get_dividend_history( String.t(), keyword() ) :: {:ok, [%{date: Date.t(), amount: float()}]} | {:error, error()}
Fetches the per-payment dividend history for a ticker via the chart
endpoint's events=div stream.
Returns {:ok, entries} — each entry %{date: Date.t(), amount: float}, sorted ascending by date — or {:ok, []} when the symbol
pays no dividends (or Yahoo reports none for the range). Consumers
infer payment schedules (frequency, months) from these entries.
Options:
:range— Yahoo range string, default"2y"(enough to see a quarterly pattern twice).
@spec get_earnings_date(String.t()) :: {:ok, %{date: Date.t(), date_end: Date.t() | nil, estimated?: boolean()}} | {:error, error()}
Fetches the next scheduled earnings report for a ticker via the
quoteSummary endpoint's calendarEvents module.
Returns {:ok, %{date:, date_end:, estimated?:}} — or {:error, :not_found} when the symbol has no scheduled report. :not_found is a
normal answer, not a failure: ETFs, funds and bond trackers have no
earnings at all, and Yahoo answers 404 for them.
estimated? comes from Yahoo's own isEarningsDateEstimate flag — a
projection from the previous cycle rather than a date the company has
confirmed. A good share of large caps sit in that state at any time, and
the distinction matters to anyone timing a position around the report, so
it is reported rather than flattened away.
date_end was nil in every response observed while this was written.
Yahoo has historically returned earningsDate as a two-element range for
unconfirmed reports, so the field is kept and a second date degrades into
a range rather than being silently dropped — but do not lean on it.
estimated? is the signal, not the presence of date_end.
Coverage is not limited to US listings: European names report here even though most of them have no option chain (SAN.MC, DGE.L, BMW.DE and ENEL.MI all return dates).
@spec get_financial_data(String.t()) :: {:ok, %{ total_debt: float() | nil, debt_to_equity: float() | nil, current_ratio: float() | nil, quick_ratio: float() | nil, total_cash: float() | nil, ebitda: float() | nil }} | {:error, error()}
Fetches key leverage / balance-sheet figures for a ticker via the
quoteSummary endpoint (financialData module).
Returns {:ok, %{total_debt, debt_to_equity, current_ratio, quick_ratio, total_cash, ebitda}} — each value a float or nil — or {:error, :not_found}
when Yahoo exposes no financialData (common for funds/ETFs and many
non-US tickers). debt_to_equity is Yahoo's percentage figure
(e.g. 151.4 = 151.4%).
@spec get_fund_profile(String.t()) :: {:ok, %{ expense_ratio: float() | nil, total_assets: float() | nil, fund_category: String.t() | nil, fund_family: String.t() | nil, inception_date: Date.t() | nil, top_holdings: [ %{symbol: String.t() | nil, name: String.t() | nil, weight: float()} ], sector_weights: %{optional(String.t()) => float()} }} | {:error, error()}
Fetches fund/ETF profile data for a ticker via the quoteSummary endpoint
(fundProfile, defaultKeyStatistics, and topHoldings modules).
Returns {:ok, %{expense_ratio, total_assets, fund_category, fund_family, inception_date, top_holdings, sector_weights}} for a fund, or
{:error, :not_found} for a single stock (Yahoo exposes no fundProfile
module) — so this doubles as an ETF discriminator.
Units: expense_ratio and the weight/sector_weights values are
percentages (Yahoo returns fractions; they are multiplied by 100 here).
total_assets is the fund's net assets (AUM) in its quoted currency.
top_holdings is a list of %{symbol, name, weight} (most funds return the
top 10), and sector_weights is a %{display_sector_name => percent} map.
Fetches the current FX rate between two ISO 4217 currency codes — one
unit of from expressed in to.
Returns {:ok, 1.0} for identity pairs without hitting the API.
Returns {:ok, rate} (a float) on success, or {:error, reason} on
failure (including :not_found when Yahoo has no quote for the pair).
@spec get_news( String.t(), keyword() ) :: {:ok, [ %{ title: String.t(), url: String.t() | nil, publisher: String.t() | nil, published_at: DateTime.t() | nil } ]} | {:error, error()}
Fetches recent news headlines for a ticker via Yahoo's
/v1/finance/search endpoint (its news stream).
Returns {:ok, items} — each %{title:, url:, publisher:, published_at:}
with published_at a UTC DateTime (or nil), most-recent first — or
{:ok, []} when Yahoo returns no news.
Options:
:count— max headlines to request, default 8.
@spec get_option_chain( String.t(), keyword() ) :: {:ok, option_chain()} | {:error, error()}
Fetches the option chain for one symbol and one expiry.
Yahoo returns contracts for a single expiry per call — the nearest, unless
:expiry names another — but the response also carries the full list of
expiries available and the underlying's current price. So a caller needs
neither a discovery call for the dates nor a separate quote for the spot;
one request answers all three.
Returns {:ok, chain} where chain is an option_chain/0: contracts
holds calls and puts together, each tagged side: :call | :put, in Yahoo's
order (ascending by strike within each side). An unknown symbol comes back
as an empty contracts list rather than an error — Yahoo answers 200 with
no options block — so callers distinguish "no chain" from "call failed".
contract_size is normalized from Yahoo's "REGULAR"/"MINI" strings to
the share count they mean (100 / 10). It is reported per contract rather
than assumed, because mini options and non-US listings are exactly the
cases where assuming 100 is wrong rather than merely unsupported.
Options:
:expiry— aDate; defaults to Yahoo's nearest expiry.
Examples
{:ok, chain} = YahooFinanceEx.get_option_chain("KO")
chain.spot #=> 62.15
length(chain.expirations) #=> 14
Enum.count(chain.contracts, & &1.side == :put) #=> 30
@spec get_price_history( String.t(), keyword() ) :: {:ok, %{currency: String.t() | nil, points: [%{date: Date.t(), close: float()}]}} | {:error, error()}
Fetches the monthly closing-price history for a ticker via the chart endpoint (the price series alongside the dividend stream).
Returns {:ok, %{currency: currency, points: points}} — each point
%{date: Date.t(), close: float}, sorted ascending by date, skipping months
Yahoo reports as null, and points: [] when the symbol has no price data.
Consumers use it (paired with the dividend history) to build a historical
yield band.
currency is the unit the closes are in, and it is not always the major one
The chart endpoint answers in whatever unit the venue quotes in, which
for a London listing is pence: DGE.L comes back around 1641 for a share
worth £16.41, tagged "GBp" rather than "GBP". Johannesburg ("ZAc") and
Tel Aviv ("ILA") do the same.
Nothing in the series itself reveals this — a price chart looks identical
either way, and only a consumer putting a close next to real money (a
dividend, a holding, a quote) finds out. So the meta currency is reported
verbatim alongside the points, and converting to the major unit is the
caller's to do, exactly as it already is for get_quote/1.
nil when Yahoo omits it, which it does for some symbols — treat that as
"unknown", not as "major unit".
Options:
:range— Yahoo range string, default"6y"(enough for a ~5-year yield band plus a buffer).
@spec get_quote(String.t()) :: {:ok, YahooFinanceEx.Quote.t()} | {:error, error()}
Fetches a single stock quote.
Returns {:ok, %YahooFinanceEx.Quote{}} on success, or {:error, reason}
with one of the error/0 shapes on failure.
Retries once on transient auth errors (Yahoo invalidates sessions occasionally); deeper failures bubble up.
@spec get_quotes([String.t()]) :: {:ok, %{required(String.t()) => per_symbol_result()}} | {:error, error()}
Fetches quotes for many symbols in one or more batched HTTP calls.
Returns {:ok, results_map} where results_map is %{symbol => {:ok, Quote.t()} | {:error, :not_found}} — i.e. each requested symbol
is present in the map, mapped to its individual result. Symbols Yahoo
doesn't recognize come back as {:error, :not_found}.
Top-level errors ({:auth_failed, _}, {:transport, _}, etc.) abort
the whole call and are returned as {:error, reason}.
Symbols are batched in groups of 50 (Yahoo's per-request ceiling). Duplicates and empty lists are tolerated.
@spec search( String.t(), keyword() ) :: {:ok, [search_result()]} | {:error, error()}
Searches Yahoo Finance for tickers matching a free-text query (a
ticker fragment or a company name) via the /v1/finance/search
autocomplete endpoint.
Returns {:ok, results} — each result %{symbol:, name:, exchange:, type:}, in Yahoo's relevance order — or {:ok, []} for a blank
query or no matches. type is Yahoo's quoteType ("EQUITY",
"ETF", "MUTUALFUND", "INDEX", …) so callers can filter to the
instruments they care about; name falls back shortname →
longname → symbol.
Options:
:count— max results to request, default 10.