kiwoomRest 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kiwoomRest/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ __version__ = "0.0.1"
2
+
3
+ from .rest_api import (
4
+ KwRestApi as KwRestApi,
5
+ ResponseData as ResponseData,
6
+ )
kiwoomRest/rest_api.py ADDED
@@ -0,0 +1,445 @@
1
+ ############################################################################
2
+ # 키움증권 REST API 비동기 클라이언트
3
+ # 키움증권 가이드 참고: https://openapi.kiwoom.com/guide/apiguide
4
+ ############################################################################
5
+
6
+ import asyncio
7
+ import aiohttp
8
+ import json
9
+ import time
10
+
11
+ BASE_URL_REAL = "https://api.kiwoom.com"
12
+ BASE_URL_SIMUL = "https://mockapi.kiwoom.com"
13
+ WSS_URL_REAL = "wss://api.kiwoom.com:10000/api/dostk/websocket"
14
+ WSS_URL_SIMULATION = "wss://mockapi.kiwoom.com:10000/api/dostk/websocket"
15
+
16
+ from .tr_code_to_path import tr_code_to_path
17
+
18
+
19
+ class ResponseData:
20
+ def __init__(self,
21
+ api_id: str
22
+ ) -> None:
23
+ self.api_id = api_id
24
+ self.path = str()
25
+ self.return_code = int(-1)
26
+ self.return_msg = str()
27
+ self.cont_yn = str()
28
+ self.next_key = str()
29
+ self.body = {}
30
+ # additional variables
31
+ self.inputs = dict()
32
+ self.in_cont_yn = str()
33
+ self.in_next_key = str()
34
+ self.request_time = 0.0
35
+ self.elapsed_ms = 0.0
36
+
37
+ def __str__(self) -> str:
38
+ return f"ResponseData(api_id={self.api_id}, path={self.path}, return_code={self.return_code}, return_msg={self.return_msg}, cont_yn={self.cont_yn}, next_key={self.next_key}, body={self.body})"
39
+
40
+ class KwRestApi(object):
41
+
42
+ class _event_signal:
43
+ class _slot:
44
+ def __init__(self, func):
45
+ self.func = func
46
+ self.is_coroutine = asyncio.iscoroutinefunction(func)
47
+ def __eq__(self, other):
48
+ return self.func == other
49
+ def __init__(self):
50
+ self.__slots: list[KwRestApi._event_signal._slot] = []
51
+ def connect(self, func):
52
+ # check callable
53
+ if not hasattr(func, "__call__") :
54
+ raise ValueError("slot must be callable")
55
+ # check exist
56
+ exist_slot = next((s for s in self.__slots if s.func == func), None)
57
+ if exist_slot:
58
+ return
59
+ # add slot
60
+ self.__slots.append(self._slot(func))
61
+ def disconnect(self, func):
62
+ exist_slot = next((s for s in self.__slots if s.func == func), None)
63
+ if exist_slot:
64
+ self.__slots.remove(exist_slot)
65
+ def disconnect_all(self):
66
+ self.__slots.clear()
67
+ async def emit_signal(self, *args):
68
+ for slot in self.__slots:
69
+ if slot.is_coroutine:
70
+ await slot.func(*args)
71
+ else:
72
+ slot.func(*args)
73
+
74
+ class _asyncNode:
75
+ def __init__(self, hashid):
76
+ self.__event = asyncio.Event()
77
+ self.hashid = hashid
78
+ self.jsondata = {}
79
+
80
+ def set(self):
81
+ self.__event.set()
82
+
83
+ async def wait(self, timeout=None):
84
+ if timeout is None:
85
+ await self.__event.wait()
86
+ return True
87
+ try:
88
+ await asyncio.wait_for(self.__event.wait(), timeout)
89
+ return True
90
+ except asyncio.TimeoutError:
91
+ return False
92
+
93
+ def __init__(self):
94
+ super().__init__()
95
+
96
+ self._access_token = ""
97
+ self._http = None
98
+ self._websocket = None
99
+ self._base_url:str = ""
100
+ self._connected:bool = False
101
+ self._is_simulation:bool = False
102
+ self._last_message:str = ""
103
+ self._timeout = 5
104
+ self._last_response_value = None
105
+
106
+ # 이벤트 핸들러
107
+ self._on_message = self._event_signal()
108
+ self._on_realtime = self._event_signal()
109
+
110
+ # 웹소켓 비동기 요청 노드
111
+ self._ws_asyncnode:KwRestApi._asyncNode | None = None
112
+
113
+ @property
114
+ def connected(self) -> bool:
115
+ """로그인 연결상태.
116
+ True: 연결됨, False: 연결안됨
117
+
118
+ A readonly property.
119
+ """
120
+ return self._connected
121
+
122
+ @property
123
+ def is_simulation(self) -> bool:
124
+ """서버모드
125
+ True: 모의투자, False: 실투자
126
+
127
+ A readonly property.
128
+ """
129
+ return self._is_simulation
130
+
131
+ @property
132
+ def last_message(self) -> str:
133
+ """last error message.
134
+
135
+ A readonly property.
136
+ """
137
+ return self._last_message
138
+
139
+ @property
140
+ def on_message(self) :
141
+ """메시지 수신 이벤트 핸들러, 웹소켓 오류/끊김 시 발생
142
+ on_message(msg:str)
143
+ """
144
+ return self._on_message
145
+
146
+ @property
147
+ def on_realtime(self) :
148
+ """실시간 데이터 수신 이벤트 핸들러
149
+ on_realtime(realdata:dict)
150
+ """
151
+ return self._on_realtime
152
+
153
+ @property
154
+ def last_response(self) -> ResponseData | None:
155
+ """마지막 요청 응답값
156
+ ResponseValue: 응답값
157
+ """
158
+ return self._last_response_value
159
+
160
+ @property
161
+ def access_token(self) -> str:
162
+ """access token
163
+ access_token: access token
164
+ """
165
+ return self._access_token
166
+
167
+ @property
168
+ def timeout(self) -> int:
169
+ """timeout
170
+ timeout: timeout
171
+ """
172
+ return self._timeout
173
+
174
+ @timeout.setter
175
+ def timeout(self, value: int) -> None:
176
+ """timeout setter
177
+ value: timeout
178
+ """
179
+ if value <= 0:
180
+ raise ValueError("timeout must be greater than 0")
181
+ self._timeout = value
182
+
183
+ async def close(self) -> None:
184
+ """연결 종료"""
185
+ self._connected = False
186
+ if self._websocket and not self._websocket.closed:
187
+ await self._websocket.close()
188
+ if self._http and not self._http.closed:
189
+ await self._http.close()
190
+
191
+ async def login(self, appkey:str, secretkey:str, is_simulation:bool = False) -> bool:
192
+ '''
193
+ 로그인 요청
194
+ appkey: 앱키
195
+ secretkey: 앱시크릿키
196
+ is_simulation: 모의투자 로그인 시 True, 실투자 로그인 시 False (default)
197
+ return: True: 성공, False: 실패, 실패시 last_message에 실패사유가 저장됨
198
+ '''
199
+ if self._connected :
200
+ self._last_message = "aleady connected"
201
+ return True
202
+
203
+ if appkey == "" or secretkey == "":
204
+ self._last_message = "appkey or secretkey is empty"
205
+ return False
206
+
207
+ self._is_simulation = is_simulation
208
+ self._base_url = BASE_URL_SIMUL if self._is_simulation else BASE_URL_REAL
209
+
210
+ timeout = aiohttp.ClientTimeout(total=10) # 10초 타임아웃
211
+ self._http = aiohttp.ClientSession(timeout=timeout)
212
+
213
+ # 토큰 가져오기
214
+ self._connected = True
215
+ inputs = {'grant_type': 'client_credentials', 'appkey': appkey, 'secretkey': secretkey}
216
+ response = await self.request("oauth2", inputs, path="/oauth2/token")
217
+ if response.return_code == 0:
218
+ # 토큰발급 성공
219
+ token = response.body["token"]
220
+ self._http.headers["Authorization"] = f"Bearer {token}"
221
+ self._access_token = token
222
+ # 웹소켓 연결/인증요청
223
+ try:
224
+ websocket = await self._http.ws_connect(WSS_URL_SIMULATION if self._is_simulation else WSS_URL_REAL)
225
+ if not websocket.closed:
226
+ self._websocket = websocket
227
+ asyncio.create_task(self._websocket_listen())
228
+ response = await self.realtime({"trnm":"LOGIN", "token" : self._access_token})
229
+ if response.return_code == 0:
230
+ # 인증성공
231
+ self._last_message = "Login success"
232
+ return True
233
+ else:
234
+ self._last_message = response.return_msg
235
+ else:
236
+ self._last_message = "websocket connection failed."
237
+ except Exception as e:
238
+ self._last_message = str(e)
239
+ else:
240
+ self._last_message = response.return_msg
241
+ await self.close()
242
+ return False
243
+
244
+ async def request(self
245
+ , api_id:str
246
+ , indatas:dict
247
+ ,*
248
+ , cont_yn:str="N"
249
+ , next_key:str=""
250
+ , path:str=None
251
+ ) -> ResponseData:
252
+ '''
253
+ TR/실시간 요청
254
+ api_id: TR코드(api-id)
255
+ indatas: 입력데이터(dictionary)
256
+ cont_yn: TR연속요청여부
257
+ next_key: TR연속요청키
258
+ path: 요청경로(생략시 자동으로 결정)
259
+ return: ResponseValue, 성공시 return_code = 0, 그외 실패, 실패시 return_msg에 실패사유가 저장됨
260
+
261
+ example:
262
+ # 삼성전자 종목정보 조회
263
+ inputs = {
264
+ "stk_cd" : "005930"
265
+ }
266
+ response = await api.request("ka10001", inputs)
267
+ print(response)
268
+
269
+ # 조건검색식 목록 조회
270
+ inputs = {
271
+ "trnm" : "CNSRLST"
272
+ }
273
+ response = await api.request("ka10171", inputs)
274
+ print(response)
275
+ '''
276
+ result = ResponseData(api_id)
277
+ result.in_cont_yn = cont_yn
278
+ result.in_next_key = next_key
279
+ result.inputs = indatas
280
+
281
+ if not path:
282
+ path = tr_code_to_path.get(api_id, None)
283
+ if not path:
284
+ self._last_message = "Not supported tr_code-path"
285
+ result.return_msg = self._last_message
286
+ return result
287
+
288
+ result.path = path
289
+
290
+ self._last_response_value = result
291
+ if not self._connected:
292
+ self._last_message = "Not connected"
293
+ result.return_msg = self._last_message
294
+ return result
295
+
296
+ # check if websocket request
297
+ if path.endswith("/websocket"):
298
+ result.request_time = time.time()
299
+ start_time = time.perf_counter_ns()
300
+ ws_result = await self._ws_request(indatas)
301
+ result.elapsed_ms = (time.perf_counter_ns() - start_time) / 1000000
302
+ if ws_result != None:
303
+ result.return_code = ws_result.get("return_code", -1)
304
+ result.return_msg = ws_result.get("return_msg", "")
305
+ result.body = ws_result
306
+ return result
307
+ else:
308
+ result.return_code = -1
309
+ result.return_msg = self._last_message
310
+ return result
311
+
312
+ headers = dict()
313
+ headers["api-id"] = api_id
314
+ headers["cont-yn"] = cont_yn
315
+ headers["next-key"] = next_key
316
+
317
+ try:
318
+ result.request_time = time.time()
319
+ start_time = time.perf_counter_ns()
320
+ response = await self._http.post(self._base_url + path, headers=headers, json=indatas)
321
+ response_data:dict = await response.json()
322
+ result.elapsed_ms = (time.perf_counter_ns() - start_time) / 1000000
323
+ if response.status != 200:
324
+ self._last_message = f"Request failed. {response.status}"
325
+ result.return_msg = self._last_message
326
+ return result
327
+ result.cont_yn = response.headers.get("cont-yn", "N")
328
+ result.next_key = response.headers.get("next-key", "")
329
+ result.body = response_data
330
+ result.return_code = result.body.get("return_code", -1)
331
+ self._last_message = result.body.get("return_msg", "")
332
+ result.return_msg = self._last_message
333
+ except Exception as e:
334
+ self._last_message = str(e)
335
+ result.return_msg = self._last_message
336
+
337
+ return result
338
+
339
+ def realtime(self, indatas:dict):
340
+ '''
341
+ # ex: 삼성전자, SK하이닉스 실시간 체결 시세 등록
342
+ inputs = {
343
+ "trnm" : "REG",
344
+ "grp_no" : "1",
345
+ "refresh" : "1",
346
+ "data" : [{
347
+ " item" : [ "005930", "000660" ],
348
+ " type" : [ "0B" ] # 우선호가시세 까지 함께 등록할 경우 ["0B", "0C" ]로 설정
349
+ }]
350
+ }
351
+ response = await api.realtime(inputs)
352
+ print(response)
353
+
354
+ # 실시간 시세 해제 시 "REG" -> "REMOVE" 로 변경
355
+ inputs["trnm"] = "REMOVE"
356
+ response = await api.realtime(inputs)
357
+ '''
358
+ return self.request("realtime", indatas, path="/api/dostk/websocket", cont_yn="")
359
+
360
+ async def ws_sendmessage(self, text:str) -> bool:
361
+ '''
362
+ 웹소켓 메시지 전송
363
+ text: 전송할 메시지
364
+ return: True: 성공, False: 실패, 실패시 last_message에 실패사유가 저장됨
365
+ '''
366
+ if not self._connected or self._websocket.closed:
367
+ self._last_message = "Not connected"
368
+ return False
369
+ await self._websocket.send_str(text)
370
+ return True
371
+
372
+ async def _ws_request(self, indatas:dict) -> dict | None:
373
+ # check connection
374
+ if not self._connected:
375
+ self._last_message = "Not connected"
376
+ return None
377
+
378
+ # check async node
379
+ if self._ws_asyncnode != None:
380
+ # already requested
381
+ self._last_message = "Already requested"
382
+ return None
383
+
384
+ trnm = indatas.get("trnm", None)
385
+ if trnm == None:
386
+ self._last_message = "trnm is not define."
387
+ return None
388
+
389
+ # create async node
390
+ node = KwRestApi._asyncNode(trnm)
391
+ self._ws_asyncnode = node
392
+
393
+ # send message
394
+ try:
395
+ await self._websocket.send_json(indatas)
396
+ # wait for response
397
+ if not await self._ws_asyncnode.wait(self.timeout):
398
+ # timeout
399
+ self._last_message = "Timeout"
400
+ self._ws_asyncnode = None
401
+ return None
402
+ except Exception as e:
403
+ self._last_message = f"websocket request error. {e}"
404
+ self._ws_asyncnode = None
405
+ return None
406
+
407
+ # set response and return
408
+ self._ws_asyncnode = None
409
+ return node.jsondata
410
+
411
+ async def _websocket_listen(self):
412
+ async for msg in self._websocket:
413
+ if msg.type == aiohttp.WSMsgType.TEXT:
414
+ json_text = msg.data
415
+ try:
416
+ jsondata = json.loads(json_text)
417
+ trnm = jsondata.get("trnm", None)
418
+ if trnm != None:
419
+ # proc PING
420
+ if trnm == "PING":
421
+ await self._websocket.send_str(json_text)
422
+ continue
423
+ # check async node
424
+ if self._ws_asyncnode != None and self._ws_asyncnode.hashid == trnm:
425
+ # setting jsondata, set event
426
+ self._ws_asyncnode.jsondata = jsondata
427
+ self._ws_asyncnode.set()
428
+ continue
429
+ # raise event
430
+ await self._on_realtime.emit_signal(jsondata)
431
+ except Exception as e:
432
+ self._last_message = f"websocket exception. {e}"
433
+ await self._on_message.emit_signal(self._last_message)
434
+ continue
435
+
436
+ elif msg.type == aiohttp.WSMsgType.CLOSED:
437
+ break
438
+
439
+ elif msg.type == aiohttp.WSMsgType.ERROR:
440
+ self._last_message = f"websocket error. {msg}"
441
+ await self._on_message.emit_signal(self._last_message)
442
+
443
+ if self._connected:
444
+ self._last_message = f"websocket closed."
445
+ await self._on_message.emit_signal(self._last_message)
@@ -0,0 +1,205 @@
1
+ tr_code_to_path:dict = {
2
+
3
+ ## 국내주식
4
+
5
+ # 종목정보
6
+ "ka10001" : "/api/dostk/stkinfo",
7
+ "ka10002" : "/api/dostk/stkinfo",
8
+ "ka10003" : "/api/dostk/stkinfo",
9
+ "ka10013" : "/api/dostk/stkinfo",
10
+ "ka10015" : "/api/dostk/stkinfo",
11
+ "ka10016" : "/api/dostk/stkinfo",
12
+ "ka10017" : "/api/dostk/stkinfo",
13
+ "ka10018" : "/api/dostk/stkinfo",
14
+ "ka10019" : "/api/dostk/stkinfo",
15
+ "ka10024" : "/api/dostk/stkinfo",
16
+ "ka10025" : "/api/dostk/stkinfo",
17
+ "ka10026" : "/api/dostk/stkinfo",
18
+ "ka10028" : "/api/dostk/stkinfo",
19
+ "ka10043" : "/api/dostk/stkinfo",
20
+ "ka10052" : "/api/dostk/stkinfo",
21
+ "ka10054" : "/api/dostk/stkinfo",
22
+ "ka10055" : "/api/dostk/stkinfo",
23
+ "ka10058" : "/api/dostk/stkinfo",
24
+ "ka10059" : "/api/dostk/stkinfo",
25
+ "ka10061" : "/api/dostk/stkinfo",
26
+ "ka10084" : "/api/dostk/stkinfo",
27
+ "ka10095" : "/api/dostk/stkinfo",
28
+ "ka10099" : "/api/dostk/stkinfo",
29
+ "ka10100" : "/api/dostk/stkinfo",
30
+ "ka10101" : "/api/dostk/stkinfo",
31
+ "ka10102" : "/api/dostk/stkinfo",
32
+ "ka90003" : "/api/dostk/stkinfo",
33
+ "ka90004" : "/api/dostk/stkinfo",
34
+ "ka90012" : "/api/dostk/stkinfo",
35
+
36
+ # 시세
37
+ "ka10004" : "/api/dostk/mrkcond",
38
+ "ka10005" : "/api/dostk/mrkcond",
39
+ "ka10006" : "/api/dostk/mrkcond",
40
+ "ka10007" : "/api/dostk/mrkcond",
41
+ "ka10011" : "/api/dostk/mrkcond",
42
+ "ka10044" : "/api/dostk/mrkcond",
43
+ "ka10045" : "/api/dostk/mrkcond",
44
+ "ka10046" : "/api/dostk/mrkcond",
45
+ "ka10047" : "/api/dostk/mrkcond",
46
+ "ka10063" : "/api/dostk/mrkcond",
47
+ "ka10066" : "/api/dostk/mrkcond",
48
+ "ka10078" : "/api/dostk/mrkcond",
49
+ "ka10086" : "/api/dostk/mrkcond",
50
+ "ka10087" : "/api/dostk/mrkcond",
51
+ "ka90005" : "/api/dostk/mrkcond",
52
+ "ka90006" : "/api/dostk/mrkcond",
53
+ "ka90007" : "/api/dostk/mrkcond",
54
+ "ka90008" : "/api/dostk/mrkcond",
55
+ "ka90010" : "/api/dostk/mrkcond",
56
+ "ka90013" : "/api/dostk/mrkcond",
57
+
58
+ # 기관/외국인
59
+ "ka10008" : "/api/dostk/frgnistt",
60
+ "ka10009" : "/api/dostk/frgnistt",
61
+ "ka10131" : "/api/dostk/frgnistt",
62
+
63
+ # 업종
64
+ "ka10010" : "/api/dostk/sect",
65
+ "ka10051" : "/api/dostk/sect",
66
+ "ka20001" : "/api/dostk/sect",
67
+ "ka20002" : "/api/dostk/sect",
68
+ "ka20003" : "/api/dostk/sect",
69
+ "ka20009" : "/api/dostk/sect",
70
+
71
+ # 순위정보
72
+ "ka10020" : "/api/dostk/rkinfo",
73
+ "ka10021" : "/api/dostk/rkinfo",
74
+ "ka10022" : "/api/dostk/rkinfo",
75
+ "ka10023" : "/api/dostk/rkinfo",
76
+ "ka10027" : "/api/dostk/rkinfo",
77
+ "ka10029" : "/api/dostk/rkinfo",
78
+ "ka10030" : "/api/dostk/rkinfo",
79
+ "ka10031" : "/api/dostk/rkinfo",
80
+ "ka10032" : "/api/dostk/rkinfo",
81
+ "ka10033" : "/api/dostk/rkinfo",
82
+ "ka10034" : "/api/dostk/rkinfo",
83
+ "ka10035" : "/api/dostk/rkinfo",
84
+ "ka10036" : "/api/dostk/rkinfo",
85
+ "ka10037" : "/api/dostk/rkinfo",
86
+ "ka10038" : "/api/dostk/rkinfo",
87
+ "ka10039" : "/api/dostk/rkinfo",
88
+ "ka10040" : "/api/dostk/rkinfo",
89
+ "ka10042" : "/api/dostk/rkinfo",
90
+ "ka10053" : "/api/dostk/rkinfo",
91
+ "ka10062" : "/api/dostk/rkinfo",
92
+ "ka10065" : "/api/dostk/rkinfo",
93
+ "ka10069" : "/api/dostk/rkinfo",
94
+ "ka10098" : "/api/dostk/rkinfo",
95
+ "ka90009" : "/api/dostk/rkinfo",
96
+
97
+ # ELW
98
+ "ka10048" : "/api/dostk/elw",
99
+ "ka10050" : "/api/dostk/elw",
100
+ "ka30001" : "/api/dostk/elw",
101
+ "ka30002" : "/api/dostk/elw",
102
+ "ka30003" : "/api/dostk/elw",
103
+ "ka30004" : "/api/dostk/elw",
104
+ "ka30005" : "/api/dostk/elw",
105
+ "ka30009" : "/api/dostk/elw",
106
+ "ka30010" : "/api/dostk/elw",
107
+ "ka30011" : "/api/dostk/elw",
108
+ "ka30012" : "/api/dostk/elw",
109
+
110
+ # 차트
111
+ "ka10060" : "/api/dostk/chart",
112
+ "ka10064" : "/api/dostk/chart",
113
+ "ka10079" : "/api/dostk/chart",
114
+ "ka10080" : "/api/dostk/chart",
115
+ "ka10081" : "/api/dostk/chart",
116
+ "ka10082" : "/api/dostk/chart",
117
+ "ka10083" : "/api/dostk/chart",
118
+ "ka10094" : "/api/dostk/chart",
119
+ "ka20004" : "/api/dostk/chart",
120
+ "ka20005" : "/api/dostk/chart",
121
+ "ka20006" : "/api/dostk/chart",
122
+ "ka20007" : "/api/dostk/chart",
123
+ "ka20008" : "/api/dostk/chart",
124
+ "ka20019" : "/api/dostk/chart",
125
+
126
+ # 계좌
127
+ "ka10072" : "/api/dostk/acnt",
128
+ "ka10073" : "/api/dostk/acnt",
129
+ "ka10074" : "/api/dostk/acnt",
130
+ "ka10075" : "/api/dostk/acnt",
131
+ "ka10076" : "/api/dostk/acnt",
132
+ "ka10077" : "/api/dostk/acnt",
133
+ "ka10085" : "/api/dostk/acnt",
134
+ "ka10088" : "/api/dostk/acnt",
135
+ "ka10170" : "/api/dostk/acnt",
136
+ "kt00001" : "/api/dostk/acnt",
137
+ "kt00002" : "/api/dostk/acnt",
138
+ "kt00003" : "/api/dostk/acnt",
139
+ "kt00004" : "/api/dostk/acnt",
140
+ "kt00005" : "/api/dostk/acnt",
141
+ "kt00007" : "/api/dostk/acnt",
142
+ "kt00008" : "/api/dostk/acnt",
143
+ "kt00009" : "/api/dostk/acnt",
144
+ "kt00010" : "/api/dostk/acnt",
145
+ "kt00011" : "/api/dostk/acnt",
146
+ "kt00012" : "/api/dostk/acnt",
147
+ "kt00013" : "/api/dostk/acnt",
148
+ "kt00015" : "/api/dostk/acnt",
149
+ "kt00016" : "/api/dostk/acnt",
150
+ "kt00017" : "/api/dostk/acnt",
151
+ "kt00018" : "/api/dostk/acnt",
152
+
153
+ # ETF
154
+ "ka40001" : "/api/dostk/etf",
155
+ "ka40002" : "/api/dostk/etf",
156
+ "ka40003" : "/api/dostk/etf",
157
+ "ka40004" : "/api/dostk/etf",
158
+ "ka40006" : "/api/dostk/etf",
159
+ "ka40007" : "/api/dostk/etf",
160
+ "ka40008" : "/api/dostk/etf",
161
+ "ka40009" : "/api/dostk/etf",
162
+ "ka40010" : "/api/dostk/etf",
163
+
164
+ # 테마
165
+ "ka90001" : "/api/dostk/thme",
166
+ "ka90002" : "/api/dostk/thme",
167
+
168
+ # 주문
169
+ "kt10000" : "/api/dostk/ordr",
170
+ "kt10001" : "/api/dostk/ordr",
171
+ "kt10002" : "/api/dostk/ordr",
172
+ "kt10003" : "/api/dostk/ordr",
173
+
174
+ # 실시간시세
175
+ "00" : "/api/dostk/websocket",
176
+ "04" : "/api/dostk/websocket",
177
+ "0A" : "/api/dostk/websocket",
178
+ "0B" : "/api/dostk/websocket",
179
+ "0C" : "/api/dostk/websocket",
180
+ "0D" : "/api/dostk/websocket",
181
+ "0E" : "/api/dostk/websocket",
182
+ "0F" : "/api/dostk/websocket",
183
+ "0G" : "/api/dostk/websocket",
184
+ "0H" : "/api/dostk/websocket",
185
+ "0J" : "/api/dostk/websocket",
186
+ "0U" : "/api/dostk/websocket",
187
+ "0g" : "/api/dostk/websocket",
188
+ "0m" : "/api/dostk/websocket",
189
+ "0s" : "/api/dostk/websocket",
190
+ "0u" : "/api/dostk/websocket",
191
+ "0w" : "/api/dostk/websocket",
192
+ "1h" : "/api/dostk/websocket",
193
+
194
+ # 조건검색
195
+ "ka10171" : "/api/dostk/websocket",
196
+ "ka10172" : "/api/dostk/websocket",
197
+ "ka10173" : "/api/dostk/websocket",
198
+ "ka10174" : "/api/dostk/websocket",
199
+
200
+ # 신용주문
201
+ "kt10006" : "/api/dostk/crdordr",
202
+ "kt10007" : "/api/dostk/crdordr",
203
+ "kt10008" : "/api/dostk/crdordr",
204
+ "kt10009" : "/api/dostk/crdordr",
205
+ }
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.3
2
+ Name: kiwoomRest
3
+ Version: 0.0.1
4
+ Summary: package for Kiwoom RestAPI
5
+ Project-URL: Homepage, https://github.com/teranum/python-packages/tree/master/kiwoomRest
6
+ Author-email: teranum <teranum@gmail.com>
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.8
11
+ Requires-Dist: aiohttp
12
+ Description-Content-Type: text/markdown
13
+
14
+ # kiwoomRest Package
15
+
16
+ 키움증권 REST API 간편이용 패키지.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install kiwoomRest
22
+ ```
23
+
24
+ ## Usage
25
+ 모든 요청은 비동기로 처리되며, 요청에 대한 응답은 await 키워드를 사용하여 받을 수 있습니다.
26
+ <BR/>
27
+ Samples: https://github.com/teranum/kiwoom-restapi-samples
28
+
29
+ ### 로그인 요청은 반드시 먼저 수행되어야 하며, 로그인이 성공하면 다른 요청을 수행할 수 있습니다.
30
+ ```python
31
+ import asyncio
32
+ from kiwoomRest import KwRestApi
33
+ from app_keys import appkey, secretkey # app_keys.py 파일에 appkey, secretkey 변수를 정의하고 사용하세요
34
+
35
+ async def main():
36
+ # Kiwoom API 객체 생성
37
+ api = KwRestApi()
38
+
39
+ # 실시간 이벤트 핸들러 등록
40
+ api.on_realtime.connect(lambda realdatas: print(f"on_realtime: {realdatas}"))
41
+
42
+ # 로그인
43
+ ret = await api.login(appkey, secretkey, is_simulation=False) # 실거래서버 사용, 모의투자서버 사용시 is_simulation=True
44
+ if not ret:
45
+ print(api.last_message)
46
+ return
47
+ print("Login success")
48
+
49
+ # 다른 작업...
50
+ await asyncio.sleep(5)
51
+
52
+ # 연결 종료
53
+ await api.close()
54
+
55
+
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ ### 조회/연속 요청
60
+ ```python
61
+ # 종목정보 리스트 조회
62
+ inputs = {
63
+ "mrkt_tp": "0" # 시장구분: 0:코스피, 10:코스닥, 3:ELW, 8:ETF, 30:K-OTC, 50:코넥스, 5:신주인수권, 4:뮤추얼펀드, 6:리츠, 9:하이일드
64
+ }
65
+ response = await api.request("ka10099", inputs)
66
+
67
+ # 주식 종목정보 조회
68
+ inputs = {
69
+ "stk_cd" : "005930" # 종목코드: 삼성전자
70
+ }
71
+ response = await api.request("ka10001", inputs)
72
+
73
+ # 주식 차트 조회
74
+ inputs = {
75
+ "stk_cd": "005930", # 종목코드: 삼성전자
76
+ "base_dt": "00000000", # 기준일자: 00000000(현재일자)
77
+ "upd_stkpc_tp": "1" # 수정주가구분: 0(미적용), 1(적용)
78
+ }
79
+ response = await api.request("ka10081", inputs)
80
+
81
+ # 연속조회
82
+ if response.cont_yn == "Y":
83
+ response = await api.request("t8410", request, cont_yn=response.cont_yn, next_key=response.next_key)
84
+
85
+ # response 출력
86
+ if response.return_code == 0:
87
+ print("조회 성공")
88
+ print(response.body)
89
+ else:
90
+ print(f"조회 실패: {response.return_msg}")
91
+
92
+ ```
93
+
94
+ ### 조건검색 조회/실시간
95
+ ```python
96
+ # 서버저장조건 리스트조회
97
+ inputs = {
98
+ "trnm" : "CNSRLST" # CNSRLST 고정값
99
+ }
100
+ response = await api.request("ka10171", inputs)
101
+
102
+ # 조건검색 요청 일반
103
+ inputs = {
104
+ "trnm" : "CNSRREQ", # CNSRREQ 고정값
105
+ "seq" : "4", # 조건검색식 일련번호
106
+ "search_type" : "0", # 0:조건검색
107
+ "stex_tp" : "K", # K:KRX
108
+ "cont_yn" : "N", # 연속조회여부 (Y:연속조회, N:단순조회)
109
+ "next_key" : "" # 연속조회키 (연속조회여부가 'Y'인 경우 필수 세팅)
110
+ }
111
+ response = await api.request("ka10172", inputs)
112
+
113
+ # 조건검색 실시간 검색 등록
114
+ api.on_realtime.connect(print) # 실시간 이벤트 핸들러 등록
115
+ inputs = {
116
+ "trnm" : "CNSRREQ", # CNSRREQ 고정값
117
+ "seq" : "4", # 조건검색식 일련번호
118
+ "search_type" : "1", # 1: 조건검색+실시간조건검색
119
+ "stex_tp" : "K" # K:KRX
120
+ }
121
+ response = await api.realtime(inputs)
122
+
123
+ if response.return_code != 0:
124
+ print(f"실시간검색 등록실패: {response.return_msg}")
125
+ return
126
+
127
+ print("실시간검색 등록성공")
128
+ print(response.body)
129
+
130
+ await asyncio.sleep(60) # 60초동안 유효, 후에 중지
131
+
132
+ # 조건검색 실시간 해제
133
+ inputs = {
134
+ "trnm" : "CNSRCLR", # CNSRCLR 고정값
135
+ "seq" : "4", # 조건검색식 일련번호
136
+ }
137
+ response = await api.realtime(inputs)
138
+ print("실시간검색 해제")
139
+
140
+ ```
141
+
142
+ ### 웹소켓 실시간 시세 등록/해제
143
+ ```python
144
+ api.on_realtime.connect(on_realtime) # 실시간 이벤트 핸들러 연결
145
+
146
+ # 삼성전자 실시간 체결 시세 등록
147
+ inputs = {
148
+ "trnm" : "REG",
149
+ "grp_no" : "1",
150
+ "refresh" : "1",
151
+ "data" : [{
152
+ " item" : ["005930"], # 삼성전자. or SK하이닉스 함께 등록할 경우 ["005930", "000660"] 로 설정
153
+ " type" : ["0B"] # 체결시세. or 우선호가시세 함께 등록할 경우 ["0B", "0C"]로 설정
154
+ }]
155
+ }
156
+ response = await api.realtime(inputs)
157
+
158
+ # 60초후 실시간 시세 중지
159
+ await asyncio.sleep(60)
160
+
161
+ # 실시간 시세 해제 시 "REG" -> "REMOVE" 로 변경
162
+ inputs["trnm"] = "REMOVE"
163
+ response = await api.realtime(inputs)
164
+
165
+ def on_realtime(realdatas):
166
+ print(f"실시간이벤트: {realdatas}")
167
+
168
+ ```
169
+
170
+ ## 프로퍼티, 메소드, 이벤트
171
+ ```python
172
+ # 프로퍼티
173
+ connected -> bool: 연결여부 (연결: True, 미연결: False)
174
+ is_simulation -> bool: 모의투자 여부 (모의투자: True, 실거래: False))
175
+ access_token -> str: 발급된 엑세스토큰
176
+ last_message -> str: 마지막 메시지, 로그인 또는 요청 실패시 사유 저장
177
+
178
+ # 메소드
179
+ login(appkey:str, secretkey:str, is_simulation:bool = False) -> bool: 로그인
180
+ appkey:str - 앱키
181
+ secretkey:str - 앱시크릿키
182
+ is_simulation:bool - 모의서버 사용여부 (모의서버: True, 실거래서버: False), 기본값: False
183
+ reutrn: bool - 로그인 성공여부 (성공: True, 실패: False), 실패시 last_message에 실패 사유 저장
184
+
185
+ request(api_id:str, indatas:dict, *, cont_yn:str='N', next_key:str='0', path:str=None) -> ResponseData: TR요청
186
+ api_id:str - api-id (TR코드)
187
+ indatas:dict - 요청 데이터
188
+ * - cont_yn, next_key, path는 옵션(기본값으로 설정됨)
189
+ cont_yn:str - 연속조회여부 (연속조회: 'Y', 단순조회: 'N'), 기본값: 'N'
190
+ next_key:str - 연속조회키 (연속조회여부가 'Y'인 경우 필수 세팅), 기본값: '0'
191
+ path:str - PATH경로, 기본값: None(자동으로 설정 됨), 설정 필요시 URL값으로 세팅 ex) '/api/dostk/stkinfo'
192
+ return: ResponseData - 응답 데이터, 요청 성공 여부는 return_code로 확인 가능 (0: 성공, 그 외: 실패)
193
+
194
+ realtime(indatas:dict) -> ResponseData: 실시간 등록/해제
195
+ indatas:dict - 실시간 요청 데이터
196
+ return: ResponseData - 응답 데이터, 요청 성공 여부는 return_code로 확인 가능 (0: 성공, 그 외: 실패)
197
+
198
+ close() -> None: 연결 종료
199
+
200
+
201
+ # 이벤트:
202
+ on_message(msg:str): 메시지 수신 이벤트 (웹소켓 오류시 발생)
203
+ msg - 메시지
204
+ - ex.1 ) 'websocket exception. {e}'
205
+ - ex.2 ) 'websocket closed. {msg}'
206
+ - ex.3 ) 'websocket error. {msg}'
207
+
208
+ on_realtime(realdatas:dict): 실시간 수신 이벤트 (실시간 데이터 수신시 발생)
209
+ realdatas - 실시간 데이터
210
+
211
+ 이벤트 핸들러 연결 : api 객체 생성시 연결
212
+ api = KwRestApi()
213
+ api.on_message.connect(print)
214
+ api.on_realtime.connect(print)
215
+
216
+ # 응답데이터 : ResponseData
217
+ return_code -> int: 응답코드 (0: 성공, 그 외: 실패)
218
+ return_msg -> str: 응답메시지 (성공/실패 메시지)
219
+ body -> dict: 응답데이터 (조회결과 데이터)
220
+ cont_yn -> str: 연속조회여부 (연속조회 있을 경우: 'Y'로 세팅됨)
221
+ next_key -> str: 연속조회키
222
+
223
+ api_id -> str: 요청 api-id (TR코드)
224
+ path -> str: 요청 PATH경로(URL)
225
+ ```
@@ -0,0 +1,7 @@
1
+ kiwoomRest/__init__.py,sha256=QGusLIiQqiPWAlKruu-G6T6G0STyyhG2jJyOaXCspJU,120
2
+ kiwoomRest/rest_api.py,sha256=W-cCUFTg1p5TIJR1d6bn391A_M9hgZYmLkkiTf6tZ0A,15975
3
+ kiwoomRest/tr_code_to_path.py,sha256=dpy5YqXyOiYGQgaW4tSo50_8ApKUSkpbYqCGOpZACGs,6781
4
+ kiwoomrest-0.0.1.dist-info/METADATA,sha256=gFxhsYYDg-03SmW-BPXz9-Att2Gx0VnXJPzLv6UGn2w,8136
5
+ kiwoomrest-0.0.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
6
+ kiwoomrest-0.0.1.dist-info/licenses/LICENSE,sha256=RGh27_uzSrWGoN1S0od88T_hkD8HqGDOBJCbBOccXKg,1087
7
+ kiwoomrest-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.26.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 teranum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+