siglab-py 0.1.30__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 +12 -2
  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 +4 -4
  18. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
  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 -347
  26. siglab_py/ordergateway/test_ordergateway.py +8 -7
  27. siglab_py/tests/integration/market_data_util_tests.py +75 -2
  28. siglab_py/tests/unit/analytic_util_tests.py +47 -12
  29. siglab_py/tests/unit/market_data_util_tests.py +45 -1
  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 +476 -67
  33. siglab_py/util/datetime_util.py +39 -0
  34. siglab_py/util/market_data_util.py +528 -98
  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.30.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -9
  42. siglab_py-0.6.33.dist-info/RECORD +56 -0
  43. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
  44. siglab_py-0.1.30.dist-info/RECORD +0 -34
  45. {siglab_py-0.1.30.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,265 +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) if rounded_slice_amount_in_base_ccy else 0
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
- if rounded_slice_amount_in_base_ccy==0:
470
- log(f"{position.ticker} Slice amount rounded to zero?! slice amount before rounding: {slice.amount}")
471
- continue
472
-
473
- orderbook = await exchange.fetch_order_book(symbol=position.ticker, limit=3) # type: ignore
474
- if position.side=='buy':
475
- asks = [ ask[0] for ask in orderbook['asks'] ]
476
- best_asks = min(asks)
477
- limit_price : float= best_asks * (1 + position.leg_room_bps/10000)
478
- else:
479
- bids = [ bid[0] for bid in orderbook['bids'] ]
480
- best_bid = max(bids)
481
- 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.
482
486
 
483
- rounded_limit_price : float = exchange.price_to_precision(position.ticker, limit_price) # type: ignore
484
- rounded_limit_price = float(rounded_limit_price)
485
-
486
- order_params = {
487
- 'reduceOnly': slice.reduce_only
488
- }
489
- if position.order_type=='limit':
490
- if not param['dry_run']:
491
- executed_order = await exchange.create_order( # type: ignore
492
- symbol = position.ticker,
493
- type = position.order_type,
494
- amount = rounded_slice_amount_in_base_ccy,
495
- price = rounded_limit_price,
496
- side = position.side,
497
- params = order_params
498
- )
487
+ slice_amount_in_base_ccy : float = slice.amount + randomized_order_amount
488
+
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)
499
505
  else:
500
- executed_order = DUMMY_EXECUTION.copy()
501
- executed_order['clientOrderId'] = str(uuid.uuid4())
502
- executed_order['timestamp'] = dt_now.timestamp()
503
- executed_order['datetime'] = dt_now
504
- executed_order['symbol'] = position.ticker
505
- executed_order['type'] = position.order_type
506
- executed_order['side'] = position.side
507
- executed_order['price'] = rounded_limit_price
508
- executed_order['average'] = rounded_limit_price
509
- executed_order['cost'] = 0
510
- executed_order['amount'] = rounded_slice_amount_in_base_ccy
511
- executed_order['filled'] = rounded_slice_amount_in_base_ccy
512
- executed_order['remaining'] = 0
513
- executed_order['status'] = 'closed'
514
- executed_order['multiplier'] = position.multiplier
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)
509
+
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
515
550
 
516
- else:
517
- if not param['dry_run']:
518
- executed_order = await exchange.create_order( # type: ignore
519
- symbol = position.ticker,
520
- type = position.order_type,
521
- amount = rounded_slice_amount_in_base_ccy,
522
- side = position.side,
523
- params = order_params
524
- )
525
551
  else:
526
- executed_order = DUMMY_EXECUTION.copy()
527
- executed_order['clientOrderId'] = str(uuid.uuid4())
528
- executed_order['timestamp'] = dt_now.timestamp()
529
- executed_order['datetime'] = dt_now
530
- executed_order['symbol'] = position.ticker
531
- executed_order['type'] = position.order_type
532
- executed_order['side'] = position.side
533
- executed_order['price'] = rounded_limit_price
534
- executed_order['average'] = rounded_limit_price
535
- executed_order['cost'] = 0
536
- executed_order['amount'] = rounded_slice_amount_in_base_ccy
537
- executed_order['filled'] = rounded_slice_amount_in_base_ccy
538
- executed_order['remaining'] = 0
539
- executed_order['status'] = 'closed'
540
- executed_order['multiplier'] = position.multiplier
541
-
542
- executed_order['slice_id'] = i
543
-
544
- '''
545
- Format of executed_order:
546
- executed_order
547
- {'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}
548
- special variables:
549
- function variables:
550
- 'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}
551
- 'id': '2245241151525347328'
552
- 'clientOrderId': 'xxx'
553
- 'timestamp': None
554
- 'datetime': None
555
- 'lastTradeTimestamp': None
556
- 'lastUpdateTimestamp': None
557
- 'symbol': 'SUSHI/USDT:USDT'
558
- 'type': 'limit'
559
- 'timeInForce': None
560
- 'postOnly': None
561
- 'side': 'buy'
562
- 'price': None
563
- 'stopLossPrice': None
564
- 'takeProfitPrice': None
565
- 'triggerPrice': None
566
- 'average': None
567
- 'cost': None
568
- 'amount': None
569
- 'filled': None
570
- 'remaining': None
571
- 'status': None
572
- 'fee': None
573
- 'trades': []
574
- 'reduceOnly': False
575
- 'fees': []
576
- 'stopPrice': None
577
- '''
578
- order_id = executed_order['id']
579
- order_status = executed_order['status']
580
- filled_amount = executed_order['filled']
581
- remaining_amount = executed_order['remaining']
582
- executed_order['multiplier'] = multiplier
583
- position.append_execution(order_id, executed_order)
584
-
585
- log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
586
-
587
- if not order_status or order_status!='closed':
588
- start_time = time.time()
589
- wait_threshold_sec = position.wait_fill_threshold_ms / 1000
590
-
591
- elapsed_sec = time.time() - start_time
592
- while elapsed_sec < wait_threshold_sec:
593
- order_update = None
594
- if order_id in executions:
595
- order_update = executions[order_id]
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
596
624
 
597
- if order_update:
598
- order_status = order_update['status']
599
- filled_amount = order_update['filled']
600
- remaining_amount = order_update['remaining']
601
- order_update['multiplier'] = multiplier
602
- position.append_execution(order_id, order_update)
603
-
604
- if remaining_amount <= 0:
605
- log(f"Limit order fully filled: {order_id}", log_level=LogLevel.INFO)
606
- break
607
-
608
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
609
-
610
-
611
- # Cancel hung limit order, resend as market
612
- if order_status!='closed':
613
- # If no update from websocket, do one last fetch via REST
614
- order_update = await exchange.fetch_order(order_id, position.ticker) # type: ignore
615
- order_status = order_update['status']
616
- filled_amount = order_update['filled']
617
- remaining_amount = order_update['remaining']
618
- order_update['multiplier'] = multiplier
619
- position.append_execution(order_id, order_update)
620
-
621
- if order_status!='closed':
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)
622
654
  order_status = order_update['status']
623
655
  filled_amount = order_update['filled']
624
656
  remaining_amount = order_update['remaining']
625
-
626
- await exchange.cancel_order(order_id, position.ticker) # type: ignore
627
- position.get_execution(order_id)['status'] = 'canceled'
628
- log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
629
-
630
- rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, remaining_amount) # type: ignore
631
- rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
632
- rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
633
- if rounded_slice_amount_in_base_ccy>0:
634
- executed_resent_order = await exchange.create_order( # type: ignore
635
- symbol=position.ticker,
636
- type='market',
637
- amount=remaining_amount,
638
- side=position.side
639
- )
640
-
641
- order_id = executed_resent_order['id']
642
- order_status = executed_resent_order['status']
643
- executed_resent_order['multiplier'] = multiplier
644
- position.append_execution(order_id, executed_resent_order)
645
-
646
- while not order_status or order_status!='closed':
647
- order_update = None
648
- if order_id in executions:
649
- order_update = executions[order_id]
650
-
651
- if order_update:
652
- 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)
653
708
  order_status = order_update['status']
654
709
  filled_amount = order_update['filled']
655
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)
656
777
 
657
- log(f"Waiting for resent market order to close {order_id} ...")
658
-
659
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
660
-
661
- log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
662
-
663
- log(f"Executed slice #{i}", log_level=LogLevel.INFO)
664
- 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)
665
- if position.order_type=='limit':
666
- log(f"{position.ticker}, limit_price: {limit_price}, rounded_limit_price, {rounded_limit_price}", log_level=LogLevel.INFO)
667
-
668
- except Exception as slice_err:
669
- log(
670
- f"Failed to execute #{i} slice: {slice.to_dict()}. {slice_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
671
- log_level=LogLevel.ERROR
672
- )
673
- finally:
674
- i += 1
675
-
676
- position.filled_amount = position.get_filled_amount()
677
- position.average_cost = position.get_average_cost()
678
- 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)
679
781
 
680
- balances = await exchange.fetch_balance() # type: ignore
681
- if param['default_type']!='spot':
682
- updated_position = await exchange.fetch_position(symbol=position.ticker) # type: ignore
683
- # After position closed, 'updated_position' can be an empty dict. hyperliquid for example.
684
- amount = (updated_position['contracts'] if updated_position else 0) * position.multiplier # in base ccy
685
- else:
686
- base_ccy : str = position.ticker.split("/")[0]
687
- amount = balances[base_ccy]['total']
688
- 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)
689
783
 
784
+ position.done = False
785
+ position.execution_err = err_msg
786
+
690
787
  async def work(
691
788
  param : Dict,
692
789
  exchange : AnyExchange,
693
- redis_client : StrictRedis
790
+ redis_client : StrictRedis,
791
+ notification_params : Dict[str, Any]
694
792
  ):
695
793
  incoming_orders_topic_regex : str = param['incoming_orders_topic_regex']
696
794
  incoming_orders_topic_regex = incoming_orders_topic_regex.replace("$GATEWAY_ID$", param['gateway_id'])
@@ -706,6 +804,7 @@ async def work(
706
804
 
707
805
  asyncio.create_task(send_heartbeat(exchange))
708
806
 
807
+ loop_i : int = 0
709
808
  while True:
710
809
  try:
711
810
  keys = redis_client.keys()
@@ -732,19 +831,19 @@ async def work(
732
831
  reduce_only=order['reduce_only'],
733
832
  fees_ccy=order['fees_ccy'] if 'fees_ccy' in order else param['default_fees_ccy'],
734
833
  slices=order['slices'],
735
- 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']
736
835
  )
737
836
  for order in orders
738
837
  ]
739
838
 
740
839
  start = time.time()
741
- 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 ]
742
841
  await asyncio.gather(*pending_executions)
743
842
  order_dispatch_elapsed_ms = int((time.time() - start) *1000)
744
843
 
745
844
  i = 0
746
845
  for position in positions:
747
- 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}")
748
847
  i += 1
749
848
 
750
849
  start = time.time()
@@ -767,10 +866,15 @@ async def work(
767
866
  log_level=LogLevel.ERROR
768
867
  )
769
868
 
869
+ if loop_i%10==0:
870
+ balances = await exchange.fetch_balance()
871
+ log(f"{param['gateway_id']}: account balances {balances}")
872
+
770
873
  except Exception as loop_error:
771
874
  log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
772
875
  finally:
773
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
876
+ loop_i += 1
877
+ await asyncio.sleep(param['loop_freq_ms']/1000)
774
878
 
775
879
  async def main():
776
880
  parse_args()
@@ -799,6 +903,8 @@ async def main():
799
903
  secret : str = param['secret']
800
904
  passphrase : str = param['passphrase']
801
905
 
906
+ notification_params : Dict[str, Any] = param['notification']
907
+
802
908
  if encrypt_decrypt_with_aws_kms:
803
909
  aws_kms_key_id = str(os.getenv('AWS_KMS_KEY_ID'))
804
910
 
@@ -810,19 +916,22 @@ async def main():
810
916
 
811
917
  redis_client : StrictRedis = init_redis_client()
812
918
 
813
- exchange : Union[AnyExchange, None] = await instantiate_exchange(
919
+ exchange : Union[AnyExchange, None] = await async_instantiate_exchange(
814
920
  gateway_id=param['gateway_id'],
815
921
  api_key=api_key,
816
922
  secret=secret,
817
923
  passphrase=passphrase,
818
924
  default_type=param['default_type'],
819
- 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']
820
928
  )
821
929
  if exchange:
822
930
  # Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
823
- balances = await exchange.fetch_balance() # type: ignore
931
+ balances = await exchange.fetch_balance()
824
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)
825
934
 
826
- 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)
827
936
 
828
937
  asyncio.run(main())