fix: correct TR_ID, path, and params for fetch_market_rankings (#155)
Some checks failed
CI / test (pull_request) Has been cancelled
Some checks failed
CI / test (pull_request) Has been cancelled
Three bugs found by comparing against KIS official GitHub examples:
1. FID_COND_SCR_DIV_CODE: "20001" → "20171" (volume-rank screen code)
2. FID_TRGT_EXLS_CLS_CODE: "000000" (6-digit) → "0000000000" (10-digit)
3. fluctuation ranking:
- TR_ID: "FHPST01710100" (invalid) → "FHPST01700000"
- path: /quotations/volume-rank → /ranking/fluctuation
- params: volume-rank params → lowercase fluctuation-specific params
(fid_rank_sort_cls_code, fid_input_cnt_1, fid_prc_cls_code,
fid_rsfl_rate1, fid_rsfl_rate2, etc.)
Note: VTS (paper trading) does not return data from ranking APIs regardless
of parameter correctness — this is a KIS policy restriction, not a code bug.
These fixes ensure correct behavior when switching to a live account.
Tests: TestFetchMarketRankings (3 tests) added to test_broker.py
Closes #155
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -304,26 +304,46 @@ class KISBroker:
|
|||||||
await self._rate_limiter.acquire()
|
await self._rate_limiter.acquire()
|
||||||
session = self._get_session()
|
session = self._get_session()
|
||||||
|
|
||||||
# TR_ID for volume ranking
|
if ranking_type == "volume":
|
||||||
tr_id = "FHPST01710000" if ranking_type == "volume" else "FHPST01710100"
|
# 거래량순위: FHPST01710000 / /quotations/volume-rank
|
||||||
|
tr_id = "FHPST01710000"
|
||||||
|
url = f"{self._base_url}/uapi/domestic-stock/v1/quotations/volume-rank"
|
||||||
|
params: dict[str, str] = {
|
||||||
|
"FID_COND_MRKT_DIV_CODE": "J",
|
||||||
|
"FID_COND_SCR_DIV_CODE": "20171",
|
||||||
|
"FID_INPUT_ISCD": "0000",
|
||||||
|
"FID_DIV_CLS_CODE": "0",
|
||||||
|
"FID_BLNG_CLS_CODE": "0",
|
||||||
|
"FID_TRGT_CLS_CODE": "111111111",
|
||||||
|
"FID_TRGT_EXLS_CLS_CODE": "0000000000",
|
||||||
|
"FID_INPUT_PRICE_1": "0",
|
||||||
|
"FID_INPUT_PRICE_2": "0",
|
||||||
|
"FID_VOL_CNT": "0",
|
||||||
|
"FID_INPUT_DATE_1": "",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 등락률순위: FHPST01700000 / /ranking/fluctuation (소문자 파라미터)
|
||||||
|
tr_id = "FHPST01700000"
|
||||||
|
url = f"{self._base_url}/uapi/domestic-stock/v1/ranking/fluctuation"
|
||||||
|
params = {
|
||||||
|
"fid_cond_mrkt_div_code": "J",
|
||||||
|
"fid_cond_scr_div_code": "20170",
|
||||||
|
"fid_input_iscd": "0000",
|
||||||
|
"fid_rank_sort_cls_code": "0000",
|
||||||
|
"fid_input_cnt_1": str(limit),
|
||||||
|
"fid_prc_cls_code": "0",
|
||||||
|
"fid_input_price_1": "0",
|
||||||
|
"fid_input_price_2": "0",
|
||||||
|
"fid_vol_cnt": "0",
|
||||||
|
"fid_trgt_cls_code": "0",
|
||||||
|
"fid_trgt_exls_cls_code": "0",
|
||||||
|
"fid_div_cls_code": "0",
|
||||||
|
"fid_rsfl_rate1": "0",
|
||||||
|
"fid_rsfl_rate2": "0",
|
||||||
|
}
|
||||||
|
|
||||||
headers = await self._auth_headers(tr_id)
|
headers = await self._auth_headers(tr_id)
|
||||||
|
|
||||||
params = {
|
|
||||||
"FID_COND_MRKT_DIV_CODE": "J", # Stock/ETF/ETN
|
|
||||||
"FID_COND_SCR_DIV_CODE": "20001", # Volume surge
|
|
||||||
"FID_INPUT_ISCD": "0000", # All stocks
|
|
||||||
"FID_DIV_CLS_CODE": "0", # All types
|
|
||||||
"FID_BLNG_CLS_CODE": "0",
|
|
||||||
"FID_TRGT_CLS_CODE": "111111111",
|
|
||||||
"FID_TRGT_EXLS_CLS_CODE": "000000",
|
|
||||||
"FID_INPUT_PRICE_1": "0",
|
|
||||||
"FID_INPUT_PRICE_2": "0",
|
|
||||||
"FID_VOL_CNT": "0",
|
|
||||||
"FID_INPUT_DATE_1": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
url = f"{self._base_url}/uapi/domestic-stock/v1/quotations/volume-rank"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with session.get(url, headers=headers, params=params) as resp:
|
async with session.get(url, headers=headers, params=params) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -296,3 +296,82 @@ class TestHashKey:
|
|||||||
mock_acquire.assert_called_once()
|
mock_acquire.assert_called_once()
|
||||||
|
|
||||||
await broker.close()
|
await broker.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# fetch_market_rankings — TR_ID, path, params (issue #155)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_ranking_mock(items: list[dict]) -> AsyncMock:
|
||||||
|
"""Build a mock HTTP response returning ranking items."""
|
||||||
|
mock_resp = AsyncMock()
|
||||||
|
mock_resp.status = 200
|
||||||
|
mock_resp.json = AsyncMock(return_value={"output": items})
|
||||||
|
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||||
|
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||||
|
return mock_resp
|
||||||
|
|
||||||
|
|
||||||
|
class TestFetchMarketRankings:
|
||||||
|
"""Verify correct TR_ID, API path, and params per ranking_type (issue #155)."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def broker(self, settings) -> KISBroker:
|
||||||
|
b = KISBroker(settings)
|
||||||
|
b._access_token = "tok"
|
||||||
|
b._token_expires_at = float("inf")
|
||||||
|
b._rate_limiter.acquire = AsyncMock()
|
||||||
|
return b
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_volume_uses_correct_tr_id_and_path(self, broker: KISBroker) -> None:
|
||||||
|
mock_resp = _make_ranking_mock([])
|
||||||
|
with patch("aiohttp.ClientSession.get", return_value=mock_resp) as mock_get:
|
||||||
|
await broker.fetch_market_rankings(ranking_type="volume")
|
||||||
|
|
||||||
|
call_kwargs = mock_get.call_args
|
||||||
|
url = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1].get("url", "")
|
||||||
|
headers = call_kwargs[1].get("headers", {})
|
||||||
|
params = call_kwargs[1].get("params", {})
|
||||||
|
|
||||||
|
assert "volume-rank" in url
|
||||||
|
assert headers.get("tr_id") == "FHPST01710000"
|
||||||
|
assert params.get("FID_COND_SCR_DIV_CODE") == "20171"
|
||||||
|
assert params.get("FID_TRGT_EXLS_CLS_CODE") == "0000000000"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_fluctuation_uses_correct_tr_id_and_path(self, broker: KISBroker) -> None:
|
||||||
|
mock_resp = _make_ranking_mock([])
|
||||||
|
with patch("aiohttp.ClientSession.get", return_value=mock_resp) as mock_get:
|
||||||
|
await broker.fetch_market_rankings(ranking_type="fluctuation")
|
||||||
|
|
||||||
|
call_kwargs = mock_get.call_args
|
||||||
|
url = call_kwargs[0][0] if call_kwargs[0] else call_kwargs[1].get("url", "")
|
||||||
|
headers = call_kwargs[1].get("headers", {})
|
||||||
|
params = call_kwargs[1].get("params", {})
|
||||||
|
|
||||||
|
assert "ranking/fluctuation" in url
|
||||||
|
assert headers.get("tr_id") == "FHPST01700000"
|
||||||
|
assert params.get("fid_cond_scr_div_code") == "20170"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_volume_returns_parsed_rows(self, broker: KISBroker) -> None:
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"mksc_shrn_iscd": "005930",
|
||||||
|
"hts_kor_isnm": "삼성전자",
|
||||||
|
"stck_prpr": "75000",
|
||||||
|
"acml_vol": "10000000",
|
||||||
|
"prdy_ctrt": "2.5",
|
||||||
|
"vol_inrt": "150",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
mock_resp = _make_ranking_mock(items)
|
||||||
|
with patch("aiohttp.ClientSession.get", return_value=mock_resp):
|
||||||
|
result = await broker.fetch_market_rankings(ranking_type="volume")
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0]["stock_code"] == "005930"
|
||||||
|
assert result[0]["price"] == 75000.0
|
||||||
|
assert result[0]["change_rate"] == 2.5
|
||||||
|
|||||||
Reference in New Issue
Block a user