1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

yfinanceを本番運用して分かった4つの落とし穴と対策 — Pythonで株価データを安定取得する方法

1
Posted at

yfinance は Python で株価データを取得する定番ライブラリ。pip install yfinance して数行書けば、すぐに株価が取れる。手軽さは圧倒的。

ただ、この「とりあえず動く」状態を本番の日次バッチに組み込むと、途端にいろいろ踏む。

RakuScan という投資分析システムで半年以上 yfinance を運用してきた中で踏んだ落とし穴と、それぞれの対策を共有する。200銘柄の日次スクリーニングを毎日回している環境での実体験がベースになっている。

RakuScan の設計思想(プラグインアーキテクチャ、多層データソース、キャッシュ戦略など)を体系的にまとめた本がある。yfinance の扱い方もその一部として第8章で詳しく書いた。

yfinance とは

Yahoo Finance の非公式 Python API。株価(OHLCV)、財務データ、配当情報、銘柄情報など幅広いデータが取得できる。

import yfinance as yf

# トヨタの株価を取得
df = yf.download("7203.T", start="2026-01-01", end="2026-04-01")
print(df.head())

# 財務データも取れる
t = yf.Ticker("7203.T")
print(t.income_stmt)
print(t.balance_sheet)

公式 API キーの申請も不要で、無料で使える。個人開発の株価データソースとして第一候補に挙がるのは当然だと思う。

ただし「非公式」である点は常に意識しておく必要がある。Yahoo Finance が提供する公開APIではなく、Web ページやエンドポイントをスクレイピングしている。この特性が、以降で書く落とし穴の多くに関わってくる。

落とし穴1: レート制限とIPブロック

症状

短時間に大量のリクエストを投げると、Yahoo Finance 側から IP ブロックされる。厄介なのは、ブロックされても例外が飛ばないケースがあること。

# 200銘柄を一気に取得しようとする
for symbol in symbols:
    df = yf.download(f"{symbol}.T", start="2026-01-01", end="2026-04-01")
    # df が空の DataFrame で返ってくる。例外は出ない。
    if df.empty:
        print(f"{symbol}: データなし")  # ブロックなのか本当にデータがないのか区別できない

エラーメッセージが出ないまま空の DataFrame が返るので、「データがない銘柄なのか、ブロックされたのか」が分からない。ログを仕込んでいないと気づかないまま不完全なデータで分析が走る。

対策

バッチサイズを制限して、バッチ間にインターバルを入れる。RakuScan では20銘柄ずつ、1秒間隔でバッチ取得している。

def fetch_batch_ohlcv(
    symbols_yf: list[str],
    days: int = 60,
    batch_size: int = 20,
    batch_interval: float = 1.0,
) -> dict[str, list]:
    for batch_start in range(0, len(symbols_yf), batch_size):
        batch = symbols_yf[batch_start: batch_start + batch_size]
        df = yf.download(batch, start=start, end=end, progress=False, auto_adjust=True)

        if df.empty:
            continue  # 空の場合はスキップ(ブロックの可能性)

        # ... 各銘柄のデータを抽出 ...

        # バッチ間で待機(最終バッチ以外)
        if batch_start + batch_size < len(symbols_yf):
            time.sleep(batch_interval)

さらに、SQLite ベースの TTL 付きキャッシュを挟んで、同じデータの重複取得を防いでいる。

# 価格データの TTL: 1日、財務データの TTL: 30日
_PRICE_TTL_DAYS = 1
_FINANCIAL_TTL_DAYS = 30

def get_prices(symbol: str, start: str, end: str) -> list[PriceRecord]:
    # 1. キャッシュから探す
    cached = _load_prices_from_db(symbol, start, end)
    if cached is not None:
        return cached  # TTL内ならAPIを叩かない

    # 2. キャッシュミス → yfinanceから取得
    records = make_provider().get_daily_prices(symbol, start, end)

    # 3. 取得結果をキャッシュに保存
    _save_prices_to_db(records)
    return records

日次スクリーニングの前に全銘柄のデータを一括プリフェッチする設計にしているので、各プラグインの実行中にはAPIコールが一切発生しない。200銘柄 × 16プラグインが走っても、すべてキャッシュからの読み出しになる。

落とし穴2: データの欠損と不整合

症状

yfinance で取得できるデータは銘柄によって大きくばらつく。特に日本株(.T 銘柄)で顕著な問題がいくつかある。

財務データのフィールド名が銘柄によって異なる。 たとえば純利益が Net Income で返る銘柄と Net Income Common Stockholders で返る銘柄がある。営業キャッシュフローも Operating Cash FlowCash Flow From Continuing Operating Activities の2パターンがある。

# 銘柄Aでは取れるが、銘柄Bでは None
t = yf.Ticker("7203.T")
inc = t.income_stmt
# inc.loc["Net Income"] → 存在する
# inc.loc["Net Income Common Stockholders"] → KeyError

None ではなく NaN が返る。 yfinance は pandas の DataFrame で結果を返すため、欠損値は Python の None ではなく NaN になる。if value is None では検出できない。

株式分割時の調整。 auto_adjust=True を指定しないと、分割前後で価格が不連続になる。テクニカル分析やリターン計算が壊れる原因になる。

対策

複数のキー名を順番に試すヘルパー関数を用意する。RakuScan ではこういう実装になっている。

def _get_financial_value(df: pd.DataFrame | None, col: object, *keys: str) -> float | None:
    """DataFrameから財務値を取得する。複数のキー名を順に試す。"""
    if df is None or df.empty:
        return None
    for key in keys:
        try:
            if key in df.index:
                val = df.loc[key, col]
                if pd.notna(val):  # NaN チェックは pd.notna() で
                    return float(val)
        except Exception:
            continue
    return None

# 使い方: 複数の候補キーをフォールバック付きで指定
net_income = _get_financial_value(inc, col, "Net Income", "Net Income Common Stockholders")
operating_cf = _get_financial_value(cf, col, "Operating Cash Flow",
                                    "Cash Flow From Continuing Operating Activities")
revenue = _get_financial_value(inc, col, "Total Revenue", "Revenue")

財務データ全体として「総資産(Total Assets)が取得できなければその期のデータは使えない」というバリデーションも入れている。

if total_assets is None:
    continue  # この期は使えない → スキップ

yf.download() には必ず auto_adjust=True を指定して、分割調整済みの価格を取得する。

落とし穴3: 並行処理との相性

症状

yfinance は内部でタイムゾーンキャッシュに SQLite を使っている。デフォルトのキャッシュディレクトリは ~/.cache/py-yfinance/ で、複数のプロセスが同時にこのディレクトリを読み書きすると SQLite の OperationalError: unable to open database file が発生する。

日次バッチと手動分析を同時に走らせたとき、あるいは月次のユニバース構築中に日次バッチが起動したときに踏む。

対策

プロセスごとにキャッシュディレクトリを分離する。RakuScan では、yfinance を初回呼び出しする前にプロセス固有の一時ディレクトリを作成し、yf.set_tz_cache_location() でキャッシュ先を切り替えている。

import atexit
import shutil
import tempfile

import yfinance as yf

_yf_cache_initialized = False

def _init_yf_cache() -> None:
    """yfinance のキャッシュをプロセス固有の一時ディレクトリに誘導する。"""
    global _yf_cache_initialized
    if _yf_cache_initialized:
        return
    cache_dir = tempfile.mkdtemp(prefix="py-yfinance-")
    yf.set_tz_cache_location(cache_dir)
    atexit.register(shutil.rmtree, cache_dir, True)  # プロセス終了時に自動削除
    _yf_cache_initialized = True

初回の yfinance 呼び出し時に一度だけ実行し、atexit で終了時に自動削除する。これで複数プロセスが同時に動いても干渉しない。

地味だが、これを入れていないと本番で突然バッチが落ちる。ローカル開発では1プロセスしか動かさないので気づきにくい。

落とし穴4: APIの仕様変更・廃止リスク

症状

yfinance は Yahoo Finance の非公式 API。Yahoo Finance 側の HTML 構造やエンドポイントが変わると、ある日突然データが取れなくなる。

実際、yfinance の GitHub Issues を見ると「昨日まで動いていたのに今日から全銘柄 empty DataFrame になった」という報告が定期的に上がっている。ライブラリのメンテナが対応してバージョンアップされるまで、数日間データが取れない状態が続くこともある。

日次で分析を回しているシステムにとって、「数日間データが取れない」は致命的。

対策

単一のデータソースに依存しない設計にする。RakuScan ではデータソースを ABC(抽象基底クラス)で抽象化し、yfinance → EDINET DB の順でフォールバックする構成にしている。yfinance が壊れても EDINET DB から財務データを取得でき、プラグイン側のコードは一切変更不要。ABC の設計やフォールバックの実装詳細は連載#2の記事で解説した。

キャッシュ層が緩衝材の役割も果たしている。TTL 内のデータがキャッシュに残っていれば、API が一時的に死んでいても直近のデータで分析を継続できる。「昨日のデータで走らせるか、何も走らせないか」なら、前者のほうが実用的。

本番運用に耐えるための設計指針

半年以上の運用で固まった原則をまとめる。

キャッシュ層は必須。 yfinance のAPIコールを最小化するために、TTL 付きキャッシュを必ず挟む。RakuScan では価格データの TTL を1日、財務データの TTL を30日に設定している。日次スクリーニング前にプリフェッチを走らせることで、プラグイン実行中のAPIコールをゼロにしている。

単一データソースに依存しない。 非公式 API は壊れる前提で設計する。ABC で抽象化してフォールバック構成にしておけば、プロバイダの追加や切り替えがプラグイン側に影響しない(連載#2で詳述)。

空の DataFrame を握りつぶさない。 df.empty のチェックは必須。空の DataFrame がそのまま後段の計算に流れると、ゼロ除算やインデックスエラーで別の場所で壊れる。「データが取れなかった」ことを明示的にログに残して、空リストを返す設計にする。

pd.notna() で欠損チェックする。 if value is None では NaN を検出できない。yfinance が返す pandas DataFrame の欠損値は NaN なので、pd.notna() または pd.isna() を使う。

テストではモックと実データの両方を使う。 yfinance の挙動をモックでテストしつつ、定期的に実データでの動作確認も行う。非公式 API は仕様が変わるので、モックだけでは「いつの間にか壊れていた」を検出できない。

まとめ

yfinance は「とりあえず動く」までは驚くほど簡単。ただし、本番で日次バッチに組み込んで安定運用するには、レート制限・データ欠損・並行処理・仕様変更の4つの落とし穴に対処する必要がある。

RakuScan ではこれらの問題を、バッチ取得+インターバル制御、TTL 付き SQLite キャッシュ、プロセス固有のキャッシュディレクトリ分離、ABC による多層データソース構成で解決した。

これらの設計判断の背景にある思想と、yfinance 以外のデータソース(J-Quants API、EDINET DB)との組み合わせ方については、以下の本で詳しく解説している。

RakuScanの設計思想を15章・約5万文字で解説した有料本を公開中。 yfinance を含む3つの無料APIの使い分け、プラグインアーキテクチャ、2層キャッシュ戦略、Claude統合分析、Discord Bot まで、投資分析システムの設計判断と実装の裏側を網羅している。この記事で触れた「落とし穴への対策」が、システム全体の設計の中でどう位置づけられているかを知りたい方向け。


この記事は Zenn にも同じ内容を投稿しています。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?