ksxt 1.0.0__py3-none-any.whl → 1.0.2__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.
@@ -2,32 +2,34 @@ from datetime import datetime, timedelta
2
2
  import json
3
3
  import os
4
4
  import time
5
- from typing import Any, Dict, Optional
5
+ from typing import Any, Dict, Literal, Optional
6
+
7
+ import pytz
8
+
6
9
  from ksxt.api.koreainvest import ImplicitAPI
7
10
  from ksxt.async_.base.async_exchange import AsyncExchange
8
11
  from ksxt.market.manager import MarketManager
9
12
  from ksxt.parser.koreainvest import KoreaInvestParser
10
13
 
14
+ import ksxt.models
15
+
11
16
 
12
17
  class KoreaInvest(AsyncExchange, ImplicitAPI):
13
18
  def __init__(self, config: Dict = None) -> None:
14
19
  super().__init__(config, "koreainvest.toml")
15
20
  self.parser = KoreaInvestParser()
16
-
17
- def safe_symbol(self, base_market: str, security: str) -> str:
18
- return f"{security}"
21
+ self.timezone = pytz.timezone("Asia/Seoul")
19
22
 
20
23
  def is_activate(self, path, security_type) -> bool:
21
24
  mode = "dev" if self.is_dev == True else "app"
22
25
 
23
26
  tr_id = self.apis[self.type][security_type][path][mode]["tr_id"]
24
27
 
25
- if not bool(tr_id):
28
+ if security_type != "token" and not bool(tr_id):
26
29
  return False
27
30
 
28
31
  return super().is_activate(path=path, security_type=security_type)
29
32
 
30
- @AsyncExchange.check_token
31
33
  def sign(
32
34
  self,
33
35
  path,
@@ -47,26 +49,24 @@ class KoreaInvest(AsyncExchange, ImplicitAPI):
47
49
  params["version"] = version
48
50
  destination = self.implode_params(destination, params)
49
51
 
50
- url = host_url + "/" + destination
52
+ url = f"{host_url}/{destination}"
51
53
 
52
54
  tr_id = self.apis[self.type][security_type][path][mode]["tr_id"]
53
55
  authorization_token = f"Bearer {self.token}"
54
- custtype = ""
55
- if "custtype" in self.apis[self.type][security_type][path]:
56
- custtype = self.apis[self.type][security_type][path]["custtype"]
57
-
58
- if headers is None:
59
- headers = {}
60
- headers.update(
61
- {
62
- "content-type": "application/json",
63
- "authorization": authorization_token,
64
- "appkey": self.open_key,
65
- "appsecret": self.secret_key,
66
- "tr_id": tr_id,
67
- "custtype": custtype,
68
- }
69
- )
56
+
57
+ if api_type == "private":
58
+ if headers is None:
59
+ headers = {}
60
+ headers.update(
61
+ {
62
+ "content-type": "application/json",
63
+ "authorization": authorization_token,
64
+ "appkey": self.open_key,
65
+ "appsecret": self.secret_key,
66
+ "tr_id": tr_id,
67
+ "custtype": "P",
68
+ }
69
+ )
70
70
 
71
71
  if method_type.upper() == "POST":
72
72
  body = json.dumps(params)
@@ -74,241 +74,57 @@ class KoreaInvest(AsyncExchange, ImplicitAPI):
74
74
 
75
75
  return {"url": url, "method": method_type, "headers": headers, "body": body, "params": params}
76
76
 
77
- # region _____
78
- def create_token(self):
79
- import logging
77
+ async def create_token(self) -> ksxt.models.KsxtTokenResponse:
78
+ params = {"grant_type": "client_credentials", "appkey": self.open_key, "appsecret": self.secret_key}
80
79
 
81
- mode = "dev" if self.is_dev == True else "app"
82
- host_url = self.apis[self.type][mode]["hostname"]
83
- destination = self.apis[self.type]["token"]["url"]
84
- url = host_url + "/" + destination
80
+ common_header = self.create_common_header(request_params=params)
85
81
 
86
- request_headers = self.prepare_request_headers()
82
+ response = await self.public_post_generate_token(self.extend(params))
87
83
 
88
- body = {"grant_type": "client_credentials", "appkey": self.open_key, "appsecret": self.secret_key}
84
+ common_response = self.get_common_response(response=response)
85
+ if common_response.success != "0":
86
+ return ksxt.models.KsxtTokenResponse(header=common_header, response=common_response, info=None)
89
87
 
90
- body = json.dumps(body, separators=(",", ":"))
88
+ parsed_info = self.parser.parse_token(response=response)
91
89
 
92
- try:
93
- res = self.fetch(url=url, method="POST", headers=request_headers, body=body)
94
- except Exception as ex:
95
- logging.error(ex)
96
- return
90
+ self.save_token(self.open_key, parsed_info.access_token, expired=parsed_info.expired_datetime)
97
91
 
98
- if "access_token" in res.keys():
99
- token = res["access_token"]
100
- token_expired = res["access_token_token_expired"]
101
- self.save_token(self.open_key, token, token_expired)
102
- else:
103
- logging.error(res["error_description"])
92
+ return ksxt.models.KsxtTokenResponse(header=common_header, response=common_response, info=parsed_info)
104
93
 
105
- def get_response(self, result, request_params: dict):
106
- if self.safe_string(result, "rt_cd") == "0":
107
- response = self.get_success_response(result)
108
- else:
109
- response = self.get_error_response(result)
110
-
111
- response = self.update_header(response, request_params)
112
- return response
113
-
114
- def get_success_response(self, result):
115
- return super().get_success_response(
116
- # 성공 실패 여부
117
- response_code="0",
118
- # 응답 코드
119
- msg_code=self.safe_string(result, "msg_cd"),
120
- # 응답 메세지
121
- msg=self.safe_string(result, "msg1"),
122
- # 원본 데이터
123
- org_data=result,
124
- )
125
-
126
- def get_error_response(self, result):
127
- return super().get_error_response(
128
- error_code=self.safe_string(result, "msg_cd"),
129
- error_message=self.safe_string(result, "msg1"),
130
- org_data=result,
131
- )
132
-
133
- def update_header(self, response: dict, request_params: dict):
134
- response = self.update_success_response_header(
135
- response, {"request_time": time.time(), "request_params": request_params}
136
- )
137
- return response
138
-
139
- # endregion ____
140
-
141
- # region public feeder
142
- @AsyncExchange.check_token
143
- async def fetch_markets(self, market_name: str):
144
- db_path = os.path.join(os.getcwd(), f".ksxt-cache_market.{datetime.now().strftime('%Y%m%d')}.db")
145
- manager = MarketManager(db_path=db_path)
146
- manager._init()
147
- if market_name.lower() == "kospi":
148
- result = manager.kospi.all()
149
- base_market = "KRW"
150
- elif market_name.lower() == "kosdaq":
151
- result = manager.kosdaq.all()
152
- base_market = "KRW"
153
- elif market_name.lower() == "nyse":
154
- result = manager.nyse.all()
155
- base_market = "USD"
156
- elif market_name.lower() == "nasdaq":
157
- result = manager.nasdaq.all()
158
- base_market = "USD"
159
- elif market_name.lower() == "amex":
160
- result = manager.amex.all()
161
- base_market = "USD"
162
- else:
163
- return self.get_error_response(
164
- error_code="market_error", error_message=f"{market_name} market is not yet supported."
94
+ def get_common_response(self, response):
95
+ if "error_code" in response:
96
+ return self.create_common_response(
97
+ success="1",
98
+ msg_code=self.safe_string(response, "error_code"),
99
+ msg=self.safe_string(response, "error_description"),
100
+ info=response,
165
101
  )
166
102
 
167
- params = {}
168
- response = self.get_response(result, params)
169
- if response["response"]["success"] != "0":
170
- return response
171
-
172
- return self.parser.parse_markets(response=response, base_market=base_market)
173
-
174
- async def fetch_trade_fee(self, symbol: Optional[str] = "", base_market: str = "KRW"):
175
- params = {"base_market": base_market}
176
-
177
- result = {"rt_cd": "0", "output": {}}
178
- response = self.get_response(result, params)
179
- if response["response"]["success"] != "0":
180
- return response
181
-
182
- return self.parser.parse_trade_fee(response, base_market)
183
-
184
- async def fetch_ticker(self, symbol: str, base_market: str = "KRW"):
185
- if base_market == "KRW":
186
- params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
187
- result = await self.private_get_fetch_ticker_price_krw(self.extend(params))
188
- elif base_market == "USD":
189
- market_code = await self.get_market_code_in_feeder(symbol=symbol, base_market=base_market)
190
- params = {"AUTH": "", "EXCD": market_code, "SYMB": symbol}
191
- result = await self.fetchTickerForUS(self.extend(params))
192
- else:
193
- return self.get_error_response(
194
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
103
+ if "rt_cd" in response and response["rt_cd"] != "0":
104
+ return self.create_common_response(
105
+ success="1",
106
+ msg_code=self.safe_string(response, "msg_cd"),
107
+ msg=self.safe_string(response, "msg1"),
108
+ info=response,
195
109
  )
196
110
 
197
- response = self.get_response(result, params)
198
- if response["response"]["success"] != "0":
199
- return response
200
-
201
- return self.parser.parse_ticker(response, base_market)
202
-
203
- async def fetch_historical_data(
204
- self,
205
- symbol: str,
206
- time_frame: str,
207
- start: Optional[str] = None,
208
- end: Optional[str] = None,
209
- base_market: str = "KRW",
210
- ):
211
- limit = 100
212
-
213
- if end is None:
214
- end = KoreaInvest.now(base_market=base_market)
215
-
216
- if start is None:
217
-
218
- if time_frame.endswith("m"):
219
- start = end - timedelta(minutes=30)
220
-
221
- if base_market == "KRW":
222
- params = {
223
- "FID_ETC_CLS_CODE": "",
224
- "FID_COND_MRKT_DIV_CODE": "J", # "U"
225
- "FID_INPUT_ISCD": symbol,
226
- "FID_INPUT_HOUR_1": start.strftime("%H%M%S"), # 업종은 조회 간격만 입력 가능
227
- "FID_PW_DATA_INCU_YN": "N",
228
- }
229
-
230
- result = await self.private_get_fetch_security_ohlcv_minute_krw(self.extend(params))
231
-
232
- elif time_frame.endswith("D"):
233
- start = end - timedelta(days=limit)
234
- elif time_frame.endswith("W"):
235
- start = end - timedelta(weeks=limit)
236
- elif time_frame.endswith("M"):
237
- start = end - timedelta(days=limit * 30)
238
- elif time_frame.endswith("Y"):
239
- start = end - timedelta(days=limit * 365)
240
- else:
241
- start = end
242
-
243
- if base_market == "KRW":
244
- params = {
245
- "FID_COND_MRKT_DIV_CODE": "J",
246
- "FID_INPUT_ISCD": symbol,
247
- "FID_INPUT_DATE_1": start.strftime("%Y%m%d"),
248
- "FID_INPUT_DATE_2": end.strftime("%Y%m%d"),
249
- "FID_PERIOD_DIV_CODE": time_frame,
250
- "FID_ORG_ADJ_PRC": "1",
251
- }
252
-
253
- result = await self.private_get_fetch_security_ohlcv_krw(self.extend(params))
254
- # index는 구분해서 날려야함
255
- # response = self.private_get_fetch_index_ohlcv_krw(self.extend(params))
256
- elif base_market == "USD":
257
- if time_frame == "D":
258
- gubn = "0"
259
- elif time_frame == "W":
260
- gubn = "1"
261
- elif time_frame == "M":
262
- gubn = "2"
263
- else:
264
- return self.get_error_response(
265
- error_code="time frame error", error_message=f"{time_frame} time-frame is not supported."
266
- )
267
-
268
- market_code = await self.get_market_code_in_feeder(symbol=symbol, base_market=base_market)
269
-
270
- params = {
271
- "AUTH": "",
272
- "EXCD": market_code,
273
- "SYMB": symbol,
274
- "GUBN": gubn,
275
- "BYMD": end.strftime("%Y%m%d"),
276
- "MODP": "1",
277
- "KEYB": "",
278
- }
279
-
280
- result = await self.fetchOHLCVforUS(self.extend(params))
281
- else:
282
- return self.get_error_response(
283
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
111
+ if "response" in response and response["response"]["success"] != "0":
112
+ return self.create_common_response(
113
+ success="1",
114
+ msg_code=self.safe_string(response["response"], "code"),
115
+ msg=self.safe_string(response["response"], "message"),
116
+ info=response,
284
117
  )
285
118
 
286
- response = self.get_response(result, params)
287
- if response["response"]["success"] != "0":
288
- return response
289
-
290
- return self.parser.parse_historical_data(response, base_market)
291
-
292
- async def fetch_is_holiday(self, dt: datetime, base_market: str = "KRW"):
293
- if base_market == "KRW":
294
- params = {"BASS_DT": dt.strftime("%Y%m%d"), "CTX_AREA_NK": "", "CTX_AREA_FK": ""}
295
- result = await self.private_get_fetch_calendar_holiday_krw(self.extend(params))
296
-
297
- response = self.get_response(result, params)
298
- if response["response"]["success"] != "0":
299
- return response
300
-
301
- return self.parser.parse_is_holiday(response, base_market)
302
- else:
303
- return self.get_error_response(
304
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
305
- )
306
-
307
- # endregion public feeder
119
+ return self.create_common_response(
120
+ success="0",
121
+ msg_code=self.safe_string(response, "msg_cd"),
122
+ msg=self.safe_string(response, "msg1"),
123
+ info=response,
124
+ )
308
125
 
309
- # region private feeder
310
- # @RestExchange.check_token
311
- async def fetch_balance(self, acc_num: str, base_market: str = "KRW"):
126
+ @AsyncExchange.check_token
127
+ async def fetch_balance(self, acc_num: str, base_market: str = "KRW") -> ksxt.models.KsxtBalanceResponse:
312
128
  if base_market == "KRW":
313
129
  params = {
314
130
  "CANO": acc_num[:8],
@@ -323,32 +139,23 @@ class KoreaInvest(AsyncExchange, ImplicitAPI):
323
139
  "CTX_AREA_FK100": "",
324
140
  "CTX_AREA_NK100": "",
325
141
  }
142
+ else:
143
+ assert ValueError(f"{base_market} is not valid value")
326
144
 
327
- result = await self.private_get_fetch_balance_krw(self.extend(params))
328
- elif base_market == "USD":
329
- market_code = await self.get_market_code_in_feeder(symbol="ALL", base_market=base_market)
330
- params = {
331
- "CANO": acc_num[:8],
332
- "ACNT_PRDT_CD": acc_num[-2:],
333
- "OVRS_EXCG_CD": market_code,
334
- "TR_CRCY_CD": "USD",
335
- "CTX_AREA_FK200": "",
336
- "CTX_AREA_NK200": "",
337
- }
145
+ common_header = self.create_common_header(request_params=params)
338
146
 
339
- result = await self.private_get_fetch_balance_usd(self.extend(params))
340
- else:
341
- return self.get_error_response(
342
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
343
- )
147
+ response = await self.private_get_fetch_balance(self.extend(params))
344
148
 
345
- response = self.get_response(result, params)
346
- if response["response"]["success"] != "0":
347
- return response
149
+ common_response = self.get_common_response(response=response)
150
+ if common_response.success != "0":
151
+ return ksxt.models.KsxtBalanceResponse(header=common_header, response=common_response, info=None)
348
152
 
349
- return self.parser.parse_balance(response, base_market)
153
+ parsed_info = self.parser.parse_balance(response=response, base_market=base_market)
350
154
 
351
- async def fetch_cash(self, acc_num: str, base_market: str = "KRW"):
155
+ return ksxt.models.KsxtBalanceResponse(header=common_header, response=common_response, info=parsed_info)
156
+
157
+ @AsyncExchange.check_token
158
+ async def fetch_cash(self, acc_num: str, base_market: str = "KRW") -> ksxt.models.KsxtCashResponse:
352
159
  if base_market == "KRW":
353
160
  params = {
354
161
  "CANO": acc_num[:8],
@@ -363,297 +170,188 @@ class KoreaInvest(AsyncExchange, ImplicitAPI):
363
170
  "CTX_AREA_FK100": "",
364
171
  "CTX_AREA_NK100": "",
365
172
  }
366
-
367
- result = await self.private_get_fetch_cash_krw(self.extend(params))
368
- elif base_market == "USD":
369
- params = {
370
- "CANO": acc_num[:8],
371
- "ACNT_PRDT_CD": acc_num[-2:],
372
- # 01: 원화, 02: 외화
373
- "WCRC_FRCR_DVSN_CD": "02",
374
- "NATN_CD": "840",
375
- "TR_MKET_CD": "00",
376
- "INQR_DVSN_CD": "00",
377
- }
378
-
379
- result = await self.fetchCashForUS(self.extend(params))
380
173
  else:
381
- return self.get_error_response(
382
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
383
- )
174
+ assert ValueError(f"{base_market} is not valid value")
384
175
 
385
- response = self.get_response(result, params)
386
- if response["response"]["success"] != "0":
387
- return response
176
+ common_header = self.create_common_header(request_params=params)
388
177
 
389
- return self.parser.parse_cash(response, base_market)
178
+ response = await self.private_get_fetch_cash(self.extend(params))
390
179
 
391
- async def fetch_orderbook(self, symbol: str, base_market: str = "KRW"):
392
- if base_market == "KRW":
393
- params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
394
-
395
- result = await self.private_get_fetch_orderbook_krw(self.extend(params))
396
- elif base_market == "USD":
397
- params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
180
+ common_response = self.get_common_response(response=response)
181
+ if common_response.success != "0":
182
+ return ksxt.models.KsxtCashResponse(header=common_header, response=common_response, info=None)
398
183
 
399
- result = await self.fetchCashForUS(self.extend(params))
400
- else:
401
- return self.get_error_response(
402
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
403
- )
184
+ parsed_info = self.parser.parse_cash(response=response, base_market=base_market)
404
185
 
405
- response = self.get_response(result, params)
406
- if response["response"]["success"] != "0":
407
- return response
186
+ return ksxt.models.KsxtCashResponse(header=common_header, response=common_response, info=parsed_info)
408
187
 
409
- return self.parser.parse_orderbook(response, base_market)
410
-
411
- async def fetch_pnl(self, acc_num: str, include_cost: bool = True, base_market: str = "KRW"):
188
+ @AsyncExchange.check_token
189
+ async def fetch_orderbook(self, symbol: str, base_market: str = "KRW") -> ksxt.models.KsxtSingleOrderBookResponse:
412
190
  if base_market == "KRW":
413
- params = {
414
- "CANO": acc_num[:8],
415
- "ACNT_PRDT_CD": acc_num[-2:],
416
- "AFHR_FLPR_YN": "N",
417
- "OFL_YN": "",
418
- "INQR_DVSN": "00",
419
- "UNPR_DVSN": "01",
420
- "FUND_STTL_ICLD_YN": "N",
421
- "FNCG_AMT_AUTO_RDPT_YN": "N",
422
- "PRCS_DVSN": "01",
423
- "COST_ICLD_YN": "Y" if include_cost else "N",
424
- "CTX_AREA_FK100": "",
425
- "CTX_AREA_NK100": "",
426
- }
191
+ params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
192
+ else:
193
+ assert ValueError(f"{base_market} is not valid value")
427
194
 
428
- result = await self.private_get_fetch_pnl_krw(self.extend(params))
429
- elif base_market == "USD":
430
- params = {
431
- "CANO": acc_num[:8],
432
- "ACNT_PRDT_CD": acc_num[-2:],
433
- # 01: 원화, 02: 외화
434
- "WCRC_FRCR_DVSN_CD": "02",
435
- "NATN_CD": "840",
436
- "TR_MKET_CD": "00",
437
- "INQR_DVSN_CD": "00",
438
- }
195
+ common_header = self.create_common_header(request_params=params)
439
196
 
440
- result = await self.fetchCashForUS(self.extend(params))
441
- else:
442
- return self.get_error_response(
443
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
444
- )
197
+ response = await self.private_get_fetch_orderbook(self.extend(params))
445
198
 
446
- response = self.get_response(result, params)
447
- if response["response"]["success"] != "0":
448
- return response
199
+ common_response = self.get_common_response(response=response)
200
+ if common_response.success != "0":
201
+ return ksxt.models.KsxtSingleOrderBookResponse(header=common_header, response=common_response, info=None)
449
202
 
450
- return self.parser.parse_pnl(response, base_market)
203
+ parsed_info = self.parser.parse_orderbook(response=response, base_market=base_market)
451
204
 
452
- async def fetch_screener_list(self, user_id, base_market: str = "KRW"):
453
- params = {"USER_ID": user_id}
205
+ return ksxt.models.KsxtSingleOrderBookResponse(header=common_header, response=common_response, info=parsed_info)
454
206
 
207
+ @AsyncExchange.check_token
208
+ async def fetch_security(self, symbol: str, base_market: str = "KRW") -> ksxt.models.KsxtSecurityResponse:
455
209
  if base_market == "KRW":
456
- result = await self.private_get_fetch_screener_list_krw(self.extend(params))
210
+ params = {"PRDT_TYPE_CD": "300", "PDNO": symbol}
457
211
  else:
458
- return self.get_error_response(
459
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
460
- )
212
+ assert ValueError(f"{base_market} is not valid value")
461
213
 
462
- response = self.get_response(result, params)
463
- if response["response"]["success"] != "0":
464
- return response
214
+ common_header = self.create_common_header(request_params=params)
465
215
 
466
- return self.parser.parse_screener_list(response, base_market)
216
+ response = await self.private_get_fetch_security_info(self.extend(params))
467
217
 
468
- async def fetch_screener(self, user_id: str, screen_id: str, base_market: str = "KRW"):
469
- if base_market == "KRW":
470
- params = {"USER_ID": user_id, "SEQ": screen_id}
218
+ common_response = self.get_common_response(response=response)
219
+ if common_response.success != "0":
220
+ return ksxt.models.KsxtSecurityResponse(header=common_header, response=common_response, info=None)
471
221
 
472
- result = await self.private_get_fetch_screener_krw(self.extend(params))
473
- elif base_market == "USD":
474
- market_code = self.get_market_code_in_feeder(symbol="ALL", base_market=base_market)
475
- params = {"AUTH": "", "EXCD": market_code, "CO_YN_PRICECUR": 1}
476
- result = await self.fetchScreenerForUS(self.extend(params))
222
+ parsed_info = self.parser.parse_security(response=response, base_market=base_market)
477
223
 
478
- else:
479
- return self.get_error_response(
480
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
481
- )
482
-
483
- response = self.get_response(result, params)
484
- if response["response"]["success"] != "0":
485
- return response
224
+ return ksxt.models.KsxtSecurityResponse(header=common_header, response=common_response, info=parsed_info)
486
225
 
487
- return self.parser.parse_screener(response, base_market)
488
-
489
- async def fetch_security(self, symbol: str, base_market: str = "KRW"):
226
+ @AsyncExchange.check_token
227
+ async def fetch_ticker(self, symbol: str, base_market: str = "KRW") -> ksxt.models.KsxtTickerResponse:
490
228
  if base_market == "KRW":
491
- params = {"PRDT_TYPE_CD": "300", "PDNO": symbol} # 주식
492
-
493
- result = await self.private_get_fetch_security_info_krw(self.extend(params))
494
- elif base_market == "USD":
495
- params = {"PRDT_TYPE_CD": "J", "PDNO": symbol}
496
-
497
- result = await self.fetchCashForUS(self.extend(params))
229
+ params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
498
230
  else:
499
- return self.get_error_response(
500
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
501
- )
231
+ assert ValueError(f"{base_market} is not valid value")
502
232
 
503
- response = self.get_response(result, params)
504
- if response["response"]["success"] != "0":
505
- return response
233
+ common_header = self.create_common_header(request_params=params)
506
234
 
507
- return self.parser.parse_security(response, base_market)
235
+ response = await self.private_get_fetch_ticker_price(self.extend(params))
508
236
 
509
- # endregion private feeder
237
+ common_response = self.get_common_response(response=response)
238
+ if common_response.success != "0":
239
+ return ksxt.models.KsxtTickerResponse(header=common_header, response=common_response, info=None)
510
240
 
511
- # region broker
512
- async def create_order(
513
- self,
514
- acc_num: str,
515
- symbol: str,
516
- ticket_type: str,
517
- price: float,
518
- qty: float,
519
- otype: str,
520
- base_market: str = "KRW",
521
- ):
522
- if base_market == "KRW":
523
- if otype.upper() == "limit".upper():
524
- order_dvsn = "00"
525
- elif otype.upper() == "market".upper():
526
- order_dvsn = "01"
241
+ parsed_info = self.parser.parse_ticker(response=response, base_market=base_market)
527
242
 
528
- params = {
529
- "CANO": acc_num[:8],
530
- "ACNT_PRDT_CD": acc_num[-2:],
531
- "PDNO": symbol,
532
- "ORD_DVSN": order_dvsn,
533
- "ORD_QTY": str(qty), # string type 으로 설정
534
- "ORD_UNPR": str(price), # string type 으로 설정
535
- }
243
+ return ksxt.models.KsxtTickerResponse(header=common_header, response=common_response, info=parsed_info)
536
244
 
537
- if ticket_type == "EntryLong":
538
- result = await self.private_post_send_order_entry_krw(self.extend(params))
539
- elif ticket_type == "ExitLong":
540
- result = await self.private_post_send_order_exit_krw(self.extend(params))
541
- else:
542
- return
543
- elif base_market == "USD":
544
- if otype.upper() == "limit".upper():
545
- order_dvsn = "00"
546
- elif otype.upper() == "market".upper():
547
- # 미국장은 시장가를 세부적으로 구분하여 지원함. -> 시장가 거래를 우선 지원하지 않는다.
548
- # https://apiportal.koreainvestment.com/apiservice/apiservice-overseas-stock#L_e4a7e5fd-eed5-4a85-93f0-f46b804dae5f
549
- return self.get_error_response(
550
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
551
- )
245
+ @AsyncExchange.check_token
246
+ async def fetch_historical_data_index(
247
+ self, symbol: str, time_frame: str, start: str | None = None, end: str | None = None, base_market: str = "KRW"
248
+ ) -> ksxt.models.KsxtHistoricalDataResponse:
249
+ if time_frame.endswith("D"):
250
+ param_code = "D"
251
+ elif time_frame.endswith("W") or time_frame.endswith("w"):
252
+ param_code = "W"
253
+ elif time_frame.endswith("M"):
254
+ param_code = "M"
255
+ elif time_frame.endswith("Y"):
256
+ param_code = "Y"
257
+ else:
258
+ assert ValueError(f"{time_frame} is not valid value")
552
259
 
553
- if ticket_type == "entry_long":
554
- sell_type = ""
555
- elif ticket_type == "exit_long":
556
- sell_type = "00"
557
- else:
558
- return
260
+ if start is None:
261
+ start = self.now(base_market) - timedelta(days=50)
262
+ if end is None:
263
+ end = self.now(base_market)
559
264
 
560
- market_code = await self.get_market_code_in_broker(symbol=symbol, base_market=base_market)
265
+ if base_market == "KRW":
561
266
  params = {
562
- "CANO": acc_num[:8],
563
- "ACNT_PRDT_CD": acc_num[-2:],
564
- "OVRS_EXCG_CD": market_code,
565
- "PDNO": symbol,
566
- "ORD_DVSN": order_dvsn,
567
- "ORD_QTY": str(qty), # string type 으로 설정
568
- "SLL_TYPE": sell_type,
569
- "OVRS_ORD_UNPR": str(price), # string type 으로 설정
570
- "ORD_SVR_DVSN_CD": "0",
267
+ "FID_COND_MRKT_DIV_CODE": "U",
268
+ "FID_INPUT_ISCD": symbol,
269
+ "FID_INPUT_DATE_1": start.strftime("%Y%m%d"),
270
+ "FID_INPUT_DATE_2": end.strftime("%Y%m%d"),
271
+ "FID_PERIOD_DIV_CODE": param_code,
571
272
  }
572
-
573
- if ticket_type == "entry_long":
574
- result = await self.sendOrderEntryForUS(self.extend(params))
575
- elif ticket_type == "exit_long":
576
- result = await self.sendOrderExitForUS(self.extend(params))
577
- else:
578
- return
579
273
  else:
580
- return self.get_error_response(
581
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
582
- )
274
+ assert ValueError(f"{base_market} is not valid value")
583
275
 
584
- response = self.get_response(result, params)
585
- if response["response"]["success"] != "0":
586
- return response
276
+ common_header = self.create_common_header(request_params=params)
587
277
 
588
- return self.parser.parse_create_order(response, base_market)
278
+ response = await self.private_get_fetch_index_ohlcv(self.extend(params))
589
279
 
590
- async def cancel_order(
591
- self,
592
- acc_num: str,
593
- order_id: str,
594
- symbol: Optional[str] = "",
595
- qty: float = 0,
596
- base_market: str = "KRW",
597
- **kwargs,
598
- ):
599
- if base_market == "KRW":
600
- params = {
601
- "CANO": acc_num[:8],
602
- "ACNT_PRDT_CD": acc_num[-2:],
603
- "KRX_FWDG_ORD_ORGNO": "",
604
- "ORGN_ODNO": str(order_id),
605
- "RVSE_CNCL_DVSN_CD": "02",
606
- "ORD_DVSN": "00",
607
- "ORD_QTY": str(qty),
608
- "ORD_UNPR": str(0),
609
- "QTY_ALL_ORD_YN": "N",
610
- }
611
-
612
- # 수량 미입력시 전량 취소
613
- if qty == 0:
614
- params["QTY_ALL_ORD_YN"] = "Y"
280
+ common_response = self.get_common_response(response=response)
281
+ if common_response.success != "0":
282
+ return ksxt.models.KsxtHistoricalDataResponse(header=common_header, response=common_response, info=None)
615
283
 
616
- result = await self.private_post_send_cancel_order_krw(self.extend(params))
617
- elif base_market == "USD":
618
- if qty == 0:
619
- return self.get_error_response(
620
- error_code="qty_error", error_message=f"{base_market} cancel order need to set qty."
621
- )
284
+ parsed_info = self.parser.parse_historical_index_data(response=response, symbol=symbol, base_market=base_market)
622
285
 
623
- market_code = await self.get_market_code_in_broker(symbol=symbol, base_market=base_market)
624
- params = {
625
- "CANO": acc_num[:8],
626
- "ACNT_PRDT_CD": acc_num[-2:],
627
- "OVRS_EXCG_CD": market_code,
628
- "PDNO": symbol,
629
- "ORGN_ODNO": str(order_id),
630
- "RVSE_CNCL_DVSN_CD": "02",
631
- "ORD_QTY": str(qty),
632
- "OVRS_ORD_UNPR": str(0),
633
- }
286
+ return ksxt.models.KsxtHistoricalDataResponse(header=common_header, response=common_response, info=parsed_info)
634
287
 
635
- result = await self.sendCancelOrderForUS(self.extend(params))
288
+ @AsyncExchange.check_token
289
+ async def fetch_historical_data(
290
+ self, symbol: str, time_frame: str, start: str | None = None, end: str | None = None, base_market: str = "KRW"
291
+ ) -> ksxt.models.KsxtHistoricalDataResponse:
292
+ if time_frame.endswith("D"):
293
+ param_code = "D"
294
+ elif time_frame.endswith("W") or time_frame.endswith("w"):
295
+ param_code = "W"
296
+ elif time_frame.endswith("M"):
297
+ param_code = "M"
298
+ elif time_frame.endswith("Y"):
299
+ param_code = "Y"
636
300
  else:
637
- return self.get_error_response(
638
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
639
- )
301
+ assert ValueError(f"{time_frame} is not valid value")
640
302
 
641
- response = self.get_response(result, params)
642
- if response["response"]["success"] != "0":
643
- return response
303
+ if start is None:
304
+ start = self.now(base_market) - timedelta(days=100)
305
+ if end is None:
306
+ end = self.now(base_market)
644
307
 
645
- return self.parser.parse_cancel_order(response, base_market)
308
+ if base_market == "KRW":
309
+ params = {
310
+ "FID_COND_MRKT_DIV_CODE": "J",
311
+ "FID_INPUT_ISCD": symbol,
312
+ "FID_INPUT_DATE_1": start.strftime("%Y%m%d"),
313
+ "FID_INPUT_DATE_2": end.strftime("%Y%m%d"),
314
+ "FID_PERIOD_DIV_CODE": param_code,
315
+ "FID_ORG_ADJ_PRC": "0",
316
+ }
317
+ else:
318
+ assert ValueError(f"{base_market} is not valid value")
319
+
320
+ if time_frame.endswith("m"):
321
+ common_header = self.create_common_header(request_params=params)
322
+ response = await self.private_get_fetch_security_ohlcv_minute(self.extend(params))
323
+ elif time_frame.endswith("D"):
324
+ common_header = self.create_common_header(request_params=params)
325
+ response = await self.private_get_fetch_security_ohlcv_day(self.extend(params))
326
+ elif time_frame.endswith("W"):
327
+ common_header = self.create_common_header(request_params=params)
328
+ response = await self.private_get_fetch_security_ohlcv_week(self.extend(params))
329
+ elif time_frame.endswith("M"):
330
+ common_header = self.create_common_header(request_params=params)
331
+ response = await self.private_get_fetch_security_ohlcv_month(self.extend(params))
332
+ elif time_frame.endswith("Y"):
333
+ common_header = self.create_common_header(request_params=params)
334
+ response = await self.private_get_fetch_security_ohlcv_year(self.extend(params))
335
+
336
+ common_response = self.get_common_response(response=response)
337
+ if common_response.success != "0":
338
+ return ksxt.models.KsxtHistoricalDataResponse(header=common_header, response=common_response, info=None)
339
+
340
+ parsed_info = self.parser.parse_historical_data(response=response, symbol=symbol, base_market=base_market)
341
+
342
+ return ksxt.models.KsxtHistoricalDataResponse(header=common_header, response=common_response, info=parsed_info)
646
343
 
344
+ @AsyncExchange.check_token
647
345
  async def modify_order(
648
346
  self,
649
347
  acc_num: str,
650
348
  order_id: str,
651
349
  price: float,
652
350
  qty: float,
653
- symbol: Optional[str] = "",
351
+ *args,
352
+ symbol: str | None = "",
654
353
  base_market: str = "KRW",
655
- **kwargs,
656
- ):
354
+ ) -> ksxt.models.KsxtModifyOrderResponse:
657
355
  if base_market == "KRW":
658
356
  params = {
659
357
  "CANO": acc_num[:8],
@@ -666,176 +364,110 @@ class KoreaInvest(AsyncExchange, ImplicitAPI):
666
364
  "ORD_UNPR": str(price),
667
365
  "QTY_ALL_ORD_YN": "N",
668
366
  }
669
-
670
- # 수량 미입력시 전량 수정
671
- if qty == 0:
672
- params["QTY_ALL_ORD_YN"] = "Y"
673
-
674
- result = await self.private_post_send_modify_order_krw(self.extend(params))
675
- elif base_market == "USD":
676
- market_code = await self.get_market_code_in_broker(symbol=symbol, base_market=base_market)
677
- params = {
678
- "CANO": acc_num[:8],
679
- "ACNT_PRDT_CD": acc_num[-2:],
680
- "OVRS_EXCG_CD": market_code,
681
- "PDNO": symbol,
682
- "ORGN_ODNO": str(order_id),
683
- "RVSE_CNCL_DVSN_CD": "01",
684
- "ORD_QTY": str(qty),
685
- "OVRS_ORD_UNPR": str(price),
686
- }
687
-
688
- result = await self.sendModifyOrderForUS(self.extend(params))
689
367
  else:
690
- return self.get_error_response(
691
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
692
- )
368
+ assert ValueError(f"{base_market} is not valid value")
693
369
 
694
- response = self.get_response(result, params)
695
- if response["response"]["success"] != "0":
696
- return response
370
+ common_header = self.create_common_header(request_params=params)
371
+ response = await self.private_post_send_modify_order(self.extend(params))
697
372
 
698
- return self.parser.parse_modify_order(response, base_market)
373
+ common_response = self.get_common_response(response=response)
374
+ if common_response.success != "0":
375
+ return ksxt.models.KsxtModifyOrderResponse(header=common_header, response=common_response, info=None)
699
376
 
700
- async def fetch_open_order(
701
- self,
702
- acc_num: str,
703
- symbol: Optional[str] = "",
704
- start: Optional[str] = None,
705
- end: Optional[str] = None,
706
- base_market: str = "KRW",
707
- ):
708
- if start is None:
709
- start = KoreaInvest.now(base_market=base_market).strftime("%Y%m%d")
377
+ parsed_info = self.parser.parse_modify_order(response=response, base_market=base_market)
710
378
 
711
- if end is None:
712
- end = KoreaInvest.now(base_market=base_market).strftime("%Y%m%d")
379
+ return ksxt.models.KsxtModifyOrderResponse(header=common_header, response=common_response, info=parsed_info)
713
380
 
381
+ @AsyncExchange.check_token
382
+ async def cancel_order(
383
+ self, acc_num: str, order_id: str, symbol: str | None = "", qty: float = 0, *args, base_market: str = "KRW"
384
+ ) -> ksxt.models.KsxtCancelOrderResponse:
714
385
  if base_market == "KRW":
715
386
  params = {
716
387
  "CANO": acc_num[:8],
717
388
  "ACNT_PRDT_CD": acc_num[-2:],
718
- "INQR_STRT_DT": start,
719
- "INQR_END_DT": end,
720
- "SLL_BUY_DVSN_CD": "00",
721
- "INQR_DVSN": "00",
722
- "PDNO": symbol,
723
- "CCLD_DVSN": "02",
724
- "ORD_GNO_BRNO": "",
725
- "ODNO": "",
726
- "INQR_DVSN_3": "00",
727
- "INQR_DVSN_1": "",
728
- "CTX_AREA_FK100": "",
729
- "CTX_AREA_NK100": "",
389
+ "KRX_FWDG_ORD_ORGNO": "",
390
+ "ORGN_ODNO": str(order_id),
391
+ "RVSE_CNCL_DVSN_CD": "02",
392
+ "ORD_DVSN": "00",
393
+ "ORD_QTY": str(qty),
394
+ "ORD_UNPR": str(0),
395
+ "QTY_ALL_ORD_YN": "N",
730
396
  }
397
+ else:
398
+ assert ValueError(f"{base_market} is not valid value")
731
399
 
732
- result = await self.private_get_fetch_opened_order_krw(self.extend(params))
733
- elif base_market == "USD":
734
- market_code = await self.get_market_code_in_broker("ALL", base_market=base_market)
735
- params = {
736
- "CANO": acc_num[:8],
737
- "ACNT_PRDT_CD": acc_num[-2:],
738
- "PDNO": symbol if symbol is not None else "%",
739
- "OVRS_EXCG_CD": market_code,
740
- "SORT_SQN": "DS", # DS : 정순, AS : 역순
741
- "CTX_AREA_NK200": "",
742
- "CTX_AREA_FK200": "",
743
- }
400
+ common_header = self.create_common_header(request_params=params)
401
+ response = await self.private_post_send_cancel_order(self.extend(params))
744
402
 
745
- result = await self.fetchOpenedOrderForUS(self.extend(params))
746
- else:
747
- return self.get_error_response(
748
- error_code="market_error", error_message=f"{base_market} market is not yet supported."
749
- )
403
+ common_response = self.get_common_response(response=response)
404
+ if common_response.success != "0":
405
+ return ksxt.models.KsxtCancelOrderResponse(header=common_header, response=common_response, info=None)
750
406
 
751
- response = self.get_response(result, params)
752
- if response["response"]["success"] != "0":
753
- return response
407
+ parsed_info = self.parser.parse_cancel_order(response=response, base_market=base_market)
754
408
 
755
- return self.parser.parse_open_order_history(response, base_market)
409
+ return ksxt.models.KsxtCancelOrderResponse(header=common_header, response=common_response, info=parsed_info)
756
410
 
757
- async def fetch_closed_order(
411
+ @AsyncExchange.check_token
412
+ async def create_order(
758
413
  self,
759
414
  acc_num: str,
760
- symbol: Optional[str] = "",
761
- start: Optional[str] = None,
762
- end: Optional[str] = None,
415
+ symbol: str,
416
+ ticket_type: Literal["EntryLong"] | Literal["EntryShort"] | Literal["ExitLong"] | Literal["ExitShort"],
417
+ otype: Literal["limit"] | Literal["market"],
418
+ price: float | None = 0,
419
+ qty: float | None = 0,
420
+ amount: float | None = 0,
763
421
  base_market: str = "KRW",
764
- ):
765
- if start is None:
766
- start = KoreaInvest.now(base_market=base_market).strftime("%Y%m%d")
767
-
768
- if end is None:
769
- end = KoreaInvest.now(base_market=base_market).strftime("%Y%m%d")
770
-
771
- if base_market == "KRW":
772
- params = {
773
- "CANO": acc_num[:8],
774
- "ACNT_PRDT_CD": acc_num[-2:],
775
- "INQR_STRT_DT": start,
776
- "INQR_END_DT": end,
777
- "SLL_BUY_DVSN_CD": "00",
778
- "INQR_DVSN": "00",
779
- "PDNO": symbol,
780
- "CCLD_DVSN": "01",
781
- "ORD_GNO_BRNO": "",
782
- "ODNO": "",
783
- "INQR_DVSN_3": "00",
784
- "INQR_DVSN_1": "",
785
- "CTX_AREA_FK100": "",
786
- "CTX_AREA_NK100": "",
787
- }
788
-
789
- result = await self.private_get_fetch_closed_order_krw(self.extend(params))
790
- elif base_market == "USD":
791
- market_code = await self.get_market_code_in_broker("ALL", base_market=base_market)
792
- params = {
793
- "CANO": acc_num[:8],
794
- "ACNT_PRDT_CD": acc_num[-2:],
795
- "PDNO": symbol if symbol is not None else "%",
796
- "ORD_STRT_DT": start,
797
- "ORD_END_DT": end,
798
- "SLL_BUY_DVSN": "00",
799
- "CCLD_NCCS_DVSN": "01" if not self.is_dev else "00",
800
- "OVRS_EXCG_CD": market_code,
801
- "SORT_SQN": "DS", # DS : 정순, AS : 역순
802
- "ORD_DT": "",
803
- "ORD_GNO_BRNO": "",
804
- "ODNO": "",
805
- "CTX_AREA_NK200": "",
806
- "CTX_AREA_FK200": "",
807
- }
808
-
809
- result = await self.fetchClosedOrderForUS(self.extend(params))
810
-
811
- response = self.get_response(result, params)
812
- if response["response"]["success"] != "0":
813
- return response
814
-
815
- return self.parser.parse_closed_order_history(response, base_market)
816
-
817
- # endregion broker
818
-
819
- async def get_market_code_in_feeder(self, symbol: str, base_market: str = "KRW"):
422
+ ) -> ksxt.models.KsxtCreateOrderResponse:
423
+ if otype.lower() == "limit":
424
+ order_dvsn = "00"
425
+ elif otype.lower() == "market":
426
+ order_dvsn = "01"
427
+ params = {
428
+ "CANO": acc_num[:8],
429
+ "ACNT_PRDT_CD": acc_num[-2:],
430
+ "PDNO": symbol,
431
+ "ORD_DVSN": order_dvsn,
432
+ "ORD_QTY": str(qty), # string type 으로 설정
433
+ "ORD_UNPR": str(price), # string type 으로 설정
434
+ }
435
+
436
+ common_header = self.create_common_header(request_params=params)
437
+
438
+ if ticket_type == "EntryLong":
439
+ response = await self.private_post_send_order_entry(self.extend(params))
440
+ elif ticket_type == "ExitLong":
441
+ response = await self.private_post_send_order_exit(self.extend(params))
442
+
443
+ common_response = self.get_common_response(response=response)
444
+ if common_response.success != "0":
445
+ return ksxt.models.KsxtCreateOrderResponse(header=common_header, response=common_response, info=None)
446
+
447
+ parsed_info = self.parser.parse_create_order(response=response, base_market=base_market)
448
+
449
+ return ksxt.models.KsxtCreateOrderResponse(header=common_header, response=common_response, info=parsed_info)
450
+
451
+ def get_market_code_in_feeder(self, symbol: str, base_market: str = "KRW"):
820
452
  if base_market == "KRW":
821
453
  return ""
822
454
  elif base_market == "USD":
823
455
  if symbol.upper() == "ALL":
824
456
  return "NASD"
825
457
 
826
- response = await self.fetch_security(symbol=symbol, base_market=base_market)
458
+ response = self.fetch_security(symbol=symbol, base_market=base_market)
827
459
  return response["exchange"]
828
460
  else:
829
461
  return ""
830
462
 
831
- async def get_market_code_in_broker(self, symbol: str, base_market: str = "KRW"):
463
+ def get_market_code_in_broker(self, symbol: str, base_market: str = "KRW"):
832
464
  if base_market == "KRW":
833
465
  return ""
834
466
  elif base_market == "USD":
835
467
  if symbol.upper() == "ALL":
836
468
  return "NASD"
837
469
 
838
- response = await self.fetch_security(symbol=symbol, base_market=base_market)
470
+ response = self.fetch_security(symbol=symbol, base_market=base_market)
839
471
  exname = response["exchange"]
840
472
  if exname == "NYS":
841
473
  return "NYSE"