λ³Έ λ¬Έμλ ν€μμ¦κΆ REST APIλ₯Ό λννλ νλμ μΈ λΉλκΈ° νμ΄μ¬ λΌμ΄λΈλ¬λ¦¬μΈ kiwoom-rest-trade ν¨ν€μ§μ μμ€ν
μ€κ³μ ν΅μ¬ μ€κ³ κ²°μ μ λ€λ£Ήλλ€.
asyncioλ₯Ό κΈ°λ°μΌλ‘ μ€κ³ν©λλ€. λ¨, μ΄λ³΄ κ°λ°μλ₯Ό μν΄ λ΄λΆ λΉλκΈ° 루νλ₯Ό κ°μΈλ λκΈ°(Sync) μΈν°νμ΄μ€λ ν¨κ» μ 곡ν©λλ€.Pydantic v2λ₯Ό μ κ·Ήμ μΌλ‘ λμ
ν©λλ€.kiwoom_api_spec.json) κΈ°λ°μΌλ‘ λͺ¨λΈ λ° API λ©μλ μ½λλ₯Ό μλ μμ±(Codegen)νλ ꡬ쑰λ₯Ό μ·¨ν©λλ€.λΌμ΄λΈλ¬λ¦¬λ λ€μκ³Ό κ°μ΄ κ΄μ¬μ¬λ₯Ό λͺ ννκ² λΆλ¦¬νμ¬ κ΅¬μ±λ©λλ€.
kiwoom/
βββ __init__.py # ν¨ν€μ§ μ§μ
μ , ν΅ν© ν΄λΌμ΄μΈνΈ λ
ΈμΆ
βββ client.py # AsyncKiwoomClient λ° KiwoomClient(λκΈ°) μ μ
βββ auth.py # ν ν° λ°κΈ/κ°±μ λ° μλͺ
μΈμ¦ λͺ¨λ
βββ exceptions.py # ν€μ μλ¬ μ½λμ λ§μΆ μμΈ κ³μΈ΅ μ μ
βββ limiter.py # μ λ μ μ΄(Rate Limit) λ°μ½λ μ΄ν° λ° ν ν° λ²ν·
βββ websocket.py # μΉμμΌ(WebSocket) μ€μκ° μμΈ λ° μ²΄κ²° μ€νΈλ¦¬λ°
βββ models/ # Pydantic λ°μ΄ν° λͺ¨λΈ (Codegen λμ)
β βββ __init__.py
β βββ base.py # κ³΅ν΅ Pydantic λ² μ΄μ€ ν΄λμ€ μ μ
β βββ auth.py # OAuth κ΄λ ¨ μ€ν€λ§
β βββ domestic.py # κ΅λ΄μ£Όμ κ΄λ ¨ μ€ν€λ§ (μλμμ±)
β βββ overseas.py # ν΄μΈμ£Όμ κ΄λ ¨ μ€ν€λ§ (μλμμ±)
βββ api/ # REST API μλν¬μΈνΈ λ©μλ λͺ¨λ (Codegen λμ)
βββ __init__.py
βββ base.py # API μμ² μ μ‘ κ³΅ν΅ λ‘μ§
βββ auth.py # μΈμ¦ API νΈμΆλΆ
βββ domestic.py # κ΅λ΄μ£Όμ API νΈμΆλΆ (μλμμ±)
βββ overseas.py # ν΄μΈμ£Όμ API νΈμΆλΆ (μλμμ±)
μ¬μ©μ(Trader)κ° AsyncKiwoomClientλ₯Ό ν΅ν΄ κ΅λ΄ μ£Όμ νμ¬κ°λ₯Ό μ‘°ννκ³ μ²΄κ²° ν΅λ³΄λ₯Ό λ°λ μ 체μ μΈ νλ¦μ
λλ€.
graph TD
User([μ¬μ©μ λ§€λ§€ μ λ΅]) -->|1. λ©μλ νΈμΆ| Client[AsyncKiwoomClient]
Client -->|2. μ λ νλ νμΈ| Limiter[RateLimiter]
Client -->|3. μλ ν ν° κ²μ¦ & 첨λΆ| Auth[TokenManager]
Client -->|4. HTTP μμ²| HTTPX[httpx.AsyncClient]
HTTPX -->|5. JSON μλ΅| Parser[Pydantic Parser]
Parser -->|6. μλ¬ μ½λ λ°κ²¬ μ| Exc[ExceptionMapper]
Parser -->|7. μ±κ³΅ μ νμ
κ²μ¦| Model[Pydantic Models]
WS([μ€μκ° μΉμμΌ μ΄λ²€νΈ]) -->|8. λ°μ΄ν° μμ | WebSocketClient[KiwoomWebSocket]
WebSocketClient -->|9. μ¬μ©μ μ½λ°± ν¨μ νΈλ¦¬κ±°| User
Exc -.->|μμΈ λ°μ| User
Model -.->|κ²μ¦λ λ°μ΄ν° λ°ν| User
λΌμ΄λΈλ¬λ¦¬μ ν΅μ¬μ λΉλκΈ° ν΄λΌμ΄μΈνΈ(AsyncKiwoomClient)μ
λλ€. λ΄λΆ ν΅μ μλ httpx.AsyncClient 컀λ₯μ
νμ νμ©ν΄ ν¨μ¨μ κ·Ήλνν©λλ€.
λΉλκΈ°κ° μ΄μν μ¬μ©μλ₯Ό μν΄ λ€μκ³Ό κ°μ λκΈ°μ λνΌ(KiwoomClient)λ ν¨κ» μ§μν©λλ€.
# 1. λΉλκΈ°μ μ¬μ© (κΆμ₯)
import asyncio
from kiwoom import AsyncKiwoomClient
async def main():
client = AsyncKiwoomClient(appkey="...", secretkey="...")
balance = await client.domestic.get_balance(acnt_no="...")
print(balance)
asyncio.run(main())
# 2. λκΈ°μ μ¬μ© (λκΈ° λνΌ)
from kiwoom import KiwoomClient
client = KiwoomClient(appkey="...", secretkey="...")
balance = client.domestic.get_balance(acnt_no="...") # λ΄λΆμ μΌλ‘ asyncio.run λλ 루νλ₯Ό λλ € μ²λ¦¬
print(balance)
ν€μμ kiwoom_api_spec.json νμΌμλ κ° TR(μ: ka10001 - μ£ΌμκΈ°λ³Έμ 보μμ²)μ request body νλλͺ
, νμ
, νμ μ¬λΆ λ±μ΄ κ³ μ€λν λ΄κ²¨ μμ΅λλ€.
μ΄λ₯Ό νμ΄μ±νμ¬ νμ΄μ¬ μ½λλ₯Ό μλ μμ±ν©λλ€.
# [μμ] μλ μμ±λ models/domestic.py μ μΌλΆ
from pydantic import BaseModel, Field
from typing import Optional
class StockBasicInfoRequest(BaseModel):
fid_cond_mrkt_div_code: str = Field(..., description="μμ₯ꡬλΆμ½λ (J: μ£Όμ, W: ELW λ±)")
fid_input_iscd: str = Field(..., description="μ’
λͺ©μ½λ")
class StockBasicInfoResponse(BaseModel):
prdy_vrss: int = Field(..., description="μ μΌλλΉ")
stck_prpr: int = Field(..., description="μ£Όμνμ¬κ°")
# ... 45κ° νλ λ§€ν
ν€μ REST APIλ κ΅λ΄ μ£Όμ 1μ΄λΉ 5ν, λ―Έκ΅ μ£Όμ 1μ΄λΉ 10ν(νΌν¬νμ 3ν)μ μ격ν μ λ νλλ₯Ό κ°μ΅λλ€.
limiter.pyμμλ κ° μΉ΄ν
κ³ λ¦¬λ³λ‘ Semaphore λλ ν ν° λ²ν·(Token Bucket) ꡬ쑰μ λΉλκΈ° μλ μ νκΈ°λ₯Ό λ‘λλ€.
class RateLimiter:
def __init__(self, requests_per_second: float):
self.delay = 1.0 / requests_per_second
self.lock = asyncio.Lock()
self.last_called = 0.0
async def wait(self):
async with self.lock:
now = asyncio.get_event_loop().time()
elapsed = now - self.last_called
sleep_time = self.delay - elapsed
if sleep_time > 0:
await asyncio.sleep(sleep_time)
self.last_called = asyncio.get_event_loop().time()
APIλ₯Ό νΈμΆνλ λͺ¨λ μλν¬μΈνΈ λ©μλμ μ΄ λ°μ½λ μ΄ν°λ Limiterμ wait()λ₯Ό μ μ©νμ¬ νΈμΆ μ£ΌκΈ°κ° μ λ°νκ² κ°μ λλλ‘ μ²λ¦¬ν©λλ€.
kiwoom_error_codes.jsonμ κΈ°λ°νμ¬ μλ¬ μ½λλ₯Ό μ μ ν νμ΄μ¬ μ΅μ
μ
μΌλ‘ λ²μν©λλ€.
KiwoomError (μ΅μμ μμΈ)
KiwoomAuthError (8001~8200 λ²λ μΈμ¦/λ¨λ§ μ€ν¨ κ΄λ ¨ μλ¬)KiwoomRateLimitError (1700~1702 λ²λ μ λ μ΄κ³Ό κ΄λ ¨ μλ¬)KiwoomValidationError (1501~1517 λ²λ νλ/κ° μ€λ₯ κ΄λ ¨ μλ¬)KiwoomAPIError (κΈ°ν ν€μ μλ²μμ 리ν΄ν λΉμ¦λμ€ μλ¬)API μλ΅μ λ°μΌλ©΄ μλ΅ ν€λλ λ°λ λ΄μ μλ¬ μ 보λ₯Ό κ²μ¬νμ¬ μλμΌλ‘ ν΄λΉ μμΈ μΈμ€ν΄μ€λ₯Ό λ°μ(raise)μν΅λλ€.
μ€μκ° λ°μ΄ν°λ₯Ό μ‘μμ νκΈ° μν΄ μΉμμΌ μ°κ²°(kiwoom/websocket.py)μ λ©νν©λλ€.
websockets λΌμ΄λΈλ¬λ¦¬λ₯Ό μ΄μ©νμ¬ μ΄λ²€νΈ 루ν κΈ°λ°μΌλ‘ ꡬλλ©λλ€.subscribe)νκ³ , μ΄λ²€νΈ λ°μ μ μ€νν μ½λ°± ν¨μλ§ λ±λ‘ν΄ λλ©΄ λ°±κ·ΈλΌμ΄λ νμ€ν¬μμ μμ νκ² μ€νλ©λλ€.