siglab-py 0.1.29__py3-none-any.whl → 0.6.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of siglab-py might be problematic. Click here for more details.
- siglab_py/constants.py +26 -1
- siglab_py/exchanges/binance.py +38 -0
- siglab_py/exchanges/deribit.py +83 -0
- siglab_py/exchanges/futubull.py +12 -2
- siglab_py/market_data_providers/candles_provider.py +2 -2
- siglab_py/market_data_providers/candles_ta_provider.py +3 -3
- siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
- siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
- siglab_py/market_data_providers/google_monitor.py +320 -0
- siglab_py/market_data_providers/orderbooks_provider.py +15 -12
- siglab_py/market_data_providers/tg_monitor.py +428 -0
- siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
- siglab_py/ordergateway/client.py +172 -41
- siglab_py/ordergateway/encrypt_keys_util.py +1 -1
- siglab_py/ordergateway/gateway.py +456 -347
- siglab_py/ordergateway/test_ordergateway.py +8 -7
- siglab_py/tests/integration/market_data_util_tests.py +35 -1
- siglab_py/tests/unit/analytic_util_tests.py +47 -12
- siglab_py/tests/unit/simple_math_tests.py +235 -0
- siglab_py/tests/unit/trading_util_tests.py +65 -0
- siglab_py/util/analytic_util.py +478 -69
- siglab_py/util/market_data_util.py +487 -100
- siglab_py/util/notification_util.py +78 -0
- siglab_py/util/retry_util.py +11 -3
- siglab_py/util/simple_math.py +240 -0
- siglab_py/util/slack_notification_util.py +59 -0
- siglab_py/util/trading_util.py +118 -0
- {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/METADATA +5 -9
- siglab_py-0.6.12.dist-info/RECORD +44 -0
- {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/WHEEL +1 -1
- siglab_py-0.1.29.dist-info/RECORD +0 -34
- {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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()
|
|
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()
|
|
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(
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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(
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
if
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
'
|
|
552
|
-
|
|
553
|
-
'
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
627
|
-
position.
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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()
|
|
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())
|