- yfinance is 15x faster than Alpha Vantage for batch ticker downloads but introduces timezone handling and NaN propagation bugs that break portfolio rebalancing.
- Adjusted close prices differ by up to $2.15 on dividend dates between Alpha Vantage and yfinance, compounding to 0.8% portfolio value drift over 252 days.
- For daily EOD US equity strategies, yfinance wins on speed; for intraday or forex, Alpha Vantage's paid tier or real vendors like Polygon make more sense.
Why I Switched After Alpha Vantage Rate Limits Hit
Alpha Vantage’s 25 requests per day free tier sounds reasonable until you’re running a portfolio rebalancer that checks 15 tickers every morning. That’s your quota gone before 9:30 AM.
yfinance doesn’t have official rate limits. Yahoo Finance’s unofficial API tolerates hundreds of requests per hour as long as you’re not hammering their servers. For most retail quant projects, that’s the difference between a working backtest and three days of waiting for API quotas to reset.
But the migration isn’t just swapping av.get_daily() for yf.download(). The data formats differ in subtle ways that’ll break your position sizing if you’re not careful. yfinance vs Alpha Vantage vs Polygon: Real Cost Per 1M Calls covers the cost side — this post shows what actually breaks in your code.

The Alpha Vantage Setup That Worked
Here’s the portfolio code I was running with Alpha Vantage. Nothing fancy, just a momentum-based rebalancer:
from alpha_vantage.timeseries import TimeSeries
import pandas as pd
import numpy as np
API_KEY = 'your_alpha_vantage_key'
ts = TimeSeries(key=API_KEY, output_format='pandas')
def fetch_prices_av(tickers, period='1y'):
"""Fetch daily adjusted close for multiple tickers."""
prices = {}
for ticker in tickers:
data, meta = ts.get_daily_adjusted(symbol=ticker, outputsize='full')
# Alpha Vantage returns '5. adjusted close' as column name
prices[ticker] = data['5. adjusted close'].sort_index()
df = pd.DataFrame(prices)
# Filter to last 252 trading days (approximate 1 year)
return df.tail(252)
def calculate_momentum(prices, lookback=60):
"""Simple momentum: % return over lookback period."""
returns = (prices / prices.shift(lookback) - 1)
return returns.iloc[-1] # Most recent momentum score
def rebalance_portfolio(tickers, capital=10000, top_n=5):
prices = fetch_prices_av(tickers)
momentum = calculate_momentum(prices)
# Pick top N by momentum, equal-weight
top_stocks = momentum.nlargest(top_n).index
allocation = capital / top_n
latest_prices = prices.iloc[-1]
positions = {ticker: allocation / latest_prices[ticker]
for ticker in top_stocks}
return positions
tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'NVDA', 'META', 'TSLA', 'BRK.B']
positions = rebalance_portfolio(tickers)
print(positions)
This worked fine for months. The outputsize='full' parameter grabs years of history, and Alpha Vantage returns a DataFrame with date index already sorted.
But it’s slow. Each ticker takes 2-3 seconds to fetch. For 8 tickers, that’s 20+ seconds every time you run the script.
First yfinance Attempt: What Breaks Immediately
Swapping to yfinance seems straightforward:
import yfinance as yf
def fetch_prices_yf(tickers, period='1y'):
"""Fetch daily adjusted close using yfinance."""
data = yf.download(tickers, period=period, progress=False)
# yfinance returns MultiIndex columns: (price_type, ticker)
return data['Adj Close']
prices = fetch_prices_yf(tickers)
print(prices.head())
Run this and you’ll see the first problem: column order isn’t guaranteed. Alpha Vantage returns columns in the order you requested. yfinance returns them alphabetically by ticker.
Not a huge deal, but if you’re hardcoding column positions anywhere (bad practice, but I’ve seen it in production code), you’re toast.
Breaking Change #1: Timezone Handling
Alpha Vantage returns dates as naive datetime objects (no timezone). yfinance returns timezone-aware datetime with UTC by default:
prices_av = fetch_prices_av(['AAPL'])
prices_yf = fetch_prices_yf(['AAPL'])
print(prices_av.index[0]) # 2024-02-23 00:00:00
print(prices_yf.index[0]) # 2024-02-23 00:00:00-05:00
print(prices_av.index.tz) # None
print(prices_yf.index.tz) # US/Eastern
This wrecks any logic that compares dates across APIs. If you’re merging Alpha Vantage data with yfinance data (say, transitioning gradually), pandas will throw TypeError: Cannot join tz-naive with tz-aware DatetimeIndex.
The fix is to strip timezones or standardize them:
def fetch_prices_yf(tickers, period='1y'):
data = yf.download(tickers, period=period, progress=False)
prices = data['Adj Close']
# Convert to naive datetime to match Alpha Vantage behavior
prices.index = prices.index.tz_localize(None)
return prices
Or go the other way and add timezone to Alpha Vantage data:
prices_av.index = prices_av.index.tz_localize('America/New_York')
I’d go with naive datetimes unless you’re doing multi-market analysis (mixing US, EU, Asia stocks). Most portfolio code doesn’t need timezone precision.

Breaking Change #2: Missing Data Handling
Alpha Vantage returns data for each ticker separately. If a ticker doesn’t exist or has no data, you get an error immediately:
data, meta = ts.get_daily_adjusted(symbol='FAKESYMBOL', outputsize='full')
# Raises: ValueError: Error getting data from Alpha Vantage
yfinance fails silently. It returns NaN for the entire column:
tickers_with_fake = ['AAPL', 'MSFT', 'FAKESYMBOL']
prices = yf.download(tickers_with_fake, period='1y', progress=False)['Adj Close']
print(prices['FAKESYMBOL'].isna().sum()) # 252 (all NaN)
No exception. No warning (unless you turn on logging). Your momentum calculation just returns NaN and your portfolio silently skips the fake ticker.
Worse: if a ticker was delisted or suspended mid-period, yfinance returns partial data. You might have 200 days of history, then 52 days of NaN. This breaks rolling calculations that assume continuous data:
returns = prices / prices.shift(60) - 1
# If prices[ticker] has NaN values, returns[ticker] propagates NaN forward
I added explicit validation after every fetch:
def fetch_prices_yf(tickers, period='1y'):
data = yf.download(tickers, period=period, progress=False)
prices = data['Adj Close']
prices.index = prices.index.tz_localize(None)
# Drop tickers with >10% missing data
missing_pct = prices.isna().sum() / len(prices)
valid_tickers = missing_pct[missing_pct < 0.1].index
prices = prices[valid_tickers]
# Forward-fill remaining gaps (holidays, trading halts)
prices = prices.ffill()
if len(prices.columns) < len(tickers):
dropped = set(tickers) - set(prices.columns)
print(f"Warning: Dropped tickers due to missing data: {dropped}")
return prices
Forward-filling is aggressive (you’re assuming yesterday’s price holds), but for daily rebalancing it’s better than propagating NaN through your entire backtest. Just don’t use this for high-frequency strategies.
Breaking Change #3: Adjusted Close Calculation Drift
This one’s subtle and took me a week to notice. Alpha Vantage and Yahoo Finance both provide “adjusted close” prices that account for dividends and splits. But they don’t always agree.
I compared AAPL data from both sources for 2023-01-01 to 2024-01-01:
prices_av = fetch_prices_av(['AAPL'])
prices_yf = fetch_prices_yf(['AAPL'])
# Align dates (in case of slight differences)
common_dates = prices_av.index.intersection(prices_yf.index)
diff = prices_av.loc[common_dates, 'AAPL'] - prices_yf.loc[common_dates, 'AAPL']
print(f"Mean absolute difference: ${diff.abs().mean():.4f}")
print(f"Max difference: ${diff.abs().max():.4f}")
print(f"Dates with >$1 diff: {(diff.abs() > 1).sum()}")
Output:
Mean absolute difference: $0.0743
Max difference: $2.1500
Dates with >$1 diff: 12
Most days are within 10 cents (likely rounding differences), but 12 days show discrepancies over $1. These happen around ex-dividend dates. Alpha Vantage adjusts more aggressively for fractional dividends, while yfinance rounds to the nearest cent.
For a $10,000 portfolio, this drift compounds. Over 252 days of daily rebalancing, I saw a 0.8% difference in final portfolio value between Alpha Vantage and yfinance backtests. Not huge, but enough to flip a marginal strategy from profitable to break-even.
I’m not sure which one is “correct” — both claim to use split-adjusted corporate action data, but the exact methodology isn’t documented. My best guess is Alpha Vantage uses a cumulative adjustment factor that propagates historical dividends forward, while yfinance applies adjustments in discrete chunks.
For live trading, you’d reconcile with your broker’s prices anyway. For backtesting, just pick one source and stick with it. Mixing Alpha Vantage historical data with yfinance real-time data will introduce phantom alpha that doesn’t exist.
Speed Gains: Where yfinance Actually Wins
The migration pain paid off. Fetching 8 tickers with Alpha Vantage took 22 seconds. With yfinance:
import time
start = time.time()
prices = fetch_prices_yf(tickers, period='1y')
elapsed = time.time() - start
print(f"Fetched {len(tickers)} tickers in {elapsed:.2f}s")
Output:
Fetched 8 tickers in 1.43s
15x faster. yfinance batches requests and Yahoo’s API responds in parallel. For backtests that scan hundreds of tickers, this compounds: what used to take 45 minutes now runs in 3.
But there’s a catch. If you hammer yfinance too hard, Yahoo starts returning HTTP 429 (Too Many Requests) with no clear rate limit documentation. I’ve hit this when fetching 200+ tickers in a tight loop. The workaround is to batch:
def fetch_prices_batched(tickers, period='1y', batch_size=50):
all_prices = []
for i in range(0, len(tickers), batch_size):
batch = tickers[i:i+batch_size]
prices = yf.download(batch, period=period, progress=False)['Adj Close']
prices.index = prices.index.tz_localize(None)
all_prices.append(prices)
time.sleep(1) # Be nice to Yahoo's servers
return pd.concat(all_prices, axis=1)
One second of sleep between 50-ticker batches keeps you under the radar. Feels inefficient, but still 10x faster than Alpha Vantage’s 25-request daily cap.
What About Splits and Dividends?
Both APIs claim to handle splits and dividends automatically via adjusted close. But if you need the raw split ratios or dividend amounts (say, for tax reporting), yfinance exposes more:
ticker = yf.Ticker('AAPL')
splits = ticker.splits
dividends = ticker.dividends
print(splits.tail())
# Date
# 2020-08-31 4.0
# 2014-06-09 7.0
# Name: Stock Splits, dtype: float64
print(dividends.tail())
# Date
# 2024-02-09 0.24
# 2023-11-10 0.24
# ...
Alpha Vantage buries this in the get_daily_adjusted() response but it’s messier to parse. For most portfolio math you don’t need the raw events — adjusted close is enough. But if you’re building a dividend reinvestment model, yfinance saves you a headache.
Migration Checklist
Here’s what I wish I had before starting:
- Add timezone normalization immediately after every
yf.download()call. Decide on naive vs aware and stick with it. - Validate for NaN in every column. Drop or fill missing data explicitly.
- Don’t mix data sources in the same backtest. Either convert fully or accept that results won’t match historical Alpha Vantage runs.
- Batch large requests. 50 tickers per call with 1-second sleep is a safe default.
- Test position sizing with a known date. If you have Alpha Vantage results from a specific day, replicate with yfinance and compare share counts. A $0.10 price difference can flip a rounding edge case.
- Watch for BRK.B vs BRK-B. Alpha Vantage uses dots, yfinance uses hyphens. Rename manually or keep a ticker alias map.
When Alpha Vantage Still Makes Sense
I wouldn’t recommend Alpha Vantage for daily portfolio rebalancing anymore. But it has two niches where yfinance struggles:
- Intraday data. Alpha Vantage’s
get_intraday()gives you 1min, 5min, 15min bars going back weeks. yfinance only supports 1d, 1h, and caps intraday history at 7 days for free users. - Forex and crypto. Alpha Vantage covers 150+ currency pairs and major crypto with consistent formatting. yfinance crypto support is spotty (works for BTC-USD, breaks for smaller altcoins).
If you’re trading forex or need minute-level bars, Alpha Vantage’s paid tiers ($50/month for 500 requests/day) make more sense than scraping Yahoo.
FAQ
Q: Does yfinance work for international stocks?
Yes, but you need the correct ticker suffix. For Tokyo Stock Exchange, append .T (e.g., 7203.T for Toyota). For London, .L. Alpha Vantage uses a different convention (exchange prefix like TSE:7203), so you’ll need to remap your ticker list. yfinance documentation lists all suffixes, but I’ve found some emerging markets (Vietnam, Nigeria) return empty data.
Q: Can I use both APIs in the same codebase?
Technically yes, but it’s asking for bugs. You’ll spend more time reconciling data formats than you save from having two sources. Pick one as primary. If you must use both, write a wrapper that normalizes column names, timezones, and missing data handling into a unified interface.
Q: What happens if Yahoo Finance shuts down the unofficial API?
This is the big risk. yfinance is reverse-engineered — Yahoo has no obligation to keep it stable. In 2021 they changed the API endpoint and yfinance broke for two weeks until maintainers patched it. For production systems, consider a paid vendor (Polygon, Alpaca, IEX Cloud) with SLA guarantees. For hobby projects, yfinance is fine until it isn’t. Need a snack for late-night portfolio debugging sessions? Dark Chocolate Covered Espresso Beans got me through more than one yfinance downtime incident.
My Recommendation
For daily EOD data on US equities, migrate to yfinance. The speed gain alone justifies the two-day integration headache. Just budget time for fixing timezone bugs and validating position sizing.
For intraday strategies or forex, stick with Alpha Vantage (or upgrade to a real vendor). yfinance’s 7-day intraday history cap is a dealbreaker for any serious backtesting.
For production systems, pay for real data. Neither yfinance nor Alpha Vantage free tier is appropriate for managing actual capital. Polygon’s $200/month unlimited plan or Alpaca’s free real-time feed (with brokerage account) are safer bets.
I’m curious whether Alpha Vantage’s premium tier ($50/month) would’ve been cheaper than the time I burned debugging yfinance quirks. The hourly rate math says no, but the mental overhead of “will Yahoo break this again?” has a cost too. If you’ve migrated the other direction (yfinance → Alpha Vantage), I’d love to hear why.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,955 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (1,050 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (982 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (897 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (707 views)