pyguides

Build a Stock Price Tracker with yfinance

What yfinance is (and isn’t)

yfinance is an unofficial Python wrapper around Yahoo Finance’s public endpoints. It is not affiliated with Yahoo, it is not a paid data feed, and Yahoo’s terms of service restrict it to personal, non-commercial use. Treat it as a friendly shortcut for prototypes, learning, and small personal tools, not as infrastructure for production trading.

yfinance hands back pandas objects. Every call you make returns either a pandas.DataFrame or a pandas.Series, which means you can lean on pandas for filtering and resampling without writing extra glue code. When you build stock data pipelines or simple personal trackers, this combination covers most of the heavy lifting in a single dependency pair.

You will need both libraries installed:

pip install yfinance pandas

If you are starting from scratch, set up a clean environment first with the venv module so the install does not collide with system packages.

Install and first fetch

The library is small and installs in seconds. After it is available, you can grab a month of Apple prices with two lines:

import yfinance as yf

aapl = yf.Ticker("AAPL")
df = aapl.history(period="1mo", interval="1d")
print(df.tail(3))
# output:
#                                  Open       High        Low      Close     Volume  Dividends  Stock Splits
# Date
# 2026-05-13 00:00:00-04:00  214.32  217.10  213.86  216.58   45230123        0.0            0
# 2026-05-14 00:00:00-04:00  216.12  219.04  215.90  218.74   38940211        0.0            0
# 2026-05-15 00:00:00-04:00  218.95  220.41  217.80  220.13   41302877        0.0            0

yf.Ticker("AAPL") constructs the object locally. No network call happens until you ask for data, so calling .history() is what actually fetches prices. The result is a DataFrame indexed by date, with one row per trading day.

Reading the DataFrame

A .history() call gives you a DataFrame whose columns depend on which arguments you pass. Understanding those columns is the difference between a tracker that just prints numbers and one you can extend with indicators, alerts, and joins against other data. Most readers care about the standard OHLC columns plus Volume at minimum, with Dividends and Stock Splits showing up when you ask for corporate actions.

The columns you see depend on which arguments you pass to .history():

ColumnMeaning
Open, High, Low, CloseStandard OHLC prices for the bar
VolumeShares traded during the bar
DividendsCash dividend paid on that date (0.0 on non-pay dates)
Stock SplitsSplit ratio (1.0 means no split on that day)

Two details matter. First, the DatetimeIndex is timezone-aware: Yahoo returns bars in the exchange’s local time. For US tickers that is America/New_York. If you join this frame with naive timestamps you will get silent misalignment. Convert deliberately when you need to:

df_utc = df.tz_convert("UTC")

Second, since yfinance 0.2.57 (April 2025), auto_adjust defaults to True. That means Open, High, Low, Close are already adjusted for splits and dividends, and an Adj Close column may not appear at all. If you have older code that reads Adj Close, pass auto_adjust=False explicitly to restore the previous behaviour.

How do periods and intervals work in yfinance?

Ticker.history() accepts two range knobs: period and interval. Picking the right pair up front saves you from hitting Yahoo’s silent limits, which throw no error but quietly truncate your data.

Valid period values include 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. Valid interval values include 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo.

The catch is intraday data. Anything finer than 1d is capped at the most recent 60 or so trading days by Yahoo itself. Asking for period="5y", interval="1m" does not error: it silently returns about 60 days of minute bars. For long-range historical work, stick with interval="1d" or coarser.

Use start and end for reproducible runs

period is convenient but date-relative. “1mo” today is different from “1mo” next month. For reproducible analysis and for tests, pass explicit dates instead:

import yfinance as yf

df = yf.Ticker("MSFT").history(
    start="2024-01-01",
    end="2024-12-31",
    interval="1d",
    auto_adjust=True,
)
print(f"{len(df)} trading days, range {df.index.min().date()}{df.index.max().date()}")
# output: 252 trading days, range 2024-01-02 → 2024-12-31

Two small traps:

  • end is exclusive. Passing end="2023-01-01" returns rows through 2022-12-31, not including January 1st.
  • start and end accept both ISO strings and datetime objects. Mixing the two works fine.

Track multiple tickers with yf.download

For batches, use yf.download. It returns a single DataFrame whose columns are a MultiIndex: the top level is the field (Open, Close, …), the second level is the ticker.

import yfinance as yf

data = yf.download(
    ["AAPL", "MSFT", "GOOG"],
    start="2024-01-01",
    end="2024-12-31",
    auto_adjust=True,
    progress=False,
    threads=False,
)
close = data["Close"]
print(close.tail())
# output:
# Ticker            AAPL     MSFT     GOOG
# Date
# 2024-12-26  252.18  421.05  138.22
# 2024-12-27  255.10  423.40  139.51
# 2024-12-30  254.92  421.50  139.62
# 2024-12-31  254.18  418.78  139.41

Notice threads=False. Yahoo throttles aggressive clients, and threads=True parallelizes requests in a way that often trips rate limits on shared IPs. For scripts and small batches, single-threaded fetching is plenty fast and far more polite. The pandas DataFrames guide covers column selection and MultiIndex handling if you need to dig deeper.

Build a stock tracker class

Once you have fetched prices once or twice, the natural next step is wrapping the call in a small class with the methods you actually need:

import yfinance as yf
from datetime import datetime, timedelta


class StockTracker:
    def __init__(self, symbol: str):
        self.symbol = symbol
        self.ticker = yf.Ticker(symbol)

    def latest_price(self) -> float:
        info = self.ticker.info
        return info["regularMarketPrice"]

    def history(self, days: int = 30):
        end = datetime.utcnow().date()
        start = end - timedelta(days=days)
        return self.ticker.history(
            start=start.isoformat(),
            end=(end + timedelta(days=1)).isoformat(),
            interval="1d",
        )

    def moving_average(self, days: int = 30, window: int = 5) -> float:
        df = self.history(days)
        return df["Close"].rolling(window=window).mean().iloc[-1]


if __name__ == "__main__":
    t = StockTracker("AAPL")
    print(f"AAPL: ${t.latest_price():.2f}")
    print(f"5-day MA: ${t.moving_average(60, 5):.2f}")
# output (values vary):
# AAPL: $216.58
# 5-day MA: $215.42

This is where Ticker.info earns its keep. It returns a dictionary with metadata such as regularMarketPrice, previousClose, currency, marketCap, fiftyTwoWeekHigh, fiftyTwoWeekLow. The catch is that the schema is undocumented and shifts between releases, so always access fields with .get() if a missing key would crash your script:

price = self.ticker.info.get("regularMarketPrice")
if price is None:
    raise ValueError(f"No live price for {self.symbol}")

For higher-frequency polling, the functools guide shows how lru_cache can memoize recent info calls and keep you under the rate limit.

Plot the price history

A tracker is more useful when you can see it. Matplotlib is the smallest dependency that gets you a chart:

import matplotlib.pyplot as plt
import yfinance as yf

df = yf.Ticker("NVDA").history(period="6mo", interval="1d")
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(df.index, df["Close"], label="NVDA Close")
ax.plot(df.index, df["Close"].rolling(20).mean(), label="20-day MA")
ax.set_title("NVDA — last 6 months")
ax.set_ylabel("Price (USD)")
ax.legend()
fig.autofmt_xdate()
plt.tight_layout()
plt.show()

The 20-day moving average is a single line because pandas’ Series.rolling(...).mean() does the window math for you. For more involved layouts, the matplotlib subplots guide covers multi-panel charts.

Compute simple returns from the price history

Before you build stock dashboards or alerts, the natural first analysis is returns. From the same Close series, you can compute a daily percentage return in one line with pandas:

import yfinance as yf

df = yf.Ticker("AAPL").history(period="6mo", interval="1d")
returns = df["Close"].pct_change().dropna()
print(f"6-month mean daily return: {returns.mean():.4%}")
print(f"6-month daily volatility:  {returns.std():.4%}")
# output (values vary):
# 6-month mean daily return: 0.0012%
# 6-month daily volatility:  0.0184%

pct_change() lines up each bar with the previous one and returns the relative change. Drop the first row because it has no prior price to compare against. From there, the returns and volatility tutorial goes deeper into cumulative returns and Sharpe-style ratios.

Common errors and gotchas

A handful of issues come up over and over for anyone using yfinance in scripts and notebooks. Most are quiet rather than loud: empty frames, missing columns, or strange indices that look right but compare wrong. Knowing the patterns below saves a lot of debugging time, because each one presents as “the data looks weird” rather than as an exception.

The recurring offenders:

  • YFRateLimitError. Yahoo throttles heavy users. Common triggers: parallel downloads with threads=True or repeated re-fetches in a tight loop, especially from deployments on shared IPs such as Streamlit Cloud. Fix it by throttling with time.sleep, caching responses with functools.lru_cache or requests-cache, or by passing a custom session= to yf.Ticker or yf.download.
  • Empty DataFrame for a valid-looking ticker. Delisted symbols and bad symbols return an empty frame with no error. Always check if df.empty: before downstream code.
  • Adj Close disappeared. Caused by the 0.2.57 auto_adjust=True default. Either accept adjusted Close or pass auto_adjust=False explicitly.
  • Timezone surprises. The index is in the exchange’s tz, not UTC. Convert with df.tz_convert("UTC") before merging with other data.
  • Crypto and forex work too. Symbols like "BTC-USD" or "EURUSD=X" are valid. The same intraday limits apply.

Conclusion

yfinance gives you a fast on-ramp to historical and live-ish market data without an API key. Combine it with pandas to filter, resample, compute indicators, then plot with matplotlib when you want a chart. When you build stock dashboards, alerts, or research notebooks, this stack covers the basics end-to-end. For analysis beyond a single ticker, the next step is usually returns and volatility: the returns and volatility tutorial walks through that. The yfinance tutorial covers similar ground from a tutorial angle if you want a second pass.

Frequently asked questions

How do I get the latest price with yfinance?

Use yf.Ticker("AAPL").info["regularMarketPrice"]. The value is delayed roughly 15 minutes on the free Yahoo feed, so treat it as “live-ish” rather than real-time. For a guaranteed-fresh price you would need a paid data provider.

Can yfinance stream real-time tick data?

No. yfinance polls Yahoo’s public endpoints, which serve delayed snapshots. If you genuinely need tick-by-tick streaming you need a paid broker API such as Interactive Brokers or Alpaca.

How do I avoid rate limits when polling?

Cache responses with functools.lru_cache keyed on the symbol, sleep between calls, set threads=False for yf.download, pass a shared requests.Session() for connection pooling. Long-running deployments should also consider requests-cache with a short TTL on disk.

Does yfinance work for crypto and forex?

Yes. Symbols like "BTC-USD", "ETH-USD", or "EURUSD=X" go through the same code paths. The same intraday limit applies: minute bars only go back ~60 trading days.

How do I build a stock dashboard with yfinance?

Wrap a StockTracker (or similar) in a thin layer that calls .history() on a schedule, store results to a CSV or SQLite file, then read from that file in a Streamlit or Dash frontend. The history fetch is the slow part; the dashboard itself can refresh near-realtime against the cached file.

See also