bitunix-automated-crypto-trading 2.6.7__py3-none-any.whl → 2.6.8__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 -81
- bitunix_automated_crypto_trading/BitunixApi.py +278 -278
- bitunix_automated_crypto_trading/BitunixSignal.py +1099 -1099
- bitunix_automated_crypto_trading/BitunixWebSocket.py +254 -254
- bitunix_automated_crypto_trading/DataFrameHtmlRenderer.py +74 -74
- bitunix_automated_crypto_trading/NotificationManager.py +23 -23
- bitunix_automated_crypto_trading/ThreadManager.py +68 -68
- bitunix_automated_crypto_trading/TickerManager.py +635 -635
- bitunix_automated_crypto_trading/bitunix.py +597 -594
- bitunix_automated_crypto_trading/config.py +90 -90
- bitunix_automated_crypto_trading/logger.py +84 -84
- {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/METADATA +36 -36
- bitunix_automated_crypto_trading-2.6.8.dist-info/RECORD +17 -0
- bitunix_automated_crypto_trading/config.txt +0 -60
- bitunix_automated_crypto_trading/sampleenv.txt +0 -5
- bitunix_automated_crypto_trading/static/chart.css +0 -28
- bitunix_automated_crypto_trading/static/chart.js +0 -362
- bitunix_automated_crypto_trading/static/modal.css +0 -68
- bitunix_automated_crypto_trading/static/modal.js +0 -147
- bitunix_automated_crypto_trading/static/script.js +0 -166
- bitunix_automated_crypto_trading/static/styles.css +0 -118
- bitunix_automated_crypto_trading/templates/charts.html +0 -98
- bitunix_automated_crypto_trading/templates/login.html +0 -19
- bitunix_automated_crypto_trading/templates/main.html +0 -551
- bitunix_automated_crypto_trading/templates/modal-chart.html +0 -26
- bitunix_automated_crypto_trading/templates/modal-config.html +0 -34
- bitunix_automated_crypto_trading/templates/modal-logs.html +0 -15
- bitunix_automated_crypto_trading-2.6.7.dist-info/RECORD +0 -31
- {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/WHEEL +0 -0
- {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/entry_points.txt +0 -0
- {bitunix_automated_crypto_trading-2.6.7.dist-info → bitunix_automated_crypto_trading-2.6.8.dist-info}/top_level.txt +0 -0
@@ -1,254 +1,254 @@
|
|
1
|
-
import websockets
|
2
|
-
import json
|
3
|
-
import time
|
4
|
-
import hashlib
|
5
|
-
import asyncio
|
6
|
-
import random
|
7
|
-
import string
|
8
|
-
from typing import Callable
|
9
|
-
import threading
|
10
|
-
from
|
11
|
-
logger = Logger(__name__).get_logger()
|
12
|
-
import gc
|
13
|
-
|
14
|
-
class BitunixPublicWebSocketClient:
|
15
|
-
def __init__(self, api_key, secret_key, type):
|
16
|
-
self.api_key = api_key
|
17
|
-
self.secret_key = secret_key
|
18
|
-
self.type = type
|
19
|
-
self.url = "wss://fapi.bitunix.com/public/"
|
20
|
-
self.websocket = None
|
21
|
-
self.running=False
|
22
|
-
self.loop = asyncio.new_event_loop()
|
23
|
-
self.loop_thread = threading.Thread(target=self.start_loop)
|
24
|
-
self.loop_thread.daemon = True # Ensure the thread closes with the program
|
25
|
-
self.loop_thread.start()
|
26
|
-
|
27
|
-
|
28
|
-
def start_loop(self):
|
29
|
-
asyncio.set_event_loop(self.loop)
|
30
|
-
self.loop.run_forever()
|
31
|
-
|
32
|
-
def stop_loop(self):
|
33
|
-
# Stop the loop in a thread-safe way
|
34
|
-
self.loop.call_soon_threadsafe(self.loop.stop)
|
35
|
-
|
36
|
-
# Wait for the thread to finish
|
37
|
-
self.loop_thread.join()
|
38
|
-
logger.info("Event loop stopped cleanly")
|
39
|
-
|
40
|
-
|
41
|
-
async def run_websocket(self, process_func: Callable):
|
42
|
-
self.running = True
|
43
|
-
while self.running:
|
44
|
-
try:
|
45
|
-
async with websockets.connect(self.url, ping_interval=None, open_timeout=30) as self.websocket:
|
46
|
-
connect_response = await self.websocket.recv()
|
47
|
-
logger.info(f"{self.url} {self.type} websocket connect Response: {connect_response}")
|
48
|
-
|
49
|
-
self.heartbeat_task = asyncio.create_task(self.send_heartbeat(self.websocket))
|
50
|
-
|
51
|
-
for ticker in self.tickerList:
|
52
|
-
subscribe_message = self.create_subscription_message(ticker)
|
53
|
-
if subscribe_message:
|
54
|
-
await self.websocket.send(subscribe_message)
|
55
|
-
|
56
|
-
self.recv_task = asyncio.create_task(self.receive_messages(process_func))
|
57
|
-
await self.recv_task
|
58
|
-
except websockets.exceptions.ConnectionClosedError as e:
|
59
|
-
logger.warning(f"{self.url} {self.type} WebSocket connection closed. Retrying in 5 seconds: {e}")
|
60
|
-
await asyncio.sleep(5) # Delay before retrying
|
61
|
-
except Exception as e:
|
62
|
-
logger.error(f"{self.url} {self.type} Unexpected error during WebSocket operation: {e}")
|
63
|
-
await asyncio.sleep(5) # Delay before retrying
|
64
|
-
|
65
|
-
def create_subscription_message(self, ticker):
|
66
|
-
if self.type == 'depth':
|
67
|
-
dep = {"symbol": ticker, "ch": "depth_book1"}
|
68
|
-
return json.dumps({"op": "subscribe", "args": [dep]})
|
69
|
-
elif self.type == 'kline':
|
70
|
-
kli = {"symbol": ticker, "ch": "market_kline_1min"}
|
71
|
-
return json.dumps({"op": "subscribe", "args": [kli]})
|
72
|
-
elif self.type == 'ticker':
|
73
|
-
tic = {"symbol": ticker, "ch": "ticker"}
|
74
|
-
return json.dumps({"op": "subscribe", "args": [tic]})
|
75
|
-
return None
|
76
|
-
|
77
|
-
async def receive_messages(self, process_func: Callable):
|
78
|
-
try:
|
79
|
-
while self.running:
|
80
|
-
message = await self.websocket.recv()
|
81
|
-
await process_func(message)
|
82
|
-
logger.warning(f"{self.url} {self.type} WebSocket connection closed")
|
83
|
-
except asyncio.CancelledError:
|
84
|
-
logger.info(f"{self.url} {self.type} WebSocket receive task cancelled")
|
85
|
-
self.running = False
|
86
|
-
except websockets.exceptions.ConnectionClosedError as e:
|
87
|
-
logger.info(f"{self.url} {self.type} Websocket: Connection closed: {e}")
|
88
|
-
except Exception as e:
|
89
|
-
logger.info(f"{self.url} {self.type} Websocket: Unexpected error: {e}")
|
90
|
-
pass
|
91
|
-
del message, process_func
|
92
|
-
gc.collect()
|
93
|
-
|
94
|
-
async def send_heartbeat(self, websocket):
|
95
|
-
try:
|
96
|
-
while True:
|
97
|
-
await websocket.send(json.dumps({"op": "ping", "ping": int(time.time())}))
|
98
|
-
await asyncio.sleep(30)
|
99
|
-
except asyncio.CancelledError:
|
100
|
-
logger.info(f"{self.url} {self.type} WebSocket hearbeat task cancelled")
|
101
|
-
except websockets.exceptions.ConnectionClosed:
|
102
|
-
logger.info(f"{self.url} {self.type} WebSocket connection for heartbeat is closed")
|
103
|
-
except Exception as e:
|
104
|
-
logger.info(f"{self.url} {self.type} Websocket for heartbeat: Unexpected error: {e}")
|
105
|
-
pass
|
106
|
-
|
107
|
-
async def stop_websocket(self):
|
108
|
-
try:
|
109
|
-
self.running = False
|
110
|
-
# Cancel the receive task
|
111
|
-
if hasattr(self, "recv_task") and self.recv_task:
|
112
|
-
self.loop.call_soon_threadsafe(self.recv_task.cancel) # Schedule cancellation
|
113
|
-
|
114
|
-
# Cancel the heartbeat task
|
115
|
-
if hasattr(self, "heartbeat_task") and self.heartbeat_task:
|
116
|
-
self.loop.call_soon_threadsafe(self.heartbeat_task.cancel) # Schedule cancellation
|
117
|
-
|
118
|
-
# Close the WebSocket
|
119
|
-
if self.websocket is not None:
|
120
|
-
self.loop.call_soon_threadsafe(self.websocket.close) #
|
121
|
-
|
122
|
-
except Exception as e:
|
123
|
-
logger.error(f"{self.url} {self.type} Unexpected error during WebSocket shutdown: {e}")
|
124
|
-
finally:
|
125
|
-
# Stop the event loop
|
126
|
-
self.loop.call_soon_threadsafe(self.loop.stop)
|
127
|
-
logger.info(f"{self.url} {self.type} WebSocket thread stopped and resources cleaned up.")
|
128
|
-
|
129
|
-
class BitunixPrivateWebSocketClient:
|
130
|
-
def __init__(self, api_key, secret_key):
|
131
|
-
self.api_key = api_key
|
132
|
-
self.secret_key = secret_key
|
133
|
-
self.url = "wss://fapi.bitunix.com/private/"
|
134
|
-
self.websocket = None
|
135
|
-
self.running=False
|
136
|
-
self.loop = asyncio.new_event_loop()
|
137
|
-
self.loop_thread = threading.Thread(target=self.start_loop)
|
138
|
-
self.loop_thread.daemon = True # Ensure the thread closes with the program
|
139
|
-
self.loop_thread.start()
|
140
|
-
|
141
|
-
|
142
|
-
def start_loop(self):
|
143
|
-
asyncio.set_event_loop(self.loop)
|
144
|
-
self.loop.run_forever()
|
145
|
-
|
146
|
-
def stop_loop(self):
|
147
|
-
# Stop the loop in a thread-safe way
|
148
|
-
self.loop.call_soon_threadsafe(self.loop.stop)
|
149
|
-
|
150
|
-
# Wait for the thread to finish
|
151
|
-
self.loop_thread.join()
|
152
|
-
logger.info("Event loop stopped cleanly")
|
153
|
-
|
154
|
-
async def run_websocket(self, process_func: Callable):
|
155
|
-
self.running = True
|
156
|
-
while self.running:
|
157
|
-
try:
|
158
|
-
async with websockets.connect(self.url, ping_interval=None, open_timeout=30) as self.websocket:
|
159
|
-
connect_response = await self.websocket.recv()
|
160
|
-
logger.info(f"{self.url} websocket connect Response: {connect_response}")
|
161
|
-
|
162
|
-
self.heartbeat_task = asyncio.create_task(self.send_heartbeat(self.websocket))
|
163
|
-
|
164
|
-
nonce = await self.generate_nonce(32)
|
165
|
-
sign, timestamp = await self.generate_signature(self.api_key, self.secret_key, nonce)
|
166
|
-
login_request = {
|
167
|
-
"op": "login",
|
168
|
-
"args": [
|
169
|
-
{
|
170
|
-
"apiKey": self.api_key,
|
171
|
-
"timestamp": timestamp,
|
172
|
-
"nonce": nonce,
|
173
|
-
"sign": sign,
|
174
|
-
}
|
175
|
-
],
|
176
|
-
}
|
177
|
-
await self.websocket.send(json.dumps(login_request))
|
178
|
-
login_response = await self.websocket.recv()
|
179
|
-
logger.info(f"{self.url} Login Response: {login_response}")
|
180
|
-
|
181
|
-
self.recv_task = asyncio.create_task(self.receive_messages(process_func))
|
182
|
-
await self.recv_task
|
183
|
-
|
184
|
-
except websockets.exceptions.ConnectionClosedError as e:
|
185
|
-
logger.warning(f"{self.type} WebSocket connection closed. Retrying in 5 seconds: {e}")
|
186
|
-
await asyncio.sleep(5) # Delay before retrying
|
187
|
-
except Exception as e:
|
188
|
-
logger.error(f"Unexpected error during {self.type} WebSocket operation: {e}")
|
189
|
-
await asyncio.sleep(5) # Delay before retrying
|
190
|
-
|
191
|
-
async def receive_messages(self, process_func: Callable):
|
192
|
-
try:
|
193
|
-
while self.running:
|
194
|
-
message = await self.websocket.recv()
|
195
|
-
await process_func(message)
|
196
|
-
logger.warning(f"{self.type} WebSocket connection closed")
|
197
|
-
except asyncio.CancelledError:
|
198
|
-
logger.info(f"{self.url} WebSocket receive task cancelled")
|
199
|
-
self.running = False
|
200
|
-
except websockets.exceptions.ConnectionClosedError as e:
|
201
|
-
logger.info(f"{self.url} Websocket: Connection closed: {e}")
|
202
|
-
except Exception as e:
|
203
|
-
logger.info(f"{self.url} Websocket: Unexpected error: {e}")
|
204
|
-
pass
|
205
|
-
del message, process_func
|
206
|
-
gc.collect()
|
207
|
-
|
208
|
-
async def send_heartbeat(self, websocket):
|
209
|
-
try:
|
210
|
-
while True:
|
211
|
-
await websocket.send(json.dumps({"op": "ping", "ping": int(time.time())}))
|
212
|
-
await asyncio.sleep(30)
|
213
|
-
except asyncio.CancelledError:
|
214
|
-
logger.info(f"{self.url} WebSocket hearbeat task cancelled")
|
215
|
-
except websockets.exceptions.ConnectionClosed:
|
216
|
-
logger.info(f"{self.url} WebSocket connection for heartbeat is closed")
|
217
|
-
except Exception as e:
|
218
|
-
logger.info(f"{self.url} Websocket for heartbeat: Unexpected error: {e}")
|
219
|
-
pass
|
220
|
-
|
221
|
-
async def stop_websocket(self):
|
222
|
-
try:
|
223
|
-
self.running = False
|
224
|
-
# Cancel the receive task
|
225
|
-
if hasattr(self, "recv_task") and self.recv_task:
|
226
|
-
self.loop.call_soon_threadsafe(self.recv_task.cancel) # Schedule cancellation
|
227
|
-
|
228
|
-
# Cancel the heartbeat task
|
229
|
-
if hasattr(self, "heartbeat_task") and self.heartbeat_task:
|
230
|
-
self.loop.call_soon_threadsafe(self.heartbeat_task.cancel) # Schedule cancellation
|
231
|
-
|
232
|
-
# Close the WebSocket
|
233
|
-
if self.websocket is not None:
|
234
|
-
self.loop.call_soon_threadsafe(self.websocket.close) #
|
235
|
-
|
236
|
-
except Exception as e:
|
237
|
-
logger.error(f"Unexpected error during {self.url} WebSocket shutdown: {e}")
|
238
|
-
finally:
|
239
|
-
# Stop the event loop
|
240
|
-
self.loop.call_soon_threadsafe(self.loop.stop)
|
241
|
-
logger.info(f"{self.url} WebSocket thread stopped and resources cleaned up.")
|
242
|
-
|
243
|
-
|
244
|
-
async def generate_signature(self, api_key, secret_key, nonce):
|
245
|
-
timestamp = int(time.time())
|
246
|
-
pre_sign = f"{nonce}{timestamp}{api_key}"
|
247
|
-
sign = hashlib.sha256(pre_sign.encode()).hexdigest()
|
248
|
-
final_sign = hashlib.sha256((sign + secret_key).encode()).hexdigest()
|
249
|
-
return final_sign, timestamp
|
250
|
-
|
251
|
-
async def generate_nonce(self, length=32):
|
252
|
-
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
253
|
-
|
254
|
-
|
1
|
+
import websockets
|
2
|
+
import json
|
3
|
+
import time
|
4
|
+
import hashlib
|
5
|
+
import asyncio
|
6
|
+
import random
|
7
|
+
import string
|
8
|
+
from typing import Callable
|
9
|
+
import threading
|
10
|
+
from logger import Logger
|
11
|
+
logger = Logger(__name__).get_logger()
|
12
|
+
import gc
|
13
|
+
|
14
|
+
class BitunixPublicWebSocketClient:
|
15
|
+
def __init__(self, api_key, secret_key, type):
|
16
|
+
self.api_key = api_key
|
17
|
+
self.secret_key = secret_key
|
18
|
+
self.type = type
|
19
|
+
self.url = "wss://fapi.bitunix.com/public/"
|
20
|
+
self.websocket = None
|
21
|
+
self.running=False
|
22
|
+
self.loop = asyncio.new_event_loop()
|
23
|
+
self.loop_thread = threading.Thread(target=self.start_loop)
|
24
|
+
self.loop_thread.daemon = True # Ensure the thread closes with the program
|
25
|
+
self.loop_thread.start()
|
26
|
+
|
27
|
+
|
28
|
+
def start_loop(self):
|
29
|
+
asyncio.set_event_loop(self.loop)
|
30
|
+
self.loop.run_forever()
|
31
|
+
|
32
|
+
def stop_loop(self):
|
33
|
+
# Stop the loop in a thread-safe way
|
34
|
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
35
|
+
|
36
|
+
# Wait for the thread to finish
|
37
|
+
self.loop_thread.join()
|
38
|
+
logger.info("Event loop stopped cleanly")
|
39
|
+
|
40
|
+
|
41
|
+
async def run_websocket(self, process_func: Callable):
|
42
|
+
self.running = True
|
43
|
+
while self.running:
|
44
|
+
try:
|
45
|
+
async with websockets.connect(self.url, ping_interval=None, open_timeout=30) as self.websocket:
|
46
|
+
connect_response = await self.websocket.recv()
|
47
|
+
logger.info(f"{self.url} {self.type} websocket connect Response: {connect_response}")
|
48
|
+
|
49
|
+
self.heartbeat_task = asyncio.create_task(self.send_heartbeat(self.websocket))
|
50
|
+
|
51
|
+
for ticker in self.tickerList:
|
52
|
+
subscribe_message = self.create_subscription_message(ticker)
|
53
|
+
if subscribe_message:
|
54
|
+
await self.websocket.send(subscribe_message)
|
55
|
+
|
56
|
+
self.recv_task = asyncio.create_task(self.receive_messages(process_func))
|
57
|
+
await self.recv_task
|
58
|
+
except websockets.exceptions.ConnectionClosedError as e:
|
59
|
+
logger.warning(f"{self.url} {self.type} WebSocket connection closed. Retrying in 5 seconds: {e}")
|
60
|
+
await asyncio.sleep(5) # Delay before retrying
|
61
|
+
except Exception as e:
|
62
|
+
logger.error(f"{self.url} {self.type} Unexpected error during WebSocket operation: {e}")
|
63
|
+
await asyncio.sleep(5) # Delay before retrying
|
64
|
+
|
65
|
+
def create_subscription_message(self, ticker):
|
66
|
+
if self.type == 'depth':
|
67
|
+
dep = {"symbol": ticker, "ch": "depth_book1"}
|
68
|
+
return json.dumps({"op": "subscribe", "args": [dep]})
|
69
|
+
elif self.type == 'kline':
|
70
|
+
kli = {"symbol": ticker, "ch": "market_kline_1min"}
|
71
|
+
return json.dumps({"op": "subscribe", "args": [kli]})
|
72
|
+
elif self.type == 'ticker':
|
73
|
+
tic = {"symbol": ticker, "ch": "ticker"}
|
74
|
+
return json.dumps({"op": "subscribe", "args": [tic]})
|
75
|
+
return None
|
76
|
+
|
77
|
+
async def receive_messages(self, process_func: Callable):
|
78
|
+
try:
|
79
|
+
while self.running:
|
80
|
+
message = await self.websocket.recv()
|
81
|
+
await process_func(message)
|
82
|
+
logger.warning(f"{self.url} {self.type} WebSocket connection closed")
|
83
|
+
except asyncio.CancelledError:
|
84
|
+
logger.info(f"{self.url} {self.type} WebSocket receive task cancelled")
|
85
|
+
self.running = False
|
86
|
+
except websockets.exceptions.ConnectionClosedError as e:
|
87
|
+
logger.info(f"{self.url} {self.type} Websocket: Connection closed: {e}")
|
88
|
+
except Exception as e:
|
89
|
+
logger.info(f"{self.url} {self.type} Websocket: Unexpected error: {e}")
|
90
|
+
pass
|
91
|
+
del message, process_func
|
92
|
+
gc.collect()
|
93
|
+
|
94
|
+
async def send_heartbeat(self, websocket):
|
95
|
+
try:
|
96
|
+
while True:
|
97
|
+
await websocket.send(json.dumps({"op": "ping", "ping": int(time.time())}))
|
98
|
+
await asyncio.sleep(30)
|
99
|
+
except asyncio.CancelledError:
|
100
|
+
logger.info(f"{self.url} {self.type} WebSocket hearbeat task cancelled")
|
101
|
+
except websockets.exceptions.ConnectionClosed:
|
102
|
+
logger.info(f"{self.url} {self.type} WebSocket connection for heartbeat is closed")
|
103
|
+
except Exception as e:
|
104
|
+
logger.info(f"{self.url} {self.type} Websocket for heartbeat: Unexpected error: {e}")
|
105
|
+
pass
|
106
|
+
|
107
|
+
async def stop_websocket(self):
|
108
|
+
try:
|
109
|
+
self.running = False
|
110
|
+
# Cancel the receive task
|
111
|
+
if hasattr(self, "recv_task") and self.recv_task:
|
112
|
+
self.loop.call_soon_threadsafe(self.recv_task.cancel) # Schedule cancellation
|
113
|
+
|
114
|
+
# Cancel the heartbeat task
|
115
|
+
if hasattr(self, "heartbeat_task") and self.heartbeat_task:
|
116
|
+
self.loop.call_soon_threadsafe(self.heartbeat_task.cancel) # Schedule cancellation
|
117
|
+
|
118
|
+
# Close the WebSocket
|
119
|
+
if self.websocket is not None:
|
120
|
+
self.loop.call_soon_threadsafe(self.websocket.close) #
|
121
|
+
|
122
|
+
except Exception as e:
|
123
|
+
logger.error(f"{self.url} {self.type} Unexpected error during WebSocket shutdown: {e}")
|
124
|
+
finally:
|
125
|
+
# Stop the event loop
|
126
|
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
127
|
+
logger.info(f"{self.url} {self.type} WebSocket thread stopped and resources cleaned up.")
|
128
|
+
|
129
|
+
class BitunixPrivateWebSocketClient:
|
130
|
+
def __init__(self, api_key, secret_key):
|
131
|
+
self.api_key = api_key
|
132
|
+
self.secret_key = secret_key
|
133
|
+
self.url = "wss://fapi.bitunix.com/private/"
|
134
|
+
self.websocket = None
|
135
|
+
self.running=False
|
136
|
+
self.loop = asyncio.new_event_loop()
|
137
|
+
self.loop_thread = threading.Thread(target=self.start_loop)
|
138
|
+
self.loop_thread.daemon = True # Ensure the thread closes with the program
|
139
|
+
self.loop_thread.start()
|
140
|
+
|
141
|
+
|
142
|
+
def start_loop(self):
|
143
|
+
asyncio.set_event_loop(self.loop)
|
144
|
+
self.loop.run_forever()
|
145
|
+
|
146
|
+
def stop_loop(self):
|
147
|
+
# Stop the loop in a thread-safe way
|
148
|
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
149
|
+
|
150
|
+
# Wait for the thread to finish
|
151
|
+
self.loop_thread.join()
|
152
|
+
logger.info("Event loop stopped cleanly")
|
153
|
+
|
154
|
+
async def run_websocket(self, process_func: Callable):
|
155
|
+
self.running = True
|
156
|
+
while self.running:
|
157
|
+
try:
|
158
|
+
async with websockets.connect(self.url, ping_interval=None, open_timeout=30) as self.websocket:
|
159
|
+
connect_response = await self.websocket.recv()
|
160
|
+
logger.info(f"{self.url} websocket connect Response: {connect_response}")
|
161
|
+
|
162
|
+
self.heartbeat_task = asyncio.create_task(self.send_heartbeat(self.websocket))
|
163
|
+
|
164
|
+
nonce = await self.generate_nonce(32)
|
165
|
+
sign, timestamp = await self.generate_signature(self.api_key, self.secret_key, nonce)
|
166
|
+
login_request = {
|
167
|
+
"op": "login",
|
168
|
+
"args": [
|
169
|
+
{
|
170
|
+
"apiKey": self.api_key,
|
171
|
+
"timestamp": timestamp,
|
172
|
+
"nonce": nonce,
|
173
|
+
"sign": sign,
|
174
|
+
}
|
175
|
+
],
|
176
|
+
}
|
177
|
+
await self.websocket.send(json.dumps(login_request))
|
178
|
+
login_response = await self.websocket.recv()
|
179
|
+
logger.info(f"{self.url} Login Response: {login_response}")
|
180
|
+
|
181
|
+
self.recv_task = asyncio.create_task(self.receive_messages(process_func))
|
182
|
+
await self.recv_task
|
183
|
+
|
184
|
+
except websockets.exceptions.ConnectionClosedError as e:
|
185
|
+
logger.warning(f"{self.type} WebSocket connection closed. Retrying in 5 seconds: {e}")
|
186
|
+
await asyncio.sleep(5) # Delay before retrying
|
187
|
+
except Exception as e:
|
188
|
+
logger.error(f"Unexpected error during {self.type} WebSocket operation: {e}")
|
189
|
+
await asyncio.sleep(5) # Delay before retrying
|
190
|
+
|
191
|
+
async def receive_messages(self, process_func: Callable):
|
192
|
+
try:
|
193
|
+
while self.running:
|
194
|
+
message = await self.websocket.recv()
|
195
|
+
await process_func(message)
|
196
|
+
logger.warning(f"{self.type} WebSocket connection closed")
|
197
|
+
except asyncio.CancelledError:
|
198
|
+
logger.info(f"{self.url} WebSocket receive task cancelled")
|
199
|
+
self.running = False
|
200
|
+
except websockets.exceptions.ConnectionClosedError as e:
|
201
|
+
logger.info(f"{self.url} Websocket: Connection closed: {e}")
|
202
|
+
except Exception as e:
|
203
|
+
logger.info(f"{self.url} Websocket: Unexpected error: {e}")
|
204
|
+
pass
|
205
|
+
del message, process_func
|
206
|
+
gc.collect()
|
207
|
+
|
208
|
+
async def send_heartbeat(self, websocket):
|
209
|
+
try:
|
210
|
+
while True:
|
211
|
+
await websocket.send(json.dumps({"op": "ping", "ping": int(time.time())}))
|
212
|
+
await asyncio.sleep(30)
|
213
|
+
except asyncio.CancelledError:
|
214
|
+
logger.info(f"{self.url} WebSocket hearbeat task cancelled")
|
215
|
+
except websockets.exceptions.ConnectionClosed:
|
216
|
+
logger.info(f"{self.url} WebSocket connection for heartbeat is closed")
|
217
|
+
except Exception as e:
|
218
|
+
logger.info(f"{self.url} Websocket for heartbeat: Unexpected error: {e}")
|
219
|
+
pass
|
220
|
+
|
221
|
+
async def stop_websocket(self):
|
222
|
+
try:
|
223
|
+
self.running = False
|
224
|
+
# Cancel the receive task
|
225
|
+
if hasattr(self, "recv_task") and self.recv_task:
|
226
|
+
self.loop.call_soon_threadsafe(self.recv_task.cancel) # Schedule cancellation
|
227
|
+
|
228
|
+
# Cancel the heartbeat task
|
229
|
+
if hasattr(self, "heartbeat_task") and self.heartbeat_task:
|
230
|
+
self.loop.call_soon_threadsafe(self.heartbeat_task.cancel) # Schedule cancellation
|
231
|
+
|
232
|
+
# Close the WebSocket
|
233
|
+
if self.websocket is not None:
|
234
|
+
self.loop.call_soon_threadsafe(self.websocket.close) #
|
235
|
+
|
236
|
+
except Exception as e:
|
237
|
+
logger.error(f"Unexpected error during {self.url} WebSocket shutdown: {e}")
|
238
|
+
finally:
|
239
|
+
# Stop the event loop
|
240
|
+
self.loop.call_soon_threadsafe(self.loop.stop)
|
241
|
+
logger.info(f"{self.url} WebSocket thread stopped and resources cleaned up.")
|
242
|
+
|
243
|
+
|
244
|
+
async def generate_signature(self, api_key, secret_key, nonce):
|
245
|
+
timestamp = int(time.time())
|
246
|
+
pre_sign = f"{nonce}{timestamp}{api_key}"
|
247
|
+
sign = hashlib.sha256(pre_sign.encode()).hexdigest()
|
248
|
+
final_sign = hashlib.sha256((sign + secret_key).encode()).hexdigest()
|
249
|
+
return final_sign, timestamp
|
250
|
+
|
251
|
+
async def generate_nonce(self, length=32):
|
252
|
+
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
253
|
+
|
254
|
+
|
@@ -1,74 +1,74 @@
|
|
1
|
-
import pandas as pd
|
2
|
-
from jinja2 import Template
|
3
|
-
|
4
|
-
class DataFrameHtmlRenderer:
|
5
|
-
def __init__(self, hide_columns=None, color_column_mapping=None):
|
6
|
-
"""
|
7
|
-
Initialize the renderer with columns to hide and column-color mapping.
|
8
|
-
"""
|
9
|
-
self.hide_columns = hide_columns or []
|
10
|
-
self.color_column_mapping = color_column_mapping or {}
|
11
|
-
|
12
|
-
def preprocess_styles(self, dataframe):
|
13
|
-
"""
|
14
|
-
Precomputes the styles for columns that need coloring based on the provided DataFrame.
|
15
|
-
Returns a DataFrame with styles.
|
16
|
-
"""
|
17
|
-
style_df = pd.DataFrame(index=dataframe.index, columns=dataframe.columns)
|
18
|
-
for col, color_col in self.color_column_mapping.items():
|
19
|
-
if col in dataframe.columns and color_col in dataframe.columns:
|
20
|
-
style_df[col] = dataframe[color_col] # Copy the color values
|
21
|
-
return style_df.fillna("") # Fill empty cells with blank strings
|
22
|
-
|
23
|
-
def render_html(self, dataframe):
|
24
|
-
"""
|
25
|
-
Renders the DataFrame as an HTML string with conditional coloring for specified columns.
|
26
|
-
"""
|
27
|
-
style_df = self.preprocess_styles(dataframe)
|
28
|
-
|
29
|
-
# Create combined_data for all columns
|
30
|
-
combined_data = []
|
31
|
-
for index, row in dataframe.iterrows():
|
32
|
-
row_data = []
|
33
|
-
for col in dataframe.columns:
|
34
|
-
row_data.append({
|
35
|
-
"value": row[col], # The cell's value
|
36
|
-
"style": style_df.at[index, col], # The cell's style (if any)
|
37
|
-
"column": col # The column name for the cell
|
38
|
-
})
|
39
|
-
combined_data.append(row_data)
|
40
|
-
|
41
|
-
# Jinja2 HTML template
|
42
|
-
template = Template("""
|
43
|
-
<table border="1" style="border-collapse: collapse; width: 100%; font-size: small;">
|
44
|
-
<thead>
|
45
|
-
<tr>
|
46
|
-
{% for column in columns if column not in hide_columns %}
|
47
|
-
<th>{{ column }}</th>
|
48
|
-
{% endfor %}
|
49
|
-
</tr>
|
50
|
-
</thead>
|
51
|
-
<tbody>
|
52
|
-
{% for row in combined_data %}
|
53
|
-
<tr>
|
54
|
-
{% for cell in row %}
|
55
|
-
{% if cell.column not in hide_columns %}
|
56
|
-
<td {% if cell.style %} style="background-color: {{ cell.style }};" {% endif %}>
|
57
|
-
{{ cell.value }}
|
58
|
-
</td>
|
59
|
-
{% endif %}
|
60
|
-
{% endfor %}
|
61
|
-
</tr>
|
62
|
-
{% endfor %}
|
63
|
-
</tbody>
|
64
|
-
</table>
|
65
|
-
""")
|
66
|
-
|
67
|
-
# Render the HTML
|
68
|
-
html_output = template.render(
|
69
|
-
columns=dataframe.columns.tolist(), # All columns
|
70
|
-
hide_columns=self.hide_columns, # Columns to hide
|
71
|
-
combined_data=combined_data # Combined data (values + styles)
|
72
|
-
)
|
73
|
-
return html_output
|
74
|
-
|
1
|
+
import pandas as pd
|
2
|
+
from jinja2 import Template
|
3
|
+
|
4
|
+
class DataFrameHtmlRenderer:
|
5
|
+
def __init__(self, hide_columns=None, color_column_mapping=None):
|
6
|
+
"""
|
7
|
+
Initialize the renderer with columns to hide and column-color mapping.
|
8
|
+
"""
|
9
|
+
self.hide_columns = hide_columns or []
|
10
|
+
self.color_column_mapping = color_column_mapping or {}
|
11
|
+
|
12
|
+
def preprocess_styles(self, dataframe):
|
13
|
+
"""
|
14
|
+
Precomputes the styles for columns that need coloring based on the provided DataFrame.
|
15
|
+
Returns a DataFrame with styles.
|
16
|
+
"""
|
17
|
+
style_df = pd.DataFrame(index=dataframe.index, columns=dataframe.columns)
|
18
|
+
for col, color_col in self.color_column_mapping.items():
|
19
|
+
if col in dataframe.columns and color_col in dataframe.columns:
|
20
|
+
style_df[col] = dataframe[color_col] # Copy the color values
|
21
|
+
return style_df.fillna("") # Fill empty cells with blank strings
|
22
|
+
|
23
|
+
def render_html(self, dataframe):
|
24
|
+
"""
|
25
|
+
Renders the DataFrame as an HTML string with conditional coloring for specified columns.
|
26
|
+
"""
|
27
|
+
style_df = self.preprocess_styles(dataframe)
|
28
|
+
|
29
|
+
# Create combined_data for all columns
|
30
|
+
combined_data = []
|
31
|
+
for index, row in dataframe.iterrows():
|
32
|
+
row_data = []
|
33
|
+
for col in dataframe.columns:
|
34
|
+
row_data.append({
|
35
|
+
"value": row[col], # The cell's value
|
36
|
+
"style": style_df.at[index, col], # The cell's style (if any)
|
37
|
+
"column": col # The column name for the cell
|
38
|
+
})
|
39
|
+
combined_data.append(row_data)
|
40
|
+
|
41
|
+
# Jinja2 HTML template
|
42
|
+
template = Template("""
|
43
|
+
<table border="1" style="border-collapse: collapse; width: 100%; font-size: small;">
|
44
|
+
<thead>
|
45
|
+
<tr>
|
46
|
+
{% for column in columns if column not in hide_columns %}
|
47
|
+
<th>{{ column }}</th>
|
48
|
+
{% endfor %}
|
49
|
+
</tr>
|
50
|
+
</thead>
|
51
|
+
<tbody>
|
52
|
+
{% for row in combined_data %}
|
53
|
+
<tr>
|
54
|
+
{% for cell in row %}
|
55
|
+
{% if cell.column not in hide_columns %}
|
56
|
+
<td {% if cell.style %} style="background-color: {{ cell.style }};" {% endif %}>
|
57
|
+
{{ cell.value }}
|
58
|
+
</td>
|
59
|
+
{% endif %}
|
60
|
+
{% endfor %}
|
61
|
+
</tr>
|
62
|
+
{% endfor %}
|
63
|
+
</tbody>
|
64
|
+
</table>
|
65
|
+
""")
|
66
|
+
|
67
|
+
# Render the HTML
|
68
|
+
html_output = template.render(
|
69
|
+
columns=dataframe.columns.tolist(), # All columns
|
70
|
+
hide_columns=self.hide_columns, # Columns to hide
|
71
|
+
combined_data=combined_data # Combined data (values + styles)
|
72
|
+
)
|
73
|
+
return html_output
|
74
|
+
|