YahooFinanceEx (YahooFinanceEx v0.12.0)

Copy Markdown View Source

Elixir client for Yahoo! Finance.

v0.11 surface:

  • get_quote/1 — single-symbol quote (carries quote_type since 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>=X quote symbol.
  • get_asset_profile/1 — company profile (sector, industry, website, description) via the quoteSummary endpoint's assetProfile module (v0.3; website + description added in v0.6).
  • get_financial_data/1 — leverage figures (total debt, debt/equity, current & quick ratio, cash, EBITDA) via the quoteSummary endpoint's financialData module (v0.5).
  • get_dividend_history/2 — per-payment dividend history via the chart endpoint's events=div stream (v0.3); the raw material for payment-schedule inference.
  • search/2 — free-text ticker/company autocomplete via the search endpoint (v0.4).
  • get_news/2 — recent news headlines via the search endpoint's news stream (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 the quoteSummary endpoint's fundProfile/defaultKeyStatistics/topHoldings modules; :not_found for 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 the quoteSummary endpoint's calendarEvents module (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

error()

@type error() ::
  {:auth_failed, term()}
  | {:http_status, non_neg_integer()}
  | {:transport, term()}
  | :not_found

Errors returned by the public functions.

option_chain()

@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.

option_contract()

@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.

per_symbol_result()

@type per_symbol_result() :: {:ok, YahooFinanceEx.Quote.t()} | {:error, :not_found}

Per-symbol result inside a batched get_quotes/1 response.

search_result()

@type search_result() :: %{
  symbol: String.t(),
  name: String.t(),
  exchange: String.t() | nil,
  type: String.t() | nil
}

One match returned by search/2.

Functions

get_asset_profile(symbol)

@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).

get_dividend_history(symbol, opts \\ [])

@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).

get_earnings_date(symbol)

@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).

get_financial_data(symbol)

@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%).

get_fund_profile(symbol)

@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.

get_fx_rate(currency, currency)

@spec get_fx_rate(String.t(), String.t()) :: {:ok, float()} | {:error, error()}

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).

get_news(symbol, opts \\ [])

@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.

get_option_chain(symbol, opts \\ [])

@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 — a Date; 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

get_price_history(symbol, opts \\ [])

@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).

get_quote(symbol)

@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.

get_quotes(symbols)

@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.

search(query, opts \\ [])

@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 shortnamelongname → symbol.

Options:

  • :count — max results to request, default 10.