feat(signal_v2-phase3a): kis_client REST + 4 integration tests
KISClient: 분봉 (FHKST03010200) + 호가 (FHKST01010200) async REST. V1 토큰 파일 (signal_v1/data/kis_token.json) read-only 공유, mtime 캐시. 초당 2회 throttle. exponential retry (max 3, 1s/2s/4s). 4 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
128
signal_v2/tests/test_kis_client.py
Normal file
128
signal_v2/tests/test_kis_client.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user