# `YahooFinanceEx`
[🔗](https://github.com/fleveque/yahoo_finance_ex/blob/v0.13.0/lib/yahoo_finance_ex.ex#L1)

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.

# `error`

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

Errors returned by the public functions.

# `option_chain`

```elixir
@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`

```elixir
@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`

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

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

# `search_result`

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

One match returned by `search/2`.

# `get_asset_profile`

```elixir
@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`

```elixir
@spec get_dividend_history(
  String.t(),
  keyword()
) ::
  {:ok,
   %{currency: String.t() | nil, points: [%{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, %{currency: currency, points: points}}` — each point
`%{date: Date.t(), amount: float}`, sorted ascending by date, and
`points: []` when the symbol pays no dividends (or Yahoo reports none for
the range). Consumers infer payment schedules (frequency, months) from
these points.

## `currency` is the unit the amounts are in

Same story as `get_price_history/2`, and the same field: this is the same
chart response, so the amounts arrive in whatever unit the **venue** quotes
in. A London payment comes back as `80.0` pence, not `0.8` pounds, tagged
`"GBp"`.

It matters most for the pairing. A yield computed from these amounts *and*
those closes is correct in either unit, because the two cancel — which is
exactly how a caller converting only one side turns a right answer into one
that is 100× out. Report both, convert both, or convert neither.

Note this differs from `get_quote/1`'s `dividend`, which Yahoo already
reports in the major unit. Two endpoints, two conventions.

`nil` when Yahoo omits the meta — "unknown", not "major unit".

Options:

  * `:range` — Yahoo range string, default `"2y"` (enough to see a
    quarterly pattern twice).

# `get_earnings_date`

```elixir
@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`

```elixir
@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`

```elixir
@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()) =&gt; 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`

```elixir
@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`

```elixir
@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`

```elixir
@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 `t: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`

```elixir
@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`

```elixir
@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 `t:error/0` shapes on failure.

Retries once on transient auth errors (Yahoo invalidates sessions
occasionally); deeper failures bubble up.

# `get_quotes`

```elixir
@spec get_quotes([String.t()]) ::
  {:ok, %{required(String.t()) =&gt; 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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
