← 태버지 목록
yahoo_chart_client.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""Yahoo Finance Chart API 직접 호출 클라이언트 (yfinance 우회).

IP rotation (yfinance_proxy.ip_rotating_session) 과 함께 사용하면
yfinance 1.2 의 curl_cffi 요구사항 없이 Yahoo 데이터 수집 가능.

지원 기능:
    - 배치 OHLC 다운로드 (fetch_ohlcv_batch)
    - 시가총액 등 기본 정보 (fetch_quote_summary)

사용 예:
    from backend.app.services.yahoo_chart_client import fetch_ohlcv_batch
    from backend.app.services.yfinance_proxy import ip_rotating_session

    with ip_rotating_session() as session:
        df = fetch_ohlcv_batch(["AAPL", "MSFT"], session=session, range_str="60d")
"""
from __future__ import annotations

import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any

import pandas as pd
import requests

logger = logging.getLogger(__name__)

_BASE = "https://query1.finance.yahoo.com"
_HEADERS = {
    # Yahoo가 2026년부터 흔한 봇 UA(Chrome/120 등)를 429로 차단 — 짧은 AppleWebKit UA는 통과.
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
    "Accept": "application/json",
}
_TIMEOUT = 20
_MAX_WORKERS = 8  # connection pool (default 10) 안에 맞춤


def _fetch_single_chart(
    ticker: str,
    range_str: str,
    interval: str,
    session: requests.Session,
    use_adjclose: bool = False,
) -> tuple[str, pd.DataFrame | None]:
    """단일 티커의 OHLCV 조회. 실패 시 (ticker, None) 반환.

    Args:
        use_adjclose: True면 indicators.adjclose.adjclose를 Close로 사용 (배당·분할 조정됨).
            yfinance의 auto_adjust=True와 동일한 효과.
    """
    url = f"{_BASE}/v8/finance/chart/{ticker}"
    params = {
        "interval": interval,
        "range": range_str,
        "includePrePost": "false",
        "events": "div,splits",  # adjclose 활성화
    }
    try:
        resp = session.get(url, params=params, headers=_HEADERS, timeout=_TIMEOUT)
        resp.raise_for_status()
        data = resp.json()
    except (requests.RequestException, ValueError) as exc:
        logger.debug("Chart API failed for %s: %s", ticker, exc)
        return ticker, None

    chart = data.get("chart") or {}
    result = (chart.get("result") or [None])[0]
    if not result:
        return ticker, None

    timestamps = result.get("timestamp") or []
    if not timestamps:
        return ticker, None
    indicators = result.get("indicators") or {}
    quote = (indicators.get("quote") or [{}])[0]

    close_values = quote.get("close") or []
    if use_adjclose:
        adj_list = indicators.get("adjclose") or [{}]
        adj_values = adj_list[0].get("adjclose") or []
        if adj_values:
            close_values = adj_values

    df = pd.DataFrame(
        {
            "Open": quote.get("open") or [],
            "High": quote.get("high") or [],
            "Low": quote.get("low") or [],
            "Close": close_values,
            "Volume": quote.get("volume") or [],
        },
        index=pd.to_datetime(timestamps, unit="s", utc=True).tz_convert("UTC").normalize(),
    )
    df.index.name = "Date"
    return ticker, df


def fetch_ohlcv_batch(
    tickers: list[str],
    session: requests.Session,
    range_str: str = "60d",
    interval: str = "1d",
    max_workers: int = _MAX_WORKERS,
    use_adjclose: bool = False,
) -> pd.DataFrame:
    """배치 OHLCV 조회. yfinance download() 와 비슷한 MultiIndex DataFrame 반환.

    Args:
        use_adjclose: True면 adjusted close (배당·분할 조정) 사용. yfinance auto_adjust=True와 동일.

    Returns:
        pd.DataFrame with MultiIndex columns (ticker, OHLCV field)
        빈 DataFrame 이면 모든 티커 실패.
    """
    results: dict[str, pd.DataFrame] = {}
    failed: list[str] = []

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(_fetch_single_chart, t, range_str, interval, session, use_adjclose): t
            for t in tickers
        }
        for fut in as_completed(futures):
            ticker, df = fut.result()
            if df is not None and not df.empty:
                results[ticker] = df
            else:
                failed.append(ticker)

    if failed:
        logger.warning("Failed to fetch %d/%d tickers: %s", len(failed), len(tickers), failed[:5])

    if not results:
        return pd.DataFrame()

    # yfinance 와 동일한 MultiIndex 포맷으로 결합
    combined = pd.concat(results, axis=1, names=["Ticker", "OHLCV"])
    # reorder: swap levels so it becomes (Ticker, OHLCV) with ticker on top
    return combined


# NOTE: /v7/finance/quote 와 /v10/finance/quoteSummary 는 2023년부터 crumb 인증 필요.
# 메타데이터(sector/industry/name/marketCap) 조회는 yfinance 1.2+ 의
# Ticker.info 를 그대로 사용하세요 (신고가 20건 정도면 레이트리밋 걱정 없음).
노트북 실행 중...
yahoo_chart_client.py
0.0초
Papermill로 모든 셀을 다시 실행하고 있습니다.
수십 초에서 몇 분 걸릴 수 있습니다. 창을 닫지 마세요.