ksxt 1.0.0__tar.gz → 1.0.1__tar.gz

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 (134) hide show
  1. {ksxt-1.0.0/src/ksxt.egg-info → ksxt-1.0.1}/PKG-INFO +1 -1
  2. {ksxt-1.0.0 → ksxt-1.0.1}/pyproject.toml +1 -1
  3. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/__pycache__/koreainvest.cpython-312.pyc +0 -0
  4. ksxt-1.0.1/src/ksxt/async_/__pycache__/koreainvest.cpython-312.pyc +0 -0
  5. ksxt-1.0.1/src/ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc +0 -0
  6. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/base/async_exchange.py +35 -42
  7. ksxt-1.0.1/src/ksxt/async_/koreainvest.py +481 -0
  8. ksxt-1.0.1/src/ksxt/base/__pycache__/exchange.cpython-312.pyc +0 -0
  9. ksxt-1.0.1/src/ksxt/base/__pycache__/rate_limiter.cpython-312.pyc +0 -0
  10. ksxt-1.0.1/src/ksxt/base/__pycache__/rest_exchange.cpython-312.pyc +0 -0
  11. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/exchange.py +5 -0
  12. ksxt-1.0.1/src/ksxt/base/rate_limiter.py +88 -0
  13. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/rest_exchange.py +52 -26
  14. ksxt-1.0.1/src/ksxt/config/__init__.py +5 -0
  15. ksxt-1.0.1/src/ksxt/config/__pycache__/__init__.cpython-312.pyc +0 -0
  16. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/config/bithumb.toml +2 -1
  17. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/config/upbit.toml +6 -1
  18. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/koreainvest.py +6 -26
  19. {ksxt-1.0.0 → ksxt-1.0.1/src/ksxt.egg-info}/PKG-INFO +1 -1
  20. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt.egg-info/SOURCES.txt +2 -1
  21. ksxt-1.0.0/src/ksxt/async_/__pycache__/koreainvest.cpython-312.pyc +0 -0
  22. ksxt-1.0.0/src/ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc +0 -0
  23. ksxt-1.0.0/src/ksxt/async_/base/throttler.py +0 -63
  24. ksxt-1.0.0/src/ksxt/async_/koreainvest.py +0 -849
  25. ksxt-1.0.0/src/ksxt/base/__pycache__/exchange.cpython-312.pyc +0 -0
  26. ksxt-1.0.0/src/ksxt/base/__pycache__/rest_exchange.cpython-312.pyc +0 -0
  27. ksxt-1.0.0/src/ksxt/config/__init__.py +0 -3
  28. ksxt-1.0.0/src/ksxt/config/__pycache__/__init__.cpython-312.pyc +0 -0
  29. {ksxt-1.0.0 → ksxt-1.0.1}/LICENSE.txt +0 -0
  30. {ksxt-1.0.0 → ksxt-1.0.1}/MANIFEST.in +0 -0
  31. {ksxt-1.0.0 → ksxt-1.0.1}/README.md +0 -0
  32. {ksxt-1.0.0 → ksxt-1.0.1}/setup.cfg +0 -0
  33. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/__init__.py +0 -0
  34. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/__pycache__/__init__.cpython-312.pyc +0 -0
  35. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/__pycache__/bithumb.cpython-312.pyc +0 -0
  36. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/__pycache__/upbit.cpython-312.pyc +0 -0
  37. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/__init__.py +0 -0
  38. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/__pycache__/__init__.cpython-312.pyc +0 -0
  39. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/__pycache__/bithumb.cpython-312.pyc +0 -0
  40. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/__pycache__/koreainvest.cpython-312.pyc +0 -0
  41. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/__pycache__/upbit.cpython-312.pyc +0 -0
  42. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/auto/api_generator.py +0 -0
  43. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/auto/bithumb.py +0 -0
  44. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/auto/koreainvest.py +0 -0
  45. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/auto/upbit.py +0 -0
  46. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/bithumb.py +0 -0
  47. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/koreainvest.py +0 -0
  48. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/api/upbit.py +0 -0
  49. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/__init__.py +0 -0
  50. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/__pycache__/__init__.cpython-312.pyc +0 -0
  51. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/__pycache__/bithumb.cpython-312.pyc +0 -0
  52. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/__pycache__/upbit.cpython-312.pyc +0 -0
  53. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/base/__init__.py +0 -0
  54. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/base/__pycache__/__init__.cpython-312.pyc +0 -0
  55. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/base/__pycache__/throttler.cpython-312.pyc +0 -0
  56. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/bithumb.py +0 -0
  57. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/async_/upbit.py +0 -0
  58. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/__init__.py +0 -0
  59. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/__pycache__/__init__.cpython-312.pyc +0 -0
  60. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/__pycache__/errors.cpython-312.pyc +0 -0
  61. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/__pycache__/types.cpython-312.pyc +0 -0
  62. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/com_exchange.py +0 -0
  63. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/errors.py +0 -0
  64. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/base/types.py +0 -0
  65. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/bithumb.py +0 -0
  66. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/config/koreainvest.toml +0 -0
  67. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/config/token.toml +0 -0
  68. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/__pycache__/base.cpython-312.pyc +0 -0
  69. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/__pycache__/db.cpython-312.pyc +0 -0
  70. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/__pycache__/logging.cpython-312.pyc +0 -0
  71. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/__pycache__/manager.cpython-312.pyc +0 -0
  72. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/__pycache__/markets.cpython-312.pyc +0 -0
  73. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/base.py +0 -0
  74. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/db.py +0 -0
  75. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/krx/__pycache__/kosdaq.cpython-312.pyc +0 -0
  76. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/krx/__pycache__/kospi.cpython-312.pyc +0 -0
  77. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/krx/__pycache__/stock.cpython-312.pyc +0 -0
  78. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/krx/kosdaq.py +0 -0
  79. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/krx/kospi.py +0 -0
  80. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/krx/stock.py +0 -0
  81. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/logging.py +0 -0
  82. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/manager.py +0 -0
  83. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/markets.py +0 -0
  84. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/__pycache__/amex.cpython-312.pyc +0 -0
  85. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/__pycache__/nasdaq.cpython-312.pyc +0 -0
  86. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/__pycache__/nyse.cpython-312.pyc +0 -0
  87. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/__pycache__/stock.cpython-312.pyc +0 -0
  88. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/amex.py +0 -0
  89. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/nasdaq.py +0 -0
  90. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/nyse.py +0 -0
  91. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/market/us/stock.py +0 -0
  92. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__init__.py +0 -0
  93. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/__init__.cpython-312.pyc +0 -0
  94. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/balance.cpython-312.pyc +0 -0
  95. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/cash.cpython-312.pyc +0 -0
  96. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/common.cpython-312.pyc +0 -0
  97. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/error.cpython-312.pyc +0 -0
  98. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/historical.cpython-312.pyc +0 -0
  99. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/market.cpython-312.pyc +0 -0
  100. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/order.cpython-312.pyc +0 -0
  101. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/orderbook.cpython-312.pyc +0 -0
  102. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/ticker.cpython-312.pyc +0 -0
  103. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/token.cpython-312.pyc +0 -0
  104. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/__pycache__/transaction.cpython-312.pyc +0 -0
  105. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/balance.py +0 -0
  106. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/cash.py +0 -0
  107. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/common.py +0 -0
  108. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/error.py +0 -0
  109. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/historical.py +0 -0
  110. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/market.py +0 -0
  111. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/order.py +0 -0
  112. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/orderbook.py +0 -0
  113. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/ticker.py +0 -0
  114. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/token.py +0 -0
  115. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/models/transaction.py +0 -0
  116. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/__pycache__/bithumb.cpython-312.pyc +0 -0
  117. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/__pycache__/koreainvest.cpython-312.pyc +0 -0
  118. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/__pycache__/parser.cpython-312.pyc +0 -0
  119. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/__pycache__/upbit.cpython-312.pyc +0 -0
  120. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/bithumb.py +0 -0
  121. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/koreainvest.py +0 -0
  122. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/parser.py +0 -0
  123. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/parser/upbit.py +0 -0
  124. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/sample/symbol_sync.ipynb +0 -0
  125. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/upbit.py +0 -0
  126. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/utils/__pycache__/safer.cpython-312.pyc +0 -0
  127. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/utils/__pycache__/sorter.cpython-312.pyc +0 -0
  128. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/utils/__pycache__/timer.cpython-312.pyc +0 -0
  129. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/utils/safer.py +0 -0
  130. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/utils/sorter.py +0 -0
  131. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt/utils/timer.py +0 -0
  132. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt.egg-info/dependency_links.txt +0 -0
  133. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt.egg-info/requires.txt +0 -0
  134. {ksxt-1.0.0 → ksxt-1.0.1}/src/ksxt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ksxt
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  License: MIT License
5
5
 
6
6
  Copyright © 2023 AMOSA
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "ksxt"
11
- version = "1.0.0"
11
+ version = "1.0.1"
12
12
  readme = "README.md"
13
13
  license = { file="LICENSE.txt" }
14
14
 
@@ -1,19 +1,14 @@
1
1
  import asyncio
2
+ import time
2
3
  import aiohttp
3
- import json
4
- import os
5
- import platform
6
- import tomllib
7
4
  from datetime import datetime
8
- from pathlib import Path
9
5
  from typing import Any, Dict, Literal, Optional
10
6
 
11
- import yarl
12
7
 
13
- from ksxt.async_.base.throttler import Throttler
14
8
  from ksxt.base.errors import NotSupportedError
9
+ from ksxt.base.rate_limiter import RateLimiterContext
15
10
  from ksxt.base.rest_exchange import RestExchange
16
- from ksxt.config import CONFIG_DIR
11
+ from ksxt.config import VALID_METHODS
17
12
  import ksxt.models
18
13
 
19
14
 
@@ -23,22 +18,26 @@ class AsyncExchange(RestExchange):
23
18
  def __init__(self, config: Dict = None, filename: str = None):
24
19
  super().__init__(config, filename)
25
20
 
26
- self.asyncio_loop = asyncio.get_event_loop()
27
- self.session = aiohttp.ClientSession()
28
- self.throttle = Throttler({}, self.asyncio_loop)
21
+ self.asyncio_loop = None
22
+ self.session: aiohttp.ClientSession = None
29
23
 
30
24
  async def initialize(self):
31
25
  if self.asyncio_loop is None:
32
26
  self.asyncio_loop = asyncio.get_event_loop()
33
- if self.session is None:
27
+
28
+ if self.session is None or (
29
+ self.session_last_used and (time.time() - self.session_last_used > self.session_lifetime)
30
+ ):
31
+ if self.session:
32
+ await self.session.close()
34
33
  self.session = aiohttp.ClientSession()
35
- if self.throttle is None:
36
- self.throttle = Throttler({}, self.asyncio_loop)
34
+ self.session_last_used = time.time()
37
35
 
38
36
  async def close(self):
39
37
  if self.session:
40
38
  await self.session.close()
41
39
  self.session = None
40
+ self.session_last_used = None
42
41
 
43
42
  async def __aenter__(self):
44
43
  await self.initialize()
@@ -47,49 +46,43 @@ class AsyncExchange(RestExchange):
47
46
  async def __aexit__(self, *args):
48
47
  await self.close()
49
48
 
50
- def _get_api_from_file(self, filename: str):
51
- if filename is None:
52
- tr_config_filename = "tr_dev.json" if self.is_dev else "tr_app.json"
53
- else:
54
- tr_config_filename = filename
55
-
56
- config_path = os.path.join(CONFIG_DIR, tr_config_filename)
49
+ async def fetch(self, url, method="GET", headers=None, body=None, params=None):
50
+ # Ensure that resources are initialized before the request
51
+ await self.initialize()
57
52
 
58
- if Path(tr_config_filename).suffix == ".json":
59
- with open(
60
- config_path,
61
- encoding="utf-8",
62
- ) as f:
63
- c = json.load(f)
64
- return {"apis": c[self.name]}
53
+ method_lower = method.lower()
54
+ if method_lower not in VALID_METHODS:
55
+ raise ValueError(f"Invalid HTTP method: {method}")
65
56
 
66
- elif Path(tr_config_filename).suffix == ".toml":
67
- with open(config_path, mode="rb") as f:
68
- c = tomllib.load(f)
69
- return c
57
+ session_method = getattr(self.session, method.lower())
70
58
 
71
- async def fetch(self, url, method="GET", headers=None, body=None, params=None):
72
- request_headers = headers
73
- request_body = str(body).encode() if body else None
74
- request_params = params
59
+ # TODO : Set rate limiters value when config load
60
+ api_name = ""
75
61
 
76
- session_method = getattr(self.session, method.lower())
62
+ if api_name and api_name in self.rate_limiters:
63
+ await self.rate_limiters[api_name].async_acquire()
77
64
 
78
65
  try:
79
66
  async with session_method(
80
67
  url,
81
- # yarl.URL(url, encoded=True),
82
- headers=request_headers,
83
- data=request_body,
84
- params=request_params,
68
+ headers=headers,
69
+ data=str(body).encode() if body else None,
70
+ params=params,
85
71
  timeout=aiohttp.ClientTimeout(total=int(self.timeout / 1000)),
86
72
  ) as response:
87
73
  http_response = await response.text(errors="replace")
88
74
  json_response = self.parse_json(http_response)
89
75
  return json_response
90
- except asyncio.TimeoutError as e:
76
+ except (asyncio.TimeoutError, aiohttp.ClientError) as e:
91
77
  details = f"{self.id} {method} {url}"
92
78
  raise TimeoutError(details) from e
79
+ finally:
80
+ if (
81
+ api_name
82
+ and api_name in self.rate_limiters
83
+ and isinstance(self.rate_limiters[api_name], RateLimiterContext)
84
+ ):
85
+ self.rate_limiters[api_name].release()
93
86
 
94
87
  async def fetch2(
95
88
  self, path, security_type, params={}, headers: Optional[Any] = None, body: Optional[Any] = None, config={}
@@ -0,0 +1,481 @@
1
+ from datetime import datetime, timedelta
2
+ import json
3
+ import os
4
+ import time
5
+ from typing import Any, Dict, Literal, Optional
6
+
7
+ import pytz
8
+
9
+ from ksxt.api.koreainvest import ImplicitAPI
10
+ from ksxt.async_.base.async_exchange import AsyncExchange
11
+ from ksxt.market.manager import MarketManager
12
+ from ksxt.parser.koreainvest import KoreaInvestParser
13
+
14
+ import ksxt.models
15
+
16
+
17
+ class KoreaInvest(AsyncExchange, ImplicitAPI):
18
+ def __init__(self, config: Dict = None) -> None:
19
+ super().__init__(config, "koreainvest.toml")
20
+ self.parser = KoreaInvestParser()
21
+ self.timezone = pytz.timezone("Asia/Seoul")
22
+
23
+ def is_activate(self, path, security_type) -> bool:
24
+ mode = "dev" if self.is_dev == True else "app"
25
+
26
+ tr_id = self.apis[self.type][security_type][path][mode]["tr_id"]
27
+
28
+ if security_type != "token" and not bool(tr_id):
29
+ return False
30
+
31
+ return super().is_activate(path=path, security_type=security_type)
32
+
33
+ def sign(
34
+ self,
35
+ path,
36
+ security_type,
37
+ method_type,
38
+ api_type: Any = "public",
39
+ headers: Optional[Any] = None,
40
+ body: Optional[Any] = None,
41
+ params: Optional[Any] = None,
42
+ config={},
43
+ ):
44
+ mode = "dev" if self.is_dev == True else "app"
45
+
46
+ host_url = self.apis[self.type][mode]["hostname"]
47
+ destination = self.apis[self.type][security_type][path]["url"]
48
+ version = self.apis[self.type]["version"]
49
+ params["version"] = version
50
+ destination = self.implode_params(destination, params)
51
+
52
+ url = f"{host_url}/{destination}"
53
+
54
+ tr_id = self.apis[self.type][security_type][path][mode]["tr_id"]
55
+ authorization_token = f"Bearer {self.token}"
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
+
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
+ async def create_token(self) -> ksxt.models.KsxtTokenResponse:
78
+ params = {"grant_type": "client_credentials", "appkey": self.open_key, "appsecret": self.secret_key}
79
+
80
+ common_header = self.create_common_header(request_params=params)
81
+
82
+ response = await self.public_post_generate_token(self.extend(params))
83
+
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)
87
+
88
+ parsed_info = self.parser.parse_token(response=response)
89
+
90
+ self.save_token(self.open_key, parsed_info.access_token, expired=parsed_info.expired_datetime)
91
+
92
+ return ksxt.models.KsxtTokenResponse(header=common_header, response=common_response, info=parsed_info)
93
+
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,
101
+ )
102
+
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,
109
+ )
110
+
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,
117
+ )
118
+
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
+ )
125
+
126
+ @AsyncExchange.check_token
127
+ async def fetch_balance(self, acc_num: str, base_market: str = "KRW") -> ksxt.models.KsxtBalanceResponse:
128
+ if base_market == "KRW":
129
+ params = {
130
+ "CANO": acc_num[:8],
131
+ "ACNT_PRDT_CD": acc_num[-2:],
132
+ "AFHR_FLPR_YN": "N",
133
+ "OFL_YN": "",
134
+ "INQR_DVSN": "01",
135
+ "UNPR_DVSN": "01",
136
+ "FUND_STTL_ICLD_YN": "N",
137
+ "FNCG_AMT_AUTO_RDPT_YN": "N",
138
+ "PRCS_DVSN": "01",
139
+ "CTX_AREA_FK100": "",
140
+ "CTX_AREA_NK100": "",
141
+ }
142
+ else:
143
+ assert ValueError(f"{base_market} is not valid value")
144
+
145
+ common_header = self.create_common_header(request_params=params)
146
+
147
+ response = await self.private_get_fetch_balance(self.extend(params))
148
+
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)
152
+
153
+ parsed_info = self.parser.parse_balance(response=response, base_market=base_market)
154
+
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:
159
+ if base_market == "KRW":
160
+ params = {
161
+ "CANO": acc_num[:8],
162
+ "ACNT_PRDT_CD": acc_num[-2:],
163
+ "AFHR_FLPR_YN": "N",
164
+ "OFL_YN": "",
165
+ "INQR_DVSN": "01",
166
+ "UNPR_DVSN": "01",
167
+ "FUND_STTL_ICLD_YN": "N",
168
+ "FNCG_AMT_AUTO_RDPT_YN": "N",
169
+ "PRCS_DVSN": "01",
170
+ "CTX_AREA_FK100": "",
171
+ "CTX_AREA_NK100": "",
172
+ }
173
+ else:
174
+ assert ValueError(f"{base_market} is not valid value")
175
+
176
+ common_header = self.create_common_header(request_params=params)
177
+
178
+ response = await self.private_get_fetch_cash(self.extend(params))
179
+
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)
183
+
184
+ parsed_info = self.parser.parse_cash(response=response, base_market=base_market)
185
+
186
+ return ksxt.models.KsxtCashResponse(header=common_header, response=common_response, info=parsed_info)
187
+
188
+ @AsyncExchange.check_token
189
+ async def fetch_orderbook(self, symbol: str, base_market: str = "KRW") -> ksxt.models.KsxtSingleOrderBookResponse:
190
+ if base_market == "KRW":
191
+ params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
192
+ else:
193
+ assert ValueError(f"{base_market} is not valid value")
194
+
195
+ common_header = self.create_common_header(request_params=params)
196
+
197
+ response = await self.private_get_fetch_orderbook(self.extend(params))
198
+
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)
202
+
203
+ parsed_info = self.parser.parse_orderbook(response=response, base_market=base_market)
204
+
205
+ return ksxt.models.KsxtSingleOrderBookResponse(header=common_header, response=common_response, info=parsed_info)
206
+
207
+ @AsyncExchange.check_token
208
+ async def fetch_security(self, symbol: str, base_market: str = "KRW") -> ksxt.models.KsxtSecurityResponse:
209
+ if base_market == "KRW":
210
+ params = {"PRDT_TYPE_CD": "300", "PDNO": symbol}
211
+ else:
212
+ assert ValueError(f"{base_market} is not valid value")
213
+
214
+ common_header = self.create_common_header(request_params=params)
215
+
216
+ response = await self.private_get_fetch_security_info(self.extend(params))
217
+
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)
221
+
222
+ parsed_info = self.parser.parse_security(response=response, base_market=base_market)
223
+
224
+ return ksxt.models.KsxtSecurityResponse(header=common_header, response=common_response, info=parsed_info)
225
+
226
+ @AsyncExchange.check_token
227
+ async def fetch_ticker(self, symbol: str, base_market: str = "KRW") -> ksxt.models.KsxtTickerResponse:
228
+ if base_market == "KRW":
229
+ params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": symbol}
230
+ else:
231
+ assert ValueError(f"{base_market} is not valid value")
232
+
233
+ common_header = self.create_common_header(request_params=params)
234
+
235
+ response = await self.private_get_fetch_ticker_price(self.extend(params))
236
+
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)
240
+
241
+ parsed_info = self.parser.parse_ticker(response=response, base_market=base_market)
242
+
243
+ return ksxt.models.KsxtTickerResponse(header=common_header, response=common_response, info=parsed_info)
244
+
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")
259
+
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)
264
+
265
+ if base_market == "KRW":
266
+ params = {
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,
272
+ }
273
+ else:
274
+ assert ValueError(f"{base_market} is not valid value")
275
+
276
+ common_header = self.create_common_header(request_params=params)
277
+
278
+ response = await self.private_get_fetch_index_ohlcv(self.extend(params))
279
+
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)
283
+
284
+ parsed_info = self.parser.parse_historical_index_data(response=response, symbol=symbol, base_market=base_market)
285
+
286
+ return ksxt.models.KsxtHistoricalDataResponse(header=common_header, response=common_response, info=parsed_info)
287
+
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"
300
+ else:
301
+ assert ValueError(f"{time_frame} is not valid value")
302
+
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)
307
+
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)
343
+
344
+ @AsyncExchange.check_token
345
+ async def modify_order(
346
+ self,
347
+ acc_num: str,
348
+ order_id: str,
349
+ price: float,
350
+ qty: float,
351
+ *args,
352
+ symbol: str | None = "",
353
+ base_market: str = "KRW",
354
+ ) -> ksxt.models.KsxtModifyOrderResponse:
355
+ if base_market == "KRW":
356
+ params = {
357
+ "CANO": acc_num[:8],
358
+ "ACNT_PRDT_CD": acc_num[-2:],
359
+ "KRX_FWDG_ORD_ORGNO": "",
360
+ "ORGN_ODNO": str(order_id),
361
+ "RVSE_CNCL_DVSN_CD": "01",
362
+ "ORD_DVSN": "00",
363
+ "ORD_QTY": str(qty),
364
+ "ORD_UNPR": str(price),
365
+ "QTY_ALL_ORD_YN": "N",
366
+ }
367
+ else:
368
+ assert ValueError(f"{base_market} is not valid value")
369
+
370
+ common_header = self.create_common_header(request_params=params)
371
+ response = await self.private_post_send_modify_order(self.extend(params))
372
+
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)
376
+
377
+ parsed_info = self.parser.parse_modify_order(response=response, base_market=base_market)
378
+
379
+ return ksxt.models.KsxtModifyOrderResponse(header=common_header, response=common_response, info=parsed_info)
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:
385
+ if base_market == "KRW":
386
+ params = {
387
+ "CANO": acc_num[:8],
388
+ "ACNT_PRDT_CD": acc_num[-2:],
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",
396
+ }
397
+ else:
398
+ assert ValueError(f"{base_market} is not valid value")
399
+
400
+ common_header = self.create_common_header(request_params=params)
401
+ response = await self.private_post_send_cancel_order(self.extend(params))
402
+
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)
406
+
407
+ parsed_info = self.parser.parse_cancel_order(response=response, base_market=base_market)
408
+
409
+ return ksxt.models.KsxtCancelOrderResponse(header=common_header, response=common_response, info=parsed_info)
410
+
411
+ @AsyncExchange.check_token
412
+ async def create_order(
413
+ self,
414
+ acc_num: str,
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,
421
+ 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"):
452
+ if base_market == "KRW":
453
+ return ""
454
+ elif base_market == "USD":
455
+ if symbol.upper() == "ALL":
456
+ return "NASD"
457
+
458
+ response = self.fetch_security(symbol=symbol, base_market=base_market)
459
+ return response["exchange"]
460
+ else:
461
+ return ""
462
+
463
+ def get_market_code_in_broker(self, symbol: str, base_market: str = "KRW"):
464
+ if base_market == "KRW":
465
+ return ""
466
+ elif base_market == "USD":
467
+ if symbol.upper() == "ALL":
468
+ return "NASD"
469
+
470
+ response = self.fetch_security(symbol=symbol, base_market=base_market)
471
+ exname = response["exchange"]
472
+ if exname == "NYS":
473
+ return "NYSE"
474
+ elif exname == "NAS":
475
+ return "NASD"
476
+ elif exname == "AMS":
477
+ return "AMEX"
478
+ else:
479
+ return ""
480
+ else:
481
+ return ""
@@ -20,6 +20,11 @@ class Exchange:
20
20
  is_dev = False
21
21
 
22
22
  session = None
23
+ # 세션 유효 시간 (초)
24
+ session_lifetime = 10
25
+ # 세션의 마지막 사용 시간
26
+ session_last_used: time = None
27
+
23
28
  timeout = 10000 # milliseconds = seconds * 1000
24
29
  synchronous = True
25
30