siglab-py 0.1.29__py3-none-any.whl → 0.6.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of siglab-py might be problematic. Click here for more details.

Files changed (32) hide show
  1. siglab_py/constants.py +26 -1
  2. siglab_py/exchanges/binance.py +38 -0
  3. siglab_py/exchanges/deribit.py +83 -0
  4. siglab_py/exchanges/futubull.py +12 -2
  5. siglab_py/market_data_providers/candles_provider.py +2 -2
  6. siglab_py/market_data_providers/candles_ta_provider.py +3 -3
  7. siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
  8. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
  9. siglab_py/market_data_providers/google_monitor.py +320 -0
  10. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  11. siglab_py/market_data_providers/tg_monitor.py +428 -0
  12. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  13. siglab_py/ordergateway/client.py +172 -41
  14. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  15. siglab_py/ordergateway/gateway.py +456 -347
  16. siglab_py/ordergateway/test_ordergateway.py +8 -7
  17. siglab_py/tests/integration/market_data_util_tests.py +35 -1
  18. siglab_py/tests/unit/analytic_util_tests.py +47 -12
  19. siglab_py/tests/unit/simple_math_tests.py +235 -0
  20. siglab_py/tests/unit/trading_util_tests.py +65 -0
  21. siglab_py/util/analytic_util.py +478 -69
  22. siglab_py/util/market_data_util.py +487 -100
  23. siglab_py/util/notification_util.py +78 -0
  24. siglab_py/util/retry_util.py +11 -3
  25. siglab_py/util/simple_math.py +240 -0
  26. siglab_py/util/slack_notification_util.py +59 -0
  27. siglab_py/util/trading_util.py +118 -0
  28. {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/METADATA +5 -9
  29. siglab_py-0.6.12.dist-info/RECORD +44 -0
  30. {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/WHEEL +1 -1
  31. siglab_py-0.1.29.dist-info/RECORD +0 -34
  32. {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,7 @@ from constants import JSON_SERIALIZABLE_TYPES
12
12
 
13
13
  '''
14
14
  set PYTHONPATH=%PYTHONPATH%;D:\dev\siglab\siglab_py
15
- python test_ordergateway.py --gateway_id bybit_03
15
+ python test_ordergateway.py --gateway_id hyperliquid_01
16
16
  '''
17
17
 
18
18
  param : Dict[str, str] = {
@@ -91,27 +91,28 @@ if __name__ == '__main__':
91
91
 
92
92
  positions_3 : List[DivisiblePosition] = [
93
93
  DivisiblePosition(
94
- ticker = 'BTC/USDT:USDT',
94
+ ticker = 'BTC/USDC:USDC',
95
95
  side = 'sell',
96
- amount = 0.01,
96
+ amount = 0.00100,
97
97
  leg_room_bps = 5,
98
- reduce_only=False,
98
+ reduce_only=True,
99
99
  order_type = 'limit',
100
100
  slices=1,
101
- wait_fill_threshold_ms=60000
101
+ wait_fill_threshold_ms=60000,
102
+ fees_ccy='USDC'
102
103
  )
103
104
  ]
104
105
 
105
106
 
106
107
  executed_positions : Union[Dict[JSON_SERIALIZABLE_TYPES, JSON_SERIALIZABLE_TYPES], None] = execute_positions(
107
108
  redis_client=redis_client,
108
- positions=positions_2,
109
+ positions=positions_3,
109
110
  ordergateway_pending_orders_topic=ordergateway_pending_orders_topic,
110
111
  ordergateway_executions_topic=ordergateway_executions_topic
111
112
  )
112
113
  if executed_positions:
113
114
  for position in executed_positions:
114
- print(f"{position['ticker']} {position['side']} amount: {position['amount']} leg_room_bps: {position['leg_room_bps']} slices: {position['slices']}, filled_amount: {position['filled_amount']}, average_cost: {position['average_cost']}, # executions: {len(position['executions'])}") # type: ignore
115
+ print(f"{position['ticker']} {position['side']} amount: {position['amount']} leg_room_bps: {position['leg_room_bps']} slices: {position['slices']}, filled_amount: {position['filled_amount']}, average_cost: {position['average_cost']}, # executions: {len(position['executions'])}, done: {position['done']}, execution_err: {position['execution_err']}") # type: ignore
115
116
 
116
117
 
117
118
 
@@ -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(),
@@ -118,6 +118,40 @@ class MarketDataUtilTests(unittest.TestCase):
118
118
  assert set(pd_candles.columns) >= expected_columns, "Missing expected columns."
119
119
  assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
120
120
  assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
121
+
122
+ def test_aggregate_candles(self):
123
+ end_date : datetime = datetime.today()
124
+ start_date : datetime = end_date + timedelta(hours=-8)
125
+
126
+ param = {
127
+ 'apiKey' : None,
128
+ 'secret' : None,
129
+ 'password' : None,
130
+ 'subaccount' : None,
131
+ 'rateLimit' : 100, # In ms
132
+ 'options' : {
133
+ 'defaultType': 'swap' }
134
+ }
135
+
136
+ exchange : Exchange = okx(param) # type: ignore
137
+ normalized_symbols = [ 'BTC/USDT:USDT' ]
138
+ pd_candles: Union[pd.DataFrame, None] = fetch_candles(
139
+ start_ts=start_date.timestamp(),
140
+ end_ts=end_date.timestamp(),
141
+ exchange=exchange,
142
+ normalized_symbols=normalized_symbols,
143
+ candle_size='15m' # <---- aggregate 1m into 15m candles
144
+ )[normalized_symbols[0]]
145
+
146
+ assert pd_candles is not None
147
+ pd_candles['timestamp_ms_gap'] = pd_candles['timestamp_ms'].diff()
148
+ timestamp_ms_gap_median = pd_candles['timestamp_ms_gap'].median()
149
+ NUM_MS_IN_1HR = 60*60*1000
150
+ expected_15m_gap_ms = NUM_MS_IN_1HR/4
151
+ assert(timestamp_ms_gap_median==expected_15m_gap_ms)
152
+ total_num_rows = pd_candles.shape[0]
153
+ num_rows_with_15min_gaps = pd_candles[pd_candles.timestamp_ms_gap!=timestamp_ms_gap_median].shape[0]
154
+ assert(num_rows_with_15min_gaps/total_num_rows <= 0.4) # Why not 100% match? minute bars may have gaps (Also depends on what ticker)
121
155
 
122
156
  def test_fetch_candles_futubull(self):
123
157
  # You need Futu OpenD running and you need entitlements
@@ -2,7 +2,7 @@ import unittest
2
2
  from typing import List
3
3
  from pathlib import Path
4
4
 
5
- from util.analytic_util import compute_candles_stats
5
+ from util.analytic_util import compute_candles_stats, lookup_fib_target
6
6
 
7
7
  import pandas as pd
8
8
 
@@ -38,31 +38,41 @@ class AnalyticUtilTests(unittest.TestCase):
38
38
  pd_candles=pd_candles,
39
39
  boillenger_std_multiples=2,
40
40
  sliding_window_how_many_candles=20,
41
- pypy_compat=True
41
+ pypy_compat=True # Slopes calculation? Set pypy_compat to False
42
42
  )
43
43
 
44
44
  expected_columns : List[str] = [
45
45
  'exchange', 'symbol', 'timestamp_ms',
46
46
  'open', 'high', 'low', 'close', 'volume',
47
47
  'datetime', 'datetime_utc', 'year', 'month', 'day', 'hour', 'minute', 'dayofweek',
48
- 'pct_chg_on_close', 'candle_height',
48
+ 'pct_chg_on_close', 'candle_height', 'candle_body_height',
49
49
  'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr',
50
- 'is_green', 'pct_change_close',
50
+ 'is_green', 'candle_class', '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
+ 'vwap_short_periods', 'vwap_long_periods',
54
+ 'candle_height_percent', 'candle_height_percent_rounded', 'candle_body_height_percent', 'candle_body_height_percent_rounded',
55
+ 'log_return', 'interval_hist_vol', 'annualized_hist_vol',
53
56
  'chop_against_ema',
54
57
  'ema_volume_short_periods', 'ema_volume_long_periods',
58
+ 'ema_cross', 'ema_cross_last', 'ema_bullish_cross_last_id', 'ema_bearish_cross_last_id',
55
59
  'max_short_periods', 'max_long_periods', 'idmax_short_periods', 'idmax_long_periods', 'min_short_periods', 'min_long_periods', 'idmin_short_periods', 'idmin_long_periods',
56
- 'h_l', 'h_pc', 'l_pc', 'tr', 'atr',
60
+ 'max_candle_body_height_percent_long_periods', 'idmax_candle_body_height_percent_long_periods',
61
+ 'min_candle_body_height_percent_long_periods', 'idmin_candle_body_height_percent_long_periods',
62
+ 'price_swing_short_periods', 'price_swing_long_periods',
63
+ 'trend_from_highs_long_periods', 'trend_from_lows_long_periods', 'trend_from_highs_short_periods', 'trend_from_lows_short_periods',
64
+ 'h_l', 'h_pc', 'l_pc', 'tr', 'atr', 'atr_avg_short_periods', 'atr_avg_long_periods',
57
65
  'hurst_exp',
58
66
  'boillenger_upper', 'boillenger_lower', 'boillenger_channel_height', 'boillenger_upper_agg', 'boillenger_lower_agg', 'boillenger_channel_height_agg',
59
67
  'aggressive_up', 'aggressive_up_index', 'aggressive_up_candle_height', 'aggressive_up_candle_high', 'aggressive_up_candle_low', 'aggressive_down', 'aggressive_down_index', 'aggressive_down_candle_height', 'aggressive_down_candle_high', 'aggressive_down_candle_low',
60
68
  'fvg_low', 'fvg_high', 'fvg_gap', 'fvg_mitigated',
61
- 'close_delta', 'close_delta_percent', 'up', 'down', 'rsi', 'ema_rsi', 'typical_price',
62
- 'money_flow', 'money_flow_positive', 'money_flow_negative', 'positive_flow_sum', 'negative_flow_sum', 'money_flow_ratio', 'mfi',
63
- 'macd', 'signal', 'macd_minus_signal',
64
- 'fib_618_short_periods', 'fib_618_long_periods',
65
- 'gap_close_vs_ema',
69
+ 'close_delta', 'close_delta_percent', 'up', 'down',
70
+ 'rsi', 'rsi_bucket', 'ema_rsi', 'rsi_max', 'rsi_idmax', 'rsi_min', 'rsi_idmin', 'rsi_trend', 'rsi_trend_from_highs', 'rsi_trend_from_lows', 'rsi_divergence',
71
+ 'typical_price',
72
+ 'money_flow', 'money_flow_positive', 'money_flow_negative', 'positive_flow_sum', 'negative_flow_sum', 'money_flow_ratio', 'mfi', 'mfi_bucket',
73
+ 'macd', 'signal', 'macd_minus_signal', 'macd_cross', 'macd_bullish_cross_last_id', 'macd_bearish_cross_last_id', 'macd_cross_last',
74
+ 'fib_0.618_short_periods', 'fib_0.618_long_periods',
75
+ 'gap_close_vs_ema', 'gap_close_vs_ema_percent',
66
76
  'close_above_or_below_ema',
67
77
  'close_vs_ema_inflection'
68
78
  ]
@@ -70,4 +80,29 @@ class AnalyticUtilTests(unittest.TestCase):
70
80
  missing_columns = [ expected for expected in expected_columns if expected not in pd_candles.columns.to_list() ]
71
81
  unexpected_columns = [ actual for actual in pd_candles.columns.to_list() if actual not in expected_columns ]
72
82
 
73
- assert(pd_candles.columns.to_list()==expected_columns)
83
+ assert(pd_candles.columns.to_list()==expected_columns)
84
+
85
+ def test_lookup_fib_target(self):
86
+ data_dir = Path(__file__).parent.parent.parent.parent / "data"
87
+ csv_path = data_dir / "sample_btc_candles.csv"
88
+ pd_candles : pd.DataFrame = pd.read_csv(csv_path)
89
+ target_fib_level : float = 0.618
90
+ compute_candles_stats(
91
+ pd_candles=pd_candles,
92
+ boillenger_std_multiples=2,
93
+ sliding_window_how_many_candles=20,
94
+ target_fib_level=target_fib_level,
95
+ pypy_compat=True # Slopes calculation? Set pypy_compat to False
96
+ )
97
+
98
+ last_row = pd_candles.iloc[-1]
99
+ result = lookup_fib_target(
100
+ row=last_row,
101
+ pd_candles=pd_candles,
102
+ target_fib_level=target_fib_level
103
+ )
104
+ if result:
105
+ assert(result['short_periods']['min']<result['short_periods']['fib_target']<result['short_periods']['max'])
106
+ assert(result['long_periods']['min']<result['long_periods']['fib_target']<result['long_periods']['max'])
107
+
108
+
@@ -0,0 +1,235 @@
1
+ import unittest
2
+ from typing import List, Dict, Union
3
+
4
+ from util.simple_math import generate_rand_nums, round_to_level, bucket_series, bucketize_val
5
+
6
+ class SimpleMathTests(unittest.TestCase):
7
+
8
+ def test_generate_rand_nums(self):
9
+ range_min : float = 0
10
+ range_max : float = 1
11
+ size : int = 100
12
+ percentage_in_range : float = 91
13
+ abs_min : float = -0.5
14
+ abs_max : float = 1.1
15
+
16
+ rand_nums : List[float] = generate_rand_nums(
17
+ range_min = range_min,
18
+ range_max = range_max,
19
+ size = size,
20
+ percent_in_range = percentage_in_range,
21
+ abs_min = abs_min,
22
+ abs_max = abs_max
23
+ )
24
+
25
+ assert(len(rand_nums)==size)
26
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
27
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
28
+
29
+
30
+ range_min = -1
31
+ range_max = 1
32
+ percentage_in_range = 91
33
+ abs_min = -1.5
34
+ abs_max = 1.5
35
+
36
+ rand_nums : List[float] = generate_rand_nums(
37
+ range_min = range_min,
38
+ range_max = range_max,
39
+ size = size,
40
+ percent_in_range = percentage_in_range,
41
+ abs_min = abs_min,
42
+ abs_max = abs_max
43
+ )
44
+
45
+ assert(len(rand_nums)==size)
46
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
47
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
48
+
49
+
50
+ range_min = 0
51
+ range_max = 100
52
+ percentage_in_range = 91
53
+ abs_min = -150
54
+ abs_max = 150
55
+
56
+ rand_nums : List[float] = generate_rand_nums(
57
+ range_min = range_min,
58
+ range_max = range_max,
59
+ size = size,
60
+ percent_in_range = percentage_in_range,
61
+ abs_min = abs_min,
62
+ abs_max = abs_max
63
+ )
64
+
65
+ assert(len(rand_nums)==size)
66
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
67
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
68
+
69
+
70
+ range_min = -100
71
+ range_max = 100
72
+ percentage_in_range = 91
73
+ abs_min = -150
74
+ abs_max = 150
75
+
76
+ rand_nums : List[float] = generate_rand_nums(
77
+ range_min = range_min,
78
+ range_max = range_max,
79
+ size = size,
80
+ percent_in_range = percentage_in_range,
81
+ abs_min = abs_min,
82
+ abs_max = abs_max
83
+ )
84
+
85
+ assert(len(rand_nums)==size)
86
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
87
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
88
+
89
+ def test_round_to_level(self):
90
+ prices = [
91
+ { 'price' : 15080, 'rounded' : 15000},
92
+ { 'price' : 15180, 'rounded' : 15200},
93
+ { 'price' : 25080, 'rounded' : 25200},
94
+ { 'price' : 25180, 'rounded' : 25200},
95
+ { 'price' : 25380, 'rounded' : 25500},
96
+ { 'price' : 95332, 'rounded' : 95000},
97
+ { 'price' : 95878, 'rounded' : 96000},
98
+ { 'price' : 103499, 'rounded' : 103000},
99
+ { 'price' : 103500, 'rounded' : 104000},
100
+ { 'price' : 150800, 'rounded' : 150000},
101
+ { 'price' : 151800, 'rounded' : 152000}
102
+ ]
103
+ for entry in prices:
104
+ price = entry['price']
105
+ expected = entry['rounded']
106
+ rounded_price = round_to_level(price, level_granularity=0.01)
107
+ print(f"{price} rounded to: {rounded_price}")
108
+ assert(rounded_price==expected)
109
+
110
+ def test_bucket_series(self):
111
+
112
+ level_granularity : float = 0.1
113
+
114
+ range_min : float = 0
115
+ range_max : float = 1
116
+ size : int = 100
117
+ percentage_in_range : float = 91
118
+ abs_min : float = -0.5
119
+ abs_max : float = 1.1
120
+
121
+ rand_nums : List[float] = generate_rand_nums(
122
+ range_min = range_min,
123
+ range_max = range_max,
124
+ size = size,
125
+ percent_in_range = percentage_in_range,
126
+ abs_min = abs_min,
127
+ abs_max = abs_max
128
+ )
129
+
130
+ buckets : Dict[
131
+ str,
132
+ Dict[str,Union[float, List[float]]]
133
+ ] = bucket_series(
134
+ values = rand_nums,
135
+ outlier_threshold_percent = 10,
136
+ level_granularity=level_granularity
137
+ )
138
+
139
+ bucketized = []
140
+ for num in rand_nums:
141
+ bucketized.append(
142
+ bucketize_val(num, buckets=buckets)
143
+ )
144
+
145
+
146
+ range_min = -1
147
+ range_max = 1
148
+ size : int = 100
149
+ percentage_in_range = 91
150
+ abs_min = -1.5
151
+ abs_max = 1.5
152
+
153
+ rand_nums : List[float] = generate_rand_nums(
154
+ range_min = range_min,
155
+ range_max = range_max,
156
+ size = size,
157
+ percent_in_range = percentage_in_range,
158
+ abs_min = abs_min,
159
+ abs_max = abs_max
160
+ )
161
+
162
+ buckets = bucket_series(
163
+ values = rand_nums,
164
+ outlier_threshold_percent = 10,
165
+ level_granularity=level_granularity
166
+ )
167
+
168
+
169
+ range_min = 0
170
+ range_max = 100
171
+ size : int = 100
172
+ percentage_in_range = 91
173
+ abs_min = -0.5
174
+ abs_max = 150
175
+
176
+ rand_nums : List[float] = generate_rand_nums(
177
+ range_min = range_min,
178
+ range_max = range_max,
179
+ size = size,
180
+ percent_in_range = percentage_in_range,
181
+ abs_min = abs_min,
182
+ abs_max = abs_max
183
+ )
184
+
185
+ buckets = bucket_series(
186
+ values = rand_nums,
187
+ outlier_threshold_percent = 10,
188
+ level_granularity=level_granularity
189
+ )
190
+
191
+
192
+ range_min = -100
193
+ range_max = 100
194
+ size : int = 100
195
+ percentage_in_range = 91
196
+ abs_min = -150
197
+ abs_max = 150
198
+
199
+ rand_nums : List[float] = generate_rand_nums(
200
+ range_min = range_min,
201
+ range_max = range_max,
202
+ size = size,
203
+ percent_in_range = percentage_in_range,
204
+ abs_min = abs_min,
205
+ abs_max = abs_max
206
+ )
207
+
208
+ buckets = bucket_series(
209
+ values = rand_nums,
210
+ outlier_threshold_percent = 10,
211
+ level_granularity=level_granularity
212
+ )
213
+
214
+
215
+ range_min = 20_000
216
+ range_max = 120_000
217
+ size : int = 100
218
+ percentage_in_range = 91
219
+ abs_min = 15_000
220
+ abs_max = 130_000
221
+
222
+ rand_nums : List[float] = generate_rand_nums(
223
+ range_min = range_min,
224
+ range_max = range_max,
225
+ size = size,
226
+ percent_in_range = percentage_in_range,
227
+ abs_min = abs_min,
228
+ abs_max = abs_max
229
+ )
230
+
231
+ buckets = bucket_series(
232
+ values = rand_nums,
233
+ outlier_threshold_percent = 10,
234
+ level_granularity=level_granularity
235
+ )
@@ -0,0 +1,65 @@
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
+ Have a look at this for a visual explaination how "Gradually tightened stops" works:
10
+ https://github.com/r0bbar/siglab/blob/master/siglab_py/tests/manual/trading_util_tests.ipynb
11
+ https://norman-lm-fung.medium.com/gradually-tightened-trailing-stops-f7854bf1e02b
12
+ '''
13
+
14
+ # @unittest.skip("Skip all integration tests.")
15
+ class TradingUtilTests(unittest.TestCase):
16
+ def test_calc_eff_trailing_sl_case1(self):
17
+ tp_min_percent : float = 1.5
18
+ tp_max_percent : float = 2.5
19
+ sl_percent_trailing : float = 50 # Trailing stop loss in percent
20
+ default_effective_tp_trailing_percent : float = 50
21
+
22
+ pnl_percent_notional : float = 0.5 # Trade's current pnl in percent.
23
+
24
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
25
+ tp_min_percent = tp_min_percent,
26
+ tp_max_percent = tp_max_percent,
27
+ sl_percent_trailing = sl_percent_trailing,
28
+ pnl_percent_notional = pnl_percent_notional,
29
+ default_effective_tp_trailing_percent = default_effective_tp_trailing_percent
30
+ )
31
+ assert(effective_tp_trailing_percent==50) # Generous trailing SL when trading starting out and pnl small.
32
+
33
+ def test_calc_eff_trailing_sl_case2(self):
34
+ tp_min_percent : float = 1.5
35
+ tp_max_percent : float = 2.5
36
+ sl_percent_trailing : float = 50 # Trailing stop loss in percent
37
+ default_effective_tp_trailing_percent : float = 50
38
+
39
+ pnl_percent_notional : float = 2 # Trade's current pnl in percent.
40
+
41
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
42
+ tp_min_percent = tp_min_percent,
43
+ tp_max_percent = tp_max_percent,
44
+ sl_percent_trailing = sl_percent_trailing,
45
+ pnl_percent_notional = pnl_percent_notional,
46
+ default_effective_tp_trailing_percent = default_effective_tp_trailing_percent
47
+ )
48
+ assert(effective_tp_trailing_percent==25) # Intermediate trailing SL
49
+
50
+ def test_calc_eff_trailing_sl_case3(self):
51
+ tp_min_percent : float = 1.5
52
+ tp_max_percent : float = 2.5
53
+ sl_percent_trailing : float = 50 # Trailing stop loss in percent
54
+ default_effective_tp_trailing_percent : float = 50
55
+
56
+ pnl_percent_notional : float = 2.5 # Trade's current pnl in percent.
57
+
58
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
59
+ tp_min_percent = tp_min_percent,
60
+ tp_max_percent = tp_max_percent,
61
+ sl_percent_trailing = sl_percent_trailing,
62
+ pnl_percent_notional = pnl_percent_notional,
63
+ default_effective_tp_trailing_percent = default_effective_tp_trailing_percent
64
+ )
65
+ assert(effective_tp_trailing_percent==0) # Most tight trailing SL