ksxt 1.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ksxt/__pycache__/koreainvest.cpython-312.pyc +0 -0
- ksxt/async_/__pycache__/koreainvest.cpython-312.pyc +0 -0
- ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc +0 -0
- ksxt/async_/base/async_exchange.py +35 -42
- ksxt/async_/koreainvest.py +271 -639
- ksxt/base/__pycache__/exchange.cpython-312.pyc +0 -0
- ksxt/base/__pycache__/rate_limiter.cpython-312.pyc +0 -0
- ksxt/base/__pycache__/rest_exchange.cpython-312.pyc +0 -0
- ksxt/base/exchange.py +5 -0
- ksxt/base/rate_limiter.py +88 -0
- ksxt/base/rest_exchange.py +52 -26
- ksxt/config/__init__.py +2 -0
- ksxt/config/__pycache__/__init__.cpython-312.pyc +0 -0
- ksxt/config/bithumb.toml +2 -1
- ksxt/config/upbit.toml +6 -1
- ksxt/koreainvest.py +6 -26
- {ksxt-1.0.0.dist-info → ksxt-1.0.1.dist-info}/METADATA +1 -1
- {ksxt-1.0.0.dist-info → ksxt-1.0.1.dist-info}/RECORD +21 -20
- {ksxt-1.0.0.dist-info → ksxt-1.0.1.dist-info}/WHEEL +1 -1
- ksxt/async_/base/throttler.py +0 -63
- {ksxt-1.0.0.dist-info → ksxt-1.0.1.dist-info}/LICENSE.txt +0 -0
- {ksxt-1.0.0.dist-info → ksxt-1.0.1.dist-info}/top_level.txt +0 -0
Binary file
|
Binary file
|
Binary file
|
ksxt/base/exchange.py
CHANGED
@@ -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가 필요하지 않음
|
ksxt/base/rest_exchange.py
CHANGED
@@ -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
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
Binary file
|
ksxt/config/bithumb.toml
CHANGED
ksxt/config/upbit.toml
CHANGED
@@ -6,7 +6,8 @@
|
|
6
6
|
id = 'upbit'
|
7
7
|
name = 'Upbit'
|
8
8
|
countries = ['KR']
|
9
|
-
rate_limit =
|
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(
|
288
|
-
"FID_INPUT_DATE_2": end.strftime(
|
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 ""
|
@@ -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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
39
|
-
ksxt/base/
|
37
|
+
ksxt/base/exchange.py,sha256=z4Xhkkc0VRZoaoO8QMSHDvHLU6cgIi9VfpaWu7w8kp0,16787
|
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=
|
44
|
-
ksxt/base/__pycache__/
|
43
|
+
ksxt/base/__pycache__/exchange.cpython-312.pyc,sha256=fTNuJeA4AGMHSzEFEiAiwyIBtNzmhB53L7_xP2zw4ok,21658
|
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=
|
47
|
-
ksxt/config/bithumb.toml,sha256=
|
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=
|
51
|
-
ksxt/config/__pycache__/__init__.cpython-312.pyc,sha256=
|
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
|
@@ -112,8 +113,8 @@ ksxt/utils/timer.py,sha256=4_rhXdQDFkKshS5utWrgTteIe71tVSTL1zNW-IjOngM,1228
|
|
112
113
|
ksxt/utils/__pycache__/safer.cpython-312.pyc,sha256=GxRVIQqkB2OhLPrnBuQtFAtytEocdxWqykuutRkJuA8,1893
|
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.
|
116
|
-
ksxt-1.0.
|
117
|
-
ksxt-1.0.
|
118
|
-
ksxt-1.0.
|
119
|
-
ksxt-1.0.
|
116
|
+
ksxt-1.0.1.dist-info/LICENSE.txt,sha256=vyuXQcPOZ9BriMQz3h1k3jQTrKGsAjohf8WQHHf6xqo,1080
|
117
|
+
ksxt-1.0.1.dist-info/METADATA,sha256=kLTzqD_vS4HMmnf8LA1SFHdCFgOx9n-xfii-MngXo-c,1649
|
118
|
+
ksxt-1.0.1.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
|
119
|
+
ksxt-1.0.1.dist-info/top_level.txt,sha256=XLUhkZCur5Pe0BPUV3J0syngIPz7jBb2YlQR4epo5kI,5
|
120
|
+
ksxt-1.0.1.dist-info/RECORD,,
|
ksxt/async_/base/throttler.py
DELETED
@@ -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
|
File without changes
|
File without changes
|