ksxt 0.0.8__py3-none-any.whl → 0.0.10__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.
Files changed (141) hide show
  1. ksxt/__init__.py +3 -1
  2. ksxt/__pycache__/__init__.cpython-312.pyc +0 -0
  3. ksxt/__pycache__/bithumb.cpython-312.pyc +0 -0
  4. ksxt/__pycache__/koreainvest.cpython-312.pyc +0 -0
  5. ksxt/__pycache__/upbit.cpython-312.pyc +0 -0
  6. ksxt/api/__init__.py +26 -0
  7. ksxt/api/__pycache__/__init__.cpython-312.pyc +0 -0
  8. ksxt/api/__pycache__/bithumb.cpython-312.pyc +0 -0
  9. ksxt/api/__pycache__/koreainvest.cpython-312.pyc +0 -0
  10. ksxt/api/__pycache__/upbit.cpython-312.pyc +0 -0
  11. ksxt/api/auto/api_generator.py +54 -0
  12. ksxt/api/auto/bithumb.py +35 -0
  13. ksxt/api/auto/koreainvest.py +49 -0
  14. ksxt/api/auto/upbit.py +39 -0
  15. ksxt/api/bithumb.py +42 -0
  16. ksxt/api/koreainvest.py +40 -0
  17. ksxt/api/upbit.py +54 -0
  18. ksxt/async_/__init__.py +4 -0
  19. ksxt/async_/__pycache__/__init__.cpython-312.pyc +0 -0
  20. ksxt/async_/__pycache__/bithumb.cpython-312.pyc +0 -0
  21. ksxt/async_/__pycache__/koreainvest.cpython-312.pyc +0 -0
  22. ksxt/async_/__pycache__/upbit.cpython-312.pyc +0 -0
  23. ksxt/async_/base/__init__.py +0 -0
  24. ksxt/async_/base/__pycache__/__init__.cpython-312.pyc +0 -0
  25. ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc +0 -0
  26. ksxt/async_/base/__pycache__/throttler.cpython-312.pyc +0 -0
  27. ksxt/async_/base/async_exchange.py +232 -0
  28. ksxt/async_/base/throttler.py +63 -0
  29. ksxt/async_/bithumb.py +455 -0
  30. ksxt/async_/koreainvest.py +849 -0
  31. ksxt/async_/upbit.py +488 -0
  32. ksxt/base/__pycache__/__init__.cpython-312.pyc +0 -0
  33. ksxt/base/__pycache__/errors.cpython-312.pyc +0 -0
  34. ksxt/base/__pycache__/exchange.cpython-312.pyc +0 -0
  35. ksxt/base/__pycache__/rest_exchange.cpython-312.pyc +0 -0
  36. ksxt/base/__pycache__/types.cpython-312.pyc +0 -0
  37. ksxt/base/com_exchange.py +2 -2
  38. ksxt/base/errors.py +10 -0
  39. ksxt/base/exchange.py +188 -497
  40. ksxt/base/rest_exchange.py +297 -113
  41. ksxt/base/types.py +1 -36
  42. ksxt/bithumb.py +504 -0
  43. ksxt/config/__init__.py +2 -1
  44. ksxt/config/__pycache__/__init__.cpython-312.pyc +0 -0
  45. ksxt/config/bithumb.toml +380 -0
  46. ksxt/config/koreainvest.toml +312 -0
  47. ksxt/config/token.toml +7 -0
  48. ksxt/config/upbit.toml +428 -0
  49. ksxt/koreainvest.py +409 -1055
  50. ksxt/market/__pycache__/base.cpython-312.pyc +0 -0
  51. ksxt/market/__pycache__/db.cpython-312.pyc +0 -0
  52. ksxt/market/__pycache__/logging.cpython-312.pyc +0 -0
  53. ksxt/market/__pycache__/manager.cpython-312.pyc +0 -0
  54. ksxt/market/__pycache__/markets.cpython-312.pyc +0 -0
  55. ksxt/market/base.py +50 -50
  56. ksxt/market/db.py +5 -4
  57. ksxt/market/krx/__pycache__/kosdaq.cpython-312.pyc +0 -0
  58. ksxt/market/krx/__pycache__/kospi.cpython-312.pyc +0 -0
  59. ksxt/market/krx/__pycache__/stock.cpython-312.pyc +0 -0
  60. ksxt/market/krx/kosdaq.py +150 -147
  61. ksxt/market/krx/kospi.py +179 -175
  62. ksxt/market/krx/stock.py +136 -134
  63. ksxt/market/logging.py +4 -4
  64. ksxt/market/manager.py +10 -12
  65. ksxt/market/markets.py +1 -1
  66. ksxt/market/us/__pycache__/amex.cpython-312.pyc +0 -0
  67. ksxt/market/us/__pycache__/nasdaq.cpython-312.pyc +0 -0
  68. ksxt/market/us/__pycache__/nyse.cpython-312.pyc +0 -0
  69. ksxt/market/us/__pycache__/stock.cpython-312.pyc +0 -0
  70. ksxt/market/us/amex.py +31 -31
  71. ksxt/market/us/nasdaq.py +31 -31
  72. ksxt/market/us/nyse.py +31 -31
  73. ksxt/market/us/stock.py +20 -28
  74. ksxt/models/__init__.py +16 -0
  75. ksxt/models/__pycache__/__init__.cpython-312.pyc +0 -0
  76. ksxt/models/__pycache__/balance.cpython-312.pyc +0 -0
  77. ksxt/models/__pycache__/cash.cpython-312.pyc +0 -0
  78. ksxt/models/__pycache__/common.cpython-312.pyc +0 -0
  79. ksxt/models/__pycache__/error.cpython-312.pyc +0 -0
  80. ksxt/models/__pycache__/historical.cpython-312.pyc +0 -0
  81. ksxt/models/__pycache__/market.cpython-312.pyc +0 -0
  82. ksxt/models/__pycache__/order.cpython-312.pyc +0 -0
  83. ksxt/models/__pycache__/orderbook.cpython-312.pyc +0 -0
  84. ksxt/models/__pycache__/ticker.cpython-312.pyc +0 -0
  85. ksxt/models/__pycache__/token.cpython-312.pyc +0 -0
  86. ksxt/models/__pycache__/transaction.cpython-312.pyc +0 -0
  87. ksxt/models/balance.py +30 -0
  88. ksxt/models/cash.py +15 -0
  89. ksxt/models/common.py +31 -0
  90. ksxt/models/error.py +13 -0
  91. ksxt/models/historical.py +26 -0
  92. ksxt/models/market.py +81 -0
  93. ksxt/models/order.py +42 -0
  94. ksxt/models/orderbook.py +32 -0
  95. ksxt/models/ticker.py +25 -0
  96. ksxt/models/token.py +14 -0
  97. ksxt/models/transaction.py +79 -0
  98. ksxt/parser/__pycache__/bithumb.cpython-312.pyc +0 -0
  99. ksxt/parser/__pycache__/koreainvest.cpython-312.pyc +0 -0
  100. ksxt/parser/__pycache__/parser.cpython-312.pyc +0 -0
  101. ksxt/parser/__pycache__/upbit.cpython-312.pyc +0 -0
  102. ksxt/parser/bithumb.py +300 -0
  103. ksxt/parser/koreainvest.py +323 -0
  104. ksxt/parser/parser.py +114 -0
  105. ksxt/parser/upbit.py +308 -0
  106. ksxt/upbit.py +499 -0
  107. ksxt/utils/__pycache__/safer.cpython-312.pyc +0 -0
  108. ksxt/utils/__pycache__/sorter.cpython-312.pyc +0 -0
  109. ksxt/utils/__pycache__/timer.cpython-312.pyc +0 -0
  110. ksxt/utils/safer.py +48 -0
  111. ksxt/utils/sorter.py +8 -0
  112. ksxt/utils/timer.py +47 -0
  113. {ksxt-0.0.8.dist-info → ksxt-0.0.10.dist-info}/METADATA +11 -1
  114. ksxt-0.0.10.dist-info/RECORD +119 -0
  115. {ksxt-0.0.8.dist-info → ksxt-0.0.10.dist-info}/WHEEL +1 -1
  116. ksxt/__pycache__/__init__.cpython-39.pyc +0 -0
  117. ksxt/__pycache__/koreainvest.cpython-39.pyc +0 -0
  118. ksxt/base/__pycache__/__init__.cpython-39.pyc +0 -0
  119. ksxt/base/__pycache__/exchange.cpython-39.pyc +0 -0
  120. ksxt/base/__pycache__/rest_exchange.cpython-39.pyc +0 -0
  121. ksxt/base/__pycache__/restexchange.cpython-39.pyc +0 -0
  122. ksxt/base/__pycache__/types.cpython-39.pyc +0 -0
  123. ksxt/base/api_response.py +0 -68
  124. ksxt/config/__pycache__/__init__.cpython-39.pyc +0 -0
  125. ksxt/config/tr_app.json +0 -381
  126. ksxt/config/tr_dev.json +0 -446
  127. ksxt/market/__pycache__/base.cpython-39.pyc +0 -0
  128. ksxt/market/__pycache__/db.cpython-39.pyc +0 -0
  129. ksxt/market/__pycache__/logging.cpython-39.pyc +0 -0
  130. ksxt/market/__pycache__/manager.cpython-39.pyc +0 -0
  131. ksxt/market/__pycache__/markets.cpython-39.pyc +0 -0
  132. ksxt/market/krx/__pycache__/kosdaq.cpython-39.pyc +0 -0
  133. ksxt/market/krx/__pycache__/kospi.cpython-39.pyc +0 -0
  134. ksxt/market/krx/__pycache__/stock.cpython-39.pyc +0 -0
  135. ksxt/market/us/__pycache__/amex.cpython-39.pyc +0 -0
  136. ksxt/market/us/__pycache__/nasdaq.cpython-39.pyc +0 -0
  137. ksxt/market/us/__pycache__/nyse.cpython-39.pyc +0 -0
  138. ksxt/market/us/__pycache__/stock.cpython-39.pyc +0 -0
  139. ksxt-0.0.8.dist-info/RECORD +0 -49
  140. {ksxt-0.0.8.dist-info → ksxt-0.0.10.dist-info}/LICENSE.txt +0 -0
  141. {ksxt-0.0.8.dist-info → ksxt-0.0.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,849 @@
1
+ from datetime import datetime, timedelta
2
+ import json
3
+ import os
4
+ import time
5
+ from typing import Any, Dict, Optional
6
+ from ksxt.api.koreainvest import ImplicitAPI
7
+ from ksxt.async_.base.async_exchange import AsyncExchange
8
+ from ksxt.market.manager import MarketManager
9
+ from ksxt.parser.koreainvest import KoreaInvestParser
10
+
11
+
12
+ class KoreaInvest(AsyncExchange, ImplicitAPI):
13
+ def __init__(self, config: Dict = None) -> None:
14
+ super().__init__(config, "koreainvest.toml")
15
+ self.parser = KoreaInvestParser()
16
+
17
+ def safe_symbol(self, base_market: str, security: str) -> str:
18
+ return f"{security}"
19
+
20
+ def is_activate(self, path, security_type) -> bool:
21
+ mode = "dev" if self.is_dev == True else "app"
22
+
23
+ tr_id = self.apis[self.type][security_type][path][mode]["tr_id"]
24
+
25
+ if not bool(tr_id):
26
+ return False
27
+
28
+ return super().is_activate(path=path, security_type=security_type)
29
+
30
+ @AsyncExchange.check_token
31
+ def sign(
32
+ self,
33
+ path,
34
+ security_type,
35
+ method_type,
36
+ api_type: Any = "public",
37
+ headers: Optional[Any] = None,
38
+ body: Optional[Any] = None,
39
+ params: Optional[Any] = None,
40
+ config={},
41
+ ):
42
+ mode = "dev" if self.is_dev == True else "app"
43
+
44
+ host_url = self.apis[self.type][mode]["hostname"]
45
+ destination = self.apis[self.type][security_type][path]["url"]
46
+ version = self.apis[self.type]["version"]
47
+ params["version"] = version
48
+ destination = self.implode_params(destination, params)
49
+
50
+ url = host_url + "/" + destination
51
+
52
+ tr_id = self.apis[self.type][security_type][path][mode]["tr_id"]
53
+ 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
+ )
70
+
71
+ if method_type.upper() == "POST":
72
+ body = json.dumps(params)
73
+ params = {}
74
+
75
+ return {"url": url, "method": method_type, "headers": headers, "body": body, "params": params}
76
+
77
+ # region _____
78
+ def create_token(self):
79
+ import logging
80
+
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
85
+
86
+ request_headers = self.prepare_request_headers()
87
+
88
+ body = {"grant_type": "client_credentials", "appkey": self.open_key, "appsecret": self.secret_key}
89
+
90
+ body = json.dumps(body, separators=(",", ":"))
91
+
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
97
+
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"])
104
+
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."
165
+ )
166
+
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."
195
+ )
196
+
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."
284
+ )
285
+
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
308
+
309
+ # region private feeder
310
+ # @RestExchange.check_token
311
+ async def fetch_balance(self, acc_num: str, base_market: str = "KRW"):
312
+ if base_market == "KRW":
313
+ params = {
314
+ "CANO": acc_num[:8],
315
+ "ACNT_PRDT_CD": acc_num[-2:],
316
+ "AFHR_FLPR_YN": "N",
317
+ "OFL_YN": "",
318
+ "INQR_DVSN": "01",
319
+ "UNPR_DVSN": "01",
320
+ "FUND_STTL_ICLD_YN": "N",
321
+ "FNCG_AMT_AUTO_RDPT_YN": "N",
322
+ "PRCS_DVSN": "01",
323
+ "CTX_AREA_FK100": "",
324
+ "CTX_AREA_NK100": "",
325
+ }
326
+
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
+ }
338
+
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
+ )
344
+
345
+ response = self.get_response(result, params)
346
+ if response["response"]["success"] != "0":
347
+ return response
348
+
349
+ return self.parser.parse_balance(response, base_market)
350
+
351
+ async def fetch_cash(self, acc_num: str, base_market: str = "KRW"):
352
+ if base_market == "KRW":
353
+ params = {
354
+ "CANO": acc_num[:8],
355
+ "ACNT_PRDT_CD": acc_num[-2:],
356
+ "AFHR_FLPR_YN": "N",
357
+ "OFL_YN": "",
358
+ "INQR_DVSN": "01",
359
+ "UNPR_DVSN": "01",
360
+ "FUND_STTL_ICLD_YN": "N",
361
+ "FNCG_AMT_AUTO_RDPT_YN": "N",
362
+ "PRCS_DVSN": "01",
363
+ "CTX_AREA_FK100": "",
364
+ "CTX_AREA_NK100": "",
365
+ }
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
+ else:
381
+ return self.get_error_response(
382
+ error_code="market_error", error_message=f"{base_market} market is not yet supported."
383
+ )
384
+
385
+ response = self.get_response(result, params)
386
+ if response["response"]["success"] != "0":
387
+ return response
388
+
389
+ return self.parser.parse_cash(response, base_market)
390
+
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}
398
+
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
+ )
404
+
405
+ response = self.get_response(result, params)
406
+ if response["response"]["success"] != "0":
407
+ return response
408
+
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"):
412
+ 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
+ }
427
+
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
+ }
439
+
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
+ )
445
+
446
+ response = self.get_response(result, params)
447
+ if response["response"]["success"] != "0":
448
+ return response
449
+
450
+ return self.parser.parse_pnl(response, base_market)
451
+
452
+ async def fetch_screener_list(self, user_id, base_market: str = "KRW"):
453
+ params = {"USER_ID": user_id}
454
+
455
+ if base_market == "KRW":
456
+ result = await self.private_get_fetch_screener_list_krw(self.extend(params))
457
+ else:
458
+ return self.get_error_response(
459
+ error_code="market_error", error_message=f"{base_market} market is not yet supported."
460
+ )
461
+
462
+ response = self.get_response(result, params)
463
+ if response["response"]["success"] != "0":
464
+ return response
465
+
466
+ return self.parser.parse_screener_list(response, base_market)
467
+
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}
471
+
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))
477
+
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
486
+
487
+ return self.parser.parse_screener(response, base_market)
488
+
489
+ async def fetch_security(self, symbol: str, base_market: str = "KRW"):
490
+ 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))
498
+ else:
499
+ return self.get_error_response(
500
+ error_code="market_error", error_message=f"{base_market} market is not yet supported."
501
+ )
502
+
503
+ response = self.get_response(result, params)
504
+ if response["response"]["success"] != "0":
505
+ return response
506
+
507
+ return self.parser.parse_security(response, base_market)
508
+
509
+ # endregion private feeder
510
+
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"
527
+
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
+ }
536
+
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
+ )
552
+
553
+ if ticket_type == "entry_long":
554
+ sell_type = ""
555
+ elif ticket_type == "exit_long":
556
+ sell_type = "00"
557
+ else:
558
+ return
559
+
560
+ market_code = await self.get_market_code_in_broker(symbol=symbol, base_market=base_market)
561
+ 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",
571
+ }
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
+ else:
580
+ return self.get_error_response(
581
+ error_code="market_error", error_message=f"{base_market} market is not yet supported."
582
+ )
583
+
584
+ response = self.get_response(result, params)
585
+ if response["response"]["success"] != "0":
586
+ return response
587
+
588
+ return self.parser.parse_create_order(response, base_market)
589
+
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"
615
+
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
+ )
622
+
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
+ }
634
+
635
+ result = await self.sendCancelOrderForUS(self.extend(params))
636
+ else:
637
+ return self.get_error_response(
638
+ error_code="market_error", error_message=f"{base_market} market is not yet supported."
639
+ )
640
+
641
+ response = self.get_response(result, params)
642
+ if response["response"]["success"] != "0":
643
+ return response
644
+
645
+ return self.parser.parse_cancel_order(response, base_market)
646
+
647
+ async def modify_order(
648
+ self,
649
+ acc_num: str,
650
+ order_id: str,
651
+ price: float,
652
+ qty: float,
653
+ symbol: Optional[str] = "",
654
+ base_market: str = "KRW",
655
+ **kwargs,
656
+ ):
657
+ if base_market == "KRW":
658
+ params = {
659
+ "CANO": acc_num[:8],
660
+ "ACNT_PRDT_CD": acc_num[-2:],
661
+ "KRX_FWDG_ORD_ORGNO": "",
662
+ "ORGN_ODNO": str(order_id),
663
+ "RVSE_CNCL_DVSN_CD": "01",
664
+ "ORD_DVSN": "00",
665
+ "ORD_QTY": str(qty),
666
+ "ORD_UNPR": str(price),
667
+ "QTY_ALL_ORD_YN": "N",
668
+ }
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
+ else:
690
+ return self.get_error_response(
691
+ error_code="market_error", error_message=f"{base_market} market is not yet supported."
692
+ )
693
+
694
+ response = self.get_response(result, params)
695
+ if response["response"]["success"] != "0":
696
+ return response
697
+
698
+ return self.parser.parse_modify_order(response, base_market)
699
+
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")
710
+
711
+ if end is None:
712
+ end = KoreaInvest.now(base_market=base_market).strftime("%Y%m%d")
713
+
714
+ if base_market == "KRW":
715
+ params = {
716
+ "CANO": acc_num[:8],
717
+ "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": "",
730
+ }
731
+
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
+ }
744
+
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
+ )
750
+
751
+ response = self.get_response(result, params)
752
+ if response["response"]["success"] != "0":
753
+ return response
754
+
755
+ return self.parser.parse_open_order_history(response, base_market)
756
+
757
+ async def fetch_closed_order(
758
+ self,
759
+ acc_num: str,
760
+ symbol: Optional[str] = "",
761
+ start: Optional[str] = None,
762
+ end: Optional[str] = None,
763
+ 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"):
820
+ if base_market == "KRW":
821
+ return ""
822
+ elif base_market == "USD":
823
+ if symbol.upper() == "ALL":
824
+ return "NASD"
825
+
826
+ response = await self.fetch_security(symbol=symbol, base_market=base_market)
827
+ return response["exchange"]
828
+ else:
829
+ return ""
830
+
831
+ async def get_market_code_in_broker(self, symbol: str, base_market: str = "KRW"):
832
+ if base_market == "KRW":
833
+ return ""
834
+ elif base_market == "USD":
835
+ if symbol.upper() == "ALL":
836
+ return "NASD"
837
+
838
+ response = await self.fetch_security(symbol=symbol, base_market=base_market)
839
+ exname = response["exchange"]
840
+ if exname == "NYS":
841
+ return "NYSE"
842
+ elif exname == "NAS":
843
+ return "NASD"
844
+ elif exname == "AMS":
845
+ return "AMEX"
846
+ else:
847
+ return ""
848
+ else:
849
+ return ""