-
Notifications
You must be signed in to change notification settings - Fork 1
/
ws_bfx_handler.py
394 lines (311 loc) · 15.3 KB
/
ws_bfx_handler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# Import default libraries
import time, datetime
import json, requests
import hmac, hashlib
# Import connectivity libraries
import websocket
# Import multi-threading libraries
from multiprocessing import Queue
from threading import Thread, Event, Timer
# Load api keys
from api_keys import *
# Load settings for websocket
import ws_bfx_settings
import logging_handler
# URLs
url_bfx = 'wss://api.bitfinex.com/ws/2'
# BFX Websocket class
class bfx_websocket(Thread):
def __init__(self):
# Class name
self.__name = 'bfx_ws'
print(self.__name + ' thread - initializing ... ', end='')
# Internal class variables
self.__key = bfx_api_pkey
self.__skey = bfx_api_skey
# Internal status variables
self._isActive = False
# Internal class events
self._connected = Event()
self._disconnected = Event()
self._pause = Event()
# Channel mappings
self._channel_ids = {}
# Event handlers
self._event_handlers = {
'info': self.__handle_event_info,
'auth': self.__handle_event_auth,
'subscribed': self.__handle_event_subscribed
}
# Data handlers
self._data_handlers = {
'account': self.__process_data_account,
'ticker': self.__process_data_ticker,
'trades': self.__process_data_trades
}
# Data handlers
self._data_account_handlers = {
'ps': self.__handle_data_account_ps,
'ws': self.__handle_data_account_ws,
'os': self.__handle_data_account_os,
'fcs': self.__handle_data_account_fcs,
'fls': self.__handle_data_account_fls,
'fos': self.__handle_data_account_fos
}
# Data queues
self.data_queue = Queue()
# Data Grids
self.account_orders = {}
self.account_funding_positions = {}
# Websocket specific variables
self.ws_version = None
self.ws_userid = None
# Establish as new independent thread
Thread.__init__(self)
self.daemon = True
print('done.')
# Create a logging object to store all incoming messages from the websocket
self.store_raw = False
if self.store_raw:
self.data_raw_log = logging_handler.setup_logger(self.__name, 'raw_data.log')
def run(self):
print(self.__name + ' thread - starting.')
if self.store_raw:
self.data_raw_log.info(self.__name + ' thread - starting.')
self._connect()
def stop(self):
# Disconnect event
self._disconnected.set()
if self.store_raw:
self.data_raw_log.info(self.__name + ' thread - stopped.')
try:
# Check ws connection, close if its open
if self.ws:
self.ws.close()
self._isActive = False
# Give thread a second to process close operation
self.join(timeout=1)
return True
except Exception as e:
print(self.__name + ' thread - Error on stop! Error code: ' + str(e))
return False
# ===================================== Main Loop for websocket connection ===================================== #
def _connect(self):
# Start the websocket object
websocket.enableTrace(False)
self.ws = websocket.WebSocketApp(
url_bfx,
on_open=self._bfx_auth_open,
on_close=self._on_close,
on_error=self._on_error,
on_message=self._on_message
)
# Run loop
self.ws.run_forever()
# self.ws.run_forever(ping_interval=60, ping_timeout=65)
while not self._disconnected.is_set():
print('test')
self.ws.keep_running = True
self.ws.run_forever()
# ===================================== Connection functions ===================================== #
def _bfx_auth_open(self, ws):
# Create encoded payload for authentication
nonce = str(int(time.time() * 1000000))
auth_payload = 'AUTH' + nonce
signature = hmac.new(self.__skey, auth_payload.encode(), hashlib.sha384).hexdigest()
payload = {
'apiKey': self.__key,
'event': 'auth',
'authPayload': auth_payload,
'authNonce': nonce,
'authSig': signature
}
# Send payload to establish authenticated connection to account
print(self.__name + ' thread - Establishing authenticated connection to account.')
try:
self.ws.send(json.dumps(payload))
self._connected.set()
except websocket.WebSocketConnectionClosedException:
print(self.__name + ' thread - Exception! Payload failed to send, websocket connection is closed!')
except Exception as e:
print(self.__name + ' thread - Exception! Exception type: ' + str(e))
def _on_close(self, ws):
self._connected.clear()
self._disconnected.set()
self.ws.close()
print(self.__name + ' thread - Websocket connection has been closed.')
def _on_error(self, ws, error):
print(error)
# Todo: Create an actual error handlers
def _on_message(self, ws, message):
# Decode incoming json message
try:
data = json.loads(message)
except json.JSONDecodeError:
print(self.__name + ' thread - Exception! Bad JSON Message received! Msg: ' + str(message))
return
except Exception as e:
print(message)
print(self.__name + ' thread - Exception! Exception type: ' + str(e))
return
if isinstance(data, dict): # Message is a dictionary | Event type
if self.store_raw:
self.data_raw_log.info(self.__name + ' thread - Raw event: ' + str(data))
if data['event'] in self._event_handlers:
self._event_handlers[data['event']](data)
else:
print(self.__name + ' thread - Missing event handler for: "' + data['event'] + '". ', end='')
print('Event Contents: ' + str(data))
return
else: # Message is a list | Data type
if data[1] == 'hb': # Handle heart beats differently
self.__handle_data_heartbeat(data)
else:
if self.store_raw:
self.data_raw_log.info(self.__name + ' thread - Raw data : ' + str(data))
# Grab channel_name for data_handler identification
try:
channel_pair = self._channel_ids[data[0]]
except:
print(self.__name + ' thread - Warning[Exception]! Unmapped channel for: ' + str(data[0]) + '.')
print(data)
return
# Pass data to data handler for processing
try:
self._data_handlers[channel_pair[0]](data, channel_pair)
except:
print(self.__name + ' thread - Warning[Exception]! Missing data handler for channel pair: "' +
str(channel_pair) + '".')
print(data)
return
# ===================================== External Facing Functions ===================================== #
def subscribe_to_channel(self, channel, pair):
channel = channel.lower()
# Check if channel is a proper channel subscription
if channel in ws_bfx_settings.bfx_public_channels:
if pair in ws_bfx_settings.bfx_trading_pairs:
# Generate subscription payload
if channel != 'book':
payload = {'event': 'subscribe', 'channel': channel, 'pair': pair}
else:
payload = {'event': 'subscribe', 'channel': channel, 'pair': pair,
'prec': ws_bfx_settings.bfx_book_pair_precision[pair],
'length': ws_bfx_settings.bfx_book_pair_length[pair]
}
output_string = self.__name + ' thread - Sending subscription request for CHANNEL: "' + channel
output_string += '" | PAIR: "' + pair + '".'
print(output_string)
# Send subscribe payload through websocket
try:
self.ws.send(json.dumps(payload))
self._connected.set()
except websocket.WebSocketConnectionClosedException:
print(self.__name + ' thread - Exception! Payload failed to send, websocket connection is closed!')
except Exception as e:
print(self.__name + ' thread - Exception! Exception type: ' + str(e))
else:
print(self.__name + ' thread - Warning! Received subscription request to unsupported pair.', end='')
print(' Unsupported Pair: ' + pair)
else:
print(self.__name + ' thread - Warning! Received subscription request to unsupported channel.', end='')
print(' Unsupported Channel: ' + channel)
# ===================================== Event handlers ===================================== #
def __handle_event_info(self, data):
if 'version' in data:
self.ws_version = data['version']
if 'platform' in data:
if 'status' in data['platform']:
if data['platform']['status'] == 1:
print(self.__name + ' thread - BFX Websocket platform is currently active!')
# Todo: have an actual handler for when the platform goes down
else:
print(self.__name + ' thread - BFX Webscoket platform is currently offline!')
# Todo: have an actual handler for when the platform goes down
def __handle_event_auth(self, data):
if 'status' in data and data['status'] == 'OK': # Authenticated channel subscription successful
self._channel_ids[('account', data['chanId'])] = data['chanId']
self._channel_ids[data['chanId']] = ('account', data['chanId'])
print(self.__name + ' thread - Authenticated account channel created. ChanId: ' + str(data['chanId']))
else:
print(self.__name + ' thread - BFX Websocket failed to establish authenticated channel subscription!')
# Todo: Add handlers that will try to re-authenticate when the initial auth. fails
def __handle_event_subscribed(self, data):
self._channel_ids[data['chanId']] = (data['channel'], data['symbol'])
self._channel_ids[(data['channel'], data['symbol'])] = data['chanId']
print(self.__name + ' thread - Successful subscription to CHANNEL: "' + str(data['channel'])
+ '" | PAIR: "' + data['symbol'] + '" | ChanID: ' + str(data['chanId']) + '.')
# ===================================== General Data Handlers ===================================== #
def __handle_data_heartbeat(self, data):
# TODO: Add proper heartbeat, ping/pong handler. Need a seperate thread for message monitoring
if False:
print(self.__name + ' thread - Heartbeat {currently ignored}.')
# ===================================== Ticker Data handlers ===================================== #
def __process_data_ticker(self, data, pair):
print('Ticker data received')
print(data)
# ===================================== Trades Data handlers ===================================== #
def __process_data_trades(self, data, pair):
if pair[1][0] == 'f':
if isinstance(data[1], list): # Snapshot of data
self.data_queue.put((('funding', 'trades'), (pair, data[1])))
else: # Update of data
if data[1] == 'ftu':
self.data_queue.put((('funding', 'trades'), (pair,data[2])))
elif pair[1][0] == 't':
print('Received trading data')
else:
print(self.__name + ' thread - Warning! Unrecognized pair type: "' + str(pair) + '".')
# ===================================== Account Data handlers ===================================== #
def __process_data_account(self, data, pair):
try:
self._data_account_handlers[data[1]](data, pair)
except:
print(self.__name + ' thread - Warning [Exception]! Missing handler for account update type: "' +
str(data[1]) + '".')
print(data)
# Account Position Snapshots - Snapshot of positions that are created through margin borrowing
def __handle_data_account_ps(self, data, pair):
# Todo: Add handler for position snapshot
print(self.__name + ' thread - Position Snapshot received {currently ignored}.')
# Account Wallet Snapshot - Snapshot of what's current in the wallet
def __handle_data_account_ws(self, data, pair):
# Wallet balances snapshot
for balance in data[2]:
if balance[0] in ['exchange', 'margin', 'funding']:
self.data_queue.put((('account', 'wallet'), balance[0:3]))
else:
print(self.__name + ' thread - Invalid account type received! Balance Snapshot: ' + str(balance))
# Account Orders Snapshot - Snapshot of currently active orders
def __handle_data_account_os(self, data, pair):
# Order statuses snapshot
print(self.__name + ' thread - Orders snapshot received! {currently ignored}')
# TODO: Add proper order snapshot handling
# Account Funding Loans Snapshot - Snapshot of funds that have been loaned from other people
def __handle_data_account_fls(self, data, pair):
# We currently don't care about funding loans because we will never borrow
print(self.__name + ' thread - Funding loans snapshot received! {currently ignored}')
# Account Funding Offers Snapshot - Snapshot of active funding being offered to other people
def __handle_data_account_fos(self, data, pair):
# TODO: Add proper funding snapshot handling
print(self.__name + ' thread - Funding Offers Snapshot received {currently ignored}.')
# Account Funding Credits Snapshot - Snapshot of all currently active funding
def __handle_data_account_fcs(self, data, pair):
for position in data[2]:
self.__update_funding_positions(position)
# ===================================== Internal Functions ===================================== #
# Processes data from account funding credits - Includes MySQL Data storage
def __update_funding_positions(self, new_position):
position_update = {
'offerId': new_position[0],
'symbol' : new_position[1],
'side' : new_position[2],
'amount' : new_position[5],
'status' : new_position[7],
'rate' : new_position[11],
'period' : new_position[12],
'created': new_position[3],
'open' : new_position[13],
'payout' : new_position[14],
'update' : new_position[4]
}
self.data_queue.put((('lending', 'position'), position_update))