siglab-py 0.1.19__py3-none-any.whl → 0.6.33__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 (45) hide show
  1. siglab_py/algo/__init__.py +0 -0
  2. siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
  3. siglab_py/algo/strategy_base.py +122 -0
  4. siglab_py/algo/strategy_executor.py +1308 -0
  5. siglab_py/algo/tp_algo.py +529 -0
  6. siglab_py/backtests/__init__.py +0 -0
  7. siglab_py/backtests/backtest_core.py +2405 -0
  8. siglab_py/backtests/coinflip_15m_crypto.py +432 -0
  9. siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
  10. siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
  11. siglab_py/constants.py +26 -1
  12. siglab_py/exchanges/binance.py +38 -0
  13. siglab_py/exchanges/deribit.py +83 -0
  14. siglab_py/exchanges/futubull.py +33 -3
  15. siglab_py/market_data_providers/candles_provider.py +11 -10
  16. siglab_py/market_data_providers/candles_ta_provider.py +5 -5
  17. siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +238 -0
  18. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +224 -0
  19. siglab_py/market_data_providers/google_monitor.py +320 -0
  20. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  21. siglab_py/market_data_providers/tg_monitor.py +428 -0
  22. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  23. siglab_py/ordergateway/client.py +172 -41
  24. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  25. siglab_py/ordergateway/gateway.py +456 -344
  26. siglab_py/ordergateway/test_ordergateway.py +8 -7
  27. siglab_py/tests/integration/market_data_util_tests.py +80 -6
  28. siglab_py/tests/unit/analytic_util_tests.py +67 -4
  29. siglab_py/tests/unit/market_data_util_tests.py +96 -0
  30. siglab_py/tests/unit/simple_math_tests.py +252 -0
  31. siglab_py/tests/unit/trading_util_tests.py +65 -0
  32. siglab_py/util/analytic_util.py +484 -66
  33. siglab_py/util/datetime_util.py +39 -0
  34. siglab_py/util/market_data_util.py +564 -74
  35. siglab_py/util/module_util.py +40 -0
  36. siglab_py/util/notification_util.py +78 -0
  37. siglab_py/util/retry_util.py +16 -3
  38. siglab_py/util/simple_math.py +262 -0
  39. siglab_py/util/slack_notification_util.py +59 -0
  40. siglab_py/util/trading_util.py +118 -0
  41. {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -13
  42. siglab_py-0.6.33.dist-info/RECORD +56 -0
  43. {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
  44. siglab_py-0.1.19.dist-info/RECORD +0 -31
  45. {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
- from ctypes import ArgumentError
1
+ # type: ignore
2
2
  import sys
3
3
  import traceback
4
4
  import os
5
+ import random
5
6
  from dotenv import load_dotenv
6
7
  from enum import Enum
7
8
  import argparse
@@ -19,19 +20,45 @@ from re import Pattern
19
20
  from redis import StrictRedis
20
21
  import asyncio
21
22
 
22
- from util.aws_util import AwsKmsUtil
23
-
24
23
  import ccxt.pro as ccxtpro
25
24
 
25
+ from siglab_py.util.retry_util import retry
26
+ from siglab_py.util.aws_util import AwsKmsUtil
26
27
  from siglab_py.exchanges.any_exchange import AnyExchange
28
+ from siglab_py.util.market_data_util import async_instantiate_exchange
27
29
  from siglab_py.ordergateway.client import Order, DivisiblePosition
30
+ from siglab_py.constants import LogLevel
31
+ from siglab_py.util.notification_util import dispatch_notification
32
+
33
+ current_filename = os.path.basename(__file__)
34
+
35
+ '''
36
+ Error: RuntimeError: aiodns needs a SelectorEventLoop on Windows.
37
+ Hack, by far the filthest hack I done in my career: Set SelectorEventLoop on Windows
38
+ '''
39
+ if sys.platform == 'win32':
40
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
28
41
 
29
42
  '''
30
43
  Usage:
31
- python gateway.py --gateway_id bybit_01 --default_type linear --rate_limit_ms 100
44
+ set PYTHONPATH=%PYTHONPATH%;D:\dev\siglab\siglab_py
45
+ python gateway.py --gateway_id hyperliquid_01 --default_type linear --rate_limit_ms 100 --encrypt_decrypt_with_aws_kms Y --aws_kms_key_id xxx --apikey xxx --secret xxxx --verbose N --slack_info_url=https://hooks.slack.com/services/xxx --slack_critial_url=https://hooks.slack.com/services/xxx --slack_alert_url=https://hooks.slack.com/services/xxx
32
46
 
33
47
  --default_type defaults to linear
48
+ --default_sub_type defaults to None (Depends on your exchange/broker, if they requires this)
34
49
  --rate_limit_ms defaults to 100
50
+ --encrypt_decrypt_with_aws_kms If you encrypt apikey and secret using AMS KMS, then set to Y. If apikey and secret in unencrypted plain text, set to N.
51
+ --passphrase is optional, this depends on the exchange.
52
+ --verbose logging verbosity, Y or N (default)
53
+ --gateway_id contains two parts separated by underscore. Gateway.py will parse 'hyperliquid_01' into two parts: 'hyperliquid' (Exchange name) and '01' (Use this for your sub account ID). Exchange name need be spelt exactly. Please have a look at market_data_util async_instantiate_exchange.
54
+ --slack_info_url, --slack_critical_url and --slack_alert_url are if you want gateway to dispatch Slack notification on events.
55
+ --order_amount_randomize_max_pct adds small variance to sliced order amount (Default max 10% on sliced amount) to cover your track in order executions, this is useful especially when executing bigger orders during quieter hours.
56
+ How to get Slack webhook urls? https://medium.com/@natalia_assad/how-send-a-table-to-slack-using-python-d1a20b08abe0
57
+
58
+ Another example:
59
+ python gateway.py --gateway_id hyperliquid_01 --default_type linear --rate_limit_ms 100 --slack_info_url=https://hooks.slack.com/services/xxx --slack_critial_url=https://hooks.slack.com/services/yyy --slack_alert_url=https://hooks.slack.com/services/zzz
60
+
61
+ gateway.py takes outgoing orders from redis and publish executions back to redis when done. Redis configuration in param['mds']['redis']. Start redis before starting gateway.py.
35
62
 
36
63
  This script is pypy compatible:
37
64
  pypy gateway.py --gateway_id bybit_01 --default_type linear --rate_limit_ms 100
@@ -82,7 +109,12 @@ To debug from vscode, launch.json:
82
109
  "--aws_kms_key_id", "",
83
110
  "--apikey", "xxx",
84
111
  "--secret", "xxx",
85
- "--passphrase", "xxx"
112
+ "--passphrase", "xxx",
113
+ "--verbose", "N",
114
+
115
+ "--slack_info_url", "https://hooks.slack.com/services/xxx",
116
+ "--slack_critial_url", "https://hooks.slack.com/services/xxx",
117
+ "--slack_alert_url", "https://hooks.slack.com/services/xxx",
86
118
  ],
87
119
  "env": {
88
120
  "PYTHONPATH": "${workspaceFolder}"
@@ -102,7 +134,7 @@ To debug from vscode, launch.json:
102
134
  "order_type": "limit",
103
135
  "leg_room_bps": 5,
104
136
  "slices": 5,
105
- "wait_fill_threshold_ms": 15000,
137
+ "wait_fill_threshold_ms": 5000,
106
138
  "executions": {},
107
139
  "filled_amount": 0,
108
140
  "average_cost": 0
@@ -120,7 +152,7 @@ To debug from vscode, launch.json:
120
152
  "order_type": "limit",
121
153
  "leg_room_bps": 5,
122
154
  "slices": 5,
123
- "wait_fill_threshold_ms": 15000,
155
+ "wait_fill_threshold_ms": 5000,
124
156
  "executions": {
125
157
  "xxx": { <-- order id from exchange
126
158
  "info": { <-- ccxt convention, raw response from exchanges under info tag
@@ -185,7 +217,23 @@ param : Dict = {
185
217
  "executions_publish_topic" : r"ordergateway_executions_$GATEWAY_ID$",
186
218
 
187
219
  "default_fees_ccy" : None,
220
+ "order_amount_randomize_max_pct" : 0,
188
221
  "loop_freq_ms" : 500, # reduce this if you need trade faster
222
+ "loops_random_delay_multiplier" : 1, # Add randomness to time between slices are sent off. Set to 1 if no random delay needed.
223
+ "wait_fill_threshold_ms" : 5000,
224
+
225
+ 'current_filename' : current_filename,
226
+
227
+ 'notification' : {
228
+ 'footer' : None,
229
+
230
+ # slack webhook url's for notifications
231
+ 'slack' : {
232
+ 'info' : { 'webhook_url' : None },
233
+ 'critical' : { 'webhook_url' : None },
234
+ 'alert' : { 'webhook_url' : None },
235
+ }
236
+ },
189
237
 
190
238
  'mds' : {
191
239
  'topics' : {
@@ -251,21 +299,29 @@ def log(message : str, log_level : LogLevel = LogLevel.INFO):
251
299
  logger.error(f"{datetime.now()} {message}")
252
300
 
253
301
  def parse_args():
254
- parser = argparse.ArgumentParser() # type: ignore
302
+ parser = argparse.ArgumentParser()
255
303
 
256
304
  parser.add_argument("--gateway_id", help="gateway_id: Where are you sending your order?", default=None)
257
305
  parser.add_argument("--dry_run", help="Y or N (default, for testing). If Y, orders won't be dispatched to exchange, gateway will fake reply.", default='N')
258
306
  parser.add_argument("--default_type", help="default_type: spot, linear, inverse, futures ...etc", default='linear')
307
+ parser.add_argument("--default_sub_type", help="default_sub_type", default=None)
259
308
  parser.add_argument("--rate_limit_ms", help="rate_limit_ms: Check your exchange rules", default=100)
260
309
 
261
310
  parser.add_argument("--default_fees_ccy", help="If you're trading crypto, CEX fees USDT, DEX fees USDC in many cases. Default None, in which case gateway won't aggregatge fees from executions for you.", default=None)
311
+ parser.add_argument("--order_amount_randomize_max_pct", help="Randomize order amount to hide your track? This is max percentage variance applied on sliced amount (not entire order amount)", default=10)
262
312
  parser.add_argument("--loop_freq_ms", help="Loop delays. Reduce this if you want to trade faster.", default=500)
313
+ parser.add_argument("--wait_fill_threshold_ms", help="Wait for fills for how long?", default=5000)
263
314
 
264
315
  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')
265
316
  parser.add_argument("--aws_kms_key_id", help="AWS KMS key ID", default=None)
266
317
  parser.add_argument("--apikey", help="Exchange apikey", default=None)
267
318
  parser.add_argument("--secret", help="Exchange secret", default=None)
268
319
  parser.add_argument("--passphrase", help="Exchange passphrase", default=None)
320
+ parser.add_argument("--verbose", help="logging verbosity, Y or N (default).", default='N')
321
+
322
+ parser.add_argument("--slack_info_url", help="Slack webhook url for INFO", default=None)
323
+ parser.add_argument("--slack_critial_url", help="Slack webhook url for CRITICAL", default=None)
324
+ parser.add_argument("--slack_alert_url", help="Slack webhook url for ALERT", default=None)
269
325
 
270
326
  args = parser.parse_args()
271
327
  param['gateway_id'] = args.gateway_id
@@ -279,9 +335,12 @@ def parse_args():
279
335
  param['dry_run'] = False
280
336
 
281
337
  param['default_type'] = args.default_type
338
+ param['default_sub_type'] = args.default_sub_type
282
339
  param['rate_limit_ms'] = int(args.rate_limit_ms)
283
340
  param['default_fees_ccy'] = args.default_fees_ccy
341
+ param['order_amount_randomize_max_pct'] = float(args.order_amount_randomize_max_pct)
284
342
  param['loop_freq_ms'] = int(args.loop_freq_ms)
343
+ param['wait_fill_threshold_ms'] = int(args.wait_fill_threshold_ms)
285
344
 
286
345
  if args.encrypt_decrypt_with_aws_kms:
287
346
  if args.encrypt_decrypt_with_aws_kms=='Y':
@@ -295,6 +354,19 @@ def parse_args():
295
354
  param['apikey'] = args.apikey
296
355
  param['secret'] = args.secret
297
356
  param['passphrase'] = args.passphrase
357
+ if args.verbose:
358
+ if args.verbose=='Y':
359
+ param['verbose'] = True
360
+ else:
361
+ param['verbose'] = False
362
+ else:
363
+ param['verbose'] = False
364
+
365
+ param['notification']['slack']['info']['webhook_url'] = args.slack_info_url
366
+ param['notification']['slack']['critical']['webhook_url'] = args.slack_critial_url
367
+ param['notification']['slack']['alert']['webhook_url'] = args.slack_alert_url
368
+
369
+ param['notification']['footer'] = f"From {param['current_filename']} {param['gateway_id']}"
298
370
 
299
371
  def init_redis_client() -> StrictRedis:
300
372
  redis_client : StrictRedis = StrictRedis(
@@ -311,120 +383,27 @@ def init_redis_client() -> StrictRedis:
311
383
 
312
384
  return redis_client
313
385
 
314
- async def instantiate_exchange(
315
- gateway_id : str,
316
- api_key : str,
317
- secret : str,
318
- passphrase : str,
319
- default_type : str,
320
- rate_limit_ms : float = 100
321
- ) -> Union[AnyExchange, None]:
322
- exchange : Union[AnyExchange, None] = None
323
- exchange_name : str = gateway_id.split('_')[0]
324
- exchange_name =exchange_name.lower().strip()
325
-
326
- # Look at ccxt exchange.describe. under 'options' \ 'defaultType' (and 'defaultSubType') for what markets the exchange support.
327
- # https://docs.ccxt.com/en/latest/manual.html#instantiation
328
- exchange_params : Dict[str, Any]= {
329
- 'apiKey' : api_key,
330
- 'secret' : secret,
331
- 'enableRateLimit' : True,
332
- 'rateLimit' : rate_limit_ms,
333
- 'options' : {
334
- 'defaultType' : default_type
335
- }
336
- }
337
-
338
- if exchange_name=='binance':
339
- # spot, future, margin, delivery, option
340
- # https://github.com/ccxt/ccxt/blob/master/python/ccxt/binance.py#L1298
341
- exchange = ccxtpro.binance(exchange_params) # type: ignore
342
- elif exchange_name=='bybit':
343
- # spot, linear, inverse, futures
344
- # https://github.com/ccxt/ccxt/blob/master/python/ccxt/bybit.py#L1041
345
- exchange = ccxtpro.bybit(exchange_params) # type: ignore
346
- elif exchange_name=='okx':
347
- # 'funding', spot, margin, future, swap, option
348
- # https://github.com/ccxt/ccxt/blob/master/python/ccxt/okx.py#L1144
349
- exchange_params['password'] = passphrase
350
- exchange = ccxtpro.okx(exchange_params) # type: ignore
351
- elif exchange_name=='deribit':
352
- # spot, swap, future
353
- # https://github.com/ccxt/ccxt/blob/master/python/ccxt/deribit.py#L360
354
- exchange = ccxtpro.deribit(exchange_params) # type: ignore
355
- elif exchange_name=='hyperliquid':
356
- '''
357
- https://app.hyperliquid.xyz/API
358
-
359
- defaultType from ccxt: swap
360
- https://github.com/ccxt/ccxt/blob/master/python/ccxt/hyperliquid.py#L225
361
-
362
- How to integrate? You can skip first 6 min: https://www.youtube.com/watch?v=UuBr331wxr4&t=363s
363
-
364
- Example,
365
- API credentials created under "\ More \ API":
366
- Ledger Arbitrum Wallet Address: 0xAAAAA <-- This is your Ledger Arbitrum wallet address with which you connect to Hyperliquid.
367
- API Wallet Address 0xBBBBB <-- Generated
368
- privateKey 0xCCCCC
369
-
370
- Basic connection via CCXT:
371
- import asyncio
372
- import ccxt.pro as ccxtpro
373
-
374
- async def main():
375
- rate_limit_ms = 100
376
- exchange_params = {
377
- "walletAddress" : "0xAAAAA", # Ledger Arbitrum Wallet Address here! Not the generated address.
378
- "privateKey" : "0xCCCCC"
379
- }
380
- exchange = ccxtpro.hyperliquid(exchange_params)
381
- balances = await exchange.fetch_balance()
382
- print(balances)
383
-
384
- asyncio.run(main())
385
- '''
386
- exchange = ccxtpro.hyperliquid(
387
- {
388
- "walletAddress" : api_key,
389
- "privateKey" : secret,
390
- 'enableRateLimit' : True,
391
- 'rateLimit' : rate_limit_ms
392
- }
393
- ) # type: ignore
394
- else:
395
- raise ArgumentError(f"Unsupported exchange {exchange_name}, check gateway_id {gateway_id}.")
396
-
397
- await exchange.load_markets() # type: ignore
398
-
399
- '''
400
- Is this necessary? The added trouble is for example bybit.authenticate requires arg 'url'. binance doesn't. And fetch_balance already test credentials.
401
-
402
- try:
403
- await exchange.authenticate() # type: ignore
404
- except Exception as swallow_this_error:
405
- pass
406
- '''
407
-
408
- return exchange
409
-
410
386
  async def watch_orders_task(
411
387
  exchange : AnyExchange,
412
388
  executions : Dict[str, Dict[str, Any]]
413
389
  ):
414
390
  while True:
415
391
  try:
416
- order_updates = await exchange.watch_orders() # type: ignore
392
+ order_updates = await exchange.watch_orders()
417
393
  for order_update in order_updates:
418
394
  order_id = order_update['id']
419
395
  executions[order_id] = order_update
420
396
 
421
- log(f"order updates: {order_updates}", log_level=LogLevel.INFO)
397
+ log(f"order updates: {json.dumps(order_updates, indent=4)}", log_level=LogLevel.INFO)
422
398
  except Exception as loop_err:
423
399
  print(f"watch_orders_task error: {loop_err}")
424
400
 
425
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
401
+ await asyncio.sleep(param['loop_freq_ms']/1000)
426
402
 
427
403
  async def send_heartbeat(exchange):
404
+ if not exchange.clients:
405
+ log(f'Please check https://github.com/ccxt/ccxt/blob/master/python/ccxt/pro/{exchange.name}, exchange.clients empty?')
406
+ return
428
407
 
429
408
  await asyncio.sleep(10)
430
409
 
@@ -432,262 +411,384 @@ async def send_heartbeat(exchange):
432
411
  try:
433
412
  first_ws_url = next(iter(exchange.clients))
434
413
  client = exchange.clients[first_ws_url]
435
- message = exchange.ping(client)
436
- await client.send(message)
437
- log('Heartbeat sent')
414
+ if exchange.ping:
415
+ message = exchange.ping(client)
416
+ await client.send(message)
417
+ log('Heartbeat sent')
418
+ else:
419
+ log(f'Please check https://github.com/ccxt/ccxt/blob/master/python/ccxt/pro/{exchange.name} if ping was implemented')
420
+
438
421
  except Exception as hb_error:
439
- log(f'Failed to send heartbeat: {hb_error}')
422
+ log(f'Failed to send heartbeat: {hb_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}')
440
423
  finally:
441
- await asyncio.sleep(30)
424
+ await asyncio.sleep(20)
442
425
 
443
426
  async def execute_one_position(
444
427
  exchange : AnyExchange,
445
428
  position : DivisiblePosition,
446
429
  param : Dict,
447
- executions : Dict[str, Dict[str, Any]]
430
+ executions : Dict[str, Dict[str, Any]],
431
+ notification_params : Dict[str, Any]
448
432
  ):
449
- market : Dict[str, Any] = exchange.markets[position.ticker] if position.ticker in exchange.markets else None # type: ignore
450
- if not market:
451
- raise ArgumentError(f"Market not found for {position.ticker} under {exchange.name}") # type: ignore
433
+ try:
434
+ market : Dict[str, Any] = exchange.markets[position.ticker] if position.ticker in exchange.markets else None
435
+ if not market:
436
+ raise ArgumentError(f"Market not found for {position.ticker} under {exchange.name}")
452
437
 
453
- min_amount = float(market['limits']['amount']['min']) if market['limits']['amount']['min'] else 0 # type: ignore
454
- multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
455
- position.multiplier = multiplier
438
+ min_amount = float(market['limits']['amount']['min']) if market['limits']['amount']['min'] else 0
439
+ multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
440
+ position.multiplier = multiplier
456
441
 
457
- slices : List[Order] = position.to_slices()
458
- i = 0
459
- for slice in slices:
460
- try:
461
- dt_now : datetime = datetime.now()
462
-
463
- slice_amount_in_base_ccy : float = slice.amount
464
- 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).
465
- rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, rounded_slice_amount_in_base_ccy) # type: ignore
466
- rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
467
- rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
468
-
469
- orderbook = await exchange.fetch_order_book(symbol=position.ticker, limit=3) # type: ignore
470
- if position.side=='buy':
471
- asks = [ ask[0] for ask in orderbook['asks'] ]
472
- best_asks = min(asks)
473
- limit_price : float= best_asks * (1 + position.leg_room_bps/10000)
474
- else:
475
- bids = [ bid[0] for bid in orderbook['bids'] ]
476
- best_bid = max(bids)
477
- limit_price : float = best_bid * (1 - position.leg_room_bps/10000)
442
+ order_amount_randomize_max_pct : float = param['order_amount_randomize_max_pct']
443
+
444
+ log(f"{position.ticker} min_amount: {min_amount}, multiplier: {multiplier}, order_amount_randomize_max_pct: {order_amount_randomize_max_pct}")
445
+
446
+ slices : List[Order] = position.to_slices()
447
+
448
+ # Residual handling in last slice
449
+ if len(slices)>1:
450
+ last_slice = slices[-1]
451
+ last_slice_rounded_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, last_slice.amount/multiplier) # After divided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
452
+ last_slice_rounded_amount_in_base_ccy = float(last_slice_rounded_amount_in_base_ccy) if last_slice_rounded_amount_in_base_ccy else 0
453
+ if last_slice_rounded_amount_in_base_ccy<=min_amount:
454
+ slices.pop()
455
+ slices[-1].amount += last_slice.amount
456
+
457
+ log(f"{position.ticker} Last slice residual smaller than min_amount. Amount is added to prev slice instead. last_slice_amount: {last_slice.amount/multiplier}, last_slice_rounded_amount: {last_slice_rounded_amount_in_base_ccy}")
458
+
459
+ @retry(num_attempts=3, pause_between_retries_ms=3000)
460
+ async def _fetch_order(
461
+ order_id : str,
462
+ ticker : str,
463
+ exchange : AnyExchange,
464
+ ):
465
+ order_update = await exchange.fetch_order(order_id, ticker)
466
+ return order_update
467
+
468
+ randomized_order_amount : float = 0
469
+ last_randomized_order_amount : float = 0
470
+ apply_last_randomized_amount : bool = False # False: Apply new variance, True: Apply -1 * last_randomized_order_amount
471
+ i = 0
472
+ for slice in slices:
473
+ try:
474
+ log(f"{position.ticker} sending slice# {i}")
475
+
476
+ dt_now : datetime = datetime.now()
477
+
478
+ if len(slices)>1:
479
+ if not apply_last_randomized_amount and i<len(slices):
480
+ randomized_order_amount = slice.amount * (order_amount_randomize_max_pct * random.uniform(-1, 1)) /100
481
+ last_randomized_order_amount = randomized_order_amount
482
+
483
+ else:
484
+ randomized_order_amount = -1 * last_randomized_order_amount # Apply the opposite of last slice's variance
485
+ last_randomized_order_amount = 0 # If # slices == 5, last slice don't apply random amount, so reset last_randomized_order_amount to zero.
478
486
 
479
- rounded_limit_price : float = exchange.price_to_precision(position.ticker, limit_price) # type: ignore
480
- rounded_limit_price = float(rounded_limit_price)
481
-
482
- order_params = {
483
- 'reduceOnly': slice.reduce_only
484
- }
485
- if position.order_type=='limit':
486
- if not param['dry_run']:
487
- executed_order = await exchange.create_order( # type: ignore
488
- symbol = position.ticker,
489
- type = position.order_type,
490
- amount = rounded_slice_amount_in_base_ccy,
491
- price = rounded_limit_price,
492
- side = position.side,
493
- params = order_params
494
- )
495
- else:
496
- executed_order = DUMMY_EXECUTION.copy()
497
- executed_order['clientOrderId'] = str(uuid.uuid4())
498
- executed_order['timestamp'] = dt_now.timestamp()
499
- executed_order['datetime'] = dt_now
500
- executed_order['symbol'] = position.ticker
501
- executed_order['type'] = position.order_type
502
- executed_order['side'] = position.side
503
- executed_order['price'] = rounded_limit_price
504
- executed_order['average'] = rounded_limit_price
505
- executed_order['cost'] = 0
506
- executed_order['amount'] = rounded_slice_amount_in_base_ccy
507
- executed_order['filled'] = rounded_slice_amount_in_base_ccy
508
- executed_order['remaining'] = 0
509
- executed_order['status'] = 'closed'
510
- executed_order['multiplier'] = position.multiplier
487
+ slice_amount_in_base_ccy : float = slice.amount + randomized_order_amount
511
488
 
512
- else:
513
- if not param['dry_run']:
514
- executed_order = await exchange.create_order( # type: ignore
515
- symbol = position.ticker,
516
- type = position.order_type,
517
- amount = rounded_slice_amount_in_base_ccy,
518
- side = position.side,
519
- params = order_params
520
- )
489
+ apply_last_randomized_amount = not apply_last_randomized_amount
490
+
491
+ rounded_slice_amount_in_base_ccy = slice_amount_in_base_ccy / multiplier # After divided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
492
+ rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, rounded_slice_amount_in_base_ccy)
493
+ rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy) if rounded_slice_amount_in_base_ccy else 0
494
+ rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
495
+
496
+ if rounded_slice_amount_in_base_ccy==0:
497
+ log(f"{position.ticker} Slice amount rounded to zero?! slice amount before rounding: {slice.amount}")
498
+ continue
499
+
500
+ orderbook = await exchange.fetch_order_book(symbol=position.ticker, limit=3)
501
+ if position.side=='buy':
502
+ asks = [ ask[0] for ask in orderbook['asks'] ]
503
+ best_asks = min(asks)
504
+ limit_price : float= best_asks * (1 + position.leg_room_bps/10000)
521
505
  else:
522
- executed_order = DUMMY_EXECUTION.copy()
523
- executed_order['clientOrderId'] = str(uuid.uuid4())
524
- executed_order['timestamp'] = dt_now.timestamp()
525
- executed_order['datetime'] = dt_now
526
- executed_order['symbol'] = position.ticker
527
- executed_order['type'] = position.order_type
528
- executed_order['side'] = position.side
529
- executed_order['price'] = rounded_limit_price
530
- executed_order['average'] = rounded_limit_price
531
- executed_order['cost'] = 0
532
- executed_order['amount'] = rounded_slice_amount_in_base_ccy
533
- executed_order['filled'] = rounded_slice_amount_in_base_ccy
534
- executed_order['remaining'] = 0
535
- executed_order['status'] = 'closed'
536
- executed_order['multiplier'] = position.multiplier
537
-
538
- executed_order['slice_id'] = i
539
-
540
- '''
541
- Format of executed_order:
542
- executed_order
543
- {'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}
544
- special variables:
545
- function variables:
546
- 'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}
547
- 'id': '2245241151525347328'
548
- 'clientOrderId': 'xxx'
549
- 'timestamp': None
550
- 'datetime': None
551
- 'lastTradeTimestamp': None
552
- 'lastUpdateTimestamp': None
553
- 'symbol': 'SUSHI/USDT:USDT'
554
- 'type': 'limit'
555
- 'timeInForce': None
556
- 'postOnly': None
557
- 'side': 'buy'
558
- 'price': None
559
- 'stopLossPrice': None
560
- 'takeProfitPrice': None
561
- 'triggerPrice': None
562
- 'average': None
563
- 'cost': None
564
- 'amount': None
565
- 'filled': None
566
- 'remaining': None
567
- 'status': None
568
- 'fee': None
569
- 'trades': []
570
- 'reduceOnly': False
571
- 'fees': []
572
- 'stopPrice': None
573
- '''
574
- order_id = executed_order['id']
575
- order_status = executed_order['status']
576
- filled_amount = executed_order['filled']
577
- remaining_amount = executed_order['remaining']
578
- executed_order['multiplier'] = multiplier
579
- position.append_execution(order_id, executed_order)
580
-
581
- log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
582
-
583
- if not order_status or order_status!='closed':
584
- start_time = time.time()
585
- wait_threshold_sec = position.wait_fill_threshold_ms / 1000
586
-
587
- elapsed_sec = time.time() - start_time
588
- while elapsed_sec < wait_threshold_sec:
589
- order_update = None
590
- if order_id in executions:
591
- order_update = executions[order_id]
506
+ bids = [ bid[0] for bid in orderbook['bids'] ]
507
+ best_bid = max(bids)
508
+ limit_price : float = best_bid * (1 - position.leg_room_bps/10000)
592
509
 
593
- if order_update:
594
- order_status = order_update['status']
595
- filled_amount = order_update['filled']
596
- remaining_amount = order_update['remaining']
597
- order_update['multiplier'] = multiplier
598
- position.append_execution(order_id, order_update)
599
-
600
- if remaining_amount <= 0:
601
- log(f"Limit order fully filled: {order_id}", log_level=LogLevel.INFO)
602
- break
603
-
604
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
605
-
606
-
510
+ rounded_limit_price : float = exchange.price_to_precision(position.ticker, limit_price)
511
+ rounded_limit_price = float(rounded_limit_price)
512
+
513
+ order_params = {
514
+ 'reduceOnly': slice.reduce_only
515
+ }
516
+ if position.order_type=='limit':
517
+ if position.leg_room_bps<0:
518
+ order_params['postOnly'] = True
519
+ else:
520
+ log(
521
+ f"{position.side} {rounded_slice_amount_in_base_ccy} {position.ticker}. Limit order to be sent as Market order. Invalid leg_room_bps: {position.leg_room_bps}. By convention, leg_room_bps more positive means you want your order to get filled more aggressively. To post limit orders, leg_room_bps should be negative.",
522
+ log_level=LogLevel.WARNING
523
+ )
524
+
525
+ if not param['dry_run']:
526
+ executed_order = await exchange.create_order(
527
+ symbol = position.ticker,
528
+ type = position.order_type,
529
+ amount = rounded_slice_amount_in_base_ccy,
530
+ price = rounded_limit_price,
531
+ side = position.side,
532
+ params = order_params
533
+ )
534
+ else:
535
+ executed_order = DUMMY_EXECUTION.copy()
536
+ executed_order['clientOrderId'] = str(uuid.uuid4())
537
+ executed_order['timestamp'] = dt_now.timestamp()
538
+ executed_order['datetime'] = dt_now
539
+ executed_order['symbol'] = position.ticker
540
+ executed_order['type'] = position.order_type
541
+ executed_order['side'] = position.side
542
+ executed_order['price'] = rounded_limit_price
543
+ executed_order['average'] = rounded_limit_price
544
+ executed_order['cost'] = 0
545
+ executed_order['amount'] = rounded_slice_amount_in_base_ccy
546
+ executed_order['filled'] = rounded_slice_amount_in_base_ccy
547
+ executed_order['remaining'] = 0
548
+ executed_order['status'] = 'closed'
549
+ executed_order['multiplier'] = position.multiplier
607
550
 
608
- # Cancel hung limit order, resend as market
609
- if order_status!='closed':
610
- # If no update from websocket, do one last fetch via REST
611
- order_update = await exchange.fetch_order(order_id, position.ticker) # type: ignore
612
- order_status = order_update['status']
613
- filled_amount = order_update['filled']
614
- remaining_amount = order_update['remaining']
615
- order_update['multiplier'] = multiplier
616
- position.append_execution(order_id, order_update)
617
-
618
- if order_status!='closed':
551
+ else:
552
+ if not param['dry_run']:
553
+ executed_order = await exchange.create_order(
554
+ symbol = position.ticker,
555
+ type = position.order_type,
556
+ amount = rounded_slice_amount_in_base_ccy,
557
+ side = position.side,
558
+ params = order_params
559
+ )
560
+ else:
561
+ executed_order = DUMMY_EXECUTION.copy()
562
+ executed_order['clientOrderId'] = str(uuid.uuid4())
563
+ executed_order['timestamp'] = dt_now.timestamp()
564
+ executed_order['datetime'] = dt_now
565
+ executed_order['symbol'] = position.ticker
566
+ executed_order['type'] = position.order_type
567
+ executed_order['side'] = position.side
568
+ executed_order['price'] = rounded_limit_price
569
+ executed_order['average'] = rounded_limit_price
570
+ executed_order['cost'] = 0
571
+ executed_order['amount'] = rounded_slice_amount_in_base_ccy
572
+ executed_order['filled'] = rounded_slice_amount_in_base_ccy
573
+ executed_order['remaining'] = 0
574
+ executed_order['status'] = 'closed'
575
+ executed_order['multiplier'] = position.multiplier
576
+
577
+ executed_order['slice_id'] = i
578
+
579
+ '''
580
+ Format of executed_order:
581
+ executed_order
582
+ {'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}
583
+ special variables:
584
+ function variables:
585
+ 'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}
586
+ 'id': '2245241151525347328'
587
+ 'clientOrderId': 'xxx'
588
+ 'timestamp': None
589
+ 'datetime': None
590
+ 'lastTradeTimestamp': None
591
+ 'lastUpdateTimestamp': None
592
+ 'symbol': 'SUSHI/USDT:USDT'
593
+ 'type': 'limit'
594
+ 'timeInForce': None
595
+ 'postOnly': None
596
+ 'side': 'buy'
597
+ 'price': None
598
+ 'stopLossPrice': None
599
+ 'takeProfitPrice': None
600
+ 'triggerPrice': None
601
+ 'average': None
602
+ 'cost': None
603
+ 'amount': None
604
+ 'filled': None
605
+ 'remaining': None
606
+ 'status': None
607
+ 'fee': None
608
+ 'trades': []
609
+ 'reduceOnly': False
610
+ 'fees': []
611
+ 'stopPrice': None
612
+ '''
613
+ order_id = executed_order['id']
614
+ order_status = executed_order['status']
615
+ filled_amount = executed_order['filled']
616
+ remaining_amount = executed_order['remaining']
617
+ executed_order['multiplier'] = multiplier
618
+ position.append_execution(order_id, executed_order)
619
+
620
+ log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
621
+
622
+ if not order_status or order_status!='closed':
623
+ wait_threshold_sec = position.wait_fill_threshold_ms / 1000
624
+
625
+ start_time = time.time()
626
+ elapsed_sec = time.time() - start_time
627
+ while elapsed_sec < wait_threshold_sec:
628
+ order_update = None
629
+ if order_id in executions:
630
+ order_update = executions[order_id]
631
+
632
+ if order_update:
633
+ order_status = order_update['status']
634
+ filled_amount = order_update['filled']
635
+ remaining_amount = order_update['remaining']
636
+ order_update['multiplier'] = multiplier
637
+ position.append_execution(order_id, order_update)
638
+
639
+ if remaining_amount <= 0:
640
+ log(f"Limit order fully filled: {order_id}, order_update: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
641
+ break
642
+
643
+ loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
644
+ loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
645
+ await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
646
+
647
+ elapsed_sec = time.time() - start_time
648
+ log(f"{position.ticker} waiting for order update ... elapsed_sec: {elapsed_sec}")
649
+
650
+ # Cancel hung limit order, resend as market
651
+ if order_status!='closed':
652
+ # If no update from websocket, do one last fetch via REST
653
+ order_update = await _fetch_order(order_id, position.ticker, exchange)
619
654
  order_status = order_update['status']
620
655
  filled_amount = order_update['filled']
621
656
  remaining_amount = order_update['remaining']
622
-
623
- await exchange.cancel_order(order_id, position.ticker) # type: ignore
624
- position.get_execution(order_id)['status'] = 'canceled'
625
- log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
626
-
627
- rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, remaining_amount) # type: ignore
628
- rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
629
- rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
630
- if rounded_slice_amount_in_base_ccy>0:
631
- executed_resent_order = await exchange.create_order( # type: ignore
632
- symbol=position.ticker,
633
- type='market',
634
- amount=remaining_amount,
635
- side=position.side
636
- )
637
-
638
- order_id = executed_resent_order['id']
639
- order_status = executed_resent_order['status']
640
- executed_resent_order['multiplier'] = multiplier
641
- position.append_execution(order_id, executed_resent_order)
642
-
643
- while not order_status or order_status!='closed':
644
- order_update = None
645
- if order_id in executions:
646
- order_update = executions[order_id]
647
-
648
- if order_update:
649
- order_id = order_update['id']
657
+ order_update['multiplier'] = multiplier
658
+
659
+ position.append_execution(order_id, order_update)
660
+
661
+ if order_status!='closed':
662
+ log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
663
+ await exchange.cancel_order(order_id, position.ticker)
664
+ position.get_execution(order_id)['status'] = 'canceled'
665
+ log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
666
+
667
+ rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, remaining_amount)
668
+ rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
669
+ rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
670
+ if rounded_slice_amount_in_base_ccy>0:
671
+ executed_resent_order = await exchange.create_order(
672
+ symbol=position.ticker,
673
+ type='market',
674
+ amount=remaining_amount,
675
+ side=position.side
676
+ )
677
+
678
+ order_id = executed_resent_order['id']
679
+ order_status = executed_resent_order['status']
680
+ executed_resent_order['multiplier'] = multiplier
681
+ position.append_execution(order_id, executed_resent_order)
682
+
683
+ wait_threshold_sec = position.wait_fill_threshold_ms / 1000
684
+
685
+ start_time = time.time()
686
+ elapsed_sec = time.time() - start_time
687
+ while (not order_status or order_status!='closed') and (elapsed_sec < wait_threshold_sec):
688
+ order_update = None
689
+ if order_id in executions:
690
+ order_update = executions[order_id]
691
+
692
+ if order_update:
693
+ order_id = order_update['id']
694
+ order_status = order_update['status']
695
+ filled_amount = order_update['filled']
696
+ remaining_amount = order_update['remaining']
697
+
698
+ elapsed_sec = time.time() - start_time
699
+ log(f"Waiting for resent market order to close {order_id} ... elapsed_sec: {elapsed_sec}")
700
+
701
+ loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
702
+ loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
703
+ await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
704
+
705
+ if (not order_status or order_status!='closed'):
706
+ # If no update from websocket, do one last fetch via REST
707
+ order_update = await _fetch_order(order_id, position.ticker, exchange)
650
708
  order_status = order_update['status']
651
709
  filled_amount = order_update['filled']
652
710
  remaining_amount = order_update['remaining']
711
+ order_update['multiplier'] = multiplier
712
+
713
+ log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount} {json.dumps(order_update, indent=4)}")
714
+ else:
715
+ log(f"{position.ticker} {order_id} status (From REST): {json.dumps(order_update, indent=4)}")
716
+
717
+ slice.dispatched_price = rounded_limit_price
718
+ slice.dispatched_amount = rounded_slice_amount_in_base_ccy
719
+ position.dispatched_slices.append(slice)
720
+
721
+ log(f"Executed slice #{i}", log_level=LogLevel.INFO)
722
+ log(f"{json.dumps(slice.to_dict(), indent=4)}")
723
+ 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)
724
+ if position.order_type=='limit':
725
+ log(f"{position.ticker}, limit_price: {limit_price}, rounded_limit_price, {rounded_limit_price}", log_level=LogLevel.INFO)
726
+
727
+ except Exception as slice_err:
728
+ log(
729
+ f"Failed to execute #{i} slice: {slice.to_dict()}. {slice_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
730
+ log_level=LogLevel.ERROR
731
+ )
732
+ raise slice_err
733
+ finally:
734
+ log(f"{position.ticker} done slice# {i}")
735
+ i += 1
736
+
737
+ log(f"{position.ticker} patch_executions")
738
+ position.patch_executions()
739
+
740
+ log(f"Dispatched slices:")
741
+ for dispatched_slice in position.dispatched_slices:
742
+ log(f"{json.dumps(dispatched_slice.to_dict(), indent=4)}")
743
+
744
+ position_from_exchange = await exchange.fetch_position(position.ticker)
745
+ log(f"position update:")
746
+ log(f"{json.dumps(position_from_exchange, indent=4)}")
747
+
748
+ position.filled_amount = position.get_filled_amount()
749
+ position.average_cost = position.get_average_cost()
750
+ position.fees = position.get_fees()
751
+
752
+ balances = await exchange.fetch_balance()
753
+ if param['default_type']!='spot':
754
+ updated_position = await exchange.fetch_position(symbol=position.ticker)
755
+ # After position closed, 'updated_position' can be an empty dict. hyperliquid for example.
756
+ amount = (updated_position['contracts'] if updated_position else 0) * position.multiplier # in base ccy
757
+ else:
758
+ base_ccy : str = position.ticker.split("/")[0]
759
+ amount = balances[base_ccy]['total']
760
+ position.pos = amount
761
+
762
+ position.done = True
763
+
764
+ log(f"Executions:")
765
+ log(f"{json.dumps(position.to_dict(), indent=4)}")
766
+
767
+ notification_summary = {
768
+ 'ticker' : position.ticker,
769
+ 'side' : position.side,
770
+ 'num_executions' : len(position.get_executions()),
771
+ 'filled_amount' : position.filled_amount,
772
+ 'average_cost' : position.average_cost,
773
+ 'pos' : position.pos,
774
+ 'done' : position.done
775
+ }
776
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position done. {position.ticker} {position.side} {position.amount}", message=notification_summary, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
653
777
 
654
- log(f"Waiting for resent market order to close {order_id} ...")
655
-
656
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
657
-
658
- log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
659
-
660
- log(f"Executed slice #{i}", log_level=LogLevel.INFO)
661
- 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)
662
- if position.order_type=='limit':
663
- log(f"{position.ticker}, limit_price: {limit_price}, rounded_limit_price, {rounded_limit_price}", log_level=LogLevel.INFO)
664
-
665
- except Exception as slice_err:
666
- log(
667
- f"Failed to execute #{i} slice: {slice.to_dict()}. {slice_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
668
- log_level=LogLevel.ERROR
669
- )
670
- finally:
671
- i += 1
672
-
673
- position.filled_amount = position.get_filled_amount()
674
- position.average_cost = position.get_average_cost()
675
- position.fees = position.get_fees()
778
+ except Exception as position_execution_err:
779
+ err_msg = f"{position.ticker} Execution failed: {position_execution_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
780
+ log(err_msg)
676
781
 
677
- balances = await exchange.fetch_balance() # type: ignore
678
- if param['default_type']!='spot':
679
- updated_position = await exchange.fetch_position(symbol=position.ticker) # type: ignore
680
- # After position closed, 'updated_position' can be an empty dict. hyperliquid for example.
681
- amount = (updated_position['contracts'] if updated_position else 0) * position.multiplier # in base ccy
682
- else:
683
- base_ccy : str = position.ticker.split("/")[0]
684
- amount = balances[base_ccy]['total']
685
- position.pos = amount
782
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} {position.ticker} execute_one_position failed!!!", message=err_msg, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR, logger=logger)
686
783
 
784
+ position.done = False
785
+ position.execution_err = err_msg
786
+
687
787
  async def work(
688
788
  param : Dict,
689
789
  exchange : AnyExchange,
690
- redis_client : StrictRedis
790
+ redis_client : StrictRedis,
791
+ notification_params : Dict[str, Any]
691
792
  ):
692
793
  incoming_orders_topic_regex : str = param['incoming_orders_topic_regex']
693
794
  incoming_orders_topic_regex = incoming_orders_topic_regex.replace("$GATEWAY_ID$", param['gateway_id'])
@@ -703,6 +804,7 @@ async def work(
703
804
 
704
805
  asyncio.create_task(send_heartbeat(exchange))
705
806
 
807
+ loop_i : int = 0
706
808
  while True:
707
809
  try:
708
810
  keys = redis_client.keys()
@@ -729,19 +831,19 @@ async def work(
729
831
  reduce_only=order['reduce_only'],
730
832
  fees_ccy=order['fees_ccy'] if 'fees_ccy' in order else param['default_fees_ccy'],
731
833
  slices=order['slices'],
732
- wait_fill_threshold_ms=order['wait_fill_threshold_ms']
834
+ wait_fill_threshold_ms=order['wait_fill_threshold_ms'] if order['wait_fill_threshold_ms']>0 else param['wait_fill_threshold_ms']
733
835
  )
734
836
  for order in orders
735
837
  ]
736
838
 
737
839
  start = time.time()
738
- pending_executions = [ execute_one_position(exchange, position, param, executions) for position in positions ]
840
+ pending_executions = [ execute_one_position(exchange, position, param, executions, notification_params) for position in positions ]
739
841
  await asyncio.gather(*pending_executions)
740
842
  order_dispatch_elapsed_ms = int((time.time() - start) *1000)
741
843
 
742
844
  i = 0
743
845
  for position in positions:
744
- log(f"{i} {position.ticker}, {position.side} # executions: {len(position.get_executions())}, filled_amount: {position.filled_amount}, average_cost: {position.average_cost}, pos: {position.pos}, order_dispatch_elapsed_ms: {order_dispatch_elapsed_ms}")
846
+ log(f"{i} {position.ticker}, {position.side} # executions: {len(position.get_executions())}, filled_amount: {position.filled_amount}, average_cost: {position.average_cost}, pos: {position.pos}, done: {position.done}, error: {position.execution_err}, order_dispatch_elapsed_ms: {order_dispatch_elapsed_ms}")
745
847
  i += 1
746
848
 
747
849
  start = time.time()
@@ -764,10 +866,15 @@ async def work(
764
866
  log_level=LogLevel.ERROR
765
867
  )
766
868
 
869
+ if loop_i%10==0:
870
+ balances = await exchange.fetch_balance()
871
+ log(f"{param['gateway_id']}: account balances {balances}")
872
+
767
873
  except Exception as loop_error:
768
874
  log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
769
875
  finally:
770
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
876
+ loop_i += 1
877
+ await asyncio.sleep(param['loop_freq_ms']/1000)
771
878
 
772
879
  async def main():
773
880
  parse_args()
@@ -796,6 +903,8 @@ async def main():
796
903
  secret : str = param['secret']
797
904
  passphrase : str = param['passphrase']
798
905
 
906
+ notification_params : Dict[str, Any] = param['notification']
907
+
799
908
  if encrypt_decrypt_with_aws_kms:
800
909
  aws_kms_key_id = str(os.getenv('AWS_KMS_KEY_ID'))
801
910
 
@@ -807,19 +916,22 @@ async def main():
807
916
 
808
917
  redis_client : StrictRedis = init_redis_client()
809
918
 
810
- exchange : Union[AnyExchange, None] = await instantiate_exchange(
919
+ exchange : Union[AnyExchange, None] = await async_instantiate_exchange(
811
920
  gateway_id=param['gateway_id'],
812
921
  api_key=api_key,
813
922
  secret=secret,
814
923
  passphrase=passphrase,
815
924
  default_type=param['default_type'],
816
- rate_limit_ms=param['rate_limit_ms']
925
+ default_sub_type=param['default_sub_type'],
926
+ rate_limit_ms=param['rate_limit_ms'],
927
+ verbose=param['verbose']
817
928
  )
818
929
  if exchange:
819
930
  # Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
820
- balances = await exchange.fetch_balance() # type: ignore
931
+ balances = await exchange.fetch_balance()
821
932
  log(f"{param['gateway_id']}: account balances {balances}")
933
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} started", message=balances['total'], footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
822
934
 
823
- await work(param=param, exchange=exchange, redis_client=redis_client)
935
+ await work(param=param, exchange=exchange, redis_client=redis_client, notification_params=notification_params)
824
936
 
825
937
  asyncio.run(main())