DevLab / finance / yahoo-finance-stock-data-fetcher
FINANCE PYTHON BEGINNER

Yahoo Finance Stock Data Fetcher

Analysts, DevOps engineers, and hobbyist investors frequently need to pull historical stock price data for a handful of tickers to run quick comparisons, b

Python3.10+
OSLinux / macOS
Lines~163
Packages2 deps
Install Dependencies
$ pip install yfinance pandas
Usage
$ python3 stock_fetch.py --tickers AAPL,MSFT,GOOG --period 1y --output stocks.csv --plot
#finance#python#beginner#yfinance#pandas

Yahoo Finance Stock Data Fetcher

Problem Statement

Analysts, DevOps engineers, and hobbyist investors frequently need to pull historical stock price data for a handful of tickers to run quick comparisons, build dashboards, or feed into a backtesting pipeline. Manually visiting Yahoo Finance and downloading CSVs one ticker at a time is tedious and error-prone, especially when you need consistent periods, adjusted prices, and a clean tabular output.

This script solves that by wrapping yfinance in a small, scriptable CLI. You provide a comma-separated list of tickers, a period (e.g. 1y, 6mo, 5d), and an output path. The script fetches historical OHLCV data for each ticker, concatenates it into a single pandas DataFrame, writes it to CSV, and optionally renders a closing-price chart. It handles network failures, invalid tickers, and missing data gracefully so it can be safely scheduled in a cron job or CI pipeline.

How It Works

The script follows a simple linear data flow: parse args → fetch data → combine → persist → visualize. Each ticker is downloaded independently via yfinance.download() so one bad symbol doesn't poison the whole batch; failures are logged and skipped. The per-ticker frames are annotated with a Ticker column and stacked with pd.concat(), producing a tidy long-format DataFrame that's easy to filter and group later.

Design decisions worth noting: (1) yfinance.download() is called with auto_adjust=True so splits and dividends are baked into prices — the right default for analysis. (2) The --plot flag uses pandas' built-in matplotlib backend to avoid extra dependencies. (3) Configuration is layered: CLI args override environment variables, which override hardcoded defaults — a standard pattern for 12-factor-style tools.

Full Script

#!/usr/bin/env python3
"""
Yahoo Finance Stock Data Fetcher

Fetch historical OHLCV data for one or more tickers from Yahoo Finance,
save the combined result to CSV, and optionally render a closing-price chart.

Usage:
    python3 stock_fetch.py --tickers AAPL,MSFT,GOOG --period 1y --output stocks.csv --plot
"""

from __future__ import annotations

import argparse
import logging
import os
import sys
from pathlib import Path

import pandas as pd
import yfinance as yf

# ---------------------------------------------------------------------------
# Configuration (environment variables with sensible defaults)
# ---------------------------------------------------------------------------
DEFAULT_PERIOD: str = os.environ.get("STOCK_DEFAULT_PERIOD", "1y")
DEFAULT_INTERVAL: str = os.environ.get("STOCK_DEFAULT_INTERVAL", "1d")
DEFAULT_OUTPUT: str = os.environ.get("STOCK_DEFAULT_OUTPUT", "stocks.csv")
LOG_LEVEL: str = os.environ.get("STOCK_LOG_LEVEL", "INFO").upper()

# Valid values accepted by Yahoo Finance; we validate up front to fail fast.
VALID_PERIODS = {"1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"}
VALID_INTERVALS = {"1m", "2m", "5m", "15m", "30m", "60m", "90m", "1h", "1d", "5d", "1wk", "1mo", "3mo"}

logging.basicConfig(
    level=LOG_LEVEL,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)
log = logging.getLogger("stock_fetch")


# ---------------------------------------------------------------------------
# Core logic
# ---------------------------------------------------------------------------
def parse_tickers(raw: str) -> list[str]:
    """Split a comma-separated ticker string into a deduplicated uppercase list."""
    tickers = [t.strip().upper() for t in raw.split(",") if t.strip()]
    if not tickers:
        raise ValueError("No tickers provided. Use --tickers AAPL,MSFT,...")
    # Preserve order while removing duplicates.
    return list(dict.fromkeys(tickers))


def fetch_ticker(ticker: str, period: str, interval: str) -> pd.DataFrame | None:
    """
    Download historical data for a single ticker.

    Returns a DataFrame with a 'Ticker' column, or None if the download fails
    or Yahoo returns an empty frame (typically an invalid symbol).
    """
    log.info("Fetching %s (period=%s, interval=%s)", ticker, period, interval)
    try:
        df = yf.download(
            ticker,
            period=period,
            interval=interval,
            auto_adjust=True,   # Adjust for splits/dividends — right default for analysis.
            progress=False,     # Silence yfinance's tqdm bar; we log ourselves.
            threads=False,      # Single-threaded keeps logs deterministic.
        )
    except Exception as exc:  # yfinance can raise a variety of network errors.
        log.error("Failed to download %s: %s", ticker, exc)
        return None

    if df is None or df.empty:
        log.warning("No data returned for %s — skipping (invalid symbol or delisted?)", ticker)
        return None

    # yfinance sometimes returns a MultiIndex column set even for one ticker.
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)

    df = df.reset_index()
    df["Ticker"] = ticker
    return df


def fetch_all(tickers: list[str], period: str, interval: str) -> pd.DataFrame:
    """Fetch every ticker and concatenate the results into one long DataFrame."""
    frames: list[pd.DataFrame] = []
    for ticker in tickers:
        frame = fetch_ticker(ticker, period, interval)
        if frame is not None:
            frames.append(frame)

    if not frames:
        raise RuntimeError("No data could be fetched for any of the requested tickers.")

    combined = pd.concat(frames, ignore_index=True)
    # Sort so CSV output is deterministic and easy to diff.
    sort_cols = [c for c in ("Ticker", "Date") if c in combined.columns]
    if sort_cols:
        combined = combined.sort_values(sort_cols).reset_index(drop=True)
    return combined


def save_csv(df: pd.DataFrame, output_path: Path) -> None:
    """Write the DataFrame to CSV, creating parent directories as needed."""
    output_path.parent.mkdir(parents=True, exist_ok=True)
    df.to_csv(output_path, index=False)
    log.info("Wrote %d rows to %s", len(df), output_path)


def plot_closes(df: pd.DataFrame) -> None:
    """Render a closing-price line chart, one series per ticker."""
    if "Close" not in df.columns or "Date" not in df.columns:
        log.warning("Cannot plot: missing 'Date' or 'Close' column.")
        return

    try:
        import matplotlib.pyplot as plt  # Imported lazily so --plot is optional.
    except ImportError:
        log.error("matplotlib is required for --plot. Install with: pip install matplotlib")
        return

    pivot = df.pivot_table(index="Date", columns="Ticker", values="Close", aggfunc="last")
    ax = pivot.plot(figsize=(11, 6), linewidth=1.6)
    ax.set_title("Closing Price")
    ax.set_xlabel("Date")
    ax.set_ylabel("Price (adjusted)")
    ax.grid(alpha=0.3)
    plt.tight_layout()
    plt.show()


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Fetch historical stock data from Yahoo Finance.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        "--tickers",
        required=True,
        help="Comma-separated ticker symbols, e.g. AAPL,MSFT,GOOG",
    )
    parser.add_argument(
        "--period",
        default=DEFAULT_PERIOD,
        choices=sorted(VALID_PERIODS),
        help="Lookback window.",
    )
    parser.add_argument(
        "--interval",
        default=DEFAULT_INTERVAL,
        choices=sorted(VALID_INTERVALS),
        help="Sampling interval between data points.",
    )
    parser.add_argument(
        "--output",
        default=DEFAULT_OUTPUT,
        help="Path to the output CSV file.",
    )
    parser.add_argument(
        "--plot",
        action="store_true",
        help="Render a closing-price chart after saving.",
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)

    try:
        tickers = parse_tickers(args.tickers)
    except ValueError as exc:
        log.error("%s", exc)
        return 2

    log.info("Requested tickers: %s", ", ".join(tickers))

    try:
        df = fetch_all(tickers, args.period, args.interval)
    except RuntimeError as exc:
        log.error("%s", exc)
        return 1

    save_csv(df, Path(args.output))

    if args.plot:
        plot_closes(df)

    return 0


if __name__ == "__main__":
    sys.exit(main())

Configuration

Setting Env var Default Notes
Default period STOCK_DEFAULT_PERIOD 1y Used when --period is not passed. Must be one of the values in VALID_PERIODS.
Default interval STOCK_DEFAULT_INTERVAL 1d Used when --interval is not passed. Intraday intervals (1m, 5m, …) only work for short periods on Yahoo.
Default output STOCK_DEFAULT_OUTPUT stocks.csv Used when --output is not passed. Parent directories are created automatically.
Log level STOCK_LOG_LEVEL INFO Set to DEBUG for verbose output, WARNING to quiet the script in cron.

CLI flags always override environment variables, which override the hardcoded defaults — the standard precedence for scriptable tools.

Usage Examples

1. Fetch one year of daily data for three tickers and save to CSV:

$ python3 stock_fetch.py --tickers AAPL,MSFT,GOOG --period 1y --output stocks.csv
2024-06-01 10:14:02 [INFO] Requested tickers: AAPL, MSFT, GOOG
2024-06-01 10:14:02 [INFO] Fetching AAPL (period=1y, interval=1d)
2024-06-01 10:14:03 [INFO] Fetching MSFT (period=1y, interval=1d)
2024-06-01 10:14:04 [INFO] Fetching GOOG (period=1y, interval=1d)
2024-06-01 10:14:05 [INFO] Wrote 756 rows to stocks.csv

2. Fetch six months and render a chart:

$ python3 stock_fetch.py --tickers NVDA,AMD --period 6mo --output chips.csv --plot

A matplotlib window pops up showing two closing-price lines.

3. Handle a bad ticker gracefully (script still exits 0 if any succeed):

$ python3 stock_fetch.py --tickers AAPL,ZZZZ --period 1mo
2024-06-01 10:15:00 [INFO] Fetching AAPL (period=1mo, interval=1d)
2024-06-01 10:15:01 [INFO] Fetching ZZZZ (period=1mo, interval=1d)
2024-06-01 10:15:02 [WARNING] No data returned for ZZZZ — skipping (invalid symbol or delisted?)
2024-06-01 10:15:02 [INFO] Wrote 22 rows to stocks.csv

Error Handling

  • Invalid --period / --interval: argparse rejects them with exit code 2 before any network call.
  • Empty --tickers: parse_tickers() raises ValueError; main() logs it and returns 2.
  • Network failures / rate limiting: yf.download() exceptions are caught per ticker; the script logs the error and continues with the remaining symbols.
  • Invalid or delisted symbols: Yahoo returns an empty frame; the script logs a warning and skips that ticker.
  • All tickers fail: fetch_all() raises RuntimeError; the script logs and exits with code 1 so cron/CI can detect the failure.
  • Missing matplotlib when --plot is passed: caught at import time with a clear install hint; the CSV is still written.
  • Unwritable output path: Path.mkdir(parents=True, exist_ok=True) plus pandas' own OSError will surface a traceback — intentional, since permission issues usually need operator attention.

Extending Ideas

  • Add a --start / --end date range mode using yf.download(start=..., end=...) instead of period — useful for reproducible research windows.
  • Persist to SQLite or Postgres instead of CSV by swapping save_csv() for a DataFrame.to_sql() call; the long-format schema (Ticker, Date, OHLCV) maps cleanly to a table with a composite primary key.
  • Add technical indicators (SMA, EMA, RSI) as extra columns using pandas.rolling() after the fetch — a natural next step for analysis pipelines.
  • Schedule as a cron job with STOCK_LOG_LEVEL=WARNING and a timestamped output path like data/$(date +%F).csv to build a rolling historical archive automatically.
// RELATED_SCRIPTS
← Back to DevLab valtersit.com