feat(signal_v2-phase3b): kis_client.get_daily_ohlcv (60 daily bars)
TR_ID FHKST03010100 (수정주가 일봉). KIS returns descending; client reverses to ascending and trims to last N days. 1 new test, 34 total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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:]
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user