# How to replace yfinance in a script

Porting a yfinance script to a keyed API: the DataFrame changes shape, Close stops meaning the same thing, and the dates move. What to map, column by column.

*https://stockmarketstack.com/how-to/replace-yfinance-in-a-script · next to Stock Market Data APIs*

**Answer:** Write a small adapter that returns the DataFrame yfinance used to, then swap the source behind it. Tiingo is the closest match for daily bars, with raw and adjusted columns side by side; EODHD matches Yahoo's global coverage. The traps are in the mapping: yfinance adjusts by default, its unadjusted Close is still split-adjusted, and each vendor's dates, column names and free limits differ.

## The tools that do this

*In the order this page recommends trying them. Paid placement does not affect this order.*

1. [Tiingo](https://stockmarketstack.com/tools/tiingo.md) — Raw and adjusted open, high, low, close and volume in one response, plus dividend and split columns. Free is 50 requests an hour and 500 symbols a month.
2. [EODHD](https://stockmarketstack.com/tools/eodhd.md) — The global reach yfinance had, with an exchange suffix on every ticker. Raw prices, one adjusted close, and CSV unless you ask for JSON.
3. [Massive](https://stockmarketstack.com/tools/massive.md) — Split-adjusted by default and never dividend-adjusted, dated by millisecond timestamp. Free is two years at five calls a minute.
4. [Alpha Vantage](https://stockmarketstack.com/tools/alpha-vantage.md) — Free key, but raw prices only and the last 100 days; full history and adjusted close are premium. Numbers arrive as strings.

## The short way

Don't rewrite the script; give it the DataFrame it already expects. Most yfinance code reads
`yf.download(...)` once and then never touches yfinance again, so the port is one function. Here
is the shape yfinance 1.7.0 hands back for a single ticker on 4 October 2026, and an adapter that
returns the same shape from [Tiingo](https://stockmarketstack.com/tools/tiingo):

```python
import pandas as pd
import requests
import yfinance as yf

before = yf.download("AAPL", start="2020-01-01", end="2026-09-01")
# columns: MultiIndex ('Close', 'AAPL'), ('High', 'AAPL'), ... — even for one ticker
# index:   'Date', timezone-naive; prices adjusted for splits AND dividends

YF_NAMES = {"adjOpen": "Open", "adjHigh": "High", "adjLow": "Low",
            "adjClose": "Close", "adjVolume": "Volume"}

def daily(ticker, start, end, token="YOUR_API_KEY"):
    url = f"https://api.tiingo.com/tiingo/daily/{ticker}/prices"
    rows = requests.get(url, params={"startDate": start, "endDate": end,
                                     "token": token}).json()
    df = pd.DataFrame(rows)
    df["date"] = pd.to_datetime(df["date"], utc=True).dt.tz_convert(None)
    df = df.set_index("date").rename_axis("Date").rename(columns=YF_NAMES)
    return df[["Open", "High", "Low", "Close", "Volume"]]

after = daily("AAPL", "2020-01-01", "2026-09-01")
```

Two decisions are hidden in there. Mapping Tiingo's `adj*` columns onto yfinance's names
reproduces yfinance's default, `auto_adjust=True`; if your script passed `auto_adjust=False`, read
the next section before mapping anything. And the adapter returns flat columns, which is what
`yf.download(..., multi_level_index=False)` returns — code that indexes `before["Close"]["AAPL"]`
needs that line changed too.

Choosing between providers is a separate question, answered in [downloading historical prices in
Python](https://stockmarketstack.com/how-to/download-historical-prices-in-python). This page is about the port.

## What the options are

**Closest drop-in for daily bars.** [Tiingo](https://stockmarketstack.com/tools/tiingo) returns `open`, `high`, `low`,
`close` and `volume` as traded, `adjOpen` through `adjVolume` adjusted by the CRSP method, and
`divCash` and `splitFactor` on every row — which is yfinance's `actions=True` without a second
call. History reaches 1962. Free is 50 requests an hour, 1,000 a day and 500 unique symbols a
month; $30 a month is 10,000 an hour, personal and internal use.

**Same coverage as Yahoo.** [EODHD](https://stockmarketstack.com/tools/eodhd) spans 60-plus exchanges, which is the reason
most yfinance users were on Yahoo in the first place. Every ticker takes an exchange suffix —
`AAPL.US`, `BP.LSE`, `SAP.XETRA` — where Yahoo used `BP.L` and `SAP.DE`, so the symbol list needs
mapping as well as the columns. The free key is 20 calls a day and the past year only; full
history starts at $19.99 a month.

**US only, with a different adjustment.** [Massive](https://stockmarketstack.com/tools/massive)'s aggregates endpoint returns
`o`, `h`, `l`, `c`, `v`, `vw` and `n` with a `t` timestamp, adjusted for splits by default and
for dividends never. If your script depended on yfinance's dividend adjustment, you rebuild it
from Massive's dividends endpoint. The free Basic plan is end-of-day, two years back, five calls a
minute.

**Free, and narrower than it looks.** [Alpha Vantage](https://stockmarketstack.com/tools/alpha-vantage)'s free key reaches
`TIME_SERIES_DAILY`: raw as-traded prices, `outputsize=compact`, the latest 100 trading days.
`outputsize=full` and the adjusted endpoint are premium, from $49.99 a month. Each day is a key in
a JSON object, fields are named `"1. open"` to `"5. volume"`, and every number is a string. 25
requests a day.

Fundamentals port separately and less cleanly. yfinance's `income_stmt` has periods as columns and
line items such as `Total Revenue` as rows; Alpha Vantage's free `INCOME_STATEMENT` returns one
object per period with `totalRevenue` as a string and missing values as the string `"None"`. On
the others it is a second purchase: EODHD's fundamentals plan has no prices in it, Tiingo's
fundamentals are an add-on, and Massive's financials are a separate $29 a month below its
Advanced tier.

## Where this breaks

**`auto_adjust=False` did not mean raw.** This is the one that survives every test except a
backtest. With `auto_adjust=False`, yfinance returns `Close` and `Adj Close` — and its `Close` is
split-adjusted, only not dividend-adjusted. On 4 October 2026, Apple's close for 27 August 2020,
two sessions before its four-for-one split, came back from yfinance as 125.01 with
`auto_adjust=False`, and from EODHD's `close` as 500.04, which is what printed. Port a script that
read yfinance's `Close` onto a vendor's raw `close` and every price before a split is several times
too high. The adjusted figures did agree — 121.15 from both.

**Every vendor names and adjusts differently.** EODHD's JSON has raw OHLC, one `adjusted_close`
for splits and dividends, and a volume that is split-adjusted though the prices beside it are not.
Its CSV — the default when you leave out `fmt=json` — capitalises the headers to `Adjusted_close`,
and a misspelled `fmt` is silently ignored rather than rejected. Massive's `adjusted=true` covers
splits alone. Map each column deliberately, then check one split and one dividend by hand.
[Why adjusted close differs](https://stockmarketstack.com/guides/why-adjusted-close-differs) explains why two correct adjusted
series still disagree.

**The dates move.** `yf.download` returns a timezone-naive index for daily bars; `Ticker.history`
returns the same days as timezone-aware midnights in `America/New_York`. Massive's `t` is the Unix
millisecond start of the bar in Eastern Time, so `pd.to_datetime(t, unit="ms")` gives 05:00 on
the right day in winter and 04:00 in summer — and a join against a midnight-dated index then
matches nothing, silently. Convert to New York time and normalise to the date before joining.

**Errors that are not errors.** Alpha Vantage answers a refused request with HTTP 200 and a JSON
body whose only key is `Information`, so `raise_for_status()` passes it and the parser fails two
lines later on a missing key. Check for the data key itself on every response.

**One ticker per request.** `yf.download("AAPL MSFT ...")` fetched a whole list in one call.
Tiingo, EODHD's end-of-day endpoint and Alpha Vantage are one ticker per request, so a 500-name
universe is 500 requests: ten hours on Tiingo's free tier, twenty days on Alpha Vantage's, and
over the 500-symbol monthly ceiling at Tiingo the first time the list changes.

**The licence is still personal.** Leaving yfinance does not by itself make the data commercial.
Tiingo's $30 plan is personal and internal use, and its commercial plan is $50 for one
organisation; EODHD's cheapest licence that is not personal use is $399 a month; Massive's
individual plans exclude business use; Alpha Vantage is personal and non-commercial by default.
The difference from Yahoo is that there is now a contract to read, and a price for the use you
actually have — see [redistribution](https://stockmarketstack.com/glossary/redistribution).

## If you outgrow this

**You need intraday history.** yfinance stopped at 60 days of intraday bars; the paid tiers here
go further, and [backfilling minute bars](https://stockmarketstack.com/how-to/backfill-minute-bars) covers the shape of that
job.

**You need delisted names.** None of these adapters fixes a universe built from today's tickers.
[Backtesting on survivorship-free data](https://stockmarketstack.com/how-to/backtest-on-survivorship-free-data) is the next
step.

**You need events, not just prices.** If the script used `actions=True` or `Ticker.dividends`,
[getting split and dividend history as data](https://stockmarketstack.com/how-to/get-split-and-dividend-history-as-data)
covers the endpoints that replace them. The rest of the field is in
[market data APIs](https://stockmarketstack.com/categories/market-data-apis).

## FAQ

### Can I keep pandas-datareader and just change the source?

Not any more. pandas-datareader 0.11.0, released on 23 June 2026, removed its Yahoo, Tiingo, Stooq, Quandl and IEX readers as dependent on defunct or broken upstream APIs. The old one-word swap from yahoo to tiingo or stooq is gone; the replacement is a few lines of requests and pandas against the vendor's own API, which is what the adapter on this page is.

### Is Stooq a free replacement for yfinance?

Not for a script. Stooq's data is free for personal use, but on 4 October 2026 a scripted request to its per-symbol CSV link got Access denied even after passing the site's JavaScript check, and the bulk files sit behind an image captcha. It is a manual download now, not a data source a nightly job can call.

### Why does my ported backtest return slightly different numbers?

Usually because the adjusted series is not the same series. yfinance adjusts for splits and dividends by default, Massive's default adjusts for splits only, and Alpha Vantage's free daily endpoint adjusts for nothing. Even two vendors that both adjust for dividends can differ in method. Compare one known split and one dividend in both before trusting any difference in results.

## Sources

1. [yfinance API reference — yfinance.download](https://ranaroussi.github.io/yfinance/reference/api/yfinance.download.html) — yfinance project, read 2026-10-04
2. [End-of-Day (EOD) Stock Price API Documentation](https://www.tiingo.com/documentation/end-of-day) — Tiingo, read 2026-10-04
3. [End-Of-Day Historical Stock Market Data API](https://eodhd.com/financial-apis/api-for-historical-data-and-volumes) — EODHD, 2026-09-28
4. [Custom Bars (OHLC) — Stocks REST API](https://massive.com/docs/rest/stocks/aggregates/custom-bars) — Massive, read 2026-10-04
5. [Alpha Vantage API documentation](https://www.alphavantage.co/documentation/) — Alpha Vantage, read 2026-10-04

*Last updated 2026-10-04. A reference page, corrected in place — not a dated post.*
