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>
162 lines
5.2 KiB
Python
162 lines
5.2 KiB
Python
"""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()
|