siglab-py 0.1.19__py3-none-any.whl → 0.6.33__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- siglab_py/algo/__init__.py +0 -0
- siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
- siglab_py/algo/strategy_base.py +122 -0
- siglab_py/algo/strategy_executor.py +1308 -0
- siglab_py/algo/tp_algo.py +529 -0
- siglab_py/backtests/__init__.py +0 -0
- siglab_py/backtests/backtest_core.py +2405 -0
- siglab_py/backtests/coinflip_15m_crypto.py +432 -0
- siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
- siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
- 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 +33 -3
- siglab_py/market_data_providers/candles_provider.py +11 -10
- siglab_py/market_data_providers/candles_ta_provider.py +5 -5
- siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +238 -0
- siglab_py/market_data_providers/futu_candles_ta_to_csv.py +224 -0
- 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 -344
- siglab_py/ordergateway/test_ordergateway.py +8 -7
- siglab_py/tests/integration/market_data_util_tests.py +80 -6
- siglab_py/tests/unit/analytic_util_tests.py +67 -4
- siglab_py/tests/unit/market_data_util_tests.py +96 -0
- siglab_py/tests/unit/simple_math_tests.py +252 -0
- siglab_py/tests/unit/trading_util_tests.py +65 -0
- siglab_py/util/analytic_util.py +484 -66
- siglab_py/util/datetime_util.py +39 -0
- siglab_py/util/market_data_util.py +564 -74
- siglab_py/util/module_util.py +40 -0
- siglab_py/util/notification_util.py +78 -0
- siglab_py/util/retry_util.py +16 -3
- siglab_py/util/simple_math.py +262 -0
- siglab_py/util/slack_notification_util.py +59 -0
- siglab_py/util/trading_util.py +118 -0
- {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -13
- siglab_py-0.6.33.dist-info/RECORD +56 -0
- {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
- siglab_py-0.1.19.dist-info/RECORD +0 -31
- {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.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,262 +411,384 @@ async def send_heartbeat(exchange):
|
|
|
432
411
|
try:
|
|
433
412
|
first_ws_url = next(iter(exchange.clients))
|
|
434
413
|
client = exchange.clients[first_ws_url]
|
|
435
|
-
|
|
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
|
-
|
|
442
|
+
order_amount_randomize_max_pct : float = param['order_amount_randomize_max_pct']
|
|
443
|
+
|
|
444
|
+
log(f"{position.ticker} min_amount: {min_amount}, multiplier: {multiplier}, order_amount_randomize_max_pct: {order_amount_randomize_max_pct}")
|
|
445
|
+
|
|
446
|
+
slices : List[Order] = position.to_slices()
|
|
447
|
+
|
|
448
|
+
# Residual handling in last slice
|
|
449
|
+
if len(slices)>1:
|
|
450
|
+
last_slice = slices[-1]
|
|
451
|
+
last_slice_rounded_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, last_slice.amount/multiplier) # After divided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
|
|
452
|
+
last_slice_rounded_amount_in_base_ccy = float(last_slice_rounded_amount_in_base_ccy) if last_slice_rounded_amount_in_base_ccy else 0
|
|
453
|
+
if last_slice_rounded_amount_in_base_ccy<=min_amount:
|
|
454
|
+
slices.pop()
|
|
455
|
+
slices[-1].amount += last_slice.amount
|
|
456
|
+
|
|
457
|
+
log(f"{position.ticker} Last slice residual smaller than min_amount. Amount is added to prev slice instead. last_slice_amount: {last_slice.amount/multiplier}, last_slice_rounded_amount: {last_slice_rounded_amount_in_base_ccy}")
|
|
458
|
+
|
|
459
|
+
@retry(num_attempts=3, pause_between_retries_ms=3000)
|
|
460
|
+
async def _fetch_order(
|
|
461
|
+
order_id : str,
|
|
462
|
+
ticker : str,
|
|
463
|
+
exchange : AnyExchange,
|
|
464
|
+
):
|
|
465
|
+
order_update = await exchange.fetch_order(order_id, ticker)
|
|
466
|
+
return order_update
|
|
467
|
+
|
|
468
|
+
randomized_order_amount : float = 0
|
|
469
|
+
last_randomized_order_amount : float = 0
|
|
470
|
+
apply_last_randomized_amount : bool = False # False: Apply new variance, True: Apply -1 * last_randomized_order_amount
|
|
471
|
+
i = 0
|
|
472
|
+
for slice in slices:
|
|
473
|
+
try:
|
|
474
|
+
log(f"{position.ticker} sending slice# {i}")
|
|
475
|
+
|
|
476
|
+
dt_now : datetime = datetime.now()
|
|
477
|
+
|
|
478
|
+
if len(slices)>1:
|
|
479
|
+
if not apply_last_randomized_amount and i<len(slices):
|
|
480
|
+
randomized_order_amount = slice.amount * (order_amount_randomize_max_pct * random.uniform(-1, 1)) /100
|
|
481
|
+
last_randomized_order_amount = randomized_order_amount
|
|
482
|
+
|
|
483
|
+
else:
|
|
484
|
+
randomized_order_amount = -1 * last_randomized_order_amount # Apply the opposite of last slice's variance
|
|
485
|
+
last_randomized_order_amount = 0 # If # slices == 5, last slice don't apply random amount, so reset last_randomized_order_amount to zero.
|
|
478
486
|
|
|
479
|
-
|
|
480
|
-
rounded_limit_price = float(rounded_limit_price)
|
|
481
|
-
|
|
482
|
-
order_params = {
|
|
483
|
-
'reduceOnly': slice.reduce_only
|
|
484
|
-
}
|
|
485
|
-
if position.order_type=='limit':
|
|
486
|
-
if not param['dry_run']:
|
|
487
|
-
executed_order = await exchange.create_order( # type: ignore
|
|
488
|
-
symbol = position.ticker,
|
|
489
|
-
type = position.order_type,
|
|
490
|
-
amount = rounded_slice_amount_in_base_ccy,
|
|
491
|
-
price = rounded_limit_price,
|
|
492
|
-
side = position.side,
|
|
493
|
-
params = order_params
|
|
494
|
-
)
|
|
495
|
-
else:
|
|
496
|
-
executed_order = DUMMY_EXECUTION.copy()
|
|
497
|
-
executed_order['clientOrderId'] = str(uuid.uuid4())
|
|
498
|
-
executed_order['timestamp'] = dt_now.timestamp()
|
|
499
|
-
executed_order['datetime'] = dt_now
|
|
500
|
-
executed_order['symbol'] = position.ticker
|
|
501
|
-
executed_order['type'] = position.order_type
|
|
502
|
-
executed_order['side'] = position.side
|
|
503
|
-
executed_order['price'] = rounded_limit_price
|
|
504
|
-
executed_order['average'] = rounded_limit_price
|
|
505
|
-
executed_order['cost'] = 0
|
|
506
|
-
executed_order['amount'] = rounded_slice_amount_in_base_ccy
|
|
507
|
-
executed_order['filled'] = rounded_slice_amount_in_base_ccy
|
|
508
|
-
executed_order['remaining'] = 0
|
|
509
|
-
executed_order['status'] = 'closed'
|
|
510
|
-
executed_order['multiplier'] = position.multiplier
|
|
487
|
+
slice_amount_in_base_ccy : float = slice.amount + randomized_order_amount
|
|
511
488
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
)
|
|
489
|
+
apply_last_randomized_amount = not apply_last_randomized_amount
|
|
490
|
+
|
|
491
|
+
rounded_slice_amount_in_base_ccy = slice_amount_in_base_ccy / multiplier # After divided by multiplier, rounded_slice_amount_in_base_ccy in number of contracts actually (Not in base ccy).
|
|
492
|
+
rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, rounded_slice_amount_in_base_ccy)
|
|
493
|
+
rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy) if rounded_slice_amount_in_base_ccy else 0
|
|
494
|
+
rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
|
|
495
|
+
|
|
496
|
+
if rounded_slice_amount_in_base_ccy==0:
|
|
497
|
+
log(f"{position.ticker} Slice amount rounded to zero?! slice amount before rounding: {slice.amount}")
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
orderbook = await exchange.fetch_order_book(symbol=position.ticker, limit=3)
|
|
501
|
+
if position.side=='buy':
|
|
502
|
+
asks = [ ask[0] for ask in orderbook['asks'] ]
|
|
503
|
+
best_asks = min(asks)
|
|
504
|
+
limit_price : float= best_asks * (1 + position.leg_room_bps/10000)
|
|
521
505
|
else:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
executed_order['datetime'] = dt_now
|
|
526
|
-
executed_order['symbol'] = position.ticker
|
|
527
|
-
executed_order['type'] = position.order_type
|
|
528
|
-
executed_order['side'] = position.side
|
|
529
|
-
executed_order['price'] = rounded_limit_price
|
|
530
|
-
executed_order['average'] = rounded_limit_price
|
|
531
|
-
executed_order['cost'] = 0
|
|
532
|
-
executed_order['amount'] = rounded_slice_amount_in_base_ccy
|
|
533
|
-
executed_order['filled'] = rounded_slice_amount_in_base_ccy
|
|
534
|
-
executed_order['remaining'] = 0
|
|
535
|
-
executed_order['status'] = 'closed'
|
|
536
|
-
executed_order['multiplier'] = position.multiplier
|
|
537
|
-
|
|
538
|
-
executed_order['slice_id'] = i
|
|
539
|
-
|
|
540
|
-
'''
|
|
541
|
-
Format of executed_order:
|
|
542
|
-
executed_order
|
|
543
|
-
{'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}, 'id': '2245241151525347328', 'clientOrderId': 'xxx', 'timestamp': None, 'datetime': None, 'lastTradeTimestamp': None, 'lastUpdateTimestamp': None, 'symbol': 'SUSHI/USDT:USDT', 'type': 'limit', 'timeInForce': None, 'postOnly': None, 'side': 'buy', 'price': None, 'stopLossPrice': None, 'takeProfitPrice': None, 'triggerPrice': None, 'average': None, 'cost': None, 'amount': None, 'filled': None, 'remaining': None, 'status': None, 'fee': None, 'trades': [], 'reduceOnly': False, 'fees': [], 'stopPrice': None}
|
|
544
|
-
special variables:
|
|
545
|
-
function variables:
|
|
546
|
-
'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}
|
|
547
|
-
'id': '2245241151525347328'
|
|
548
|
-
'clientOrderId': 'xxx'
|
|
549
|
-
'timestamp': None
|
|
550
|
-
'datetime': None
|
|
551
|
-
'lastTradeTimestamp': None
|
|
552
|
-
'lastUpdateTimestamp': None
|
|
553
|
-
'symbol': 'SUSHI/USDT:USDT'
|
|
554
|
-
'type': 'limit'
|
|
555
|
-
'timeInForce': None
|
|
556
|
-
'postOnly': None
|
|
557
|
-
'side': 'buy'
|
|
558
|
-
'price': None
|
|
559
|
-
'stopLossPrice': None
|
|
560
|
-
'takeProfitPrice': None
|
|
561
|
-
'triggerPrice': None
|
|
562
|
-
'average': None
|
|
563
|
-
'cost': None
|
|
564
|
-
'amount': None
|
|
565
|
-
'filled': None
|
|
566
|
-
'remaining': None
|
|
567
|
-
'status': None
|
|
568
|
-
'fee': None
|
|
569
|
-
'trades': []
|
|
570
|
-
'reduceOnly': False
|
|
571
|
-
'fees': []
|
|
572
|
-
'stopPrice': None
|
|
573
|
-
'''
|
|
574
|
-
order_id = executed_order['id']
|
|
575
|
-
order_status = executed_order['status']
|
|
576
|
-
filled_amount = executed_order['filled']
|
|
577
|
-
remaining_amount = executed_order['remaining']
|
|
578
|
-
executed_order['multiplier'] = multiplier
|
|
579
|
-
position.append_execution(order_id, executed_order)
|
|
580
|
-
|
|
581
|
-
log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
|
|
582
|
-
|
|
583
|
-
if not order_status or order_status!='closed':
|
|
584
|
-
start_time = time.time()
|
|
585
|
-
wait_threshold_sec = position.wait_fill_threshold_ms / 1000
|
|
586
|
-
|
|
587
|
-
elapsed_sec = time.time() - start_time
|
|
588
|
-
while elapsed_sec < wait_threshold_sec:
|
|
589
|
-
order_update = None
|
|
590
|
-
if order_id in executions:
|
|
591
|
-
order_update = executions[order_id]
|
|
506
|
+
bids = [ bid[0] for bid in orderbook['bids'] ]
|
|
507
|
+
best_bid = max(bids)
|
|
508
|
+
limit_price : float = best_bid * (1 - position.leg_room_bps/10000)
|
|
592
509
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
510
|
+
rounded_limit_price : float = exchange.price_to_precision(position.ticker, limit_price)
|
|
511
|
+
rounded_limit_price = float(rounded_limit_price)
|
|
512
|
+
|
|
513
|
+
order_params = {
|
|
514
|
+
'reduceOnly': slice.reduce_only
|
|
515
|
+
}
|
|
516
|
+
if position.order_type=='limit':
|
|
517
|
+
if position.leg_room_bps<0:
|
|
518
|
+
order_params['postOnly'] = True
|
|
519
|
+
else:
|
|
520
|
+
log(
|
|
521
|
+
f"{position.side} {rounded_slice_amount_in_base_ccy} {position.ticker}. Limit order to be sent as Market order. Invalid leg_room_bps: {position.leg_room_bps}. By convention, leg_room_bps more positive means you want your order to get filled more aggressively. To post limit orders, leg_room_bps should be negative.",
|
|
522
|
+
log_level=LogLevel.WARNING
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if not param['dry_run']:
|
|
526
|
+
executed_order = await exchange.create_order(
|
|
527
|
+
symbol = position.ticker,
|
|
528
|
+
type = position.order_type,
|
|
529
|
+
amount = rounded_slice_amount_in_base_ccy,
|
|
530
|
+
price = rounded_limit_price,
|
|
531
|
+
side = position.side,
|
|
532
|
+
params = order_params
|
|
533
|
+
)
|
|
534
|
+
else:
|
|
535
|
+
executed_order = DUMMY_EXECUTION.copy()
|
|
536
|
+
executed_order['clientOrderId'] = str(uuid.uuid4())
|
|
537
|
+
executed_order['timestamp'] = dt_now.timestamp()
|
|
538
|
+
executed_order['datetime'] = dt_now
|
|
539
|
+
executed_order['symbol'] = position.ticker
|
|
540
|
+
executed_order['type'] = position.order_type
|
|
541
|
+
executed_order['side'] = position.side
|
|
542
|
+
executed_order['price'] = rounded_limit_price
|
|
543
|
+
executed_order['average'] = rounded_limit_price
|
|
544
|
+
executed_order['cost'] = 0
|
|
545
|
+
executed_order['amount'] = rounded_slice_amount_in_base_ccy
|
|
546
|
+
executed_order['filled'] = rounded_slice_amount_in_base_ccy
|
|
547
|
+
executed_order['remaining'] = 0
|
|
548
|
+
executed_order['status'] = 'closed'
|
|
549
|
+
executed_order['multiplier'] = position.multiplier
|
|
607
550
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
551
|
+
else:
|
|
552
|
+
if not param['dry_run']:
|
|
553
|
+
executed_order = await exchange.create_order(
|
|
554
|
+
symbol = position.ticker,
|
|
555
|
+
type = position.order_type,
|
|
556
|
+
amount = rounded_slice_amount_in_base_ccy,
|
|
557
|
+
side = position.side,
|
|
558
|
+
params = order_params
|
|
559
|
+
)
|
|
560
|
+
else:
|
|
561
|
+
executed_order = DUMMY_EXECUTION.copy()
|
|
562
|
+
executed_order['clientOrderId'] = str(uuid.uuid4())
|
|
563
|
+
executed_order['timestamp'] = dt_now.timestamp()
|
|
564
|
+
executed_order['datetime'] = dt_now
|
|
565
|
+
executed_order['symbol'] = position.ticker
|
|
566
|
+
executed_order['type'] = position.order_type
|
|
567
|
+
executed_order['side'] = position.side
|
|
568
|
+
executed_order['price'] = rounded_limit_price
|
|
569
|
+
executed_order['average'] = rounded_limit_price
|
|
570
|
+
executed_order['cost'] = 0
|
|
571
|
+
executed_order['amount'] = rounded_slice_amount_in_base_ccy
|
|
572
|
+
executed_order['filled'] = rounded_slice_amount_in_base_ccy
|
|
573
|
+
executed_order['remaining'] = 0
|
|
574
|
+
executed_order['status'] = 'closed'
|
|
575
|
+
executed_order['multiplier'] = position.multiplier
|
|
576
|
+
|
|
577
|
+
executed_order['slice_id'] = i
|
|
578
|
+
|
|
579
|
+
'''
|
|
580
|
+
Format of executed_order:
|
|
581
|
+
executed_order
|
|
582
|
+
{'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}, 'id': '2245241151525347328', 'clientOrderId': 'xxx', 'timestamp': None, 'datetime': None, 'lastTradeTimestamp': None, 'lastUpdateTimestamp': None, 'symbol': 'SUSHI/USDT:USDT', 'type': 'limit', 'timeInForce': None, 'postOnly': None, 'side': 'buy', 'price': None, 'stopLossPrice': None, 'takeProfitPrice': None, 'triggerPrice': None, 'average': None, 'cost': None, 'amount': None, 'filled': None, 'remaining': None, 'status': None, 'fee': None, 'trades': [], 'reduceOnly': False, 'fees': [], 'stopPrice': None}
|
|
583
|
+
special variables:
|
|
584
|
+
function variables:
|
|
585
|
+
'info': {'clOrdId': 'xxx', 'ordId': '2245241151525347328', 'sCode': '0', 'sMsg': 'Order placed', 'tag': 'xxx', 'ts': '1739415800635'}
|
|
586
|
+
'id': '2245241151525347328'
|
|
587
|
+
'clientOrderId': 'xxx'
|
|
588
|
+
'timestamp': None
|
|
589
|
+
'datetime': None
|
|
590
|
+
'lastTradeTimestamp': None
|
|
591
|
+
'lastUpdateTimestamp': None
|
|
592
|
+
'symbol': 'SUSHI/USDT:USDT'
|
|
593
|
+
'type': 'limit'
|
|
594
|
+
'timeInForce': None
|
|
595
|
+
'postOnly': None
|
|
596
|
+
'side': 'buy'
|
|
597
|
+
'price': None
|
|
598
|
+
'stopLossPrice': None
|
|
599
|
+
'takeProfitPrice': None
|
|
600
|
+
'triggerPrice': None
|
|
601
|
+
'average': None
|
|
602
|
+
'cost': None
|
|
603
|
+
'amount': None
|
|
604
|
+
'filled': None
|
|
605
|
+
'remaining': None
|
|
606
|
+
'status': None
|
|
607
|
+
'fee': None
|
|
608
|
+
'trades': []
|
|
609
|
+
'reduceOnly': False
|
|
610
|
+
'fees': []
|
|
611
|
+
'stopPrice': None
|
|
612
|
+
'''
|
|
613
|
+
order_id = executed_order['id']
|
|
614
|
+
order_status = executed_order['status']
|
|
615
|
+
filled_amount = executed_order['filled']
|
|
616
|
+
remaining_amount = executed_order['remaining']
|
|
617
|
+
executed_order['multiplier'] = multiplier
|
|
618
|
+
position.append_execution(order_id, executed_order)
|
|
619
|
+
|
|
620
|
+
log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
|
|
621
|
+
|
|
622
|
+
if not order_status or order_status!='closed':
|
|
623
|
+
wait_threshold_sec = position.wait_fill_threshold_ms / 1000
|
|
624
|
+
|
|
625
|
+
start_time = time.time()
|
|
626
|
+
elapsed_sec = time.time() - start_time
|
|
627
|
+
while elapsed_sec < wait_threshold_sec:
|
|
628
|
+
order_update = None
|
|
629
|
+
if order_id in executions:
|
|
630
|
+
order_update = executions[order_id]
|
|
631
|
+
|
|
632
|
+
if order_update:
|
|
633
|
+
order_status = order_update['status']
|
|
634
|
+
filled_amount = order_update['filled']
|
|
635
|
+
remaining_amount = order_update['remaining']
|
|
636
|
+
order_update['multiplier'] = multiplier
|
|
637
|
+
position.append_execution(order_id, order_update)
|
|
638
|
+
|
|
639
|
+
if remaining_amount <= 0:
|
|
640
|
+
log(f"Limit order fully filled: {order_id}, order_update: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
|
|
641
|
+
break
|
|
642
|
+
|
|
643
|
+
loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
|
|
644
|
+
loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
|
|
645
|
+
await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
|
|
646
|
+
|
|
647
|
+
elapsed_sec = time.time() - start_time
|
|
648
|
+
log(f"{position.ticker} waiting for order update ... elapsed_sec: {elapsed_sec}")
|
|
649
|
+
|
|
650
|
+
# Cancel hung limit order, resend as market
|
|
651
|
+
if order_status!='closed':
|
|
652
|
+
# If no update from websocket, do one last fetch via REST
|
|
653
|
+
order_update = await _fetch_order(order_id, position.ticker, exchange)
|
|
619
654
|
order_status = order_update['status']
|
|
620
655
|
filled_amount = order_update['filled']
|
|
621
656
|
remaining_amount = order_update['remaining']
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
position.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
657
|
+
order_update['multiplier'] = multiplier
|
|
658
|
+
|
|
659
|
+
position.append_execution(order_id, order_update)
|
|
660
|
+
|
|
661
|
+
if order_status!='closed':
|
|
662
|
+
log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
|
|
663
|
+
await exchange.cancel_order(order_id, position.ticker)
|
|
664
|
+
position.get_execution(order_id)['status'] = 'canceled'
|
|
665
|
+
log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
|
|
666
|
+
|
|
667
|
+
rounded_slice_amount_in_base_ccy = exchange.amount_to_precision(position.ticker, remaining_amount)
|
|
668
|
+
rounded_slice_amount_in_base_ccy = float(rounded_slice_amount_in_base_ccy)
|
|
669
|
+
rounded_slice_amount_in_base_ccy = rounded_slice_amount_in_base_ccy if rounded_slice_amount_in_base_ccy>min_amount else min_amount
|
|
670
|
+
if rounded_slice_amount_in_base_ccy>0:
|
|
671
|
+
executed_resent_order = await exchange.create_order(
|
|
672
|
+
symbol=position.ticker,
|
|
673
|
+
type='market',
|
|
674
|
+
amount=remaining_amount,
|
|
675
|
+
side=position.side
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
order_id = executed_resent_order['id']
|
|
679
|
+
order_status = executed_resent_order['status']
|
|
680
|
+
executed_resent_order['multiplier'] = multiplier
|
|
681
|
+
position.append_execution(order_id, executed_resent_order)
|
|
682
|
+
|
|
683
|
+
wait_threshold_sec = position.wait_fill_threshold_ms / 1000
|
|
684
|
+
|
|
685
|
+
start_time = time.time()
|
|
686
|
+
elapsed_sec = time.time() - start_time
|
|
687
|
+
while (not order_status or order_status!='closed') and (elapsed_sec < wait_threshold_sec):
|
|
688
|
+
order_update = None
|
|
689
|
+
if order_id in executions:
|
|
690
|
+
order_update = executions[order_id]
|
|
691
|
+
|
|
692
|
+
if order_update:
|
|
693
|
+
order_id = order_update['id']
|
|
694
|
+
order_status = order_update['status']
|
|
695
|
+
filled_amount = order_update['filled']
|
|
696
|
+
remaining_amount = order_update['remaining']
|
|
697
|
+
|
|
698
|
+
elapsed_sec = time.time() - start_time
|
|
699
|
+
log(f"Waiting for resent market order to close {order_id} ... elapsed_sec: {elapsed_sec}")
|
|
700
|
+
|
|
701
|
+
loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
|
|
702
|
+
loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
|
|
703
|
+
await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
|
|
704
|
+
|
|
705
|
+
if (not order_status or order_status!='closed'):
|
|
706
|
+
# If no update from websocket, do one last fetch via REST
|
|
707
|
+
order_update = await _fetch_order(order_id, position.ticker, exchange)
|
|
650
708
|
order_status = order_update['status']
|
|
651
709
|
filled_amount = order_update['filled']
|
|
652
710
|
remaining_amount = order_update['remaining']
|
|
711
|
+
order_update['multiplier'] = multiplier
|
|
712
|
+
|
|
713
|
+
log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount} {json.dumps(order_update, indent=4)}")
|
|
714
|
+
else:
|
|
715
|
+
log(f"{position.ticker} {order_id} status (From REST): {json.dumps(order_update, indent=4)}")
|
|
716
|
+
|
|
717
|
+
slice.dispatched_price = rounded_limit_price
|
|
718
|
+
slice.dispatched_amount = rounded_slice_amount_in_base_ccy
|
|
719
|
+
position.dispatched_slices.append(slice)
|
|
720
|
+
|
|
721
|
+
log(f"Executed slice #{i}", log_level=LogLevel.INFO)
|
|
722
|
+
log(f"{json.dumps(slice.to_dict(), indent=4)}")
|
|
723
|
+
log(f"{position.ticker}, multiplier: {multiplier}, slice_amount_in_base_ccy: {slice_amount_in_base_ccy}, rounded_slice_amount_in_base_ccy, {rounded_slice_amount_in_base_ccy}", log_level=LogLevel.INFO)
|
|
724
|
+
if position.order_type=='limit':
|
|
725
|
+
log(f"{position.ticker}, limit_price: {limit_price}, rounded_limit_price, {rounded_limit_price}", log_level=LogLevel.INFO)
|
|
726
|
+
|
|
727
|
+
except Exception as slice_err:
|
|
728
|
+
log(
|
|
729
|
+
f"Failed to execute #{i} slice: {slice.to_dict()}. {slice_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
|
|
730
|
+
log_level=LogLevel.ERROR
|
|
731
|
+
)
|
|
732
|
+
raise slice_err
|
|
733
|
+
finally:
|
|
734
|
+
log(f"{position.ticker} done slice# {i}")
|
|
735
|
+
i += 1
|
|
736
|
+
|
|
737
|
+
log(f"{position.ticker} patch_executions")
|
|
738
|
+
position.patch_executions()
|
|
739
|
+
|
|
740
|
+
log(f"Dispatched slices:")
|
|
741
|
+
for dispatched_slice in position.dispatched_slices:
|
|
742
|
+
log(f"{json.dumps(dispatched_slice.to_dict(), indent=4)}")
|
|
743
|
+
|
|
744
|
+
position_from_exchange = await exchange.fetch_position(position.ticker)
|
|
745
|
+
log(f"position update:")
|
|
746
|
+
log(f"{json.dumps(position_from_exchange, indent=4)}")
|
|
747
|
+
|
|
748
|
+
position.filled_amount = position.get_filled_amount()
|
|
749
|
+
position.average_cost = position.get_average_cost()
|
|
750
|
+
position.fees = position.get_fees()
|
|
751
|
+
|
|
752
|
+
balances = await exchange.fetch_balance()
|
|
753
|
+
if param['default_type']!='spot':
|
|
754
|
+
updated_position = await exchange.fetch_position(symbol=position.ticker)
|
|
755
|
+
# After position closed, 'updated_position' can be an empty dict. hyperliquid for example.
|
|
756
|
+
amount = (updated_position['contracts'] if updated_position else 0) * position.multiplier # in base ccy
|
|
757
|
+
else:
|
|
758
|
+
base_ccy : str = position.ticker.split("/")[0]
|
|
759
|
+
amount = balances[base_ccy]['total']
|
|
760
|
+
position.pos = amount
|
|
761
|
+
|
|
762
|
+
position.done = True
|
|
763
|
+
|
|
764
|
+
log(f"Executions:")
|
|
765
|
+
log(f"{json.dumps(position.to_dict(), indent=4)}")
|
|
766
|
+
|
|
767
|
+
notification_summary = {
|
|
768
|
+
'ticker' : position.ticker,
|
|
769
|
+
'side' : position.side,
|
|
770
|
+
'num_executions' : len(position.get_executions()),
|
|
771
|
+
'filled_amount' : position.filled_amount,
|
|
772
|
+
'average_cost' : position.average_cost,
|
|
773
|
+
'pos' : position.pos,
|
|
774
|
+
'done' : position.done
|
|
775
|
+
}
|
|
776
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position done. {position.ticker} {position.side} {position.amount}", message=notification_summary, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
653
777
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
|
|
659
|
-
|
|
660
|
-
log(f"Executed slice #{i}", log_level=LogLevel.INFO)
|
|
661
|
-
log(f"{position.ticker}, multiplier: {multiplier}, slice_amount_in_base_ccy: {slice_amount_in_base_ccy}, rounded_slice_amount_in_base_ccy, {rounded_slice_amount_in_base_ccy}", log_level=LogLevel.INFO)
|
|
662
|
-
if position.order_type=='limit':
|
|
663
|
-
log(f"{position.ticker}, limit_price: {limit_price}, rounded_limit_price, {rounded_limit_price}", log_level=LogLevel.INFO)
|
|
664
|
-
|
|
665
|
-
except Exception as slice_err:
|
|
666
|
-
log(
|
|
667
|
-
f"Failed to execute #{i} slice: {slice.to_dict()}. {slice_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}",
|
|
668
|
-
log_level=LogLevel.ERROR
|
|
669
|
-
)
|
|
670
|
-
finally:
|
|
671
|
-
i += 1
|
|
672
|
-
|
|
673
|
-
position.filled_amount = position.get_filled_amount()
|
|
674
|
-
position.average_cost = position.get_average_cost()
|
|
675
|
-
position.fees = position.get_fees()
|
|
778
|
+
except Exception as position_execution_err:
|
|
779
|
+
err_msg = f"{position.ticker} Execution failed: {position_execution_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
|
|
780
|
+
log(err_msg)
|
|
676
781
|
|
|
677
|
-
|
|
678
|
-
if param['default_type']!='spot':
|
|
679
|
-
updated_position = await exchange.fetch_position(symbol=position.ticker) # type: ignore
|
|
680
|
-
# After position closed, 'updated_position' can be an empty dict. hyperliquid for example.
|
|
681
|
-
amount = (updated_position['contracts'] if updated_position else 0) * position.multiplier # in base ccy
|
|
682
|
-
else:
|
|
683
|
-
base_ccy : str = position.ticker.split("/")[0]
|
|
684
|
-
amount = balances[base_ccy]['total']
|
|
685
|
-
position.pos = amount
|
|
782
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} {position.ticker} execute_one_position failed!!!", message=err_msg, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR, logger=logger)
|
|
686
783
|
|
|
784
|
+
position.done = False
|
|
785
|
+
position.execution_err = err_msg
|
|
786
|
+
|
|
687
787
|
async def work(
|
|
688
788
|
param : Dict,
|
|
689
789
|
exchange : AnyExchange,
|
|
690
|
-
redis_client : StrictRedis
|
|
790
|
+
redis_client : StrictRedis,
|
|
791
|
+
notification_params : Dict[str, Any]
|
|
691
792
|
):
|
|
692
793
|
incoming_orders_topic_regex : str = param['incoming_orders_topic_regex']
|
|
693
794
|
incoming_orders_topic_regex = incoming_orders_topic_regex.replace("$GATEWAY_ID$", param['gateway_id'])
|
|
@@ -703,6 +804,7 @@ async def work(
|
|
|
703
804
|
|
|
704
805
|
asyncio.create_task(send_heartbeat(exchange))
|
|
705
806
|
|
|
807
|
+
loop_i : int = 0
|
|
706
808
|
while True:
|
|
707
809
|
try:
|
|
708
810
|
keys = redis_client.keys()
|
|
@@ -729,19 +831,19 @@ async def work(
|
|
|
729
831
|
reduce_only=order['reduce_only'],
|
|
730
832
|
fees_ccy=order['fees_ccy'] if 'fees_ccy' in order else param['default_fees_ccy'],
|
|
731
833
|
slices=order['slices'],
|
|
732
|
-
wait_fill_threshold_ms=order['wait_fill_threshold_ms']
|
|
834
|
+
wait_fill_threshold_ms=order['wait_fill_threshold_ms'] if order['wait_fill_threshold_ms']>0 else param['wait_fill_threshold_ms']
|
|
733
835
|
)
|
|
734
836
|
for order in orders
|
|
735
837
|
]
|
|
736
838
|
|
|
737
839
|
start = time.time()
|
|
738
|
-
pending_executions = [ execute_one_position(exchange, position, param, executions) for position in positions ]
|
|
840
|
+
pending_executions = [ execute_one_position(exchange, position, param, executions, notification_params) for position in positions ]
|
|
739
841
|
await asyncio.gather(*pending_executions)
|
|
740
842
|
order_dispatch_elapsed_ms = int((time.time() - start) *1000)
|
|
741
843
|
|
|
742
844
|
i = 0
|
|
743
845
|
for position in positions:
|
|
744
|
-
log(f"{i} {position.ticker}, {position.side} # executions: {len(position.get_executions())}, filled_amount: {position.filled_amount}, average_cost: {position.average_cost}, pos: {position.pos}, order_dispatch_elapsed_ms: {order_dispatch_elapsed_ms}")
|
|
846
|
+
log(f"{i} {position.ticker}, {position.side} # executions: {len(position.get_executions())}, filled_amount: {position.filled_amount}, average_cost: {position.average_cost}, pos: {position.pos}, done: {position.done}, error: {position.execution_err}, order_dispatch_elapsed_ms: {order_dispatch_elapsed_ms}")
|
|
745
847
|
i += 1
|
|
746
848
|
|
|
747
849
|
start = time.time()
|
|
@@ -764,10 +866,15 @@ async def work(
|
|
|
764
866
|
log_level=LogLevel.ERROR
|
|
765
867
|
)
|
|
766
868
|
|
|
869
|
+
if loop_i%10==0:
|
|
870
|
+
balances = await exchange.fetch_balance()
|
|
871
|
+
log(f"{param['gateway_id']}: account balances {balances}")
|
|
872
|
+
|
|
767
873
|
except Exception as loop_error:
|
|
768
874
|
log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
|
|
769
875
|
finally:
|
|
770
|
-
|
|
876
|
+
loop_i += 1
|
|
877
|
+
await asyncio.sleep(param['loop_freq_ms']/1000)
|
|
771
878
|
|
|
772
879
|
async def main():
|
|
773
880
|
parse_args()
|
|
@@ -796,6 +903,8 @@ async def main():
|
|
|
796
903
|
secret : str = param['secret']
|
|
797
904
|
passphrase : str = param['passphrase']
|
|
798
905
|
|
|
906
|
+
notification_params : Dict[str, Any] = param['notification']
|
|
907
|
+
|
|
799
908
|
if encrypt_decrypt_with_aws_kms:
|
|
800
909
|
aws_kms_key_id = str(os.getenv('AWS_KMS_KEY_ID'))
|
|
801
910
|
|
|
@@ -807,19 +916,22 @@ async def main():
|
|
|
807
916
|
|
|
808
917
|
redis_client : StrictRedis = init_redis_client()
|
|
809
918
|
|
|
810
|
-
exchange : Union[AnyExchange, None] = await
|
|
919
|
+
exchange : Union[AnyExchange, None] = await async_instantiate_exchange(
|
|
811
920
|
gateway_id=param['gateway_id'],
|
|
812
921
|
api_key=api_key,
|
|
813
922
|
secret=secret,
|
|
814
923
|
passphrase=passphrase,
|
|
815
924
|
default_type=param['default_type'],
|
|
816
|
-
|
|
925
|
+
default_sub_type=param['default_sub_type'],
|
|
926
|
+
rate_limit_ms=param['rate_limit_ms'],
|
|
927
|
+
verbose=param['verbose']
|
|
817
928
|
)
|
|
818
929
|
if exchange:
|
|
819
930
|
# Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
|
|
820
|
-
balances = await exchange.fetch_balance()
|
|
931
|
+
balances = await exchange.fetch_balance()
|
|
821
932
|
log(f"{param['gateway_id']}: account balances {balances}")
|
|
933
|
+
dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} started", message=balances['total'], footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
|
|
822
934
|
|
|
823
|
-
await work(param=param, exchange=exchange, redis_client=redis_client)
|
|
935
|
+
await work(param=param, exchange=exchange, redis_client=redis_client, notification_params=notification_params)
|
|
824
936
|
|
|
825
937
|
asyncio.run(main())
|