siglab-py 0.1.0__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.

Potentially problematic release.


This version of siglab-py might be problematic. Click here for more details.

@@ -0,0 +1,658 @@
1
+ from ctypes import ArgumentError
2
+ import sys
3
+ import traceback
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from enum import Enum
7
+ import argparse
8
+ import time
9
+ from datetime import datetime, timedelta
10
+ from typing import List, Dict, Union, Any
11
+ import hashlib
12
+ from collections import deque
13
+ import logging
14
+ import json
15
+ from io import StringIO
16
+ import re
17
+ from re import Pattern
18
+ from redis import StrictRedis
19
+ import asyncio
20
+
21
+ from util.aws_util import AwsKmsUtil
22
+
23
+ import ccxt.pro as ccxtpro
24
+
25
+ from exchanges.any_exchange import AnyExchange
26
+ from ordergateway.client import Order, DivisiblePosition
27
+
28
+ '''
29
+ Usage:
30
+ python gateway.py --gateway_id bybit_01 --default_type linear --rate_limit_ms 100
31
+
32
+ --default_type defaults to linear
33
+ --rate_limit_ms defaults to 100
34
+
35
+ This script is pypy compatible:
36
+ pypy gateway.py --gateway_id bybit_01 --default_type linear --rate_limit_ms 100
37
+
38
+ In above example, $GATEWAY_ID$ is 'bybit_01'.
39
+
40
+ You should place .env.$GATEWAY_ID$ in same folder as gateway.py. Formmat should be.
41
+ ENCRYPT_DECRYPT_WITH_AWS_KMS=Y
42
+ AWS_KMS_KEY_ID=xxx
43
+ APIKEY=xxx
44
+ SECRET=xxx
45
+ PASSPHRASE=xxx
46
+
47
+ If ENCRYPT_DECRYPT_WITH_AWS_KMS set to N, APIKEY, SECRET and PASSPHRASE in un-encrypted format(Bad idea in general but if you want to quickly test things out).
48
+ If ENCRYPT_DECRYPT_WITH_AWS_KMS set to Y, APIKEY, SECRET and PASSPHRASE are decrypted using AWS KMS (You can use 'encrypt_keys_util.py' to encrypt your credentials)
49
+
50
+ Optionally, credentials can be passed in as command line arguments, which will override credentials from .env
51
+ python gateway.py --gateway_id bybit_01 --default_type linear --rate_limit_ms 100 --encrypt_decrypt_with_aws_kms Y --aws_kms_key_id xxx --apikey xxx --secret xxx --passphrase xxx
52
+
53
+ Please lookup 'defaultType' (Whether you're trading spot? Or perpectuals) via ccxt library. It's generally under exchange's method 'describe'. Looks under 'options' tag, look for 'defaultType'.
54
+ 'Perpetual contracts' are generally referred to as 'linear' or 'swap'.
55
+
56
+ Examples,
57
+ binance spot, future, margin, delivery, option https://github.com/ccxt/ccxt/blob/master/python/ccxt/binance.py#L1298
58
+ Deribit spot, swap, future https://github.com/ccxt/ccxt/blob/master/python/ccxt/deribit.py#L360
59
+ bybit supports spot, linear, inverse, futures https://github.com/ccxt/ccxt/blob/master/python/ccxt/bybit.py#L1041
60
+ okx supports funding, spot, margin, future, swap, option https://github.com/ccxt/ccxt/blob/master/python/ccxt/okx.py#L1144
61
+ hyperliquid swap only https://github.com/ccxt/ccxt/blob/master/python/ccxt/hyperliquid.py#L225
62
+
63
+ To add exchange, extend "instantiate_exchange".
64
+
65
+ To debug from vscode, launch.json:
66
+ {
67
+ "version": "0.2.0",
68
+ "configurations": [
69
+ {
70
+ "name": "Python Debugger: Current File",
71
+ "type": "debugpy",
72
+ "request": "launch",
73
+ "program": "${file}",
74
+ "console": "integratedTerminal",
75
+ "args" : [
76
+ "--gateway_id", "bybit_01",
77
+ "--default_type", "linear",
78
+ "--rate_limit_ms", "100",
79
+
80
+ "--encrypt_decrypt_with_aws_kms", "N",
81
+ "--aws_kms_key_id", "",
82
+ "--apikey", "xxx",
83
+ "--secret", "xxx",
84
+ "--passphrase", "xxx"
85
+ ],
86
+ "env": {
87
+ "PYTHONPATH": "${workspaceFolder}"
88
+ }
89
+ }
90
+ ]
91
+ }
92
+
93
+ gateway.py takes orders from redis. Strategies should publish orders under topic specified under param['incoming_orders_topic_regex'].
94
+
95
+ Expected order format:
96
+ [
97
+ {
98
+ "ticker": "SUSHI/USDT:USDT",
99
+ "side": "sell",
100
+ "amount": 10,
101
+ "order_type": "limit",
102
+ "leg_room_bps": 5,
103
+ "slices": 5,
104
+ "wait_fill_threshold_ms": 15000,
105
+ "executions": {},
106
+ "filled_amount": 0,
107
+ "average_cost": 0
108
+ }
109
+ ]
110
+
111
+ After executions, gateway.py publish back to redis under topic param['executions_publish_topic'].
112
+
113
+ Format:
114
+ [
115
+ {
116
+ "ticker": "SUSHI/USDT:USDT",
117
+ "side": "sell",
118
+ "amount": 10,
119
+ "order_type": "limit",
120
+ "leg_room_bps": 5,
121
+ "slices": 5,
122
+ "wait_fill_threshold_ms": 15000,
123
+ "executions": {
124
+ "xxx": { <-- order id from exchange
125
+ "info": { <-- ccxt convention, raw response from exchanges under info tag
126
+ ...
127
+ },
128
+ "id": "xxx", <-- order id from exchange
129
+ "clientOrderId": "xxx",
130
+ "timestamp": xxx,
131
+ "datetime": "xxx",
132
+ "lastTradeTimestamp": xxx,
133
+ "lastUpdateTimestamp": xxx,
134
+ "symbol": "SUSHI/USDT:USDT",
135
+ "type": "limit",
136
+ "timeInForce": null,
137
+ "postOnly": null,
138
+ "side": "sell",
139
+ "price": 0.8897,
140
+ "stopLossPrice": null,
141
+ "takeProfitPrice": null,
142
+ "triggerPrice": null,
143
+ "average": 0.8901,
144
+ "cost": 1.7802,
145
+ "amount": 2,
146
+ "filled": 2,
147
+ "remaining": 0,
148
+ "status": "closed",
149
+ "fee": {
150
+ "cost": 0.00053406,
151
+ "currency": "USDT"
152
+ },
153
+ "trades": [],
154
+ "reduceOnly": false,
155
+ "fees": [
156
+ {
157
+ "cost": 0.00053406,
158
+ "currency": "USDT"
159
+ }
160
+ ],
161
+ "stopPrice": null,
162
+ "multiplier": 1
163
+ },
164
+ "filled_amount": 10, <-- aggregates computed by gateway.py
165
+ "average_cost": 0.88979 <-- aggregates computed by gateway.py
166
+ }
167
+
168
+ ... more executions ...
169
+ ]
170
+ '''
171
+ class LogLevel(Enum):
172
+ CRITICAL = 50
173
+ ERROR = 40
174
+ WARNING = 30
175
+ INFO = 20
176
+ DEBUG = 10
177
+ NOTSET = 0
178
+
179
+ param : Dict = {
180
+ 'gateway_id' : '---',
181
+
182
+ "incoming_orders_topic_regex" : r"ordergateway_pending_orders_$GATEWAY_ID$",
183
+ "executions_publish_topic" : r"ordergateway_executions_$GATEWAY_ID$",
184
+
185
+ "fetch_order_status_poll_freq_ms" : 500,
186
+
187
+ 'mds' : {
188
+ 'topics' : {
189
+
190
+ },
191
+ 'redis' : {
192
+ 'host' : 'localhost',
193
+ 'port' : 6379,
194
+ 'db' : 0,
195
+ 'ttl_ms' : 1000*60*15 # 15 min?
196
+ }
197
+ }
198
+ }
199
+
200
+ logging.Formatter.converter = time.gmtime
201
+ logger = logging.getLogger()
202
+ log_level = logging.INFO # DEBUG --> INFO --> WARNING --> ERROR
203
+ logger.setLevel(log_level)
204
+ format_str = '%(asctime)s %(message)s'
205
+ formatter = logging.Formatter(format_str)
206
+ sh = logging.StreamHandler()
207
+ sh.setLevel(log_level)
208
+ sh.setFormatter(formatter)
209
+ logger.addHandler(sh)
210
+
211
+ def log(message : str, log_level : LogLevel = LogLevel.INFO):
212
+ if log_level.value<LogLevel.WARNING.value:
213
+ logger.info(f"{datetime.now()} {message}")
214
+
215
+ elif log_level.value==LogLevel.WARNING.value:
216
+ logger.warning(f"{datetime.now()} {message}")
217
+
218
+ elif log_level.value==LogLevel.ERROR.value:
219
+ logger.error(f"{datetime.now()} {message}")
220
+
221
+ def parse_args():
222
+ parser = argparse.ArgumentParser() # type: ignore
223
+
224
+ parser.add_argument("--gateway_id", help="gateway_id: Where are you sending your order?", default=None)
225
+ parser.add_argument("--default_type", help="default_type: spot, linear, inverse, futures ...etc", default='linear')
226
+ parser.add_argument("--rate_limit_ms", help="rate_limit_ms: Check your exchange rules", default=100)
227
+
228
+ parser.add_argument("--encrypt_decrypt_with_aws_kms", help="Y or N. If encrypt_decrypt_with_aws_kms=N, pass in apikey, secret and passphrase unencrypted (Not recommended, for testing only). If Y, they will be decrypted using AMS KMS key.", default='N')
229
+ parser.add_argument("--aws_kms_key_id", help="AWS KMS key ID", default=None)
230
+ parser.add_argument("--apikey", help="Exchange apikey", default=None)
231
+ parser.add_argument("--secret", help="Exchange secret", default=None)
232
+ parser.add_argument("--passphrase", help="Exchange passphrase", default=None)
233
+
234
+ args = parser.parse_args()
235
+ param['gateway_id'] = args.gateway_id
236
+ param['default_type'] = args.default_type
237
+ param['rate_limit_ms'] = int(args.rate_limit_ms)
238
+
239
+ if args.encrypt_decrypt_with_aws_kms:
240
+ if args.encrypt_decrypt_with_aws_kms=='Y':
241
+ param['encrypt_decrypt_with_aws_kms'] = True
242
+ else:
243
+ param['encrypt_decrypt_with_aws_kms'] = False
244
+ else:
245
+ param['encrypt_decrypt_with_aws_kms'] = False
246
+
247
+ param['aws_kms_key_id'] = args.aws_kms_key_id
248
+ param['apikey'] = args.apikey
249
+ param['secret'] = args.secret
250
+ param['passphrase'] = args.passphrase
251
+
252
+ def init_redis_client() -> StrictRedis:
253
+ redis_client : StrictRedis = StrictRedis(
254
+ host = param['mds']['redis']['host'],
255
+ port = param['mds']['redis']['port'],
256
+ db = 0,
257
+ ssl = False
258
+ )
259
+ try:
260
+ redis_client.keys()
261
+ except ConnectionError as redis_conn_error:
262
+ err_msg = f"Failed to connect to redis: {param['mds']['redis']['host']}, port: {param['mds']['redis']['port']}"
263
+ raise ConnectionError(err_msg)
264
+
265
+ return redis_client
266
+
267
+ def instantiate_exchange(
268
+ gateway_id : str,
269
+ api_key : str,
270
+ secret : str,
271
+ passphrase : str,
272
+ default_type : str,
273
+ rate_limit_ms : float = 100
274
+ ) -> Union[AnyExchange, None]:
275
+ exchange : Union[AnyExchange, None] = None
276
+ exchange_name : str = gateway_id.split('_')[0]
277
+ exchange_name =exchange_name.lower().strip()
278
+
279
+ # Look at ccxt exchange.describe. under 'options' \ 'defaultType' (and 'defaultSubType') for what markets the exchange support.
280
+ # https://docs.ccxt.com/en/latest/manual.html#instantiation
281
+ exchange_params : Dict[str, Any]= {
282
+ 'apiKey' : api_key,
283
+ 'secret' : secret,
284
+ 'enableRateLimit' : True,
285
+ 'rateLimit' : rate_limit_ms,
286
+ 'options' : {
287
+ 'defaultType' : default_type
288
+ }
289
+ }
290
+
291
+ if exchange_name=='binance':
292
+ # spot, future, margin, delivery, option
293
+ # https://github.com/ccxt/ccxt/blob/master/python/ccxt/binance.py#L1298
294
+ exchange = ccxtpro.binance(exchange_params) # type: ignore
295
+ elif exchange_name=='bybit':
296
+ # spot, linear, inverse, futures
297
+ # https://github.com/ccxt/ccxt/blob/master/python/ccxt/bybit.py#L1041
298
+ exchange = ccxtpro.bybit(exchange_params) # type: ignore
299
+ elif exchange_name=='okx':
300
+ # 'funding', spot, margin, future, swap, option
301
+ # https://github.com/ccxt/ccxt/blob/master/python/ccxt/okx.py#L1144
302
+ exchange_params['password'] = passphrase
303
+ exchange = ccxtpro.okx(exchange_params) # type: ignore
304
+ elif exchange_name=='deribit':
305
+ # spot, swap, future
306
+ # https://github.com/ccxt/ccxt/blob/master/python/ccxt/deribit.py#L360
307
+ exchange = ccxtpro.deribit(exchange_params) # type: ignore
308
+ elif exchange_name=='hyperliquid':
309
+ # swap
310
+ # https://github.com/ccxt/ccxt/blob/master/python/ccxt/hyperliquid.py#L225
311
+ exchange = ccxtpro.hyperliquid(exchange_params) # type: ignore
312
+ else:
313
+ raise ArgumentError(f"Unsupported exchange {exchange_name}, check gateway_id {gateway_id}.")
314
+
315
+ return exchange
316
+
317
+ async def watch_orders_task(
318
+ exchange : AnyExchange,
319
+ executions : Dict[str, Dict[str, Any]]
320
+ ):
321
+ while True:
322
+ try:
323
+ order_updates = await exchange.watch_orders() # type: ignore
324
+ for order_update in order_updates:
325
+ order_id = order_update['id']
326
+ executions[order_id] = order_update
327
+
328
+ log(f"order updates: {order_updates}", log_level=LogLevel.INFO)
329
+ except Exception as loop_err:
330
+ print(f"watch_orders_task error: {loop_err}")
331
+
332
+ await asyncio.sleep(int(param['fetch_order_status_poll_freq_ms']/1000))
333
+
334
+ async def execute_one_position(
335
+ exchange : AnyExchange,
336
+ position : DivisiblePosition,
337
+ param : Dict,
338
+ executions : Dict[str, Dict[str, Any]]
339
+ ):
340
+ await exchange.load_markets() # type: ignore
341
+ try:
342
+ await exchange.authenticate() # type: ignore
343
+ except Exception as swallow_this_error:
344
+ pass # @todo, perhaps a better way for handling this?
345
+
346
+ market : Dict[str, Any] = exchange.markets[position.ticker] if position.ticker in exchange.markets else None # type: ignore
347
+ if not market:
348
+ raise ArgumentError(f"Market not found for {position.ticker} under {exchange.name}") # type: ignore
349
+
350
+ min_amount = float(market['limits']['amount']['min']) # type: ignore
351
+ multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
352
+ position.multiplier = multiplier
353
+
354
+ slices : List[Order] = position.to_slices()
355
+ i = 0
356
+ for slice in slices:
357
+ try:
358
+ slice_amount_in_base_ccy : float = slice.amount
359
+ rounded_slice_amount_in_base_ccy = slice_amount_in_base_ccy / multiplier # After devided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
360
+ rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, rounded_slice_amount_in_base_ccy) # type: ignore
361
+ rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
362
+ rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
363
+
364
+ limit_price : float = 0
365
+ rounded_limit_price : float = 0
366
+ if position.order_type=='limit':
367
+ orderbook = await exchange.fetch_order_book(symbol=position.ticker, limit=3) # type: ignore
368
+ if position.side=='buy':
369
+ asks = [ ask[0] for ask in orderbook['asks'] ]
370
+ best_asks = min(asks)
371
+ limit_price = best_asks * (1 + position.leg_room_bps/10000)
372
+ else:
373
+ bids = [ bid[0] for bid in orderbook['bids'] ]
374
+ best_bid = max(bids)
375
+ limit_price = best_bid * (1 - position.leg_room_bps/10000)
376
+
377
+ rounded_limit_price = exchange.price_to_precision(position.ticker, limit_price) # type: ignore
378
+ rounded_limit_price = float(rounded_limit_price)
379
+
380
+ executed_order = await exchange.create_order( # type: ignore
381
+ symbol = position.ticker,
382
+ type = position.order_type,
383
+ amount = rounded_slice_amount_in_base_ccy,
384
+ price = rounded_limit_price,
385
+ side = position.side
386
+ )
387
+
388
+ else:
389
+ executed_order = await exchange.create_order( # type: ignore
390
+ symbol = position.ticker,
391
+ type = position.order_type,
392
+ amount = rounded_slice_amount_in_base_ccy,
393
+ side = position.side
394
+ )
395
+
396
+ executed_order['slice_id'] = i
397
+
398
+ '''
399
+ Format of executed_order:
400
+ executed_order
401
+ {'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}, 'id': '2245241151525347328', 'clientOrderId': 'xxx', 'timestamp': None, 'datetime': None, 'lastTradeTimestamp': None, 'lastUpdateTimestamp': None, 'symbol': 'SUSHI/USDT:USDT', 'type': 'limit', 'timeInForce': None, 'postOnly': None, 'side': 'buy', 'price': None, 'stopLossPrice': None, 'takeProfitPrice': None, 'triggerPrice': None, 'average': None, 'cost': None, 'amount': None, 'filled': None, 'remaining': None, 'status': None, 'fee': None, 'trades': [], 'reduceOnly': False, 'fees': [], 'stopPrice': None}
402
+ special variables:
403
+ function variables:
404
+ 'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}
405
+ 'id': '2245241151525347328'
406
+ 'clientOrderId': 'xxx'
407
+ 'timestamp': None
408
+ 'datetime': None
409
+ 'lastTradeTimestamp': None
410
+ 'lastUpdateTimestamp': None
411
+ 'symbol': 'SUSHI/USDT:USDT'
412
+ 'type': 'limit'
413
+ 'timeInForce': None
414
+ 'postOnly': None
415
+ 'side': 'buy'
416
+ 'price': None
417
+ 'stopLossPrice': None
418
+ 'takeProfitPrice': None
419
+ 'triggerPrice': None
420
+ 'average': None
421
+ 'cost': None
422
+ 'amount': None
423
+ 'filled': None
424
+ 'remaining': None
425
+ 'status': None
426
+ 'fee': None
427
+ 'trades': []
428
+ 'reduceOnly': False
429
+ 'fees': []
430
+ 'stopPrice': None
431
+ '''
432
+ order_id = executed_order['id']
433
+ order_status = executed_order['status']
434
+ filled_amount = executed_order['filled']
435
+ remaining_amount = executed_order['remaining']
436
+ executed_order['multiplier'] = multiplier
437
+ position.append_execution(order_id, executed_order)
438
+
439
+ log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
440
+
441
+ if not order_status or order_status!='closed':
442
+ start_time = time.time()
443
+ wait_threshold_sec = position.wait_fill_threshold_ms / 1000
444
+
445
+ elapsed_sec = time.time() - start_time
446
+ while elapsed_sec < wait_threshold_sec:
447
+ order_update = None
448
+ if order_id in executions:
449
+ order_update = executions[order_id]
450
+
451
+ if order_update:
452
+ order_status = order_update['status']
453
+ filled_amount = order_update['filled']
454
+ remaining_amount = order_update['remaining']
455
+ order_update['multiplier'] = multiplier
456
+ position.append_execution(order_id, order_update)
457
+
458
+ if remaining_amount <= 0:
459
+ log(f"Limit order fully filled: {order_id}", log_level=LogLevel.INFO)
460
+ break
461
+
462
+ await asyncio.sleep(int(param['fetch_order_status_poll_freq_ms']/1000))
463
+
464
+
465
+
466
+ # Cancel hung limit order, resend as market
467
+ if order_status!='closed':
468
+ # If no update from websocket, do one last fetch via REST
469
+ order_update = await exchange.fetch_order(order_id, position.ticker) # type: ignore
470
+ order_status = order_update['status']
471
+ filled_amount = order_update['filled']
472
+ remaining_amount = order_update['remaining']
473
+ order_update['multiplier'] = multiplier
474
+ position.append_execution(order_id, order_update)
475
+
476
+ if order_status!='closed':
477
+ order_status = order_update['status']
478
+ filled_amount = order_update['filled']
479
+ remaining_amount = order_update['remaining']
480
+
481
+ await exchange.cancel_order(order_id, position.ticker) # type: ignore
482
+ position.get_execution(order_id)['status'] = 'canceled'
483
+ log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
484
+
485
+ rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, remaining_amount) # type: ignore
486
+ rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
487
+ rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
488
+ if rounded_slice_amount_in_base_ccy>0:
489
+ executed_resent_order = await exchange.create_order( # type: ignore
490
+ symbol=position.ticker,
491
+ type='market',
492
+ amount=remaining_amount,
493
+ side=position.side
494
+ )
495
+
496
+ order_id = executed_resent_order['id']
497
+ order_status = executed_resent_order['status']
498
+ executed_resent_order['multiplier'] = multiplier
499
+ position.append_execution(order_id, executed_resent_order)
500
+
501
+ while not order_status or order_status!='closed':
502
+ order_update = None
503
+ if order_id in executions:
504
+ order_update = executions[order_id]
505
+
506
+ if order_update:
507
+ order_id = order_update['id']
508
+ order_status = order_update['status']
509
+ filled_amount = order_update['filled']
510
+ remaining_amount = order_update['remaining']
511
+
512
+ log(f"Waiting for resent market order to close {order_id} ...")
513
+
514
+ await asyncio.sleep(int(param['fetch_order_status_poll_freq_ms']/1000))
515
+
516
+ log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
517
+
518
+ log(f"Executed slice #{i}", log_level=LogLevel.INFO)
519
+ log(f"{position.ticker}, multiplier: {multiplier}, slice_amount_in_base_ccy: {slice_amount_in_base_ccy}, rounded_slice_amount_in_base_ccy, {rounded_slice_amount_in_base_ccy}", log_level=LogLevel.INFO)
520
+ if position.order_type=='limit':
521
+ log(f"{position.ticker}, limit_price: {limit_price}, rounded_limit_price, {rounded_limit_price}", log_level=LogLevel.INFO)
522
+
523
+ except Exception as slice_err:
524
+ log(
525
+ f"Failed to execute #{i} slice: {slice.to_dict()}. {slice_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
526
+ log_level=LogLevel.ERROR
527
+ )
528
+ finally:
529
+ i += 1
530
+
531
+ async def work(
532
+ param : Dict,
533
+ exchange : AnyExchange,
534
+ redis_client : StrictRedis
535
+ ):
536
+ incoming_orders_topic_regex : str = param['incoming_orders_topic_regex']
537
+ incoming_orders_topic_regex = incoming_orders_topic_regex.replace("$GATEWAY_ID$", param['gateway_id'])
538
+ incoming_orders_topic_regex_pattern : Pattern = re.compile(incoming_orders_topic_regex)
539
+
540
+ executions_publish_topic : str = param['executions_publish_topic'].replace("$GATEWAY_ID$", param['gateway_id'])
541
+
542
+ # This is how we avoid reprocess same message twice. We check message hash and cache it.
543
+ processed_hash_queue = deque(maxlen=10)
544
+
545
+ executions : Dict[str, Dict[str, Any]] = {}
546
+ asyncio.create_task(watch_orders_task(exchange, executions))
547
+
548
+ while True:
549
+ try:
550
+ keys = redis_client.keys()
551
+ for key in keys:
552
+ try:
553
+ s_key : str = key.decode("utf-8")
554
+ if incoming_orders_topic_regex_pattern.match(s_key):
555
+ orders = None
556
+ message = redis_client.get(key)
557
+ if message:
558
+ message_hash = hashlib.sha256(message).hexdigest()
559
+ message = message.decode('utf-8')
560
+ if message_hash not in processed_hash_queue: # Dont process what's been processed before.
561
+ processed_hash_queue.append(message_hash)
562
+
563
+ orders = json.loads(message)
564
+ positions : List[DivisiblePosition] = [
565
+ DivisiblePosition(
566
+ ticker=order['ticker'],
567
+ side=order['side'],
568
+ amount=order['amount'],
569
+ order_type=order['order_type'],
570
+ leg_room_bps=order['leg_room_bps'],
571
+ slices=order['slices'],
572
+ wait_fill_threshold_ms=order['wait_fill_threshold_ms']
573
+ )
574
+ for order in orders
575
+ ]
576
+
577
+ start = time.time()
578
+ pending_executions = [ execute_one_position(exchange, position, param, executions) for position in positions ]
579
+ await asyncio.gather(*pending_executions)
580
+ order_dispatch_elapsed_ms = int((time.time() - start) *1000)
581
+
582
+ i = 0
583
+ for position in positions:
584
+ log(f"{i} {position.ticker}, {position.side} # executions: {len(position.get_executions())}, filled_amount: {position.get_filled_amount()}, average_cost: {position.get_average_cost()}, order_dispatch_elapsed_ms: {order_dispatch_elapsed_ms}")
585
+ i += 1
586
+
587
+ start = time.time()
588
+ if redis_client:
589
+ redis_client.delete(key)
590
+
591
+ '''
592
+ https://redis.io/commands/set/
593
+ '''
594
+ expiry_sec : int = 60*15 # additional 15min
595
+ _positions = [ position.to_dict() for position in positions ]
596
+ redis_client.set(name=executions_publish_topic, value=json.dumps(_positions).encode('utf-8'), ex=60*15)
597
+ redis_set_elapsed_ms = int((time.time() - start) *1000)
598
+
599
+ log(f"positions published back to redis, redis_set_elapsed_ms: {redis_set_elapsed_ms}")
600
+
601
+ except Exception as key_error:
602
+ log(
603
+ f"Failed to process {key}. Error: {key_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
604
+ log_level=LogLevel.ERROR
605
+ )
606
+
607
+ except Exception as loop_error:
608
+ log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
609
+
610
+ async def main():
611
+ parse_args()
612
+
613
+ if not param['apikey']:
614
+ log("Loading credentials from .env")
615
+
616
+ load_dotenv()
617
+
618
+ encrypt_decrypt_with_aws_kms = os.getenv('ENCRYPT_DECRYPT_WITH_AWS_KMS')
619
+ encrypt_decrypt_with_aws_kms = True if encrypt_decrypt_with_aws_kms=='Y' else False
620
+
621
+ api_key : str = str(os.getenv('APIKEY'))
622
+ secret : str = str(os.getenv('SECRET'))
623
+ passphrase : str = str(os.getenv('PASSPHRASE'))
624
+ else:
625
+ log("Loading credentials from command line args")
626
+
627
+ encrypt_decrypt_with_aws_kms = param['encrypt_decrypt_with_aws_kms']
628
+ api_key : str = param['apikey']
629
+ secret : str = param['secret']
630
+ passphrase : str = param['passphrase']
631
+
632
+ if encrypt_decrypt_with_aws_kms:
633
+ aws_kms_key_id = str(os.getenv('AWS_KMS_KEY_ID'))
634
+
635
+ aws_kms = AwsKmsUtil(key_id=aws_kms_key_id, profile_name=None)
636
+ api_key = aws_kms.decrypt(api_key.encode())
637
+ secret = aws_kms.decrypt(secret.encode())
638
+ if passphrase:
639
+ passphrase = aws_kms.decrypt(passphrase.encode())
640
+
641
+ redis_client : StrictRedis = init_redis_client()
642
+
643
+ exchange : Union[AnyExchange, None] = instantiate_exchange(
644
+ gateway_id=param['gateway_id'],
645
+ api_key=api_key,
646
+ secret=secret,
647
+ passphrase=passphrase,
648
+ default_type=param['default_type'],
649
+ rate_limit_ms=param['rate_limit_ms']
650
+ )
651
+ if exchange:
652
+ # Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
653
+ balances = await exchange.fetch_balance() # type: ignore
654
+ log(f"{param['gateway_id']}: account balances {balances}")
655
+
656
+ await work(param=param, exchange=exchange, redis_client=redis_client)
657
+
658
+ asyncio.run(main())