Alpha Vantage to yfinance Migration: 3 Breaking Changes

Disclosure: As an Amazon Associate, I earn from qualifying purchases. Some links in this post are affiliate links — they cost you nothing extra.
⚡ Key Takeaways
  • 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.

Business theme with stock market document, currency, and mobile display.
Photo by Hanna Pad on Pexels

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.

Enjoying this article? Get more like it delivered to your inbox. Subscribe to the newsletter

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.

African American woman examines bitcoin and stock market trends on a whiteboard.
Photo by www.kaboompics.com on Pexels

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:

  1. Add timezone normalization immediately after every yf.download() call. Decide on naive vs aware and stick with it.
  2. Validate for NaN in every column. Drop or fill missing data explicitly.
  3. Don’t mix data sources in the same backtest. Either convert fully or accept that results won’t match historical Alpha Vantage runs.
  4. Batch large requests. 50 tickers per call with 1-second sleep is a safe default.
  5. 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.
  6. 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:

  1. 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.
  2. 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 coffee
TODAY 559 | TOTAL 147,091