bitunix-automated-crypto-trading 2.6.0__py3-none-any.whl → 2.6.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.
Files changed (33) hide show
  1. bitunix_automated_crypto_trading/AsyncThreadRunner.py +81 -0
  2. bitunix_automated_crypto_trading/BitunixApi.py +278 -0
  3. bitunix_automated_crypto_trading/BitunixSignal.py +1099 -0
  4. bitunix_automated_crypto_trading/BitunixWebSocket.py +254 -0
  5. bitunix_automated_crypto_trading/DataFrameHtmlRenderer.py +74 -0
  6. bitunix_automated_crypto_trading/NotificationManager.py +23 -0
  7. bitunix_automated_crypto_trading/ThreadManager.py +69 -0
  8. bitunix_automated_crypto_trading/TickerManager.py +636 -0
  9. bitunix_automated_crypto_trading/__init__.py +1 -0
  10. bitunix_automated_crypto_trading/bitunix.py +593 -0
  11. bitunix_automated_crypto_trading/config.py +90 -0
  12. bitunix_automated_crypto_trading/config.txt +60 -0
  13. bitunix_automated_crypto_trading/logger.py +85 -0
  14. bitunix_automated_crypto_trading/sampleenv.txt +5 -0
  15. bitunix_automated_crypto_trading/static/chart.css +28 -0
  16. bitunix_automated_crypto_trading/static/chart.js +362 -0
  17. bitunix_automated_crypto_trading/static/modal.css +68 -0
  18. bitunix_automated_crypto_trading/static/modal.js +147 -0
  19. bitunix_automated_crypto_trading/static/script.js +166 -0
  20. bitunix_automated_crypto_trading/static/styles.css +118 -0
  21. bitunix_automated_crypto_trading/templates/charts.html +98 -0
  22. bitunix_automated_crypto_trading/templates/login.html +19 -0
  23. bitunix_automated_crypto_trading/templates/main.html +551 -0
  24. bitunix_automated_crypto_trading/templates/modal-chart.html +26 -0
  25. bitunix_automated_crypto_trading/templates/modal-config.html +34 -0
  26. bitunix_automated_crypto_trading/templates/modal-logs.html +15 -0
  27. {bitunix_automated_crypto_trading-2.6.0.dist-info → bitunix_automated_crypto_trading-2.6.2.dist-info}/METADATA +1 -1
  28. bitunix_automated_crypto_trading-2.6.2.dist-info/RECORD +31 -0
  29. bitunix_automated_crypto_trading-2.6.2.dist-info/top_level.txt +1 -0
  30. bitunix_automated_crypto_trading-2.6.0.dist-info/RECORD +0 -5
  31. bitunix_automated_crypto_trading-2.6.0.dist-info/top_level.txt +0 -1
  32. {bitunix_automated_crypto_trading-2.6.0.dist-info → bitunix_automated_crypto_trading-2.6.2.dist-info}/WHEEL +0 -0
  33. {bitunix_automated_crypto_trading-2.6.0.dist-info → bitunix_automated_crypto_trading-2.6.2.dist-info}/entry_points.txt +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()