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.
ksxt/base/exchange.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import collections
2
- from distutils.util import strtobool
3
2
  import json
4
3
  from datetime import datetime
5
4
  import time
6
5
  import pytz
7
6
  from typing import Dict, Optional
7
+ import ast
8
8
 
9
9
  from requests import Session
10
10
  import urllib.parse as _urlencode
@@ -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
 
@@ -475,8 +480,8 @@ class Exchange:
475
480
  return default_value
476
481
 
477
482
  try:
478
- return bool(strtobool(value))
479
- except Exception:
483
+ return bool(ast.literal_eval(value.capitalize()))
484
+ except (ValueError, SyntaxError):
480
485
  return default_value
481
486
 
482
487
  # endregion safe method
@@ -0,0 +1,88 @@
1
+ from abc import ABC, abstractmethod
2
+ import asyncio
3
+ import time
4
+
5
+
6
+ class RateLimiterStrategy(ABC):
7
+ @abstractmethod
8
+ async def async_acquire(self):
9
+ pass
10
+
11
+ @abstractmethod
12
+ def acquire(self):
13
+ pass
14
+
15
+ @abstractmethod
16
+ def release(self):
17
+ pass
18
+
19
+
20
+ class RateLimiterContext:
21
+ def __init__(self, strategy: RateLimiterStrategy):
22
+ self._strategy = strategy
23
+
24
+ async def async_acquire(self):
25
+ await self._strategy.async_acquire()
26
+
27
+ def acquire(self):
28
+ self._strategy.acquire()
29
+
30
+ def release(self):
31
+ self._strategy.release()
32
+
33
+
34
+ class RequestRateLimiter(RateLimiterStrategy):
35
+ def __init__(self, max_requests: int, period: float = 1.0):
36
+ self.max_requests = max_requests
37
+ self.period = period
38
+ self.semaphore = asyncio.BoundedSemaphore(max_requests)
39
+ self.last_reset_time = time.time()
40
+
41
+ async def async_acquire(self):
42
+ current_time = time.time()
43
+ if current_time - self.last_reset_time > self.period:
44
+ self.semaphore = asyncio.BoundedSemaphore(self.max_requests)
45
+ self.last_reset_time = current_time
46
+
47
+ await self.semaphore.acquire()
48
+
49
+ def acquire(self):
50
+ current_time = time.time()
51
+ if current_time - self.last_reset_time > self.period:
52
+ # 동기식 세마포어는 지원되지 않으므로, 대신에 현재 상황을 조정합니다.
53
+ self.last_reset_time = current_time
54
+
55
+ # 세마포어가 동기식으로 동작하지 않으므로, 대신 제한 시간을 체크하여 동기식으로 제한
56
+ while self.semaphore._value <= 0: # 내부 값이 0 이하라면, 대기
57
+ if time.time() - self.last_reset_time > self.period:
58
+ self.semaphore = asyncio.BoundedSemaphore(self.max_requests)
59
+ break
60
+ time.sleep(0.01) # 동기식 대기
61
+
62
+ self.semaphore._value -= 1 # 수동으로 세마포어 값을 감소시킴
63
+
64
+ def release(self):
65
+ self.semaphore.release()
66
+
67
+
68
+ class TimeBasedRateLimiter(RateLimiterStrategy):
69
+ def __init__(self, period: float):
70
+ self.period = period
71
+ self.last_request_time = None
72
+
73
+ async def async_acquire(self):
74
+ current_time = time.time()
75
+ if self.last_request_time and (current_time - self.last_request_time) < self.period:
76
+ raise ValueError("요청이 너무 자주 발생했습니다. 잠시 후에 다시 시도해주세요.")
77
+
78
+ self.last_request_time = current_time
79
+
80
+ def acquire(self):
81
+ current_time = time.time()
82
+ if self.last_request_time and (current_time - self.last_request_time) < self.period:
83
+ raise ValueError("요청이 너무 자주 발생했습니다. 잠시 후에 다시 시도해주세요.")
84
+
85
+ self.last_request_time = current_time
86
+
87
+ def release(self):
88
+ pass # 이 클래스에서는 release가 필요하지 않음
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import tomllib
3
4
  import toml
4
5
  import os
5
6
  from pathlib import Path
@@ -10,6 +11,11 @@ from datetime import datetime, UTC
10
11
  import pytz
11
12
  import time
12
13
 
14
+ from ksxt.base.rate_limiter import (
15
+ RateLimiterContext,
16
+ RequestRateLimiter,
17
+ TimeBasedRateLimiter,
18
+ )
13
19
  from ksxt.base.errors import NotSupportedError
14
20
  from ksxt.base.exchange import Exchange
15
21
  from ksxt.config import CONFIG_DIR
@@ -38,6 +44,9 @@ class RestExchange(Exchange):
38
44
  def __init__(self, config: Dict = None, filename: str = None) -> None:
39
45
  super().__init__()
40
46
 
47
+
48
+ self.rate_limiters: Dict[str, RateLimiterContext] = {}
49
+
41
50
  self.headers = dict() if self.headers is None else self.headers
42
51
 
43
52
  if config is None:
@@ -52,33 +61,50 @@ class RestExchange(Exchange):
52
61
  apis = self._get_api_from_file(filename)
53
62
  Exchange.set_attr(self, apis)
54
63
 
55
- def __del__(self):
56
- if self.session:
57
- try:
58
- self.session.close()
59
- except Exception as e:
60
- pass
61
-
62
- def _get_api_from_file(self, filename: str):
64
+ def _get_api_from_file(self, filename: str) -> Dict[str, Any]:
63
65
  if filename is None:
64
- tr_config_filename = "tr_dev.json" if self.is_dev else "tr_app.json"
65
- else:
66
- tr_config_filename = filename
67
-
68
- config_path = os.path.join(CONFIG_DIR, tr_config_filename)
69
-
70
- if Path(tr_config_filename).suffix == ".json":
71
- with open(
72
- config_path,
73
- encoding="utf-8",
74
- ) as f:
75
- c = json.load(f)
76
- return {"apis": c[self.name]}
77
-
78
- elif Path(tr_config_filename).suffix == ".toml":
79
- with open(config_path, mode="r") as f:
80
- c = toml.load(f)
81
- return c
66
+ raise ValueError("Configuration filename cannot be None.")
67
+
68
+ config_path = os.path.join(CONFIG_DIR, filename)
69
+
70
+ if Path(filename).suffix != ".toml":
71
+ raise ValueError(f"Unsupported file format: {Path(filename).suffix}")
72
+
73
+ with open(config_path, mode="rb") as f:
74
+ config = tomllib.load(f)
75
+
76
+ self._setup_rate_limiters(config)
77
+ return config
78
+
79
+ def _setup_rate_limiters(self, config: Dict[str, Any]):
80
+ """RateLimiter 설정을 구성하는 메서드"""
81
+ default_rate_limit = config.get("rate_limit", 1000) # 최상위 rate_limit 값 가져오기
82
+
83
+ apis = config.get("apis", {}).get("rest", {})
84
+
85
+ def traverse_apis(api_section, section_name="", self=self):
86
+ for key, value in api_section.items():
87
+ if not isinstance(value, dict):
88
+ continue
89
+
90
+ if "url" in value: # "url"이 있는 경우, API 엔드포인트로 간주
91
+ rate_limit = value.get("rate_limit", default_rate_limit)
92
+ rate_limit_period = value.get("rate_limit_period", 1.0)
93
+
94
+ # 전략 패턴을 이용한 RateLimiter 생성
95
+ if rate_limit_period > 1.0:
96
+ strategy = TimeBasedRateLimiter(period=rate_limit_period)
97
+ else:
98
+ strategy = RequestRateLimiter(max_requests=rate_limit, period=1.0)
99
+
100
+ # API 이름을 구성
101
+ api_name = f"{section_name}.{key}" if section_name else key
102
+ self.rate_limiters[api_name] = RateLimiterContext(strategy)
103
+ else:
104
+ # value가 딕셔너리지만 "url"이 없는 경우, 하위 섹션으로 간주하고 재귀 호출
105
+ traverse_apis(value, f"{section_name}.{key}" if section_name else key)
106
+
107
+ traverse_apis(apis)
82
108
 
83
109
  def check_token(func):
84
110
  def wrapper(self, *args, **kwargs):
ksxt/config/__init__.py CHANGED
@@ -1,3 +1,5 @@
1
1
  import os
2
2
 
3
3
  CONFIG_DIR = os.path.dirname(os.path.abspath(__file__))
4
+
5
+ VALID_METHODS = {"get", "post", "put", "delete", "patch", "head", "options"}
ksxt/config/bithumb.toml CHANGED
@@ -6,7 +6,8 @@
6
6
  id = 'bithumb'
7
7
  name = 'Bithumb'
8
8
  countries = ['KR']
9
- rate_limit = 1000
9
+ rate_limit = 30 # 초당 30회로 제한
10
+ rate_limit_period = 600 # 600초(10분)당 1회로 제한
10
11
  enableRateLimit = false
11
12
  www = 'https://www.bithumb.com'
12
13
  doc = 'https://apidocs.bithumb.com/'
ksxt/config/upbit.toml CHANGED
@@ -6,7 +6,8 @@
6
6
  id = 'upbit'
7
7
  name = 'Upbit'
8
8
  countries = ['KR']
9
- rate_limit = 1000
9
+ rate_limit = 30 # 초당 30회로 제한
10
+ rate_limit_period = 600 # 600초(10분)당 1회로 제한
10
11
  enableRateLimit = false
11
12
  www = 'https://www.upbit.com'
12
13
  doc = 'https://docs.upbit.com'
@@ -112,6 +113,7 @@ version = "v1"
112
113
  url = "orders"
113
114
  method = 'POST'
114
115
  api = 'private'
116
+ rate_limit = 8
115
117
  activate = true
116
118
  __comment__ = "주문하기"
117
119
 
@@ -119,6 +121,7 @@ version = "v1"
119
121
  url = "orders"
120
122
  method = 'POST'
121
123
  api = 'private'
124
+ rate_limit = 8
122
125
  activate = true
123
126
  __comment__ = "주문하기"
124
127
 
@@ -126,6 +129,7 @@ version = "v1"
126
129
  url = "orders"
127
130
  method = 'POST'
128
131
  api = 'private'
132
+ rate_limit = 8
129
133
  activate = true
130
134
  __comment__ = "주문하기"
131
135
 
@@ -133,6 +137,7 @@ version = "v1"
133
137
  url = "orders"
134
138
  method = 'POST'
135
139
  api = 'private'
140
+ rate_limit = 8
136
141
  activate = true
137
142
  __comment__ = "주문하기"
138
143
 
ksxt/koreainvest.py CHANGED
@@ -30,7 +30,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
30
30
 
31
31
  return super().is_activate(path=path, security_type=security_type)
32
32
 
33
- # @RestExchange.check_token
34
33
  def sign(
35
34
  self,
36
35
  path,
@@ -124,23 +123,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
124
123
  info=response,
125
124
  )
126
125
 
127
- # @RestExchange.check_token
128
- # def fetch_markets(self, market_name: str) -> ksxt.models.KsxtMarketResponse:
129
- # params = {}
130
-
131
- # common_header = self.create_common_header(request_params=params)
132
-
133
- # # TODO from Database
134
- # response = None
135
-
136
- # common_response = self.get_common_response(response=response)
137
- # if common_response.success != "0":
138
- # return ksxt.models.KsxtMarketResponse(header=common_header, response=common_response, info=None)
139
-
140
- # parsed_info = self.parser.parse_markets(response=response, base_market="KRW")
141
-
142
- # return ksxt.models.KsxtMarketResponse(header=common_header, response=common_response, info=parsed_info)
143
-
144
126
  @RestExchange.check_token
145
127
  def fetch_balance(self, acc_num: str, base_market: str = "KRW") -> ksxt.models.KsxtBalanceResponse:
146
128
  if base_market == "KRW":
@@ -282,11 +264,11 @@ class KoreaInvest(RestExchange, ImplicitAPI):
282
264
 
283
265
  if base_market == "KRW":
284
266
  params = {
285
- "FID_COND_MRKT_DIV_CODE": "U",
286
- "FID_INPUT_ISCD": symbol,
287
- "FID_INPUT_DATE_1": start.strftime('%Y%m%d'),
288
- "FID_INPUT_DATE_2": end.strftime('%Y%m%d'),
289
- "FID_PERIOD_DIV_CODE": param_code
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,
290
272
  }
291
273
  else:
292
274
  assert ValueError(f"{base_market} is not valid value")
@@ -317,7 +299,7 @@ class KoreaInvest(RestExchange, ImplicitAPI):
317
299
  param_code = "Y"
318
300
  else:
319
301
  assert ValueError(f"{time_frame} is not valid value")
320
-
302
+
321
303
  if start is None:
322
304
  start = self.now(base_market) - timedelta(days=100)
323
305
  if end is None:
@@ -466,7 +448,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
466
448
 
467
449
  return ksxt.models.KsxtCreateOrderResponse(header=common_header, response=common_response, info=parsed_info)
468
450
 
469
- @RestExchange.check_token
470
451
  def get_market_code_in_feeder(self, symbol: str, base_market: str = "KRW"):
471
452
  if base_market == "KRW":
472
453
  return ""
@@ -479,7 +460,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
479
460
  else:
480
461
  return ""
481
462
 
482
- @RestExchange.check_token
483
463
  def get_market_code_in_broker(self, symbol: str, base_market: str = "KRW"):
484
464
  if base_market == "KRW":
485
465
  return ""
ksxt/models/market.py CHANGED
@@ -35,6 +35,11 @@ class SecurityData(BaseModel):
35
35
  # 최대 매도 주문 수량
36
36
  ask_max_qty: Optional[float] = 0
37
37
 
38
+ # 매수 주문 금액 단위
39
+ bid_amount_unit: Optional[float] = 0
40
+ # 매도 주문 금액 단위
41
+ ask_amount_unit: Optional[float] = 0
42
+
38
43
  # 최소 매수 주문 금액
39
44
  bid_min_amount: Optional[float] = 0
40
45
  # 최소 매도 주문 금액
ksxt/parser/bithumb.py CHANGED
@@ -226,6 +226,8 @@ class BithumbParser(BaseParser):
226
226
  ask_min_qty=safer.safe_number(market_info["ask"], "min_total"),
227
227
  bid_max_qty=safer.safe_number(market_info, "max_total"),
228
228
  ask_max_qty=safer.safe_number(market_info, "max_total"),
229
+ bid_amount_unit=1,
230
+ ask_amount_unit=1,
229
231
  bid_min_amount=safer.safe_number(market_info["bid"], "min_total"),
230
232
  ask_min_amount=safer.safe_number(market_info["ask"], "min_total"),
231
233
  bid_max_amount=safer.safe_number(market_info, "max_total"),
@@ -210,6 +210,8 @@ class KoreaInvestParser(BaseParser):
210
210
  ask_min_qty=0, # TODO : where is information?
211
211
  bid_max_qty=0, # TODO : where is information?
212
212
  ask_max_qty=0, # TODO : where is information?
213
+ bid_amount_unit=1, # TODO: where is information?
214
+ ask_amount_unit=1, # TODO : where is information?
213
215
  bid_min_amount=0, # TODO : where is information?
214
216
  ask_min_amount=0, # TODO : where is information?
215
217
  bid_max_amount=0, # TODO : where is information?
ksxt/parser/upbit.py CHANGED
@@ -232,6 +232,8 @@ class UpbitParser(BaseParser):
232
232
  ask_min_qty=safer.safe_number(market_info.get("ask", {}), "min_total"),
233
233
  bid_max_qty=safer.safe_number(market_info, "max_total"),
234
234
  ask_max_qty=safer.safe_number(market_info, "max_total"),
235
+ bid_amount_unit=1,
236
+ ask_amount_unit=1,
235
237
  bid_min_amount=safer.safe_number(market_info.get("bid", {}), "min_total"),
236
238
  ask_min_amount=safer.safe_number(market_info.get("ask", {}), "min_total"),
237
239
  bid_max_amount=safer.safe_number(market_info, "max_total"),
ksxt/utils/safer.py CHANGED
@@ -1,4 +1,4 @@
1
- from distutils.util import strtobool
1
+ import ast
2
2
 
3
3
 
4
4
  @staticmethod
@@ -43,6 +43,6 @@ def safe_boolean(dictionary, key, default_value=False):
43
43
  return default_value
44
44
 
45
45
  try:
46
- return bool(strtobool(value))
47
- except Exception:
46
+ return bool(ast.literal_eval(value.capitalize()))
47
+ except (ValueError, SyntaxError):
48
48
  return default_value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ksxt
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  License: MIT License
5
5
 
6
6
  Copyright © 2023 AMOSA
@@ -1,10 +1,10 @@
1
1
  ksxt/__init__.py,sha256=Q4kbXuxaCcQg5Z1T2z31lrUc5Cr95RIyv-y6hNf3R0E,131
2
2
  ksxt/bithumb.py,sha256=0pRy8XamlHZ2ypSeJkqiLbkqos5_l3vLKGmqL_EplS8,21405
3
- ksxt/koreainvest.py,sha256=UX2BHIU_Yymi2A5EvNOUwcWkv7g677ihzVYTCjBtfcM,20933
3
+ ksxt/koreainvest.py,sha256=vBsAtTSq7n2y_2VxADeb4ByuggoS2fw5f4G80dJMBbw,20111
4
4
  ksxt/upbit.py,sha256=ploO4Q4iX9wUfa6UagY2S5cvZvMHVDXsXF4fbu8723g,21699
5
5
  ksxt/__pycache__/__init__.cpython-312.pyc,sha256=wOfyuexJUbv8R-aktGSuw5cGx_Nz9O0ReqF9npfS50Q,311
6
6
  ksxt/__pycache__/bithumb.cpython-312.pyc,sha256=EtcBPN5niXbkjMK_PUmfOLANsdRpUqI3GW0EIAWpqVA,24716
7
- ksxt/__pycache__/koreainvest.cpython-312.pyc,sha256=xzeKWRPrgPFZ1VnEupted4XpCRePJJ-_LRU2lBe5LLM,22079
7
+ ksxt/__pycache__/koreainvest.cpython-312.pyc,sha256=NItGfTFI1TamcIVjF8v1ce1X6RrFHa2iE0umedQgqUs,21977
8
8
  ksxt/__pycache__/upbit.cpython-312.pyc,sha256=FciYM5KvlGO3-lhbeTP6E-ZWs2KiaWJkvrLTGfBmbcg,25111
9
9
  ksxt/api/__init__.py,sha256=CZ8AedRG8O9vEdSqTaot5sV3nwpxPZoVOYwrFCLUo6M,736
10
10
  ksxt/api/bithumb.py,sha256=6apoSNoiEOIkR6xQu4rtCSz7sdNdx3BHeu8-m8omh6E,2177
@@ -20,35 +20,36 @@ ksxt/api/auto/koreainvest.py,sha256=z4Bf3C7g2W-eQP99oBeM_J0mq2D3PkbQaulrDwxJB04,
20
20
  ksxt/api/auto/upbit.py,sha256=CtSG0jziqJR4FInHvwDisJfMWCUrUIQQcNPuCGGR7Do,1467
21
21
  ksxt/async_/__init__.py,sha256=ztfV65aN2mXamQSfVm3GLYB6cszkvsCSlUyhfe2H_2M,186
22
22
  ksxt/async_/bithumb.py,sha256=8u87o6HBqU1Ga0-MUBnygrkx6IjwzonImlPV3qZ_zug,19285
23
- ksxt/async_/koreainvest.py,sha256=IW0E1zDZZmCM38XRqoX8uwXxRW1Z1oPLlBWDWZ7br5o,32092
23
+ ksxt/async_/koreainvest.py,sha256=Mc6nsYPQkmc-kRs-pSNNWzKgqPbw65RCGAvmOQLHFw0,20278
24
24
  ksxt/async_/upbit.py,sha256=99IC-czWOV6SsKN645BdbM7Rg_oDo8_iwaIzfGxI_i0,21247
25
25
  ksxt/async_/__pycache__/__init__.cpython-312.pyc,sha256=iIsmwj1LPoOUx4ad9K2yCdmVi5vXv23kOTUX7I5NGx4,380
26
26
  ksxt/async_/__pycache__/bithumb.cpython-312.pyc,sha256=r8oTmvbamu7cGJZYc2YieeFo7IhZf1Rcrh6pIvYHlw8,23397
27
- ksxt/async_/__pycache__/koreainvest.cpython-312.pyc,sha256=xSyD87B8mWGLsiPtsXdFEhdMx1sLapIugyl7UnJf5Q8,31962
27
+ ksxt/async_/__pycache__/koreainvest.cpython-312.pyc,sha256=LNkF2wjNfB1tlqm9f3ck5409o4_k8SOQWm9IS7cpk9w,22813
28
28
  ksxt/async_/__pycache__/upbit.cpython-312.pyc,sha256=4f1hx8Na-P01MlYdgsU1CvPD8iT9z4t02OyVOVQkfa8,26273
29
29
  ksxt/async_/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- ksxt/async_/base/async_exchange.py,sha256=ARis9XxuWWS8k37vH4BdIGUQpS4p_PUITt0HaQkly6I,9499
31
- ksxt/async_/base/throttler.py,sha256=o6On7tIQdIVCb9reIe5u7FgqJybWZRikdunEj0FoJok,2017
30
+ ksxt/async_/base/async_exchange.py,sha256=wsX-GCcd5PgdVyJVskcKxF-fECw69uFWdtNnj0MGieU,9406
32
31
  ksxt/async_/base/__pycache__/__init__.cpython-312.pyc,sha256=jREIi8ul1C01r_-NcGb44P6KWeLzw0qVRsaHaegOf_0,153
33
- ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc,sha256=iS7jjyof971KeDV3yxsMbGZiwWuDlKgYYxhtp84G3b4,15215
32
+ ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc,sha256=jsTiwPq-M2pmbqi57MyXJqzSSa1z-F9TYrc5Fp3IczQ,15428
34
33
  ksxt/async_/base/__pycache__/throttler.cpython-312.pyc,sha256=wNDfiAHgLIXbSPtK4BOYjnnsWcZSO-Cq4NQXhzrJQpM,3192
35
34
  ksxt/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
35
  ksxt/base/com_exchange.py,sha256=q7C3Bc5q4ENvNGoghnQCN7YLttgwmFoNj9WsH5qxXNQ,287
37
36
  ksxt/base/errors.py,sha256=msGHmm9YGUOXOaK_VhOUEt5LvIDMMm6G-io8nQbNgA0,141
38
- ksxt/base/exchange.py,sha256=oXvg1kC73KZ6zHrgnmRmjsM9zTPsve3Qw_xCi0rNf2Q,16643
39
- ksxt/base/rest_exchange.py,sha256=_LpMDW_aCIW1knYUJoTRWDAyW17b0ZVdnaX4PfgzKj8,14452
37
+ ksxt/base/exchange.py,sha256=rJ-cXfhZ00nBxgDUlwP_-kVhPI3Si5YC1f0zXok9kXM,16797
38
+ ksxt/base/rate_limiter.py,sha256=9iF6OB-2u0q4nqtng8Tafn-fM3CVQ-DvdNRJ1aSCS3w,3005
39
+ ksxt/base/rest_exchange.py,sha256=4DpSpLYVPZ-WGGV0oBQIDEUDjAAmj7DrsaI1_7GjGUo,15835
40
40
  ksxt/base/types.py,sha256=3bd3obJhhX1njy_o4LW8hSAHX2MThOI-mZm6iifwno4,59
41
41
  ksxt/base/__pycache__/__init__.cpython-312.pyc,sha256=WnqBpy7gGuw6RNmEVSFdxy4sOZJgZD-fZCJRUbhOHmU,146
42
42
  ksxt/base/__pycache__/errors.cpython-312.pyc,sha256=rAZ-JEC6Ga8eAk4FbYv1foQvMiwK2qj8yVSOzPmKAKo,622
43
- ksxt/base/__pycache__/exchange.cpython-312.pyc,sha256=t-jDvi1BSgju3_FolY42GU7-eJt9y60kRN15a1OX7v4,21552
44
- ksxt/base/__pycache__/rest_exchange.cpython-312.pyc,sha256=mobfwenfwekEZN9ywDKK-eS0hTIo-vb-QqAUw6DuN-s,21780
43
+ ksxt/base/__pycache__/exchange.cpython-312.pyc,sha256=DmLd_GSYliWt4lAIuaO6zCwCQwJtYX0b6Rl2e3WtU2Q,21753
44
+ ksxt/base/__pycache__/rate_limiter.cpython-312.pyc,sha256=fWueaBMHMPK7Z3_YLJV9bPNPQBax_MF_sEIoqorqwx8,5418
45
+ ksxt/base/__pycache__/rest_exchange.cpython-312.pyc,sha256=yL-rNG758IYojfx-y39L-cPWuPrZQ1RRKEFetOI7hBk,22849
45
46
  ksxt/base/__pycache__/types.cpython-312.pyc,sha256=iF742YNlqCX4ZSIJP4FDPd-3765zIr1VWr-XWXisCsA,232
46
- ksxt/config/__init__.py,sha256=W6RIjNN-e2Mh3P7xwHE7pYVENG_drLTLck5d9nRlz14,70
47
- ksxt/config/bithumb.toml,sha256=74nzGNIGl8U6y_TaxEWJDGNMeDZLtnrKP8wjfA9wbq4,11611
47
+ ksxt/config/__init__.py,sha256=hkQx56FVJD8Gq1A_KzfOqO8BoqJRTX5TnUmqIignnlc,148
48
+ ksxt/config/bithumb.toml,sha256=JgKgHQVATsLcbxKYvekSRonJ4PjJ5x2ejp1eZHHusrQ,11693
48
49
  ksxt/config/koreainvest.toml,sha256=XQRSdEgkWlhbfS6AvJRqQJ_8210i__nfFSI9n3GcgqY,11526
49
50
  ksxt/config/token.toml,sha256=d2apbR6t929w6gLUg-tJ1JT8q6RKPTdYkghxdF9ZDeY,864
50
- ksxt/config/upbit.toml,sha256=hzOg9nxGKXB9eko59Y07kwG7MnldytFdbGjRhmgHUL0,13261
51
- ksxt/config/__pycache__/__init__.cpython-312.pyc,sha256=7T4AHX9kW8N67__FU10XfF8pkIzt31Tuhp9Pw40YwMQ,346
51
+ ksxt/config/upbit.toml,sha256=1aGKZ3HAV28rrTVGZQ418I9NJwhoRh70Vx03oKacIfY,13439
52
+ ksxt/config/__pycache__/__init__.cpython-312.pyc,sha256=R1JKzJvTBlmRvs65EiYpcn8PmiWHfpV0pERid4xZ2hI,425
52
53
  ksxt/market/base.py,sha256=SzNPJECU2iWaQEUesI_REW_A0by_Tv4hs4Hv8p_sr0I,6856
53
54
  ksxt/market/db.py,sha256=ZPr2WlQRUOjhJm1fwFBrdwWG7UJkEpy5Pr0oEQ3Nqb0,386
54
55
  ksxt/market/logging.py,sha256=hkyAhzA72-KCGcVavSOHFSz2FU5QJq_gn6WA6kXs_Pw,698
@@ -79,7 +80,7 @@ ksxt/models/cash.py,sha256=sNexdR6Ek-YZ6cVAmre-zf3K30R-6hreHuBQKYzMsPU,303
79
80
  ksxt/models/common.py,sha256=HvsNa2wlTeLdnB67untL0UzOQ5r6f2s6Gkfxeh7LTpM,827
80
81
  ksxt/models/error.py,sha256=yzpTweHMB-bi7hzD8ATKsK2klJAsbfPEBBa1A5tY6tw,291
81
82
  ksxt/models/historical.py,sha256=fWcst4WPimI2Avb4b1kOswfyf3nB4XSMgpTSmm7uTpY,655
82
- ksxt/models/market.py,sha256=SBV-LEQcM6_GnSpueInMwweQUENAMy4jptpFkW6XRzM,1857
83
+ ksxt/models/market.py,sha256=kkH-P1m1HQjJOPT1k4bk9ZgW-_kwIBO_fXHcrRlWlVE,2013
83
84
  ksxt/models/order.py,sha256=LJJd-licLirI5WrAM1MItViOX_u5U_fmpBFPpDSdHxE,867
84
85
  ksxt/models/orderbook.py,sha256=S1c03QBu1hupN87CMWLgEsh8u2GrZ_Z_BcTUuiKGnR0,773
85
86
  ksxt/models/ticker.py,sha256=pPc7GqUOJchLg7qOpKfxhTVpXygF1Otc_M9_KfHFEgY,544
@@ -91,29 +92,29 @@ ksxt/models/__pycache__/cash.cpython-312.pyc,sha256=zHfJRVx-50RgXuUvCK1aLRgJaPlO
91
92
  ksxt/models/__pycache__/common.cpython-312.pyc,sha256=G8yzihcEFUbqS5xm38_tLQahaO509Iq2iiXHdOffbWs,1363
92
93
  ksxt/models/__pycache__/error.cpython-312.pyc,sha256=nnbF5rIw8e6kr_fcn-uwD_41Ge0Du2f2vUZpK4zzRFI,774
93
94
  ksxt/models/__pycache__/historical.cpython-312.pyc,sha256=TocUsW2T7yOOU29YzSEVAKSLtDwQ77AgGGPJDJTWhlc,1291
94
- ksxt/models/__pycache__/market.cpython-312.pyc,sha256=LSMJWGMM5nzCW366uFC1jLlHDoSIBgV08p3T7I9P8L0,2964
95
+ ksxt/models/__pycache__/market.cpython-312.pyc,sha256=uGDE-2ms5RJMJv54egSQ4NiEMh-flMBYijOb46fnsgg,3078
95
96
  ksxt/models/__pycache__/order.cpython-312.pyc,sha256=jvesPAK8EDrWCsE8H7UywkNyPdMmwKDWpoHO-BHKEd8,1791
96
97
  ksxt/models/__pycache__/orderbook.cpython-312.pyc,sha256=wzEg5vuh78UOhnUvMd_K7SjfIdgPQ9lZpHlHJ4ekqs8,1517
97
98
  ksxt/models/__pycache__/ticker.cpython-312.pyc,sha256=k6_mEv_fTMtZOd6BwWUW8du_udB8LnmZqOWbZpjE1lE,1165
98
99
  ksxt/models/__pycache__/token.cpython-312.pyc,sha256=DFNk5vUNLM-McE9US441Zfmo7yyCv_Gpxxp350PmNrw,795
99
100
  ksxt/models/__pycache__/transaction.cpython-312.pyc,sha256=lSEJNLWcsivMp_DeEv2-zw75EDqKiY3YCN5hQh4fvHY,2779
100
- ksxt/parser/bithumb.py,sha256=0c0_aFkbQxHvDHOpuA4pothJbnVlO3nKYgzQgFHio_g,14236
101
- ksxt/parser/koreainvest.py,sha256=X751E-grPEY7itEGnY66ILTWG_GY3wA0XJxF-h4S1FQ,14954
101
+ ksxt/parser/bithumb.py,sha256=v6enIUbxIf5EKdKaw8Gwu1G6EgyaE96H8x3UC6Kw0g0,14300
102
+ ksxt/parser/koreainvest.py,sha256=TfKmaRVc1Tns_kYnhJa2lUjtcO88oNdBWJKLMzC3ZHo,15081
102
103
  ksxt/parser/parser.py,sha256=8H6RmI6XUBhwp3r7pFcdtUuf-7bZ5RvnObXKrcOMfyI,5147
103
- ksxt/parser/upbit.py,sha256=ZTID_1YESUcL5T44LVB9j61lj1FW-1XU6tEEj7vZqHE,14677
104
- ksxt/parser/__pycache__/bithumb.cpython-312.pyc,sha256=xsC_8d9PNgK5J490dfcsB_195PRwoGGOFraBn3FaQtY,17986
105
- ksxt/parser/__pycache__/koreainvest.cpython-312.pyc,sha256=eVc0zrgcFn39meAfemhhRfGR_dSL3rMb3fOtsMr1elQ,18284
104
+ ksxt/parser/upbit.py,sha256=13Gmnj15YV9G5C53efj4uZ-nQSaMFcPkmCuHTZ44bh4,14741
105
+ ksxt/parser/__pycache__/bithumb.cpython-312.pyc,sha256=GA6CCsqGAogCxtV0A_4TLbdEn0PT56AAwHasvA4K4Pw,18201
106
+ ksxt/parser/__pycache__/koreainvest.cpython-312.pyc,sha256=I0rSJMRPuOF1GFRDKeX-LT05psYc3sTLJd9DO0_cv1c,18505
106
107
  ksxt/parser/__pycache__/parser.cpython-312.pyc,sha256=D6qWG4dfxiRPUv6L_XAUPSnne4N_FWh1HV5URSUWo3Q,8326
107
- ksxt/parser/__pycache__/upbit.cpython-312.pyc,sha256=u-4h94dVoYiZ9WgIp_d6L2-afs1ybHhJYxfwOyoMRcI,18843
108
+ ksxt/parser/__pycache__/upbit.cpython-312.pyc,sha256=gANx4wRq38GlsXrZCEH7TPWbPkOZS0LKHpMiQiKSqqI,19058
108
109
  ksxt/sample/symbol_sync.ipynb,sha256=dnUOgEKdUuEkDo_6LZjmDtVuNh4PPmCN4Ht7Pz-0L9Y,640081
109
- ksxt/utils/safer.py,sha256=raRU2CBDtsZyyipb6xduRoTgasEtNnpZKHpbTK9wkh4,1279
110
+ ksxt/utils/safer.py,sha256=tlXDU9KDxrrM0HT2bv45hkblIgbEYr02NygdYS0jj6c,1289
110
111
  ksxt/utils/sorter.py,sha256=j7eN1Qy7Wx3cXcWyRWyO1fVq-M24TrXvcfewPR5axF4,227
111
112
  ksxt/utils/timer.py,sha256=4_rhXdQDFkKshS5utWrgTteIe71tVSTL1zNW-IjOngM,1228
112
- ksxt/utils/__pycache__/safer.cpython-312.pyc,sha256=GxRVIQqkB2OhLPrnBuQtFAtytEocdxWqykuutRkJuA8,1893
113
+ ksxt/utils/__pycache__/safer.cpython-312.pyc,sha256=UhwkADGcSzG-ZYboesTzUHMGgn5C27UvslVZLp_SsF4,1991
113
114
  ksxt/utils/__pycache__/sorter.cpython-312.pyc,sha256=7VUIekH9h5SDsz4L5bjCdSQln-fLgQZII46URitryHI,626
114
115
  ksxt/utils/__pycache__/timer.cpython-312.pyc,sha256=8coj4_LWUtYOdFVhaEHyDbDwkt3JKtmQ1pXPtnsROyY,2005
115
- ksxt-1.0.0.dist-info/LICENSE.txt,sha256=vyuXQcPOZ9BriMQz3h1k3jQTrKGsAjohf8WQHHf6xqo,1080
116
- ksxt-1.0.0.dist-info/METADATA,sha256=04MQWF0-R2Gq0MH4qXZNuSiNDWcCsn8_zUtsgOXW-Ug,1649
117
- ksxt-1.0.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
118
- ksxt-1.0.0.dist-info/top_level.txt,sha256=XLUhkZCur5Pe0BPUV3J0syngIPz7jBb2YlQR4epo5kI,5
119
- ksxt-1.0.0.dist-info/RECORD,,
116
+ ksxt-1.0.2.dist-info/LICENSE.txt,sha256=vyuXQcPOZ9BriMQz3h1k3jQTrKGsAjohf8WQHHf6xqo,1080
117
+ ksxt-1.0.2.dist-info/METADATA,sha256=44Se9OMQUCPOs0Ol4b3183PpFOrFQx7h0xUoz0fI2BY,1649
118
+ ksxt-1.0.2.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
119
+ ksxt-1.0.2.dist-info/top_level.txt,sha256=XLUhkZCur5Pe0BPUV3J0syngIPz7jBb2YlQR4epo5kI,5
120
+ ksxt-1.0.2.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,63 +0,0 @@
1
- import asyncio
2
- import collections
3
- from time import time
4
-
5
-
6
- class Throttler:
7
- def __init__(self, config, loop=None):
8
- self.loop = loop
9
- self.config = {
10
- "refillRate": 1.0,
11
- "delay": 0.001,
12
- "cost": 1.0,
13
- "tokens": 0,
14
- "maxCapacity": 2000,
15
- "capacity": 1.0,
16
- }
17
-
18
- self.config.update(config)
19
- self.queue = collections.deque()
20
- self.running = False
21
-
22
- async def looper(self):
23
- last_timestamp = time() * 1000
24
- while self.running:
25
- future, cost = self.queue[0]
26
- cost = self.config["cost"] if cost is None else cost
27
- if self.config["tokens"] >= 0:
28
- self.config["tokens"] -= cost
29
-
30
- if not future.done():
31
- future.set_result(None)
32
-
33
- self.queue.popleft()
34
-
35
- # context switch
36
- await asyncio.sleep(0)
37
-
38
- if len(self.queue) == 0:
39
- self.running = False
40
- else:
41
- await asyncio.sleep(self.config["delay"])
42
- now = time() * 1000
43
- elapsed = now - last_timestamp
44
- last_timestamp = now
45
- self.config["tokens"] = min(
46
- self.config["tokens"] + elapsed * self.config["refillRate"], self.config["capacity"]
47
- )
48
-
49
- def __call__(self, cost=None):
50
- future = asyncio.Future()
51
- if len(self.queue) > self.config["maxCapacity"]:
52
- raise RuntimeError(
53
- "throttle queue is over maxCapacity ("
54
- + str(int(self.config["maxCapacity"]))
55
- + "), see https://github.com/ccxt/ccxt/issues/11645#issuecomment-1195695526"
56
- )
57
-
58
- self.queue.append((future, cost))
59
- if not self.running:
60
- self.running = True
61
- asyncio.ensure_future(self.looper(), loop=self.loop)
62
-
63
- return future