bitunix-automated-crypto-trading 2.5.9__py3-none-any.whl → 2.6.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.
- bitunix_automated_crypto_trading/AsyncThreadRunner.py +81 -0
- bitunix_automated_crypto_trading/BitunixApi.py +278 -0
- bitunix_automated_crypto_trading/BitunixSignal.py +1099 -0
- bitunix_automated_crypto_trading/BitunixWebSocket.py +254 -0
- bitunix_automated_crypto_trading/DataFrameHtmlRenderer.py +74 -0
- bitunix_automated_crypto_trading/NotificationManager.py +23 -0
- bitunix_automated_crypto_trading/ResourceManager.py +35 -0
- bitunix_automated_crypto_trading/ThreadManager.py +69 -0
- bitunix_automated_crypto_trading/TickerManager.py +636 -0
- bitunix_automated_crypto_trading/__init__.py +1 -0
- bitunix_automated_crypto_trading/bitunix.py +593 -0
- bitunix_automated_crypto_trading/clearenv.py +8 -0
- bitunix_automated_crypto_trading/config.py +90 -0
- bitunix_automated_crypto_trading/config.txt +60 -0
- bitunix_automated_crypto_trading/logger.py +85 -0
- bitunix_automated_crypto_trading/sampleenv.txt +5 -0
- bitunix_automated_crypto_trading/static/chart.css +28 -0
- bitunix_automated_crypto_trading/static/chart.js +362 -0
- bitunix_automated_crypto_trading/static/modal.css +68 -0
- bitunix_automated_crypto_trading/static/modal.js +147 -0
- bitunix_automated_crypto_trading/static/script.js +166 -0
- bitunix_automated_crypto_trading/static/styles.css +118 -0
- bitunix_automated_crypto_trading/templates/charts.html +98 -0
- bitunix_automated_crypto_trading/templates/login.html +19 -0
- bitunix_automated_crypto_trading/templates/main.html +551 -0
- bitunix_automated_crypto_trading/templates/modal-chart.html +26 -0
- bitunix_automated_crypto_trading/templates/modal-config.html +34 -0
- bitunix_automated_crypto_trading/templates/modal-logs.html +15 -0
- {bitunix_automated_crypto_trading-2.5.9.dist-info → bitunix_automated_crypto_trading-2.6.1.dist-info}/METADATA +1 -1
- bitunix_automated_crypto_trading-2.6.1.dist-info/RECORD +33 -0
- bitunix_automated_crypto_trading-2.6.1.dist-info/entry_points.txt +2 -0
- bitunix_automated_crypto_trading-2.6.1.dist-info/top_level.txt +1 -0
- bitunix_automated_crypto_trading-2.5.9.dist-info/RECORD +0 -5
- bitunix_automated_crypto_trading-2.5.9.dist-info/entry_points.txt +0 -2
- bitunix_automated_crypto_trading-2.5.9.dist-info/top_level.txt +0 -1
- {bitunix_automated_crypto_trading-2.5.9.dist-info → bitunix_automated_crypto_trading-2.6.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
import asyncio
|
2
|
+
import threading
|
3
|
+
from logger import Logger
|
4
|
+
import os
|
5
|
+
logger = Logger(__name__).get_logger()
|
6
|
+
|
7
|
+
class AsyncThreadRunner:
|
8
|
+
def __init__(self, async_func, interval, *args, **kwargs):
|
9
|
+
self.async_func = async_func
|
10
|
+
self.interval = interval
|
11
|
+
self.args = args
|
12
|
+
self.kwargs = kwargs
|
13
|
+
self.loop = asyncio.new_event_loop()
|
14
|
+
self._stop_event = threading.Event()
|
15
|
+
self.thread = threading.Thread(target=self.thread_function)
|
16
|
+
self.task = None
|
17
|
+
|
18
|
+
def thread_function(self):
|
19
|
+
asyncio.set_event_loop(self.loop)
|
20
|
+
try:
|
21
|
+
if self.interval == 0:
|
22
|
+
self.task = asyncio.run_coroutine_threadsafe(
|
23
|
+
self.async_func(*self.args, **self.kwargs), self.loop
|
24
|
+
)
|
25
|
+
else:
|
26
|
+
self.task = asyncio.run_coroutine_threadsafe(
|
27
|
+
self.periodic_run(), self.loop
|
28
|
+
)
|
29
|
+
self.loop.run_forever()
|
30
|
+
except Exception as e:
|
31
|
+
logger.info(f"Async Thread function error: {e}")
|
32
|
+
finally:
|
33
|
+
pending = asyncio.all_tasks(self.loop)
|
34
|
+
for task in pending:
|
35
|
+
task.cancel()
|
36
|
+
try:
|
37
|
+
self.loop.run_until_complete(task)
|
38
|
+
except asyncio.CancelledError:
|
39
|
+
pass
|
40
|
+
self.loop.run_until_complete(self.loop.shutdown_asyncgens())
|
41
|
+
self.loop.close()
|
42
|
+
|
43
|
+
async def periodic_run(self):
|
44
|
+
try:
|
45
|
+
while not self._stop_event.is_set():
|
46
|
+
try:
|
47
|
+
await self.async_func(*self.args, **self.kwargs)
|
48
|
+
except Exception as e:
|
49
|
+
logger.info(f"error in periodic_run async thread {self.thread.name} {e}")
|
50
|
+
os._exit(1) # Exit the program if the thread is stopped
|
51
|
+
await asyncio.sleep(self.interval)
|
52
|
+
logger.info(f"periodic {self.thread.name} Thread stopped, exiting app.")
|
53
|
+
os._exit(1) # Exit the program if the thread is stopped
|
54
|
+
except asyncio.CancelledError:
|
55
|
+
pass
|
56
|
+
|
57
|
+
def start_thread(self, thread_name=None):
|
58
|
+
self.thread.name = thread_name
|
59
|
+
self.thread.start()
|
60
|
+
|
61
|
+
async def stop_thread(self):
|
62
|
+
"""Gracefully stop the async thread."""
|
63
|
+
# Signal any periodic task to stop
|
64
|
+
self._stop_event.set()
|
65
|
+
|
66
|
+
# Cancel and await the running task
|
67
|
+
if self.task:
|
68
|
+
self.task.cancel()
|
69
|
+
try:
|
70
|
+
await asyncio.wrap_future(self.task) # Wait for the cancellation to propagate
|
71
|
+
except asyncio.CancelledError:
|
72
|
+
logger.info(f"{self.thread.name} Task cancelled successfully.")
|
73
|
+
except Exception as e:
|
74
|
+
logger.error(f"Unexpected error while cancelling the task {self.thread.name}: {e}")
|
75
|
+
|
76
|
+
# Stop the event loop safely
|
77
|
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
78
|
+
|
79
|
+
# Wait for the thread to join
|
80
|
+
self.thread.join()
|
81
|
+
logger.info(f"{self.thread.name} Thread stopped and event loop cleaned up.")
|
@@ -0,0 +1,278 @@
|
|
1
|
+
import secrets
|
2
|
+
import base64
|
3
|
+
import time
|
4
|
+
import json
|
5
|
+
import hashlib
|
6
|
+
import asyncio
|
7
|
+
import requests
|
8
|
+
from urllib.parse import urlencode
|
9
|
+
from typing import Dict, Any
|
10
|
+
import traceback
|
11
|
+
from logger import Logger
|
12
|
+
logger = Logger(__name__).get_logger()
|
13
|
+
|
14
|
+
|
15
|
+
class BitunixApi:
|
16
|
+
|
17
|
+
def __init__(self, api_key, secret_key, settings):
|
18
|
+
self.api_key = api_key
|
19
|
+
self.secret_key = secret_key
|
20
|
+
|
21
|
+
self.session = requests.Session()
|
22
|
+
self.session.headers.update({
|
23
|
+
'Content-Type': 'application/json',
|
24
|
+
'User-Agent': 'BitunixAPI/1.0'
|
25
|
+
})
|
26
|
+
|
27
|
+
self.pending_positions_URL="https://fapi.bitunix.com/api/v1/futures/position/get_pending_positions"
|
28
|
+
self.account_Url="https://fapi.bitunix.com/api/v1/futures/account"
|
29
|
+
self.ticker_Url='https://fapi.bitunix.com/api/v1/futures/market/tickers'
|
30
|
+
self.ticker_pair_Url='https://fapi.bitunix.com/api/v1/futures/market/trading_pairs'
|
31
|
+
self.kline_Url='https://fapi.bitunix.com/api/v1/futures/market/kline'
|
32
|
+
self.depth_Url='https://fapi.bitunix.com/api/v1/futures/market/depth'
|
33
|
+
self.placeOrder_Url="https://fapi.bitunix.com/api/v1/futures/trade/place_order"
|
34
|
+
self.flashClose_Url="https://fapi.bitunix.com/api/v1/futures/trade/flash_close_position"
|
35
|
+
self.pending_order_url="https://fapi.bitunix.com/api/v1/futures/trade/get_pending_orders"
|
36
|
+
self.cancelOrder_Url="https://fapi.bitunix.com/api/v1/futures/trade/cancel_orders"
|
37
|
+
self.Trade_history_Url="https://fapi.bitunix.com/api/v1/futures/trade/get_history_trades"
|
38
|
+
self.position_history_Url="https://fapi.bitunix.com/api/v1/futures/position/get_history_positions"
|
39
|
+
|
40
|
+
async def update_settings(self, settings):
|
41
|
+
self.settings = settings
|
42
|
+
|
43
|
+
async def create_timestamp(self):
|
44
|
+
return str(int(time.time()*1000))
|
45
|
+
|
46
|
+
async def is_near_high_of_day(self, current_value, high_of_day, threshold_percentage=5):
|
47
|
+
# Calculate the difference as a percentage of the high of the day
|
48
|
+
difference_percentage = ((high_of_day - current_value) / high_of_day) * 100
|
49
|
+
# Check if the difference is within the threshold
|
50
|
+
return difference_percentage <= threshold_percentage
|
51
|
+
|
52
|
+
async def is_near_low_of_day(self, current_value, low_of_day, threshold_percentage=5):
|
53
|
+
# Calculate the difference as a percentage of the low of the day
|
54
|
+
difference_percentage = ((current_value - low_of_day) / low_of_day) * 100
|
55
|
+
# Check if the difference is within the threshold
|
56
|
+
return difference_percentage <= threshold_percentage
|
57
|
+
|
58
|
+
|
59
|
+
async def _generate_nonce(self) -> str:
|
60
|
+
random_bytes = secrets.token_bytes(32)
|
61
|
+
return base64.b64encode(random_bytes).decode('utf-8')
|
62
|
+
|
63
|
+
|
64
|
+
async def sign_request(self, nonce: str, timestamp: str,
|
65
|
+
query_params: str = None,
|
66
|
+
body: str = None) -> str:
|
67
|
+
query_string = query_params if query_params else ""
|
68
|
+
body_string = body if body else ""
|
69
|
+
message = f"{nonce}{timestamp}{self.api_key}{query_string}{body_string}"
|
70
|
+
|
71
|
+
# First SHA256 encryption
|
72
|
+
digest = hashlib.sha256(message.encode()).hexdigest()
|
73
|
+
|
74
|
+
# Second SHA256 encryption
|
75
|
+
sign = hashlib.sha256((digest + self.secret_key).encode()).hexdigest()
|
76
|
+
|
77
|
+
return sign
|
78
|
+
|
79
|
+
async def _get(self, endpoint: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
80
|
+
response = self.session.get(endpoint, params=params)
|
81
|
+
response.raise_for_status()
|
82
|
+
return response.json()
|
83
|
+
|
84
|
+
async def _get_authenticated(self, endpoint: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
|
85
|
+
timestamp = await self.create_timestamp()
|
86
|
+
nonce = await self._generate_nonce()
|
87
|
+
|
88
|
+
headers = {
|
89
|
+
"api-key": self.api_key,
|
90
|
+
"timestamp": timestamp,
|
91
|
+
"nonce": nonce,
|
92
|
+
}
|
93
|
+
|
94
|
+
query_string = urlencode(sorted(params.items())).replace('=','') if params else ""
|
95
|
+
signature = await self.sign_request(nonce, timestamp, query_params=query_string)
|
96
|
+
headers["sign"] = signature
|
97
|
+
|
98
|
+
response = self.session.get(endpoint, params=params, headers=headers)
|
99
|
+
response.raise_for_status()
|
100
|
+
return response.json()
|
101
|
+
|
102
|
+
async def _post_authenticated(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
103
|
+
timestamp = await self.create_timestamp()
|
104
|
+
nonce = await self._generate_nonce()
|
105
|
+
|
106
|
+
headers = {
|
107
|
+
"api-key": self.api_key,
|
108
|
+
"timestamp": timestamp,
|
109
|
+
"nonce": nonce,
|
110
|
+
"Content-Type": "application/json"
|
111
|
+
}
|
112
|
+
|
113
|
+
body_string = json.dumps(data, separators=(',', ':'))
|
114
|
+
signature = await self.sign_request(nonce, timestamp, body=body_string)
|
115
|
+
headers["sign"] = signature
|
116
|
+
|
117
|
+
response = self.session.post(endpoint, data=body_string, headers=headers)
|
118
|
+
logger.info(f"Response: {body_string} {response.json()}")
|
119
|
+
response.raise_for_status()
|
120
|
+
return response.json()
|
121
|
+
|
122
|
+
async def PlaceOrder(self, ticker, qty, price, side, positionId=0, tradeSide="OPEN",reduceOnly=False):
|
123
|
+
data = {
|
124
|
+
"side": side,
|
125
|
+
"orderType":"LIMIT",
|
126
|
+
"qty": qty,
|
127
|
+
"price": price,
|
128
|
+
"symbol": ticker,
|
129
|
+
"tradeSide":tradeSide,
|
130
|
+
"reduceOnly":reduceOnly,
|
131
|
+
"positionId":positionId
|
132
|
+
}
|
133
|
+
datajs = await self._post_authenticated(self.placeOrder_Url,data)
|
134
|
+
return datajs
|
135
|
+
|
136
|
+
async def FlashClose(self, positionId):
|
137
|
+
data = {
|
138
|
+
"positionId": positionId
|
139
|
+
}
|
140
|
+
datajs = await self._post_authenticated(self.flashClose_Url,data)
|
141
|
+
return datajs
|
142
|
+
|
143
|
+
async def CancelOrder(self, symbol, orderId):
|
144
|
+
data = {
|
145
|
+
"symbol": symbol,
|
146
|
+
"orderList":[{"orderId": orderId}]
|
147
|
+
}
|
148
|
+
datajs = await self._post_authenticated(self.cancelOrder_Url,data)
|
149
|
+
return datajs
|
150
|
+
|
151
|
+
async def GetTradeHistoryData(self):
|
152
|
+
tradeHistory=await self._get_authenticated(self.Trade_history_Url)
|
153
|
+
if tradeHistory['code']==0:
|
154
|
+
return tradeHistory['data']
|
155
|
+
else:
|
156
|
+
logger.info(tradeHistory['msg'])
|
157
|
+
|
158
|
+
async def GetPendingOrderData(self,dictparm={}):
|
159
|
+
orders=await self._get_authenticated(self.pending_order_url, dictparm)
|
160
|
+
if orders['code']==0:
|
161
|
+
return orders['data']
|
162
|
+
else:
|
163
|
+
logger.info(orders['msg'])
|
164
|
+
|
165
|
+
async def GetPendingPositionData(self, dictparm={}):
|
166
|
+
positions=await self._get_authenticated(self.pending_positions_URL, dictparm)
|
167
|
+
if positions['code']==0:
|
168
|
+
return positions['data']
|
169
|
+
else:
|
170
|
+
logger.info(positions['msg'])
|
171
|
+
|
172
|
+
async def GetPositionHistoryData(self, dictparm={}):
|
173
|
+
tradeHistory=await self._get_authenticated(self.position_history_Url, dictparm)
|
174
|
+
if tradeHistory['code']==0:
|
175
|
+
return tradeHistory['data']
|
176
|
+
else:
|
177
|
+
logger.info(tradeHistory['msg'])
|
178
|
+
|
179
|
+
|
180
|
+
async def GetportfolioData(self):
|
181
|
+
portfolio=await self._get_authenticated(self.account_Url, params={"marginCoin":"USDT"})
|
182
|
+
if portfolio['code']==0:
|
183
|
+
return portfolio['data']
|
184
|
+
else:
|
185
|
+
logger.info(portfolio['msg'])
|
186
|
+
|
187
|
+
|
188
|
+
async def GetTickerslastPrice(self, tickersStr):
|
189
|
+
try:
|
190
|
+
resp = self.session.get(self.ticker_Url+'?symbols='+tickersStr)
|
191
|
+
datajs = resp.json()
|
192
|
+
if datajs['code']==0:
|
193
|
+
return datajs['data']
|
194
|
+
except Exception as e:
|
195
|
+
stack = traceback.extract_stack()
|
196
|
+
function_name = stack[-2].name
|
197
|
+
logger.error(f"Function: {function_name}, {e}, {e.args}, {type(e).__name__}")
|
198
|
+
|
199
|
+
async def GetTickersPair(self, tickersStr):
|
200
|
+
try:
|
201
|
+
resp = self.session.get(self.ticker_pair_Url+'?symbols='+tickersStr)
|
202
|
+
datajs = resp.json()
|
203
|
+
if datajs['code']==0:
|
204
|
+
return datajs['data']
|
205
|
+
except Exception as e:
|
206
|
+
stack = traceback.extract_stack()
|
207
|
+
function_name = stack[-2].name
|
208
|
+
logger.error(f"Function: {function_name}, {e}, {e.args}, {type(e).__name__}")
|
209
|
+
|
210
|
+
async def GetTickerList(self, threshold, volume):
|
211
|
+
symbols=[]
|
212
|
+
try:
|
213
|
+
resp = self.session.get(self.ticker_Url)
|
214
|
+
datajs = resp.json()
|
215
|
+
for item in datajs["data"]:
|
216
|
+
if await self.is_near_high_of_day(float(item['last']), float(item['high']) ,threshold) and float(item['baseVol']) > volume:
|
217
|
+
symbols.append(item['symbol'])
|
218
|
+
if await self.is_near_low_of_day(float(item['last']), float(item['low']) ,threshold) and float(item['baseVol']) > volume:
|
219
|
+
symbols.append(item['symbol'])
|
220
|
+
#if float(item['baseVol']) > volume:
|
221
|
+
# symbols.append(item['symbol'])
|
222
|
+
return symbols
|
223
|
+
except Exception as e:
|
224
|
+
stack = traceback.extract_stack()
|
225
|
+
function_name = stack[-2].name
|
226
|
+
logger.info(f"Function: {function_name}, {e}, {e.args}, {type(e).__name__}")
|
227
|
+
|
228
|
+
async def GetTickerData(self):
|
229
|
+
try:
|
230
|
+
url = f'{self.ticker_Url}'
|
231
|
+
resp = self.session.get(url)
|
232
|
+
datajs = resp.json()
|
233
|
+
if datajs['code']==0:
|
234
|
+
return datajs['data']
|
235
|
+
except Exception as e:
|
236
|
+
stack = traceback.extract_stack()
|
237
|
+
function_name = stack[-2].name
|
238
|
+
logger.info(f"Function: {function_name}, {e}, {e.args}, {type(e).__name__}")
|
239
|
+
|
240
|
+
async def GetDepthData(self,symbol,limit):
|
241
|
+
try:
|
242
|
+
url = f'{self.depth_Url}?symbol={symbol}&limit={limit}'
|
243
|
+
resp = self.session.get(url)
|
244
|
+
datajs = resp.json()
|
245
|
+
if datajs['code']==0:
|
246
|
+
return datajs['data']
|
247
|
+
except Exception as e:
|
248
|
+
stack = traceback.extract_stack()
|
249
|
+
function_name = stack[-2].name
|
250
|
+
logger.info(f"Function: {function_name}, {e}, {e.args}, {type(e).__name__}")
|
251
|
+
|
252
|
+
async def GetKlineHistory(self, ticker, interval, limit):
|
253
|
+
data = []
|
254
|
+
lm=limit
|
255
|
+
current_unix_timestamp = int(time.time())
|
256
|
+
try:
|
257
|
+
while True:
|
258
|
+
url = f'{self.kline_Url}?symbol={ticker}&startTime={current_unix_timestamp}&interval={interval}&limit={lm}'
|
259
|
+
resp = self.session.get(url)
|
260
|
+
datajs = resp.json()
|
261
|
+
if len(datajs['data']) > 0:
|
262
|
+
current_unix_timestamp = str(int(datajs['data'][-1]['time']) + 1) # Adjust 'time' to match the appropriate key in your data
|
263
|
+
data.extend(datajs['data'])
|
264
|
+
lm=limit-len(data)
|
265
|
+
if len(data) >= limit:
|
266
|
+
break
|
267
|
+
else:
|
268
|
+
break
|
269
|
+
return data
|
270
|
+
except Exception as e:
|
271
|
+
stack = traceback.extract_stack()
|
272
|
+
function_name = stack[-2].name
|
273
|
+
logger.info(f"Function: {function_name}, {e}, {e.args}, {type(e).__name__}")
|
274
|
+
|
275
|
+
def __del__(self):
|
276
|
+
"""Cleanup method to close the session"""
|
277
|
+
if hasattr(self, 'session'):
|
278
|
+
self.session.close()
|