Pythonで株価取得|yfinanceの仕様変更とエラー対処

Pythonで株価を取得する方法 Python実装・コード

※本記事には広告・アフィリエイトリンクが含まれます。掲載内容は筆者の調査・検証に基づき、読者の判断を助ける目的で作成しています。

結論から言うと

  • yfinanceを使えば、日本株・米国株の株価データを数行で取得できます
  • 日本株は証券コードの末尾に「.T」を付けます(トヨタなら7203.T
  • ただしyfinanceは仕様変更が多く、古い記事のコードはそのままでは動きません。列がMultiIndexになる、価格が自動調整される、レート制限に当たる、の3点は必ず押さえてください

この記事では基本の取得方法に加えて、実際に動かしたときにつまずく変更点と対処法をまとめます。

動作確認環境:Python 3.12 / pandas 2.x / yfinance 0.2.6x

📘 外部参考yfinance 公式GitHubCHANGELOG(仕様変更の履歴)

インストールとバージョン確認

pip install -U yfinance

yfinanceはバージョンによって挙動が変わります。まず自分の環境を確認してください。

import yfinance as yf
print(yf.__version__)

本番で使うスクリプトでは、requirements.txtにバージョンを固定しておくことを強くおすすめします。ある日突然データの形が変わって、バックテストの結果が変わります。

yfinance==0.2.66
pandas==2.2.3

基本の取得方法

import yfinance as yf

df = yf.download("7203.T", period="3mo", auto_adjust=True, progress=False)
print(df.tail())

返ってくるDataFrameの列はOpen(始値)、High(高値)、Low(安値)、Close(終値)、Volume(出来高)です。

periodには1d / 5d / 1mo / 3mo / 6mo / 1y / 2y / 5y / 10y / ytd / maxが指定できます。期間を指定したいときはstartendを使います。

df = yf.download("7203.T", start="2020-01-01", end="2026-01-01",
                 auto_adjust=True, progress=False)

つまずきポイント1:列がMultiIndexになる

これがいちばん多い相談です。1銘柄だけ指定しても、列が2階層のMultiIndexで返ってきます。

df = yf.download("7203.T", period="1mo", progress=False)
print(df.columns)
# MultiIndex([('Close', '7203.T'), ('High', '7203.T'), ...])

df["Close"]        # Seriesではなく DataFrame が返る
df["Close"].rolling(20).mean()   # 意図せず DataFrame のまま処理が進む

以前は1銘柄なら単層、複数銘柄ならMultiIndexという挙動でした。それが統一されたため、古い記事のコードが動かなくなっています。CloseがSeriesだと思って書いたコードが、静かにDataFrameとして処理されて、あとで謎のエラーになります。

対処法は2つです。

# 方法1: multi_level_index=False を指定する(新しめのバージョン)
df = yf.download("7203.T", period="1mo", multi_level_index=False,
                 auto_adjust=True, progress=False)

# 方法2: バージョンを問わず動く汎用の関数にしておく
import pandas as pd

def fetch(ticker: str, **kw) -> pd.DataFrame:
    kw.setdefault("auto_adjust", True)
    kw.setdefault("progress", False)
    df = yf.download(ticker, **kw)
    if df.empty:
        raise RuntimeError(f"{ticker}: データが取得できませんでした")
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
    return df.dropna(how="all")

df = fetch("7203.T", period="5y")
print(type(df["Close"]))   # <class 'pandas.core.series.Series'>

自分は取得を必ず関数経由にするようにしています。ライブラリの仕様変更の影響を1箇所に閉じ込められるからです。

つまずきポイント2:auto_adjustの既定値が変わった

yf.download()auto_adjustは、以前はFalseでしたが現在はTrueが既定です。指定せずに呼ぶと警告が出ます。

auto_adjust=Trueのとき、次のことが起こります。

  • Open / High / Low / Close が株式分割と配当で調整された値になる
  • Adj Close列が消える
  • Volume が分割に応じて逆方向に調整される
設定Adj Close列価格向いている用途
auto_adjust=Trueなし調整済みバックテスト、リターン計算
auto_adjust=Falseあり実際の約定価格チャート表示、配当分析

バックテストでは必ずauto_adjust=Trueを使ってください。未調整の価格だと、株式分割の日に価格が1/3になり、そこで存在しない大暴落が発生します。移動平均クロスは全部デッドクロスになり、ドローダウン-70%という架空の結果が出ます。

逆に「今いくらで買えるか」を表示したいときはauto_adjust=Falseです。調整済み価格は過去に遡って書き換えられる値なので、実際の板の値段とは違います。

# 分割の影響を確認する
raw = yf.download("9984.T", period="10y", auto_adjust=False,
                  progress=False, multi_level_index=False)
adj = yf.download("9984.T", period="10y", auto_adjust=True,
                  progress=False, multi_level_index=False)

cmp = pd.DataFrame({"未調整": raw["Close"], "調整済": adj["Close"]})
cmp["比率"] = (cmp["未調整"] / cmp["調整済"]).round(3)
print(cmp.iloc[[0, len(cmp) // 2, -1]])   # 比率が途中で変わる=分割があった

つまずきポイント3:レート制限に当たる

短時間に大量のリクエストを送るとYFRateLimitError('Too Many Requests')が返ります。銘柄をループで回すスクリプトを書くと、すぐ遭遇します。

対処は3つの組み合わせです。

import time
import random
from pathlib import Path

CACHE = Path("./cache")
CACHE.mkdir(exist_ok=True)

def fetch_cached(ticker: str, period: str = "5y",
                 max_age_hours: float = 12) -> pd.DataFrame:
    """1. ローカルキャッシュ 2. 指数バックオフ再試行 3. 呼び出し間隔"""
    path = CACHE / f"{ticker.replace('.', '_')}_{period}.parquet"
    if path.exists():
        age = (time.time() - path.stat().st_mtime) / 3600
        if age < max_age_hours:
            return pd.read_parquet(path)

    last_err = None
    for attempt in range(4):
        try:
            df = fetch(ticker, period=period)
            df.to_parquet(path)
            return df
        except Exception as e:
            last_err = e
            wait = 2 ** attempt + random.random()
            print(f"{ticker} 再試行 {attempt + 1}/4 ({wait:.1f}秒待機): {e}")
            time.sleep(wait)

    if path.exists():          # 古くても、無いよりはマシ
        print(f"{ticker}: 取得失敗。古いキャッシュを使用します")
        return pd.read_parquet(path)
    raise RuntimeError(f"{ticker}: 取得できません") from last_err

def fetch_many(tickers: list[str], sleep: float = 1.0) -> dict:
    out = {}
    for t in tickers:
        try:
            out[t] = fetch_cached(t)
        except Exception as e:
            print("skip", t, e)
        time.sleep(sleep)      # 連打しない
    return out

それでも403や429が続く場合は、ブラウザを装うセッションを渡す方法があります。

# pip install curl_cffi
from curl_cffi import requests as cffi_requests

session = cffi_requests.Session(impersonate="chrome")
df = yf.Ticker("7203.T", session=session).history(period="1y")

ただし、これはYahoo!側の制限を回避する行為でもあります。常用するなら素直にJ-Quants APIなどの正規のデータソースに移るべきです。yfinanceは学習と検証のための道具と割り切るのが健全だと思います。

複数銘柄をまとめて取得する

NAMES = {"7203.T": "トヨタ", "6758.T": "ソニーG", "9984.T": "ソフトバンクG"}

data = yf.download(list(NAMES), period="1y", auto_adjust=True,
                   progress=False)["Close"].rename(columns=NAMES)
print(data.tail())
print("欠損数:\n", data.isna().sum())

1銘柄ずつループで取るより、まとめて1回で取るほうが速くレート制限にも当たりにくいです。ただし1つでも取得に失敗すると、その銘柄の列が全部NaNになります。黙って進むので、isna().sum()での確認を習慣にしてください。

ティッカーの書き方

種類書き方
日本株(東証)証券コード + .T7203.T(トヨタ)
日本のETF同上1306.T(TOPIX連動)
指数^ + コード^N225^GSPC^SOX
為替通貨ペア + =XUSDJPY=X
先物コード + =FNIY=F(日経先物)
米国株そのままAAPLNVDA
暗号資産通貨 + -USDBTC-USD

4桁だった証券コードに英字が入る銘柄も出てきています。130A.Tのような形式でも、そのまま.Tを付ければ取得できます。

株価以外のデータ

t = yf.Ticker("7203.T")

print(t.dividends.tail())          # 配当履歴(Series)
print(t.splits)                    # 株式分割の履歴
print(t.financials.iloc[:, :2])    # 損益計算書
print(t.balance_sheet.iloc[:, :2]) # 貸借対照表

info = t.info                      # 企業情報(辞書)
print(info.get("longName"), info.get("marketCap"))

.infoは信頼しないでください。取得が遅く、失敗しやすく、返るキーもバージョンや銘柄によって変わります。日本株では欠損も多いです。必ず.get()で取り出し、値が無い前提で書きます。

def safe_info(ticker: str, keys: list[str]) -> dict:
    try:
        info = yf.Ticker(ticker).info or {}
    except Exception as e:
        print(f"{ticker}: info取得失敗 ({e})")
        return {k: None for k in keys}
    return {k: info.get(k) for k in keys}

print(safe_info("7203.T", ["longName", "marketCap", "trailingPE",
                           "dividendYield", "sector"]))

取得したデータを検品する

無料データなので、欠損や異常値が混じります。分析に入る前に必ず確認してください。ここを飛ばすと、おかしな結果の原因究明に何時間も溶かします。

def audit(df: pd.DataFrame, name: str = "") -> None:
    close = df["Close"]
    r = close.pct_change()
    print(f"--- {name} ---")
    print(f"期間      : {df.index[0].date()} 〜 {df.index[-1].date()}  {len(df)}行")
    print(f"欠損      : {int(df.isna().sum().sum())}")
    print(f"重複日付  : {int(df.index.duplicated().sum())}")
    print(f"出来高ゼロ: {int((df['Volume'] == 0).sum())}日")
    print(f"High<Low  : {int((df['High'] < df['Low']).sum())}件")   # 明らかな異常
    print(f"±20%超    : {int((r.abs() > 0.2).sum())}日")
    gaps = df.index.to_series().diff().dt.days
    print(f"7日以上の空白: {int((gaps > 7).sum())}回")

audit(fetch("7203.T", period="10y"), "トヨタ")

±20%超が何度も出るなら、分割の調整漏れを疑います。7日以上の空白は、連休以外なら取得漏れの可能性があります。異常が見つかったときはrepair=Trueを試す手もあります。

df = yf.Ticker("7203.T").history(period="10y", auto_adjust=True, repair=True)

よくあるエラーと対処法

症状原因と対処
空のDataFrameが返るティッカーの誤り。日本株は.Tが必要
YFRateLimitErrorリクエスト過多。間隔を空ける/キャッシュする
KeyError: 'Adj Close'auto_adjust=Trueで列が消えた。Falseを明示
Seriesのはずが DataFrameMultiIndex列。get_level_values(0)で潰す
指標計算が全部NaN上と同根。列構造をprint(df.columns)で確認
タイムゾーンで結合できないdownloadはtz-naive、historyはtz-aware
数日前からデータが止まるYahoo!側の一時障害。時間をおく

最後のタイムゾーンの違いは見落としがちです。yf.download()Ticker.history()でインデックスの型が変わるため、両方で取ったデータを結合しようとするとTypeErrorになります。どちらか一方に統一するのが安全です。

次のステップ

データが取得できたら、次は可視化と指標計算です。ただしその前に、取得部分を関数にまとめてキャッシュを効かせておくことを勧めます。試行錯誤のたびにYahoo!を叩くと、すぐレート制限に当たって手が止まります。

データ取得方法の比較はこちら
yfinance以外のデータ取得手段も含めた比較記事:Python株価データ取得ライブラリ比較

※当サイトの内容は投資判断を推奨するものではありません。掲載しているコード・分析例は学習・検証目的であり、実際の投資はご自身の責任で行ってください。

🔗 関連記事

Pythonで株価チャートを表示する方法【matplotlib・yfinance】

タイトルとURLをコピーしました