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.
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 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:
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. This page is about the port.
What the options are
Closest drop-in for daily bars. 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 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'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'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 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.
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 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 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
covers the endpoints that replace them. The rest of the field is in
market data APIs.
The tools that do this
In the order this page recommends trying them. Paid placement does not affect this order.
Tiingo
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.
End-of-day equity history back to 1962, plus crypto, forex and news, from $30 a month.
$30/moFree tier
EODHD
The global reach yfinance had, with an exchange suffix on every ticker. Raw prices, one adjusted close, and CSV unless you ask for JSON.
End-of-day and fundamentals for 60+ exchanges worldwide, at a hobbyist price.
$19.99/moFree tier
Massive
Split-adjusted by default and never dividend-adjusted, dated by millisecond timestamp. Free is two years at five calls a minute.
Full-tick US equities, options and futures from a direct exchange feed.
$29/moFree tier
Alpha Vantage
Free key, but raw prices only and the last 100 days; full history and adjusted close are premium. Numbers arrive as strings.
The API most people's first script talks to — free key, wide coverage, hard rate limits.
$49.99/moFree tier
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
- yfinance API reference — yfinance.download — yfinance project, read
- End-of-Day (EOD) Stock Price API Documentation — Tiingo, read
- End-Of-Day Historical Stock Market Data API — EODHD,
- Custom Bars (OHLC) — Stocks REST API — Massive, read
- Alpha Vantage API documentation — Alpha Vantage, read
The catalogue next door
This page names a handful of cards. The rest of them are in Stock Market Data APIs, each filled in against the same schema, with the fields to narrow it yourself.
Last updated . Corrected in place: this is a reference page, not a dated post.