siglab-py 0.2.9__py3-none-any.whl → 0.3.1__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.

@@ -116,7 +116,7 @@ To debug from vscode, launch.json:
116
116
  "order_type": "limit",
117
117
  "leg_room_bps": 5,
118
118
  "slices": 5,
119
- "wait_fill_threshold_ms": 15000,
119
+ "wait_fill_threshold_ms": 5000,
120
120
  "executions": {},
121
121
  "filled_amount": 0,
122
122
  "average_cost": 0
@@ -134,7 +134,7 @@ To debug from vscode, launch.json:
134
134
  "order_type": "limit",
135
135
  "leg_room_bps": 5,
136
136
  "slices": 5,
137
- "wait_fill_threshold_ms": 15000,
137
+ "wait_fill_threshold_ms": 5000,
138
138
  "executions": {
139
139
  "xxx": { <-- order id from exchange
140
140
  "info": { <-- ccxt convention, raw response from exchanges under info tag
@@ -201,6 +201,7 @@ param : Dict = {
201
201
  "default_fees_ccy" : None,
202
202
  "loop_freq_ms" : 500, # reduce this if you need trade faster
203
203
  "loops_random_delay_multiplier" : 1, # Add randomness to time between slices are sent off. Set to 1 if no random delay needed.
204
+ "wait_fill_threshold_ms" : 5000,
204
205
 
205
206
  'current_filename' : current_filename,
206
207
 
@@ -288,6 +289,7 @@ def parse_args():
288
289
 
289
290
  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)
290
291
  parser.add_argument("--loop_freq_ms", help="Loop delays. Reduce this if you want to trade faster.", default=500)
292
+ parser.add_argument("--wait_fill_threshold_ms", help="Wait for fills for how long?", default=5000)
291
293
 
292
294
  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')
293
295
  parser.add_argument("--aws_kms_key_id", help="AWS KMS key ID", default=None)
@@ -314,6 +316,7 @@ def parse_args():
314
316
  param['rate_limit_ms'] = int(args.rate_limit_ms)
315
317
  param['default_fees_ccy'] = args.default_fees_ccy
316
318
  param['loop_freq_ms'] = int(args.loop_freq_ms)
319
+ param['wait_fill_threshold_ms'] = int(args.wait_fill_threshold_ms)
317
320
 
318
321
  if args.encrypt_decrypt_with_aws_kms:
319
322
  if args.encrypt_decrypt_with_aws_kms=='Y':
@@ -460,7 +463,7 @@ async def watch_orders_task(
460
463
  except Exception as loop_err:
461
464
  print(f"watch_orders_task error: {loop_err}")
462
465
 
463
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
466
+ await asyncio.sleep(param['loop_freq_ms']/1000)
464
467
 
465
468
  async def send_heartbeat(exchange):
466
469
 
@@ -494,10 +497,26 @@ async def execute_one_position(
494
497
  multiplier = market['contractSize'] if 'contractSize' in market and market['contractSize'] else 1
495
498
  position.multiplier = multiplier
496
499
 
500
+ log(f"{position.ticker} min_amount: {min_amount}, multiplier: {multiplier}")
501
+
497
502
  slices : List[Order] = position.to_slices()
503
+
504
+ # Residual handling in last slice
505
+ if len(slices)>1:
506
+ last_slice = slices[-1]
507
+ 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).
508
+ 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
509
+ if last_slice_rounded_amount_in_base_ccy<=min_amount:
510
+ slices.pop()
511
+ slices[-1].amount += last_slice.amount
512
+
513
+ 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}")
514
+
498
515
  i = 0
499
516
  for slice in slices:
500
517
  try:
518
+ log(f"{position.ticker} sending slice# {i}")
519
+
501
520
  dt_now : datetime = datetime.now()
502
521
 
503
522
  slice_amount_in_base_ccy : float = slice.amount
@@ -625,9 +644,9 @@ async def execute_one_position(
625
644
  log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
626
645
 
627
646
  if not order_status or order_status!='closed':
628
- start_time = time.time()
629
647
  wait_threshold_sec = position.wait_fill_threshold_ms / 1000
630
-
648
+
649
+ start_time = time.time()
631
650
  elapsed_sec = time.time() - start_time
632
651
  while elapsed_sec < wait_threshold_sec:
633
652
  order_update = None
@@ -646,8 +665,11 @@ async def execute_one_position(
646
665
  break
647
666
 
648
667
  loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
649
- loop_freq_sec : int = int(param['loop_freq_ms']/1000)
668
+ loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
650
669
  await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
670
+
671
+ elapsed_sec = time.time() - start_time
672
+ log(f"{position.ticker} waiting for order update ... elapsed_sec: {elapsed_sec}")
651
673
 
652
674
 
653
675
  # Cancel hung limit order, resend as market
@@ -659,15 +681,10 @@ async def execute_one_position(
659
681
  remaining_amount = order_update['remaining']
660
682
  order_update['multiplier'] = multiplier
661
683
 
662
- log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
663
-
664
684
  position.append_execution(order_id, order_update)
665
685
 
666
- if order_status!='closed':
667
- order_status = order_update['status']
668
- filled_amount = order_update['filled']
669
- remaining_amount = order_update['remaining']
670
-
686
+ if order_status!='closed':
687
+ log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
671
688
  await exchange.cancel_order(order_id, position.ticker) # type: ignore
672
689
  position.get_execution(order_id)['status'] = 'canceled'
673
690
  log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
@@ -688,7 +705,11 @@ async def execute_one_position(
688
705
  executed_resent_order['multiplier'] = multiplier
689
706
  position.append_execution(order_id, executed_resent_order)
690
707
 
691
- while not order_status or order_status!='closed':
708
+ wait_threshold_sec = position.wait_fill_threshold_ms / 1000
709
+
710
+ start_time = time.time()
711
+ elapsed_sec = time.time() - start_time
712
+ while (not order_status or order_status!='closed') and (elapsed_sec < wait_threshold_sec):
692
713
  order_update = None
693
714
  if order_id in executions:
694
715
  order_update = executions[order_id]
@@ -699,11 +720,24 @@ async def execute_one_position(
699
720
  filled_amount = order_update['filled']
700
721
  remaining_amount = order_update['remaining']
701
722
 
702
- log(f"Waiting for resent market order to close {order_id} ...")
723
+ elapsed_sec = time.time() - start_time
724
+ log(f"Waiting for resent market order to close {order_id} ... elapsed_sec: {elapsed_sec}")
725
+
726
+ loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
727
+ loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
728
+ await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
703
729
 
704
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
730
+ if (not order_status or order_status!='closed'):
731
+ # If no update from websocket, do one last fetch via REST
732
+ order_update = await exchange.fetch_order(order_id, position.ticker) # type: ignore
733
+ order_status = order_update['status']
734
+ filled_amount = order_update['filled']
735
+ remaining_amount = order_update['remaining']
736
+ order_update['multiplier'] = multiplier
705
737
 
706
- log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
738
+ 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)}")
739
+ else:
740
+ log(f"{position.ticker} {order_id} status (From REST): {json.dumps(order_update, indent=4)}")
707
741
 
708
742
  slice.dispatched_price = rounded_limit_price
709
743
  slice.dispatched_amount = rounded_slice_amount_in_base_ccy
@@ -722,8 +756,10 @@ async def execute_one_position(
722
756
  )
723
757
  raise slice_err
724
758
  finally:
759
+ log(f"{position.ticker} done slice# {i}")
725
760
  i += 1
726
-
761
+
762
+ log(f"{position.ticker} patch_executions")
727
763
  position.patch_executions()
728
764
 
729
765
  log(f"Dispatched slices:")
@@ -749,13 +785,22 @@ async def execute_one_position(
749
785
  log(f"Executions:")
750
786
  log(f"{json.dumps(position.to_dict(), indent=4)}")
751
787
 
752
- dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position done. {position.ticker} {position.side} {position.amount}", message=position.get_executions(), footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL)
788
+ notification_summary = {
789
+ 'ticker' : position.ticker,
790
+ 'side' : position.side,
791
+ 'num_executions' : len(position.get_executions()),
792
+ 'filled_amount' : position.filled_amount,
793
+ 'average_cost' : position.average_cost,
794
+ 'pos' : position.pos,
795
+ 'done' : position.done
796
+ }
797
+ 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)
753
798
 
754
799
  except Exception as position_execution_err:
755
- err_msg = f"Execution failed: {position_execution_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
756
- log(f"Execution failed: {err_msg}")
800
+ err_msg = f"{position.ticker} Execution failed: {position_execution_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
801
+ log(err_msg)
757
802
 
758
- dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} execute_one_position failed!!! {position.ticker} {position.side} {position.amount}", message=position.get_executions(), footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.ERROR) # type: ignore
803
+ 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) # type: ignore
759
804
 
760
805
  position.done = False
761
806
  position.execution_err = err_msg
@@ -806,7 +851,7 @@ async def work(
806
851
  reduce_only=order['reduce_only'],
807
852
  fees_ccy=order['fees_ccy'] if 'fees_ccy' in order else param['default_fees_ccy'],
808
853
  slices=order['slices'],
809
- wait_fill_threshold_ms=order['wait_fill_threshold_ms']
854
+ wait_fill_threshold_ms=order['wait_fill_threshold_ms'] if order['wait_fill_threshold_ms']>0 else param['wait_fill_threshold_ms']
810
855
  )
811
856
  for order in orders
812
857
  ]
@@ -844,7 +889,7 @@ async def work(
844
889
  except Exception as loop_error:
845
890
  log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
846
891
  finally:
847
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
892
+ await asyncio.sleep(param['loop_freq_ms']/1000)
848
893
 
849
894
  async def main():
850
895
  parse_args()
@@ -898,7 +943,7 @@ async def main():
898
943
  # Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
899
944
  balances = await exchange.fetch_balance() # type: ignore
900
945
  log(f"{param['gateway_id']}: account balances {balances}")
901
- dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} started", message=balances, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL)
946
+ dispatch_notification(title=f"{param['current_filename']} {param['gateway_id']} started", message=balances, footer=param['notification']['footer'], params=notification_params, log_level=LogLevel.CRITICAL, logger=logger)
902
947
 
903
948
  await work(param=param, exchange=exchange, redis_client=redis_client, notification_params=notification_params)
904
949
 
@@ -100,7 +100,7 @@ class MarketDataUtilTests(unittest.TestCase):
100
100
  'defaultType': 'swap' }
101
101
  }
102
102
 
103
- exchange : Exchange = okx(param)
103
+ exchange : Exchange = okx(param) # type: ignore
104
104
  normalized_symbols = [ 'BTC/USDT:USDT' ]
105
105
  pd_candles: Union[pd.DataFrame, None] = fetch_candles(
106
106
  start_ts=start_date.timestamp(),
@@ -49,7 +49,9 @@ class AnalyticUtilTests(unittest.TestCase):
49
49
  'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr',
50
50
  'is_green', 'pct_change_close',
51
51
  'sma_short_periods', 'sma_long_periods', 'ema_short_periods', 'ema_long_periods', 'ema_close',
52
- 'std', 'std_percent', 'candle_height_percent', 'candle_height_percent_rounded',
52
+ 'std', 'std_percent',
53
+ 'candle_height_percent', 'candle_height_percent_rounded',
54
+ 'log_return', 'interval_hist_vol', 'annualized_hist_vol',
53
55
  'chop_against_ema',
54
56
  'ema_volume_short_periods', 'ema_volume_long_periods',
55
57
  'max_short_periods', 'max_long_periods', 'idmax_short_periods', 'idmax_long_periods', 'min_short_periods', 'min_long_periods', 'idmin_short_periods', 'idmin_long_periods',
@@ -0,0 +1,60 @@
1
+ import unittest
2
+ from datetime import datetime, timedelta
3
+ from typing import Union
4
+ from pathlib import Path
5
+
6
+ from util.trading_util import *
7
+
8
+
9
+ # @unittest.skip("Skip all integration tests.")
10
+ class TradingUtilTests(unittest.TestCase):
11
+ def test_timestamp_to_active_trading_regions_case1(self):
12
+ tp_min_percent : float = 1.5
13
+ tp_max_percent : float = 2.5
14
+ sl_percent_trailing : float = 50 # Trailing stop loss in percent
15
+ default_effective_tp_percent_trailing : float = 50
16
+
17
+ pnl_percent_notional : float = 0.5 # Trade's current pnl in percent.
18
+
19
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
20
+ tp_min_percent = tp_min_percent,
21
+ tp_max_percent = tp_max_percent,
22
+ sl_percent_trailing = sl_percent_trailing,
23
+ pnl_percent_notional = pnl_percent_notional,
24
+ default_effective_tp_percent_trailing = default_effective_tp_percent_trailing
25
+ )
26
+ assert(effective_tp_trailing_percent==50) # Generous trailing SL when trading starting out and pnl small.
27
+
28
+ def test_timestamp_to_active_trading_regions_case2(self):
29
+ tp_min_percent : float = 1.5
30
+ tp_max_percent : float = 2.5
31
+ sl_percent_trailing : float = 50 # Trailing stop loss in percent
32
+ default_effective_tp_percent_trailing : float = 50
33
+
34
+ pnl_percent_notional : float = 2 # Trade's current pnl in percent.
35
+
36
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
37
+ tp_min_percent = tp_min_percent,
38
+ tp_max_percent = tp_max_percent,
39
+ sl_percent_trailing = sl_percent_trailing,
40
+ pnl_percent_notional = pnl_percent_notional,
41
+ default_effective_tp_percent_trailing = default_effective_tp_percent_trailing
42
+ )
43
+ assert(effective_tp_trailing_percent==25) # Intermediate trailing SL
44
+
45
+ def test_timestamp_to_active_trading_regions_case3(self):
46
+ tp_min_percent : float = 1.5
47
+ tp_max_percent : float = 2.5
48
+ sl_percent_trailing : float = 50 # Trailing stop loss in percent
49
+ default_effective_tp_percent_trailing : float = 50
50
+
51
+ pnl_percent_notional : float = 2.5 # Trade's current pnl in percent.
52
+
53
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
54
+ tp_min_percent = tp_min_percent,
55
+ tp_max_percent = tp_max_percent,
56
+ sl_percent_trailing = sl_percent_trailing,
57
+ pnl_percent_notional = pnl_percent_notional,
58
+ default_effective_tp_percent_trailing = default_effective_tp_percent_trailing
59
+ )
60
+ assert(effective_tp_trailing_percent==0) # Most tight trailing SL
@@ -101,9 +101,32 @@ def compute_candles_stats(
101
101
  pd_candles['std'] = pd_candles['close'].rolling(window=sliding_window_how_many_candles).std()
102
102
 
103
103
  pd_candles['std_percent'] = pd_candles['std'] / pd_candles['ema_close'] * 100
104
+
104
105
  pd_candles['candle_height_percent'] = pd_candles['candle_height'] / pd_candles['ema_close'] * 100
105
106
  pd_candles['candle_height_percent_rounded'] = pd_candles['candle_height_percent'].round().astype('Int64')
106
107
 
108
+ '''
109
+ To annualize volatility:
110
+ if candle_interval == '1m':
111
+ annualization_factor = np.sqrt(365 * 24 * 60) # 1-minute candles
112
+ elif candle_interval == '1h':
113
+ annualization_factor = np.sqrt(365 * 24) # 1-hour candles
114
+ elif candle_interval == '1d':
115
+ annualization_factor = np.sqrt(365) # 1-day candles
116
+ pd_candles['annualized_volatility'] = (
117
+ pd_candles['interval_historical_volatility'] * annualization_factor
118
+ )
119
+ '''
120
+ pd_candles['log_return'] = np.log(pd_candles['close'] / pd_candles['close'].shift(1))
121
+ pd_candles['interval_hist_vol'] = pd_candles['log_return'].rolling(window=sliding_window_how_many_candles).std()
122
+
123
+ time_gap_sec = int(pd_candles['timestamp_ms'].iloc[1] - pd_candles['timestamp_ms'].iloc[0])/1000
124
+ seconds_in_year = 365 * 24 * 60 * 60
125
+ candles_per_year = seconds_in_year / time_gap_sec
126
+ annualization_factor = np.sqrt(candles_per_year)
127
+ pd_candles['annualized_hist_vol'] = pd_candles['interval_hist_vol'] * annualization_factor
128
+
129
+
107
130
  pd_candles['chop_against_ema'] = (
108
131
  (~pd_candles['is_green'] & (pd_candles['close'] > pd_candles['ema_close'])) | # Case 1: Green candle and close > EMA
109
132
  (pd_candles['is_green'] & (pd_candles['close'] < pd_candles['ema_close'])) # Case 2: Red candle and close < EMA
@@ -14,19 +14,24 @@ def dispatch_notification(
14
14
  message : Union[str, Dict, pd.DataFrame],
15
15
  footer : str,
16
16
  params : Dict[str, Any],
17
- log_level : LogLevel = LogLevel.INFO
17
+ log_level : LogLevel = LogLevel.INFO,
18
+ logger = None
18
19
  ):
19
- if isinstance(message, Dict):
20
- _message = json.dumps(message, indent=2, separators=(' ', ':'))
21
- elif isinstance(message, pd.DataFrame):
22
- _message = tabulate(message, headers='keys', tablefmt='orgtbl') # type: ignore
23
- else:
24
- _message = message
20
+ try:
21
+ if isinstance(message, Dict):
22
+ _message = json.dumps(message, indent=2, separators=(' ', ':'))
23
+ elif isinstance(message, pd.DataFrame):
24
+ _message = tabulate(message, headers='keys', tablefmt='orgtbl') # type: ignore
25
+ else:
26
+ _message = message
25
27
 
26
- utc_time = datetime.now(timezone.utc)
27
- footer = f"UTC {utc_time} {footer}"
28
+ utc_time = datetime.now(timezone.utc)
29
+ footer = f"UTC {utc_time} {footer}"
28
30
 
29
- slack_dispatch_notification(title, _message, footer, params, log_level)
31
+ slack_dispatch_notification(title, _message, footer, params, log_level)
32
+ except Exception as any_notification_error:
33
+ if logger:
34
+ logger.info(f"Failed to dispatch notification: {any_notification_error}")
30
35
 
31
36
  if __name__ == '__main__':
32
37
  params : Dict[str, Any] = {
@@ -13,10 +13,14 @@ def slack_dispatch_notification(
13
13
  message : str,
14
14
  footer : str,
15
15
  params : Dict[str, Any],
16
- log_level : LogLevel = LogLevel.INFO
16
+ log_level : LogLevel = LogLevel.INFO,
17
+ max_message_len : int = 1800
17
18
  ):
18
19
  slack_params = params['slack']
19
20
 
21
+ # Slack slack ... https://stackoverflow.com/questions/60344831/slack-api-invalid-block
22
+ message = message[:max_message_len]
23
+
20
24
  if log_level.value==LogLevel.INFO.value or log_level.value==LogLevel.DEBUG.value:
21
25
  webhook_url = slack_params['info']['webhook_url']
22
26
  elif log_level.value==LogLevel.CRITICAL.value:
@@ -0,0 +1,66 @@
1
+
2
+ '''
3
+ pnl_percent_notional = Trade's current pnl in percent.
4
+
5
+ Examples,
6
+ y-axis:
7
+ max (i.e most tight) = 0%
8
+ sl_percent_trailing = 50% (Trailing stop loss in percent)
9
+
10
+ x-axis:
11
+ min TP = 1.5% <-- min TP
12
+ max TP = 2.5% <-- max TP
13
+
14
+ slope = (0-50)/(2.5-1.5) = -50
15
+ effective_tp_trailing_percent = slope * (pnl_percent_notional - 1.5%) + sl_percent_trailing
16
+
17
+ Case 1. pnl_percent_notional = 0.5% (Trade starting off, only +50bps pnl. i.e. min TP)
18
+ effective_tp_trailing_percent = slope * (pnl_percent_notional - 0.5%) + sl_percent_trailing
19
+ = -50 * (1.5-1.5) + 50%
20
+ = 0 + 50
21
+ = 50% (Most loose)
22
+
23
+ Case 2. pnl_percent_notional = 2% (Deeper into profit, +200bps pnl)
24
+ effective_tp_trailing_percent = slope * (pnl_percent_notional - 1.5%) + sl_percent_trailing
25
+ = -50 * (2-1.5) +50%
26
+ = -25 + 50
27
+ = 25% (Somewhat tight)
28
+
29
+ Case 3. pnl_percent_notional = 2.5% (Very deep in profit, +250bps pnl. i.e. max TP)
30
+ effective_tp_trailing_percent = slope * (pnl_percent_notional - 1.5%) + sl_percent_trailing
31
+ = -50 * (2.5-1.5) +50%
32
+ = -50 + 50
33
+ = 0 (Most tight)
34
+
35
+ So you see, effective_tp_trailing_percent gets smaller and smaller as pnl approach max TP, finally zero.
36
+
37
+ How to use it?
38
+ if loss_trailing>=effective_tp_trailing_percent and pnl_percent_notional > tp_min_percent:
39
+ Fire trailing stops and take profit.
40
+
41
+ What's 'loss_trailing'? 'loss_trailing' is essentially pnl drop from max_unrealized_pnl_live.
42
+
43
+ Say, when trade started off:
44
+ unrealized_pnl_live = $80
45
+ max_unrealized_pnl_live = $100
46
+ loss_trailing = (1 - unrealized_pnl_live/max_unrealized_pnl_live) = (1-80/100) = 0.2 (Or 20%)
47
+
48
+ If pnl worsen:
49
+ unrealized_pnl_live = $40
50
+ max_unrealized_pnl_live = $100
51
+ loss_trailing = (1 - unrealized_pnl_live/max_unrealized_pnl_live) = (1-40/100) = 0.6 (Or 60%)
52
+ '''
53
+ def calc_eff_trailing_sl(
54
+ tp_min_percent : float,
55
+ tp_max_percent : float,
56
+ sl_percent_trailing : float,
57
+ pnl_percent_notional : float,
58
+ default_effective_tp_trailing_percent : float = 50
59
+ ) -> float:
60
+ slope = (0 - sl_percent_trailing) / (tp_max_percent - tp_min_percent)
61
+ effective_tp_trailing_percent = (
62
+ slope * (pnl_percent_notional - tp_min_percent) + sl_percent_trailing
63
+ if pnl_percent_notional>tp_min_percent
64
+ else default_effective_tp_trailing_percent
65
+ )
66
+ return effective_tp_trailing_percent
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: siglab-py
3
- Version: 0.2.9
3
+ Version: 0.3.1
4
4
  Summary: Market data fetches, TA calculations and generic order gateway.
5
5
  Author: r0bbarh00d
6
6
  Author-email: r0bbarh00d <r0bbarh00d@gmail.com>
@@ -15,22 +15,24 @@ siglab_py/market_data_providers/test_provider.py,sha256=wBLCgcWjs7FGZJXWsNyn30lk
15
15
  siglab_py/ordergateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  siglab_py/ordergateway/client.py,sha256=EwoVKxEcngIs8-b4MThPBdZfFIWJg1OFAKG9bwC5BYw,14826
17
17
  siglab_py/ordergateway/encrypt_keys_util.py,sha256=-qi87db8To8Yf1WS1Q_Cp2Ya7ZqgWlRqSHfNXCM7wE4,1339
18
- siglab_py/ordergateway/gateway.py,sha256=nY_frBogt13pt54Yia0wMM2WOgJzCP6GpY8FFu3JlLI,42712
18
+ siglab_py/ordergateway/gateway.py,sha256=KQ5mFT458YQcAuxcQv_NK0iaz1LkG8ykV2-lZ61OerI,45891
19
19
  siglab_py/ordergateway/test_ordergateway.py,sha256=4PE2flp_soGcD3DrI7zJOzZndjkb6I5XaDrFNNq4Huo,4009
20
20
  siglab_py/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  siglab_py/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- siglab_py/tests/integration/market_data_util_tests.py,sha256=X0CiSMDfsafKcmjVKknA03vUUbMV0fAZweb3D01ikYI,7174
22
+ siglab_py/tests/integration/market_data_util_tests.py,sha256=p-RWIJZLyj0lAdfi4QTIeAttCm_e8mEVWFKh4OWuogU,7189
23
23
  siglab_py/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- siglab_py/tests/unit/analytic_util_tests.py,sha256=BzT__hxfqXMRAKvqtYDVYNrcMGGDF3-gFoXhxiJ0Lew,3703
24
+ siglab_py/tests/unit/analytic_util_tests.py,sha256=68FTWDrfXTAyFLte6wiRfwcFVJItU5F47_DebgF6hAc,3788
25
25
  siglab_py/tests/unit/market_data_util_tests.py,sha256=A1y83itISmMJdn6wLpfwcr4tGola8wTf1D1xbelMvgw,2026
26
+ siglab_py/tests/unit/trading_util_tests.py,sha256=VywV-wvRBDoN7ZE6-SgUzYyyjnd6wv0HgQD2RWpkkZQ,2689
26
27
  siglab_py/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- siglab_py/util/analytic_util.py,sha256=o9MNuOWUhw-r0id10Wjjd7a6rkL6-g3OmvajMSj1JJ4,43838
28
+ siglab_py/util/analytic_util.py,sha256=TunVYV4DhedRFk0dYhKdm4zh5ztGcORH8-MDBV2LLZg,44944
28
29
  siglab_py/util/aws_util.py,sha256=KGmjHrr1rpnnxr33nXHNzTul4tvyyxl9p6gpwNv0Ygc,2557
29
30
  siglab_py/util/market_data_util.py,sha256=9Uze8DE5z90H4Qm15R55ZllAi5trUkwCAW-BWYbfaW8,19420
30
- siglab_py/util/notification_util.py,sha256=TkFqlyGZQL4ydF3YbPk1RJ6q59P1E6EKlZNwKgzDn1k,2432
31
+ siglab_py/util/notification_util.py,sha256=vySgHjpHgwFDLW0tHSi_AGh9JBbPc25IUgvWxmjAeT8,2658
31
32
  siglab_py/util/retry_util.py,sha256=mxYuRFZRZoaQQjENcwPmxhxixtd1TFvbxIdPx4RwfRc,743
32
- siglab_py/util/slack_notification_util.py,sha256=BGnHmP2WD7syOWD3e8uJN8prifLRGffD4ViyvgET2xQ,1738
33
- siglab_py-0.2.9.dist-info/METADATA,sha256=PH5PspQhzJHgETVi5RVcmezjvXbWxdfbP8N3l8r-vG4,979
34
- siglab_py-0.2.9.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
35
- siglab_py-0.2.9.dist-info/top_level.txt,sha256=AbD4VR9OqmMOGlMJLkAVPGQMtUPIQv0t1BF5xmcLJSk,10
36
- siglab_py-0.2.9.dist-info/RECORD,,
33
+ siglab_py/util/slack_notification_util.py,sha256=G27n-adbT3Q6oaHSMvu_Nom794rrda5PprSF-zvmzkM,1912
34
+ siglab_py/util/trading_util.py,sha256=djE3_q6hAyFDuLMVsUmgbHZLzFxrlZHVqDcYRvM7n3c,3038
35
+ siglab_py-0.3.1.dist-info/METADATA,sha256=5PVfvOBOGHHHAU_9IKXJ8_D5szXcYx87Y9dxw1xArrY,979
36
+ siglab_py-0.3.1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
37
+ siglab_py-0.3.1.dist-info/top_level.txt,sha256=AbD4VR9OqmMOGlMJLkAVPGQMtUPIQv0t1BF5xmcLJSk,10
38
+ siglab_py-0.3.1.dist-info/RECORD,,