"""Tests for KISClient (REST).""" import json from pathlib import Path import httpx import pytest import respx from signal_v2.kis_client import KISClient @pytest.fixture def fake_v1_token(tmp_path): """V1 토큰 파일 fixture.""" token_file = tmp_path / "kis_token.json" token_file.write_text(json.dumps({ "access_token": "test-kis-token-abc123", "token_expired": "2099-12-31 23:59:59", })) return token_file @pytest.fixture def kis_client_factory(fake_v1_token): def _make(): return KISClient( app_key="test-app-key", app_secret="test-app-secret", account="50000000-01", is_virtual=True, v1_token_path=fake_v1_token, ) return _make @respx.mock async def test_get_minute_ohlcv_normal_returns_30_bars(kis_client_factory): """정상 200 → 30개 분봉 list 반환.""" sample_output2 = [ { "stck_bsop_date": "20260518", "stck_cntg_hour": f"09{m:02d}00", "stck_oprc": "78000", "stck_hgpr": "78500", "stck_lwpr": "77800", "stck_prpr": "78300", "cntg_vol": "12345", } for m in range(30) # 9:00-9:29 = 30 bars ] respx.get( "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice" ).mock( return_value=httpx.Response(200, json={"output2": sample_output2}) ) client = kis_client_factory() try: bars = await client.get_minute_ohlcv("005930") assert len(bars) == 30 assert bars[0]["close"] == 78300 assert "datetime" in bars[0] finally: await client.close() @respx.mock async def test_get_minute_ohlcv_429_retry_then_success(kis_client_factory, monkeypatch): """429 → exponential backoff → 200.""" sleep_calls = [] async def fake_sleep(s): sleep_calls.append(s) monkeypatch.setattr("asyncio.sleep", fake_sleep) respx.get( "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice" ).mock(side_effect=[ httpx.Response(429, text="rate limit"), httpx.Response(200, json={"output2": []}), ]) client = kis_client_factory() try: result = await client.get_minute_ohlcv("005930") assert result == [] assert 1 in sleep_calls finally: await client.close() @respx.mock async def test_get_minute_ohlcv_uses_v1_token(kis_client_factory, fake_v1_token): """KIS 호출 헤더에 V1 토큰 파일의 access_token 사용.""" route = respx.get( "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-time-itemchartprice" ).mock(return_value=httpx.Response(200, json={"output2": []})) client = kis_client_factory() try: await client.get_minute_ohlcv("005930") assert route.called req = route.calls.last.request # check authorization header contains the V1 token auth = req.headers.get("authorization", "") assert "test-kis-token-abc123" in auth finally: await client.close() @respx.mock async def test_get_asking_price_computes_bid_ratio(kis_client_factory): """호가 응답 → bid_total/(bid+ask) bid_ratio 계산.""" respx.get( "https://openapivts.koreainvestment.com:29443/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn" ).mock(return_value=httpx.Response(200, json={ "output1": { "total_bidp_rsqn": "600", "total_askp_rsqn": "400", "stck_prpr": "78500", } })) client = kis_client_factory() try: data = await client.get_asking_price("005930") assert data["bid_total"] == 600 assert data["ask_total"] == 400 assert abs(data["bid_ratio"] - 0.6) < 1e-9 assert data["current_price"] == 78500 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()