siglab-py 0.2.9__tar.gz → 0.3.0__tar.gz

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.

Files changed (42) hide show
  1. {siglab_py-0.2.9 → siglab_py-0.3.0}/PKG-INFO +1 -1
  2. {siglab_py-0.2.9 → siglab_py-0.3.0}/pyproject.toml +1 -1
  3. {siglab_py-0.2.9 → siglab_py-0.3.0}/setup.cfg +1 -1
  4. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/ordergateway/gateway.py +67 -23
  5. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/tests/integration/market_data_util_tests.py +1 -1
  6. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/tests/unit/analytic_util_tests.py +3 -1
  7. siglab_py-0.3.0/siglab_py/tests/unit/trading_util_tests.py +60 -0
  8. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/analytic_util.py +16 -0
  9. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/notification_util.py +15 -10
  10. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/slack_notification_util.py +5 -1
  11. siglab_py-0.3.0/siglab_py/util/trading_util.py +66 -0
  12. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py.egg-info/PKG-INFO +1 -1
  13. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py.egg-info/SOURCES.txt +3 -1
  14. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/__init__.py +0 -0
  15. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/constants.py +0 -0
  16. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/exchanges/__init__.py +0 -0
  17. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/exchanges/any_exchange.py +0 -0
  18. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/exchanges/futubull.py +0 -0
  19. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/__init__.py +0 -0
  20. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/aggregated_orderbook_provider.py +0 -0
  21. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/candles_provider.py +0 -0
  22. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/candles_ta_provider.py +0 -0
  23. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +0 -0
  24. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/deribit_options_expiry_provider.py +0 -0
  25. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/futu_candles_ta_to_csv.py +0 -0
  26. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/orderbooks_provider.py +0 -0
  27. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/market_data_providers/test_provider.py +0 -0
  28. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/ordergateway/__init__.py +0 -0
  29. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/ordergateway/client.py +0 -0
  30. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/ordergateway/encrypt_keys_util.py +0 -0
  31. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/ordergateway/test_ordergateway.py +0 -0
  32. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/tests/__init__.py +0 -0
  33. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/tests/integration/__init__.py +0 -0
  34. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/tests/unit/__init__.py +0 -0
  35. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/tests/unit/market_data_util_tests.py +0 -0
  36. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/__init__.py +0 -0
  37. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/aws_util.py +0 -0
  38. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/market_data_util.py +0 -0
  39. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py/util/retry_util.py +0 -0
  40. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py.egg-info/dependency_links.txt +0 -0
  41. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py.egg-info/requires.txt +0 -0
  42. {siglab_py-0.2.9 → siglab_py-0.3.0}/siglab_py.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: siglab_py
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: Market data fetches, TA calculations and generic order gateway.
5
5
  Author: r0bbarh00d
6
6
  Author-email: r0bbarh00d <r0bbarh00d@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "siglab_py"
7
- version = "0.2.9"
7
+ version = "0.3.0"
8
8
  description = "Market data fetches, TA calculations and generic order gateway."
9
9
  authors = [{name = "r0bbarh00d", email = "r0bbarh00d@gmail.com"}]
10
10
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = siglab_py
3
- version = 0.2.9
3
+ version = 0.3.0
4
4
  description = Market data fetches, TA calculations and generic order gateway.
5
5
  author = r0bbarh00d
6
6
  author_email = r0bbarh00d@gmail.com
@@ -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,25 @@ 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
+ last_slice = slices[-1]
506
+ 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).
507
+ 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
508
+ if last_slice_rounded_amount_in_base_ccy<=min_amount:
509
+ slices.pop()
510
+ slices[-1].amount += last_slice.amount
511
+
512
+ 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}")
513
+
498
514
  i = 0
499
515
  for slice in slices:
500
516
  try:
517
+ log(f"{position.ticker} sending slice# {i}")
518
+
501
519
  dt_now : datetime = datetime.now()
502
520
 
503
521
  slice_amount_in_base_ccy : float = slice.amount
@@ -625,9 +643,9 @@ async def execute_one_position(
625
643
  log(f"Order dispatched: {order_id}. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
626
644
 
627
645
  if not order_status or order_status!='closed':
628
- start_time = time.time()
629
646
  wait_threshold_sec = position.wait_fill_threshold_ms / 1000
630
-
647
+
648
+ start_time = time.time()
631
649
  elapsed_sec = time.time() - start_time
632
650
  while elapsed_sec < wait_threshold_sec:
633
651
  order_update = None
@@ -646,8 +664,11 @@ async def execute_one_position(
646
664
  break
647
665
 
648
666
  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)
667
+ loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
650
668
  await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
669
+
670
+ elapsed_sec = time.time() - start_time
671
+ log(f"{position.ticker} waiting for order update ... elapsed_sec: {elapsed_sec}")
651
672
 
652
673
 
653
674
  # Cancel hung limit order, resend as market
@@ -659,15 +680,10 @@ async def execute_one_position(
659
680
  remaining_amount = order_update['remaining']
660
681
  order_update['multiplier'] = multiplier
661
682
 
662
- log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
663
-
664
683
  position.append_execution(order_id, order_update)
665
684
 
666
- if order_status!='closed':
667
- order_status = order_update['status']
668
- filled_amount = order_update['filled']
669
- remaining_amount = order_update['remaining']
670
-
685
+ if order_status!='closed':
686
+ log(f"Final order_update before cancel+resend: {json.dumps(order_update, indent=4)}", log_level=LogLevel.INFO)
671
687
  await exchange.cancel_order(order_id, position.ticker) # type: ignore
672
688
  position.get_execution(order_id)['status'] = 'canceled'
673
689
  log(f"Canceled unfilled/partial filled order: {order_id}. Resending remaining_amount: {remaining_amount} as market order.", log_level=LogLevel.INFO)
@@ -688,7 +704,11 @@ async def execute_one_position(
688
704
  executed_resent_order['multiplier'] = multiplier
689
705
  position.append_execution(order_id, executed_resent_order)
690
706
 
691
- while not order_status or order_status!='closed':
707
+ wait_threshold_sec = position.wait_fill_threshold_ms / 1000
708
+
709
+ start_time = time.time()
710
+ elapsed_sec = time.time() - start_time
711
+ while (not order_status or order_status!='closed') and (elapsed_sec < wait_threshold_sec):
692
712
  order_update = None
693
713
  if order_id in executions:
694
714
  order_update = executions[order_id]
@@ -699,11 +719,24 @@ async def execute_one_position(
699
719
  filled_amount = order_update['filled']
700
720
  remaining_amount = order_update['remaining']
701
721
 
702
- log(f"Waiting for resent market order to close {order_id} ...")
722
+ elapsed_sec = time.time() - start_time
723
+ log(f"Waiting for resent market order to close {order_id} ... elapsed_sec: {elapsed_sec}")
724
+
725
+ loops_random_delay_multiplier : int = random.randint(1, param['loops_random_delay_multiplier']) if param['loops_random_delay_multiplier']!=1 else 1
726
+ loop_freq_sec : int = max(1, param['loop_freq_ms']/1000)
727
+ await asyncio.sleep(loop_freq_sec * loops_random_delay_multiplier)
703
728
 
704
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
729
+ if (not order_status or order_status!='closed'):
730
+ # If no update from websocket, do one last fetch via REST
731
+ order_update = await exchange.fetch_order(order_id, position.ticker) # type: ignore
732
+ order_status = order_update['status']
733
+ filled_amount = order_update['filled']
734
+ remaining_amount = order_update['remaining']
735
+ order_update['multiplier'] = multiplier
705
736
 
706
- log(f"Resent market order{order_id} filled. status: {order_status}, filled_amount: {filled_amount}, remaining_amount: {remaining_amount}")
737
+ 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)}")
738
+ else:
739
+ log(f"{position.ticker} {order_id} status (From REST): {json.dumps(order_update, indent=4)}")
707
740
 
708
741
  slice.dispatched_price = rounded_limit_price
709
742
  slice.dispatched_amount = rounded_slice_amount_in_base_ccy
@@ -722,8 +755,10 @@ async def execute_one_position(
722
755
  )
723
756
  raise slice_err
724
757
  finally:
758
+ log(f"{position.ticker} done slice# {i}")
725
759
  i += 1
726
-
760
+
761
+ log(f"{position.ticker} patch_executions")
727
762
  position.patch_executions()
728
763
 
729
764
  log(f"Dispatched slices:")
@@ -749,13 +784,22 @@ async def execute_one_position(
749
784
  log(f"Executions:")
750
785
  log(f"{json.dumps(position.to_dict(), indent=4)}")
751
786
 
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)
787
+ notification_summary = {
788
+ 'ticker' : position.ticker,
789
+ 'side' : position.side,
790
+ 'num_executions' : len(position.get_executions()),
791
+ 'filled_amount' : position.filled_amount,
792
+ 'average_cost' : position.average_cost,
793
+ 'pos' : position.pos,
794
+ 'done' : position.done
795
+ }
796
+ 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
797
 
754
798
  except Exception as position_execution_err:
755
799
  err_msg = f"Execution failed: {position_execution_err} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}"
756
800
  log(f"Execution failed: {err_msg}")
757
801
 
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
802
+ 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, logger=logger) # type: ignore
759
803
 
760
804
  position.done = False
761
805
  position.execution_err = err_msg
@@ -806,7 +850,7 @@ async def work(
806
850
  reduce_only=order['reduce_only'],
807
851
  fees_ccy=order['fees_ccy'] if 'fees_ccy' in order else param['default_fees_ccy'],
808
852
  slices=order['slices'],
809
- wait_fill_threshold_ms=order['wait_fill_threshold_ms']
853
+ wait_fill_threshold_ms=order['wait_fill_threshold_ms'] if order['wait_fill_threshold_ms']>0 else param['wait_fill_threshold_ms']
810
854
  )
811
855
  for order in orders
812
856
  ]
@@ -844,7 +888,7 @@ async def work(
844
888
  except Exception as loop_error:
845
889
  log(f"Error: {loop_error} {str(sys.exc_info()[0])} {str(sys.exc_info()[1])} {traceback.format_exc()}")
846
890
  finally:
847
- await asyncio.sleep(int(param['loop_freq_ms']/1000))
891
+ await asyncio.sleep(param['loop_freq_ms']/1000)
848
892
 
849
893
  async def main():
850
894
  parse_args()
@@ -898,7 +942,7 @@ async def main():
898
942
  # Once exchange instantiated, try fetch_balance to confirm connectivity and test credentials.
899
943
  balances = await exchange.fetch_balance() # type: ignore
900
944
  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)
945
+ 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
946
 
903
947
  await work(param=param, exchange=exchange, redis_client=redis_client, notification_params=notification_params)
904
948
 
@@ -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_historical_volatility',
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_sl_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_sl_percent_trailing = default_effective_sl_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_sl_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_sl_percent_trailing = default_effective_sl_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_sl_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_sl_percent_trailing = default_effective_sl_percent_trailing
59
+ )
60
+ assert(effective_tp_trailing_percent==0) # Most tight trailing SL
@@ -101,9 +101,25 @@ 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_historical_volatility'] = pd_candles['log_return'].rolling(window=sliding_window_how_many_candles).std()
122
+
107
123
  pd_candles['chop_against_ema'] = (
108
124
  (~pd_candles['is_green'] & (pd_candles['close'] > pd_candles['ema_close'])) | # Case 1: Green candle and close > EMA
109
125
  (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_sl_percent_trailing : float = 50
59
+ ) -> float:
60
+ slope = (0 - sl_percent_trailing) / (tp_max_percent - tp_min_percent)
61
+ effective_sl_percent_trailing = (
62
+ slope * (pnl_percent_notional - tp_min_percent) + sl_percent_trailing
63
+ if pnl_percent_notional>tp_min_percent
64
+ else default_effective_sl_percent_trailing
65
+ )
66
+ return effective_sl_percent_trailing
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: siglab-py
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: Market data fetches, TA calculations and generic order gateway.
5
5
  Author: r0bbarh00d
6
6
  Author-email: r0bbarh00d <r0bbarh00d@gmail.com>
@@ -30,10 +30,12 @@ siglab_py/tests/integration/market_data_util_tests.py
30
30
  siglab_py/tests/unit/__init__.py
31
31
  siglab_py/tests/unit/analytic_util_tests.py
32
32
  siglab_py/tests/unit/market_data_util_tests.py
33
+ siglab_py/tests/unit/trading_util_tests.py
33
34
  siglab_py/util/__init__.py
34
35
  siglab_py/util/analytic_util.py
35
36
  siglab_py/util/aws_util.py
36
37
  siglab_py/util/market_data_util.py
37
38
  siglab_py/util/notification_util.py
38
39
  siglab_py/util/retry_util.py
39
- siglab_py/util/slack_notification_util.py
40
+ siglab_py/util/slack_notification_util.py
41
+ siglab_py/util/trading_util.py