diff --git a/signal_v2/kis_client.py b/signal_v2/kis_client.py index 1f5ec73..5492360 100644 --- a/signal_v2/kis_client.py +++ b/signal_v2/kis_client.py @@ -4,7 +4,7 @@ import asyncio import json import logging import time -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from zoneinfo import ZoneInfo @@ -153,3 +153,41 @@ class KISClient: "current_price": current_price, "as_of": datetime.now(KST).isoformat(), } + + async def get_daily_ohlcv(self, ticker: str, days: int = 60) -> list[dict]: + """KRX 일봉 OHLCV (TR_ID FHKST03010100). + + Returns: [{"datetime", "open", "high", "low", "close", "volume"}, ...] + 시간 오름차순. + """ + path = "/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice" + today = datetime.now(KST).strftime("%Y%m%d") + start_date = (datetime.now(KST) - timedelta(days=days * 2)).strftime("%Y%m%d") + params = { + "FID_COND_MRKT_DIV_CODE": "J", + "FID_INPUT_ISCD": ticker, + "FID_INPUT_DATE_1": start_date, + "FID_INPUT_DATE_2": today, + "FID_PERIOD_DIV_CODE": "D", + "FID_ORG_ADJ_PRC": "1", + } + raw = await self._request_with_retry( + "GET", path, tr_id="FHKST03010100", params=params, + ) + output2 = raw.get("output2", []) + bars = [] + for row in output2: + try: + date = row["stck_bsop_date"] + bars.append({ + "datetime": f"{date[:4]}-{date[4:6]}-{date[6:]}", + "open": int(row["stck_oprc"]), + "high": int(row["stck_hgpr"]), + "low": int(row["stck_lwpr"]), + "close": int(row["stck_clpr"]), + "volume": int(row["acml_vol"]), + }) + except (KeyError, ValueError): + continue + bars.reverse() + return bars[-days:] diff --git a/signal_v2/tests/test_kis_client.py b/signal_v2/tests/test_kis_client.py index 952812f..52b4398 100644 --- a/signal_v2/tests/test_kis_client.py +++ b/signal_v2/tests/test_kis_client.py @@ -126,3 +126,36 @@ async def test_get_asking_price_computes_bid_ratio(kis_client_factory): assert "as_of" in data finally: await client.close() + + +@respx.mock +async def test_get_daily_ohlcv_returns_60_bars(kis_client_factory): + """KIS daily endpoint returns 60 ascending bars after parsing.""" + # Build 60 KIS-format daily bars (descending dates as KIS does) + sample_output2 = [] + for i in range(60): + # Generate a fake date 60 days ago, descending + day = 60 - i + sample_output2.append({ + "stck_bsop_date": f"2026{(((day-1)//30)+1):02d}{(((day-1)%30)+1):02d}", + "stck_oprc": "78000", "stck_hgpr": "78500", + "stck_lwpr": "77800", "stck_clpr": str(78000 + i), + "acml_vol": "12345", + }) + + respx.get( + "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice" + ).mock(return_value=httpx.Response(200, json={"output2": sample_output2})) + + client = kis_client_factory() + try: + bars = await client.get_daily_ohlcv("005930", days=60) + # KIS returns descending; client reverses to ascending + assert len(bars) == 60 + # Ascending order: first item has smaller datetime than last + assert bars[0]["datetime"] < bars[-1]["datetime"] + assert isinstance(bars[0]["open"], int) + assert isinstance(bars[0]["close"], int) + assert "datetime" in bars[0] + finally: + await client.close()