Files
stockautomtion/app/services/market_data.py
gerd 182c2b38e6 feat: config.yaml 기반 설정 마이그레이션 및 대시보드/종목조회 기능 추가
- config.yaml에서 직접 KIS 설정 관리 (BaseSettings 제거)
- 대시보드 KIS 계좌 잔고 실시간 연동, 로컬 DB 폴백
- 종목 조회 페이지: 시세/호가/일봉차트 (canvas) 구현
- 호가 REST API 엔드포인트 (/api/stocks/{code}/orderbook) 추가
- 토큰 캐시(.auth_cache.json) 저장/복원, 1분 재시도 제한
- KIS WebSocket 자동 연결 + 재연결 로직
- httpx 타임아웃 10초→30초, 라우터 타임아웃 핸들링 (504)
- rate limit: requests_per_second 2.0 (모의투자 초당 2건)
- 사이드바 3메뉴: 실거래 / 과거기록 / 종목조회
2026-07-17 15:25:34 +09:00

138 lines
4.8 KiB
Python

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()