# KRXProvider

KRX OpenAPI(openapi.krx.co.kr) 기반 KRX 주식 캔들 데이터 수집. IProvider 구현.
API 키가 없으면 NoApiKeyError 발생 -> ProviderRegistry에서 KRXFallbackProvider로 전환.

_BASE_URL: str = "https://data-dbg.krx.co.kr/svc/apis"
_ENDPOINTS: dict = {"KOSPI": "sto/stk_bydd_trd", "KOSDAQ": "sto/ksq_bydd_trd"}

_api_key: str                      # KRX_API_KEY (.env)
_session: requests.Session         # HTTP 세션
_throttler: SyncThrottler          # 초당 10회, 분당 500회
_normalization: NormalizationService
_null_handling: NullHandlingService
_date_cache: dict[str, list[dict]] # 날짜(YYYYMMDD) -> 전종목 raw 데이터 캐시

## Properties

archetype: str  # "STOCK"
exchange: str   # "KRX"
tradetype: str  # "SPOT"

## __init__

__init__(conn_manager: ConnectionManager) -> None
    KRX OpenAPI Provider 초기화.

    1. ApiValidationService로 KRX_API_KEY 확인 (없으면 NoApiKeyError)
    2. SyncThrottler 설정 (초당 10회, 분당 500회)
    3. requests.Session 생성
    4. NormalizationService, NullHandlingService 초기화
    5. _date_cache = {} 초기화

    Raises:
        NoApiKeyError: KRX_API_KEY 환경변수 없을 때

## Methods

fetch(symbol: Symbol, start_at: int, end_at: int) -> list[dict]
    KRX OpenAPI로 종목 일봉 데이터 조회.

    Args:
        symbol: Symbol 객체 (symbol.base = 종목코드, 예: "005930")
        start_at: 시작 Unix timestamp (초)
        end_at: 종료 Unix timestamp (초)

    Returns:
        list[dict]: 정규화된 캔들 데이터
            [{"timestamp": int, "open": int, "high": int, "low": int, "close": int, "volume": int}, ...]

    흐름:
        1. start_at ~ end_at 범위의 영업일 리스트 생성 (주말 제외)
        2. 각 영업일에 대해:
           a. _date_cache에 있으면 캐시 사용
           b. 없으면 _fetch_date(date_str) 호출 -> KOSPI+KOSDAQ 합쳐서 캐시 저장
        3. 캐시된 전종목 데이터에서 ISU_CD == symbol.base로 필터
        4. NormalizationService.normalize_krx()로 정규화
        5. NullHandlingService.handle()로 null 처리

get_market_list() -> list[dict]
    KRX 전체 상장 종목 목록 반환.

    Returns:
        list[dict]: [{"base": str, "quote": "KRW", "full_name": str, "listed_at": None}, ...]

    흐름:
        1. 최근 영업일 기준 _fetch_date() 호출
        2. 전종목 데이터에서 종목코드/종목명 추출
        3. listed_at은 이 API에서 제공하지 않으므로 None

get_data_range(symbol: Symbol) -> tuple[int | None, int | None]
    데이터 범위 반환. KRX OpenAPI는 2010년 이후 데이터 제공.

    Returns:
        tuple: (1262563200, int(time.time()))
        1262563200 = 2010-01-04 00:00:00 UTC (KRX API 최초 데이터)

get_supported_timeframes() -> list[str]
    return ["1d"] (일봉만 지원)

## Internal Methods

_fetch_date(date_str: str) -> list[dict]
    특정 날짜의 KOSPI+KOSDAQ 전종목 시세를 API로 조회하고 캐시에 저장.

    Args:
        date_str: "YYYYMMDD" 형식

    Returns:
        list[dict]: KRX API raw 응답 (OutBlock_1 병합)

    흐름:
        1. throttler.wait()
        2. KOSPI 엔드포인트 호출: GET {BASE_URL}/sto/stk_bydd_trd?AUTH_KEY={key}&basDd={date}
        3. throttler.wait()
        4. KOSDAQ 엔드포인트 호출: GET {BASE_URL}/sto/ksq_bydd_trd?AUTH_KEY={key}&basDd={date}
        5. 두 응답의 OutBlock_1 병합
        6. _date_cache[date_str]에 저장
        7. 병합된 리스트 반환

    Notes:
        - 공휴일/비영업일이면 빈 리스트 반환 (API가 빈 응답)
        - HTTP 에러 시 ServerNotRespondedError

_get_business_days(start_at: int, end_at: int) -> list[str]
    Unix timestamp 범위를 영업일(주말 제외) YYYYMMDD 리스트로 변환.

    Notes:
        - 공휴일은 별도 관리하지 않음 (API 호출 시 빈 응답으로 자연 처리)

---

**KRX API 응답 필드 매핑:**
- BAS_DD (YYYYMMDD) -> timestamp (Unix timestamp)
- ISU_CD -> 종목코드 (필터링용, "005930")
- ISU_NM -> 종목명 (get_market_list용)
- TDD_OPNPRC -> open
- TDD_HGPRC -> high
- TDD_LWPRC -> low
- TDD_CLSPRC -> close
- ACC_TRDVOL -> volume

**의존성:**
- l1/api_validation_service: API 키 확인
- l1/sync_throttler: 레이트 제한
- l3/normalization_service: 데이터 정규화 (normalize_krx 메서드 추가 필요)
- l3/null_handling_service: null 처리

**ProviderRegistry 연동:**
- ("STOCK", "KRX", "SPOT") -> KRXProvider (primary)
- KRXProvider 생성 실패 시 (NoApiKeyError) -> KRXFallbackProvider (fallback)
