from __future__ import annotations import httpx from app.core.auth import token_manager from app.core.config import get_base_url, settings from app.core.logger import setup_logger from app.core.rate_limiter import RateLimiter logger = setup_logger("market_data") _PRICE_URL = "/uapi/domestic-stock/v1/quotations/inquire-price" _CHART_URL = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice" _ORDERBOOK_URL = "/uapi/domestic-stock/v1/quotations/inquire-asking-price" _FID_INPUT_ISCD = "FID_INPUT_ISCD" _FID_COND_MRKT_DIV_CODE = "FID_COND_MRKT_DIV_CODE" class MarketDataService: def __init__(self) -> None: self._client = httpx.AsyncClient(timeout=30.0) self._rate_limiter = RateLimiter(settings.rate_limit.requests_per_second) def _check_auth(self) -> bool: if not token_manager.is_authenticated: logger.debug("API 인증되지 않음 - 시세 조회 불가") return False return True async def get_current_price(self, stock_code: str) -> dict: if not self._check_auth(): return {} await self._rate_limiter.acquire() headers = token_manager.get_auth_headers("FHKST01010100") params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code} url = f"{get_base_url()}{_PRICE_URL}" resp = await self._client.get(url, headers=headers, params=params) resp.raise_for_status() data = resp.json() if data.get("rt_cd") != "0": logger.warning("현재가 조회 실패: %s - %s", stock_code, data.get("msg1")) return {} output = data.get("output", {}) return { "stock_code": stock_code, "current_price": int(output.get("stck_prpr", 0)), "change_price": int(output.get("prdy_vrss", 0)), "change_rate": float(output.get("prdy_ctrt", 0)), "open_price": int(output.get("stck_oprc", 0)), "high_price": int(output.get("stck_hgpr", 0)), "low_price": int(output.get("stck_lwpr", 0)), "volume": int(output.get("acml_vol", 0)), "trade_amount": int(output.get("acml_tr_pbmn", 0)), "stock_name": output.get("hts_kor_isnm", ""), } async def get_daily_chart( self, stock_code: str, start_date: str, end_date: str, count: int = 30 ) -> list[dict]: if not self._check_auth(): return [] await self._rate_limiter.acquire() headers = token_manager.get_auth_headers("FHKST03010200") params = { _FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code, "FID_INPUT_DATE_1": start_date, "FID_INPUT_DATE_2": end_date, "FID_PERIOD_DIV_CODE": "D", "FID_ADJ_PRC": "1", } url = f"{get_base_url()}{_CHART_URL}" resp = await self._client.get(url, headers=headers, params=params) resp.raise_for_status() data = resp.json() if data.get("rt_cd") != "0": logger.warning("차트 조회 실패: %s - %s", stock_code, data.get("msg1")) return [] result = [] for item in data.get("output2", [])[:count]: result.append({ "date": item.get("stck_bsop_date", ""), "open": int(item.get("stck_oprc", 0)), "high": int(item.get("stck_hgpr", 0)), "low": int(item.get("stck_lwpr", 0)), "close": int(item.get("stck_clpr", 0)), "volume": int(item.get("acml_vol", 0)), }) return result async def get_orderbook(self, stock_code: str) -> dict: if not self._check_auth(): return {} await self._rate_limiter.acquire() headers = token_manager.get_auth_headers("FHKST01010200") params = {_FID_COND_MRKT_DIV_CODE: "J", _FID_INPUT_ISCD: stock_code} url = f"{get_base_url()}{_ORDERBOOK_URL}" resp = await self._client.get(url, headers=headers, params=params) resp.raise_for_status() data = resp.json() if data.get("rt_cd") != "0": return {} output = data.get("output", [{}])[0] if data.get("output") else {} return { "stock_code": stock_code, "bid_prices": [ int(output.get(f"phsc_kprc_{i}", 0)) for i in range(1, 6) ], "ask_prices": [ int(output.get(f"sats_kprc_{i}", 0)) for i in range(1, 6) ], "bid_volumes": [ int(output.get(f"phsc_vola_{i}", 0)) for i in range(1, 6) ], "ask_volumes": [ int(output.get(f"sats_ac_vola_{i}", 0)) for i in range(1, 6) ], } async def close(self) -> None: await self._client.aclose() market_data_service = MarketDataService()