siglab-py 0.1.30__py3-none-any.whl → 0.6.33__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. siglab_py/algo/__init__.py +0 -0
  2. siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
  3. siglab_py/algo/strategy_base.py +122 -0
  4. siglab_py/algo/strategy_executor.py +1308 -0
  5. siglab_py/algo/tp_algo.py +529 -0
  6. siglab_py/backtests/__init__.py +0 -0
  7. siglab_py/backtests/backtest_core.py +2405 -0
  8. siglab_py/backtests/coinflip_15m_crypto.py +432 -0
  9. siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
  10. siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
  11. siglab_py/constants.py +26 -1
  12. siglab_py/exchanges/binance.py +38 -0
  13. siglab_py/exchanges/deribit.py +83 -0
  14. siglab_py/exchanges/futubull.py +12 -2
  15. siglab_py/market_data_providers/candles_provider.py +11 -10
  16. siglab_py/market_data_providers/candles_ta_provider.py +5 -5
  17. siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
  18. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
  19. siglab_py/market_data_providers/google_monitor.py +320 -0
  20. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  21. siglab_py/market_data_providers/tg_monitor.py +428 -0
  22. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  23. siglab_py/ordergateway/client.py +172 -41
  24. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  25. siglab_py/ordergateway/gateway.py +456 -347
  26. siglab_py/ordergateway/test_ordergateway.py +8 -7
  27. siglab_py/tests/integration/market_data_util_tests.py +75 -2
  28. siglab_py/tests/unit/analytic_util_tests.py +47 -12
  29. siglab_py/tests/unit/market_data_util_tests.py +45 -1
  30. siglab_py/tests/unit/simple_math_tests.py +252 -0
  31. siglab_py/tests/unit/trading_util_tests.py +65 -0
  32. siglab_py/util/analytic_util.py +476 -67
  33. siglab_py/util/datetime_util.py +39 -0
  34. siglab_py/util/market_data_util.py +528 -98
  35. siglab_py/util/module_util.py +40 -0
  36. siglab_py/util/notification_util.py +78 -0
  37. siglab_py/util/retry_util.py +16 -3
  38. siglab_py/util/simple_math.py +262 -0
  39. siglab_py/util/slack_notification_util.py +59 -0
  40. siglab_py/util/trading_util.py +118 -0
  41. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -9
  42. siglab_py-0.6.33.dist-info/RECORD +56 -0
  43. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
  44. siglab_py-0.1.30.dist-info/RECORD +0 -34
  45. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,529 @@
1
+ import sys
2
+ import traceback
3
+ import os
4
+ import logging
5
+ from dotenv import load_dotenv
6
+ import argparse
7
+ from datetime import datetime, timezone
8
+ import time
9
+ from typing import List, Dict, Any, Union
10
+ import json
11
+ import asyncio
12
+ from redis import StrictRedis
13
+
14
+ from siglab_py.exchanges.any_exchange import AnyExchange
15
+ from siglab_py.ordergateway.client import DivisiblePosition, execute_positions
16
+ from siglab_py.util.market_data_util import async_instantiate_exchange
17
+ from siglab_py.util.trading_util import calc_eff_trailing_sl
18
+ from siglab_py.util.notification_util import dispatch_notification
19
+ from siglab_py.util.aws_util import AwsKmsUtil
20
+
21
+ from siglab_py.constants import LogLevel, JSON_SERIALIZABLE_TYPES # type: ignore
22
+
23
+ current_filename = os.path.basename(__file__)
24
+
25
+ '''
26
+ Error: RuntimeError: aiodns needs a SelectorEventLoop on Windows.
27
+ Hack, by far the filthest hack I done in my career: Set SelectorEventLoop on Windows
28
+ '''
29
+ if sys.platform == 'win32':
30
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
31
+
32
+ '''
33
+ TP algo is an excecution algo. It doesn't decide entries for you. It however, execute the trade with target TP and SL with hard and also gradually tighted trailing stops.
34
+
35
+ For more on "Gradually Tighted Stops" and 'calc_eff_trailing_sl':
36
+ https://medium.com/@norman-lm-fung/gradually-tightened-trailing-stops-f7854bf1e02b
37
+
38
+ Now why does TP algo need apikey when gateway.py is sending the orders? TP algo perform position reconciliation (For contracts), and if positions don't match, algo would terminate.
39
+ Why would you do that? Imagine if trader want to intervene and close out positions from mobile?
40
+
41
+ Usage:
42
+ set PYTHONPATH=%PYTHONPATH%;D:\dev\siglab\siglab_py
43
+ python tp_algo.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 xxx --ticker SUSHI/USDC:USDC --side sell --order_type limit --amount_base_ccy 45 --slices 3 --wait_fill_threshold_ms 15000 --leg_room_bps 5 --tp_min_percent 1.5 --tp_max_percent 2.5 --sl_percent_trailing 50 --sl_percent 1 --reversal_num_intervals 2 --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 --load_entry_from_cache N
44
+
45
+ Debug from VSCode, launch.json:
46
+ {
47
+ "version": "0.2.0",
48
+ "configurations": [
49
+ {
50
+ "name": "Python Debugger: Current File",
51
+ "type": "debugpy",
52
+ "request": "launch",
53
+ "program": "${file}",
54
+ "console": "integratedTerminal",
55
+ "args" : [
56
+ "--gateway_id", "hyperliquid_01",
57
+ "--default_type", "linear",
58
+ "--rate_limit_ms", "100",
59
+ "--encrypt_decrypt_with_aws_kms", "Y",
60
+ "--aws_kms_key_id", "xxx",
61
+ "--apikey", "xxx",
62
+ "--secret", "xxx",
63
+
64
+ "--ticker", "SUSHI/USDC:USDC",
65
+ "--side", "sell",
66
+ "--order_type", "limit",
67
+ "--amount_base_ccy", "45",
68
+ "--slices", "3",
69
+ "--wait_fill_threshold_ms", "15000",
70
+ "--leg_room_bps", "5",
71
+ "--tp_min_percent", "1.5",
72
+ "--tp_max_percent", "2.5",
73
+ "--sl_percent_trailing", "50",
74
+ "--sl_percent", "1",
75
+ "--reversal_num_intervals", "2",
76
+
77
+ "--load_entry_from_cache", "N",
78
+
79
+ "--slack_info_url", "https://hooks.slack.com/services/xxx",
80
+ "--slack_critial_url", "https://hooks.slack.com/services/xxx",
81
+ "--slack_alert_url", "https://hooks.slack.com/services/xxx",
82
+ ],
83
+ "env": {
84
+ "PYTHONPATH": "${workspaceFolder}"
85
+ }
86
+ }
87
+ ]
88
+ }
89
+ '''
90
+ param : Dict = {
91
+ 'trailing_sl_min_percent_linear': 1.0, # This is threshold used for tp_algo to decide if use linear stops tightening, or non-linear. If tp_max_percent far (>100bps), there's more uncertainty if target can be reached: Go with linear.
92
+ 'non_linear_pow' : 5, # For non-linear trailing stops tightening.
93
+
94
+ "loop_freq_ms" : 5000, # reduce this if you need trade faster
95
+
96
+ 'current_filename' : current_filename,
97
+
98
+ 'notification' : {
99
+ 'footer' : None,
100
+
101
+ # slack webhook url's for notifications
102
+ 'slack' : {
103
+ 'info' : { 'webhook_url' : None },
104
+ 'critical' : { 'webhook_url' : None },
105
+ 'alert' : { 'webhook_url' : None },
106
+ }
107
+ },
108
+
109
+ 'mds' : {
110
+ 'topics' : {
111
+
112
+ },
113
+ 'redis' : {
114
+ 'host' : 'localhost',
115
+ 'port' : 6379,
116
+ 'db' : 0,
117
+ 'ttl_ms' : 1000*60*15 # 15 min?
118
+ }
119
+ }
120
+ }
121
+
122
+ logging.Formatter.converter = time.gmtime
123
+ logger = logging.getLogger()
124
+ log_level = logging.INFO # DEBUG --> INFO --> WARNING --> ERROR
125
+ logger.setLevel(log_level)
126
+ format_str = '%(asctime)s %(message)s'
127
+ formatter = logging.Formatter(format_str)
128
+ sh = logging.StreamHandler()
129
+ sh.setLevel(log_level)
130
+ sh.setFormatter(formatter)
131
+ logger.addHandler(sh)
132
+
133
+ def log(message : str, log_level : LogLevel = LogLevel.INFO):
134
+ if log_level.value<LogLevel.WARNING.value:
135
+ logger.info(f"{datetime.now()} {message}")
136
+
137
+ elif log_level.value==LogLevel.WARNING.value:
138
+ logger.warning(f"{datetime.now()} {message}")
139
+
140
+ elif log_level.value==LogLevel.ERROR.value:
141
+ logger.error(f"{datetime.now()} {message}")
142
+
143
+ def parse_args():
144
+ parser = argparse.ArgumentParser() # type: ignore
145
+
146
+ parser.add_argument("--gateway_id", help="gateway_id: Where are you sending your order?", default=None)
147
+
148
+ parser.add_argument("--default_type", help="default_type: spot, linear, inverse, futures ...etc", default='linear')
149
+ parser.add_argument("--rate_limit_ms", help="rate_limit_ms: Check your exchange rules", default=100)
150
+ 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')
151
+ parser.add_argument("--aws_kms_key_id", help="AWS KMS key ID", default=None)
152
+ parser.add_argument("--apikey", help="Exchange apikey", default=None)
153
+ parser.add_argument("--secret", help="Exchange secret", default=None)
154
+ parser.add_argument("--passphrase", help="Exchange passphrase", default=None)
155
+
156
+ parser.add_argument("--ticker", help="Ticker you're trading. Example BTC/USDC:USDC", default=None)
157
+ parser.add_argument("--side", help="order side, for entries. buy or sell", default=None)
158
+ parser.add_argument("--order_type", help="Order type: market or limit", default=None)
159
+ parser.add_argument("--amount_base_ccy", help="Order amount in base ccy (Not # contracts). Always positive, even for sell trades.", default=None)
160
+ parser.add_argument("--leg_room_bps", help="Leg room, for Limit orders only. A more positive leg room is a more aggressive order to get filled. i.e. Buy at higher price, Sell at lower price.", default=5)
161
+ parser.add_argument("--slices", help="Algo can break down larger order into smaller slices. Default: 1", default=1)
162
+ parser.add_argument("--wait_fill_threshold_ms", help="Limit orders will be cancelled if not filled within this time. Remainder will be sent off as market order.", default=15000)
163
+
164
+ parser.add_argument("--load_entry_from_cache", help="Y or N (default). This is for algo restart scenario where you don't want make entry again. In this case existing/running position loaded from cache.", default='N')
165
+
166
+ parser.add_argument("--tp_min_percent", help="For trailing stops. Min TP in percent, i.e. No TP until pnl at least this much.", default=None)
167
+ parser.add_argument("--tp_max_percent", help="For trailing stops. Max TP in percent, i.e. Price target", default=None)
168
+ parser.add_argument("--sl_percent_trailing", help="For trailing stops. trailing SL in percent, please refer to trading_util.calc_eff_trailing_sl for documentation.", default=None)
169
+ parser.add_argument("--default_effective_tp_trailing_percent", help="Default for sl_percent_trailing when pnl still below tp_min_percent. Default: float('inf'), meaing trailing stop won't be fired.", default=float('inf'))
170
+ parser.add_argument("--sl_percent", help="Hard stop in percent.", default=2)
171
+ parser.add_argument("--reversal_num_intervals", help="How many reversal candles to confirm reversal?", default=3)
172
+ parser.add_argument("--trailing_sl_min_percent_linear", help="This is threshold used for tp_algo to decide if use linear stops tightening, or non-linear. If tp_max_percent far (>200bps for example), there's more uncertainty if target can be reached: Go with linear. Default: 2% (200 bps)", default=2.0)
173
+ parser.add_argument("--non_linear_pow", help="For non-linear trailing stops tightening, have a look at call to 'calc_eff_trailing_sl'. Default: 5", default=5)
174
+
175
+ parser.add_argument("--loop_freq_ms", help="Loop delays. Reduce this if you want to trade faster.", default=5000)
176
+
177
+ parser.add_argument("--slack_info_url", help="Slack webhook url for INFO", default=None)
178
+ parser.add_argument("--slack_critial_url", help="Slack webhook url for CRITICAL", default=None)
179
+ parser.add_argument("--slack_alert_url", help="Slack webhook url for ALERT", default=None)
180
+
181
+ args = parser.parse_args()
182
+
183
+ param['gateway_id'] = args.gateway_id
184
+ param['default_type'] = args.default_type
185
+ param['rate_limit_ms'] = int(args.rate_limit_ms)
186
+
187
+ if args.encrypt_decrypt_with_aws_kms:
188
+ if args.encrypt_decrypt_with_aws_kms=='Y':
189
+ param['encrypt_decrypt_with_aws_kms'] = True
190
+ else:
191
+ param['encrypt_decrypt_with_aws_kms'] = False
192
+ else:
193
+ param['encrypt_decrypt_with_aws_kms'] = False
194
+
195
+ param['aws_kms_key_id'] = args.aws_kms_key_id
196
+ param['apikey'] = args.apikey
197
+ param['secret'] = args.secret
198
+ param['passphrase'] = args.passphrase
199
+
200
+ param['ticker'] = args.ticker
201
+ param['side'] = args.side
202
+ param['order_type'] = args.order_type
203
+ param['amount_base_ccy'] = float(args.amount_base_ccy)
204
+ param['leg_room_bps'] = int(args.leg_room_bps)
205
+ param['slices'] = int(args.slices)
206
+ param['wait_fill_threshold_ms'] = int(args.wait_fill_threshold_ms)
207
+
208
+ if args.load_entry_from_cache:
209
+ if args.load_entry_from_cache=='Y':
210
+ param['load_entry_from_cache'] = True
211
+ else:
212
+ param['load_entry_from_cache'] = False
213
+ else:
214
+ param['load_entry_from_cache'] = False
215
+
216
+ param['tp_min_percent'] = float(args.tp_min_percent)
217
+ param['tp_max_percent'] = float(args.tp_max_percent)
218
+ param['sl_percent_trailing'] = float(args.sl_percent_trailing)
219
+ param['default_effective_tp_trailing_percent'] = float(args.default_effective_tp_trailing_percent)
220
+ param['sl_percent'] = float(args.sl_percent)
221
+ param['reversal_num_intervals'] = int(args.reversal_num_intervals)
222
+ param['trailing_sl_min_percent_linear'] = float(args.trailing_sl_min_percent_linear)
223
+ param['non_linear_pow'] = float(args.non_linear_pow)
224
+
225
+ param['loop_freq_ms'] = int(args.loop_freq_ms)
226
+
227
+ param['notification']['slack']['info']['webhook_url'] = args.slack_info_url
228
+ param['notification']['slack']['critical']['webhook_url'] = args.slack_critial_url
229
+ param['notification']['slack']['alert']['webhook_url'] = args.slack_alert_url
230
+
231
+ param['notification']['footer'] = f"From {param['current_filename']} {param['gateway_id']}"
232
+
233
+ def init_redis_client() -> StrictRedis:
234
+ redis_client : StrictRedis = StrictRedis(
235
+ host = param['mds']['redis']['host'],
236
+ port = param['mds']['redis']['port'],
237
+ db = 0,
238
+ ssl = False
239
+ )
240
+ try:
241
+ redis_client.keys()
242
+ except ConnectionError as redis_conn_error:
243
+ err_msg = f"Failed to connect to redis: {param['mds']['redis']['host']}, port: {param['mds']['redis']['port']}"
244
+ raise ConnectionError(err_msg)
245
+
246
+ return redis_client
247
+
248
+ async def main():
249
+ parse_args()
250
+ redis_client : StrictRedis = init_redis_client()
251
+
252
+ gateway_id : str = param['gateway_id']
253
+ ordergateway_pending_orders_topic = 'ordergateway_pending_orders_$GATEWAY_ID$'
254
+ ordergateway_pending_orders_topic = ordergateway_pending_orders_topic.replace("$GATEWAY_ID$", gateway_id)
255
+
256
+ ordergateway_executions_topic = "ordergateway_executions_$GATEWAY_ID$"
257
+ ordergateway_executions_topic = ordergateway_executions_topic.replace("$GATEWAY_ID$", gateway_id)
258
+
259
+ position_cacnle_file : str = f"tp_algo_position_cache_{gateway_id}.csv"
260
+
261
+ notification_params : Dict[str, Any] = param['notification']
262
+
263
+ if not param['apikey']:
264
+ log("Loading credentials from .env")
265
+
266
+ load_dotenv()
267
+
268
+ encrypt_decrypt_with_aws_kms = os.getenv('ENCRYPT_DECRYPT_WITH_AWS_KMS')
269
+ encrypt_decrypt_with_aws_kms = True if encrypt_decrypt_with_aws_kms=='Y' else False
270
+
271
+ api_key : str = str(os.getenv('APIKEY'))
272
+ secret : str = str(os.getenv('SECRET'))
273
+ passphrase : str = str(os.getenv('PASSPHRASE'))
274
+ else:
275
+ log("Loading credentials from command line args")
276
+
277
+ encrypt_decrypt_with_aws_kms = param['encrypt_decrypt_with_aws_kms']
278
+ api_key : str = param['apikey']
279
+ secret : str = param['secret']
280
+ passphrase : str = param['passphrase']
281
+
282
+ if encrypt_decrypt_with_aws_kms:
283
+ aws_kms_key_id = str(os.getenv('AWS_KMS_KEY_ID'))
284
+
285
+ aws_kms = AwsKmsUtil(key_id=aws_kms_key_id, profile_name=None)
286
+ api_key = aws_kms.decrypt(api_key.encode())
287
+ secret = aws_kms.decrypt(secret.encode())
288
+ if passphrase:
289
+ passphrase = aws_kms.decrypt(passphrase.encode())
290
+
291
+ exchange : Union[AnyExchange, None] = await async_instantiate_exchange(
292
+ gateway_id=param['gateway_id'],
293
+ api_key=api_key,
294
+ secret=secret,
295
+ passphrase=passphrase,
296
+ default_type=param['default_type'],
297
+ rate_limit_ms=param['rate_limit_ms']
298
+ )
299
+ if exchange:
300
+ markets = await exchange.load_markets() # type: ignore
301
+ market = markets[param['ticker']]
302
+ multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
303
+
304
+ balances = await exchange.fetch_balance() # type: ignore
305
+ log(f"Balances: {json.dumps(balances, indent=4)}") # type: ignore
306
+
307
+ if not param['load_entry_from_cache']:
308
+ # STEP 1. Make entry
309
+ entry_positions : List[DivisiblePosition] = [
310
+ DivisiblePosition(
311
+ ticker = param['ticker'],
312
+ side = param['side'],
313
+ amount = param['amount_base_ccy'],
314
+ leg_room_bps = param['leg_room_bps'],
315
+ order_type = param['order_type'],
316
+ slices = param['slices'],
317
+ wait_fill_threshold_ms = param['wait_fill_threshold_ms']
318
+ )
319
+ ]
320
+ executed_positions : Union[Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES], None] = execute_positions(
321
+ redis_client=redis_client,
322
+ positions=entry_positions,
323
+ ordergateway_pending_orders_topic=ordergateway_pending_orders_topic,
324
+ ordergateway_executions_topic=ordergateway_executions_topic
325
+ )
326
+ else:
327
+ with open(position_cacnle_file, 'r') as f:
328
+ executed_position = json.load(f)
329
+ executed_positions = [ executed_position ]
330
+
331
+ if executed_positions or param['load_entry_from_cache']:
332
+ executed_position = executed_positions[0] # We sent only one DivisiblePosition.
333
+ log(f"{'Entry dispatched.' if not param['load_entry_from_cache'] else 'Entry loaded from cache.'} {json.dumps(executed_position, indent=4)}") # type: ignore
334
+ if executed_position['done']:
335
+ def _reversal(
336
+ direction : str, # up or down
337
+ last_candles
338
+ ) -> bool:
339
+ if direction == "down" and all([ candle[1]<candle[4] for candle in last_candles ]): # All green?
340
+ return True
341
+ elif direction == "up" and all([ candle[1]>candle[4] for candle in last_candles ]): # All red?
342
+ return True
343
+ else:
344
+ return False
345
+
346
+ entry_price = executed_position['average_cost']
347
+ amount_filled_usdt = entry_price * abs(executed_position['filled_amount'])
348
+ if param['side'] == 'buy':
349
+ tp_min_price = entry_price * (1+param['tp_min_percent']/100)
350
+ tp_max_price = entry_price * (1+param['tp_max_percent']/100)
351
+ sl_price = entry_price * (1-param['sl_percent']/100)
352
+ else:
353
+ tp_min_price = entry_price * (1-param['tp_min_percent']/100)
354
+ tp_max_price = entry_price * (1-param['tp_max_percent']/100)
355
+ sl_price = entry_price * (1+param['sl_percent']/100)
356
+
357
+ if not param['load_entry_from_cache']:
358
+ executed_position['position'] = {
359
+ 'status' : 'open',
360
+ 'max_unrealized_pnl' : 0,
361
+ 'entry_price' : entry_price,
362
+ 'amount_base_ccy' : executed_position['filled_amount'],
363
+ 'tp_min_price' : tp_min_price,
364
+ 'tp_max_price' : tp_max_price,
365
+ 'sl_price' : sl_price,
366
+ 'multiplier' : multiplier
367
+ }
368
+ with open(position_cacnle_file, 'w') as f:
369
+ json.dump(executed_position, f)
370
+
371
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Execution done, Entry succeeded. {param['ticker']} {param['side']} {param['amount_base_ccy']} (USD amount: {amount_filled_usdt}) @ {entry_price}", message=executed_position['position'], footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
372
+
373
+ unrealized_pnl : float = 0
374
+ max_unrealized_pnl : float = 0
375
+ unrealized_pnl_percent : float = 0
376
+ max_unrealized_pnl_percent : float = 0
377
+ loss_trailing : float = 0
378
+ effective_tp_trailing_percent : float = param['default_effective_tp_trailing_percent']
379
+ reversal : bool = False
380
+ tp : bool = False
381
+ sl : bool = False
382
+ position_break : bool = False
383
+ while (not tp and not sl and not position_break):
384
+ try:
385
+ position_from_exchange = await exchange.fetch_position(param['ticker'])
386
+
387
+ if exchange.options['defaultType']!='spot':
388
+ if not position_from_exchange and param['load_entry_from_cache']:
389
+ position_break = True
390
+
391
+ err_msg = f"{param['ticker']}: Position break! expected: {executed_position['position']['amount_base_ccy']}, actual: 0"
392
+ log(err_msg)
393
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Position break! {param['ticker']}", message=err_msg, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
394
+
395
+ if position_from_exchange:
396
+ position_from_exchange_num_contracts = position_from_exchange['contracts']
397
+ if position_from_exchange and position_from_exchange['side']=='short':
398
+ position_from_exchange_num_contracts = position_from_exchange_num_contracts *-1 if position_from_exchange_num_contracts>0 else position_from_exchange_num_contracts
399
+
400
+ position_from_exchange_base_ccy = position_from_exchange_num_contracts * multiplier
401
+
402
+ if position_from_exchange_base_ccy!=executed_position['position']['amount_base_ccy']:
403
+ position_break = True
404
+
405
+ err_msg = f"{param['ticker']}: Position break! expected: {executed_position['position']['amount_base_ccy']}, actual: {position_from_exchange_base_ccy}"
406
+ log(err_msg)
407
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Position break! {param['ticker']}", message=err_msg, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
408
+
409
+ if position_break:
410
+ log(f"Position break! Exiting execution. Did you manually close the trade?")
411
+ break
412
+
413
+ else:
414
+ trailing_candles = await exchange.fetch_ohlcv(symbol=param['ticker'], timeframe='1h', limit=24) # type: ignore
415
+ trailing_candles = trailing_candles[-param['reversal_num_intervals']:]
416
+
417
+ reversal = _reversal(
418
+ direction='up' if param['side']=='buy' else 'down',
419
+ last_candles=trailing_candles
420
+ )
421
+
422
+ ob = await exchange.fetch_order_book(symbol=param['ticker'], limit=10)
423
+ best_ask = min([x[0] for x in ob['asks']])
424
+ best_bid = max([x[0] for x in ob['bids']])
425
+ mid = (best_ask+best_bid)/2
426
+ spread_bps = (best_ask/best_bid - 1) * 10000
427
+
428
+ if param['side']=='buy':
429
+ unrealized_pnl = (mid - entry_price) * param['amount_base_ccy']
430
+ else:
431
+ unrealized_pnl = (entry_price - mid) * param['amount_base_ccy']
432
+ unrealized_pnl_percent = unrealized_pnl / amount_filled_usdt * 100
433
+
434
+ if unrealized_pnl>max_unrealized_pnl:
435
+ max_unrealized_pnl = unrealized_pnl
436
+ max_unrealized_pnl_percent = max_unrealized_pnl / amount_filled_usdt * 100
437
+ executed_position['position']['max_unrealized_pnl'] = max_unrealized_pnl
438
+
439
+ with open(position_cacnle_file, 'w') as f:
440
+ json.dump(executed_position, f)
441
+
442
+ if (
443
+ (unrealized_pnl_percent>=param['tp_min_percent'] and unrealized_pnl<max_unrealized_pnl)
444
+ or loss_trailing>0 # once your trade pnl crosses tp_min_percent, trailing stops is (and will remain) active.
445
+ ):
446
+ loss_trailing = (1 - unrealized_pnl/max_unrealized_pnl) * 100
447
+
448
+ '''
449
+ Have a look at this for a visual explaination how "Gradually tightened stops" works:
450
+ https://github.com/r0bbar/siglab/blob/master/siglab_py/tests/manual/trading_util_tests.ipynb
451
+ '''
452
+ _effective_tp_trailing_percent = calc_eff_trailing_sl(
453
+ tp_min_percent = param['tp_min_percent'],
454
+ tp_max_percent = param['tp_max_percent'],
455
+ sl_percent_trailing = param['sl_percent_trailing'],
456
+ pnl_percent_notional = max_unrealized_pnl_percent, # Note: Use [max]_unrealized_pnl_percent, not unrealized_pnl_percent!
457
+ default_effective_tp_trailing_percent = param['default_effective_tp_trailing_percent'],
458
+ linear=True if param['tp_max_percent'] >= param['trailing_sl_min_percent_linear'] else False, # If tp_max_percent far (>100bps for example), there's more uncertainty if target can be reached: Go with linear.
459
+ pow=param['non_linear_pow']
460
+ )
461
+
462
+ # Once pnl pass tp_min_percent, trailing stops will be activated. Even if pnl fall back below tp_min_percent.
463
+ effective_tp_trailing_percent = min(effective_tp_trailing_percent, _effective_tp_trailing_percent)
464
+
465
+ log(f"unrealized_pnl: {round(unrealized_pnl,4)}, unrealized_pnl_percent: {round(unrealized_pnl_percent,4)}, max_unrealized_pnl_percent: {round(max_unrealized_pnl_percent,4)}, loss_trailing: {loss_trailing}, effective_tp_trailing_percent: {effective_tp_trailing_percent}, reversal: {reversal}")
466
+
467
+ # STEP 2. Exit
468
+ if unrealized_pnl>0:
469
+ if reversal and unrealized_pnl_percent >= param['tp_min_percent']:
470
+ tp = True
471
+ elif loss_trailing>=effective_tp_trailing_percent:
472
+ tp = True
473
+ else:
474
+ if abs(unrealized_pnl_percent)>=param['sl_percent']:
475
+ sl = True
476
+
477
+ if tp or sl:
478
+ exit_positions : List[DivisiblePosition] = [
479
+ DivisiblePosition(
480
+ ticker = param['ticker'],
481
+ side = 'sell' if param['side']=='buy' else 'buy',
482
+ amount = param['amount_base_ccy'],
483
+ leg_room_bps = param['leg_room_bps'],
484
+ order_type = param['order_type'],
485
+ slices = param['slices'],
486
+ wait_fill_threshold_ms = param['wait_fill_threshold_ms'],
487
+
488
+ reduce_only=True
489
+ )
490
+ ]
491
+ executed_positions : Union[Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES], None] = execute_positions(
492
+ redis_client=redis_client,
493
+ positions=exit_positions,
494
+ ordergateway_pending_orders_topic=ordergateway_pending_orders_topic,
495
+ ordergateway_executions_topic=ordergateway_executions_topic
496
+ )
497
+ if executed_positions:
498
+ executed_position_close = executed_positions[0] # We sent only one DivisiblePosition.
499
+ log(f"Position closing. {json.dumps(executed_position, indent=4)}") # type: ignore
500
+ if executed_position_close['done']:
501
+ executed_position_close['position'] = executed_position['position']
502
+
503
+ if param['side']=='buy':
504
+ closed_pnl = (executed_position_close['average_cost'] - entry_price) * param['amount_base_ccy']
505
+ else:
506
+ closed_pnl = (entry_price - executed_position_close['average_cost']) * param['amount_base_ccy']
507
+ executed_position_close['position']['max_unrealized_pnl'] = max_unrealized_pnl
508
+ executed_position_close['position']['closed_pnl'] = closed_pnl
509
+ executed_position_close['position']['status'] = 'closed'
510
+
511
+ with open(position_cacnle_file, 'w') as f:
512
+ json.dump(executed_position_close, f)
513
+
514
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Execution done, {'TP' if tp else 'SL'} succeeded. closed_pnl: {closed_pnl}", message=executed_position_close, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
515
+
516
+ else:
517
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Exit execution failed. {param['ticker']}", message=executed_position_close, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
518
+
519
+ await exchange.close()
520
+
521
+ except Exception as loop_err:
522
+ log(f"Error: {loop_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}", log_level=LogLevel.ERROR)
523
+ finally:
524
+ time.sleep(int(param['loop_freq_ms']/1000))
525
+
526
+ else:
527
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} Entry execution failed. {param['ticker']} {param['side']} {param['amount_base_ccy']}", message=executed_position, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR, logger=logger)
528
+
529
+ asyncio.run(main())
File without changes