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
@@ -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
 
@@ -1,6 +1,8 @@
1
1
  import unittest
2
2
  from datetime import datetime, timedelta
3
3
  from typing import Union
4
+ import json
5
+ import logging
4
6
  from pathlib import Path
5
7
 
6
8
  from util.market_data_util import *
@@ -100,14 +102,15 @@ class MarketDataUtilTests(unittest.TestCase):
100
102
  'defaultType': 'swap' }
101
103
  }
102
104
 
103
- exchange : Exchange = okx(param)
105
+ exchange : Exchange = okx(param) # type: ignore
104
106
  normalized_symbols = [ 'BTC/USDT:USDT' ]
105
107
  pd_candles: Union[pd.DataFrame, None] = fetch_candles(
106
108
  start_ts=start_date.timestamp(),
107
109
  end_ts=end_date.timestamp(),
108
110
  exchange=exchange,
109
111
  normalized_symbols=normalized_symbols,
110
- candle_size='1h'
112
+ candle_size='1h',
113
+ logger=logging.getLogger()
111
114
  )[normalized_symbols[0]]
112
115
 
113
116
  assert pd_candles is not None
@@ -118,6 +121,76 @@ class MarketDataUtilTests(unittest.TestCase):
118
121
  assert set(pd_candles.columns) >= expected_columns, "Missing expected columns."
119
122
  assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
120
123
  assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
124
+
125
+ def test_fetch_candles_ccxt_with_ticker_change_map(self):
126
+ ticker_change_map : List[Dict[str, Union[str, int]]] = [
127
+ {
128
+ 'new_ticker' : 'XAU/USDT:USDT',
129
+ 'old_ticker' : 'XAUT/USDT:USDT',
130
+ 'cutoff_ms' : 1768464300000
131
+ }
132
+ ]
133
+
134
+ start_date : datetime = datetime(2026,1,13,0,0,0)
135
+ end_date : datetime = datetime(2026,1,15,18,0,0)
136
+
137
+ param = {
138
+ 'apiKey' : None,
139
+ 'secret' : None,
140
+ 'password' : None,
141
+ 'subaccount' : None,
142
+ 'rateLimit' : 100, # In ms
143
+ 'options' : {
144
+ 'defaultType': 'swap' }
145
+ }
146
+
147
+ exchange : Exchange = okx(param) # type: ignore
148
+ normalized_symbols = [ 'XAU/USDT:USDT' ]
149
+ pd_candles: Union[pd.DataFrame, None] = fetch_candles(
150
+ start_ts=start_date.timestamp(),
151
+ end_ts=end_date.timestamp(),
152
+ exchange=exchange,
153
+ normalized_symbols=normalized_symbols,
154
+ candle_size='1h',
155
+ ticker_change_map=ticker_change_map,
156
+ logger=logging.getLogger()
157
+ )[normalized_symbols[0]]
158
+
159
+ assert pd_candles is not None
160
+
161
+ def test_aggregate_candles(self):
162
+ end_date : datetime = datetime.today()
163
+ start_date : datetime = end_date + timedelta(hours=-8)
164
+
165
+ param = {
166
+ 'apiKey' : None,
167
+ 'secret' : None,
168
+ 'password' : None,
169
+ 'subaccount' : None,
170
+ 'rateLimit' : 100, # In ms
171
+ 'options' : {
172
+ 'defaultType': 'swap' }
173
+ }
174
+
175
+ exchange : Exchange = okx(param) # type: ignore
176
+ normalized_symbols = [ 'BTC/USDT:USDT' ]
177
+ pd_candles: Union[pd.DataFrame, None] = fetch_candles(
178
+ start_ts=start_date.timestamp(),
179
+ end_ts=end_date.timestamp(),
180
+ exchange=exchange,
181
+ normalized_symbols=normalized_symbols,
182
+ candle_size='15m' # <---- aggregate 1m into 15m candles
183
+ )[normalized_symbols[0]]
184
+
185
+ assert pd_candles is not None
186
+ pd_candles['timestamp_ms_gap'] = pd_candles['timestamp_ms'].diff()
187
+ timestamp_ms_gap_median = pd_candles['timestamp_ms_gap'].median()
188
+ NUM_MS_IN_1HR = 60*60*1000
189
+ expected_15m_gap_ms = NUM_MS_IN_1HR/4
190
+ assert(timestamp_ms_gap_median==expected_15m_gap_ms)
191
+ total_num_rows = pd_candles.shape[0]
192
+ num_rows_with_15min_gaps = pd_candles[pd_candles.timestamp_ms_gap!=timestamp_ms_gap_median].shape[0]
193
+ 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
194
 
122
195
  def test_fetch_candles_futubull(self):
123
196
  # 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
+
@@ -1,6 +1,7 @@
1
1
  import unittest
2
2
  from datetime import datetime, timedelta
3
3
  from typing import Union
4
+ import json
4
5
  from pathlib import Path
5
6
 
6
7
  from util.market_data_util import *
@@ -49,4 +50,47 @@ class MarketDataUtilTests(unittest.TestCase):
49
50
  actual = timestamp_to_active_trading_regions(ts)
50
51
  assert(expectation==actual)
51
52
  i+=1
52
-
53
+
54
+ def test_ticker_change_map_util(self):
55
+ '''
56
+ Example OKX managers decide to give work to customers https://www.okx.com/help/okx-will-rename-xautusdt-perpetual-to-xauusdt-perpetual
57
+ OKX to rename XAUTUSDT perpetual to XAUUSDT perpetual📣
58
+
59
+ 🗓 8:05 am on Jan 15, 2026 (UTC) --> Timestamp in sec: 1768464300
60
+ 1️⃣Trading of XAUTUSDT perpetual will be suspended from 8:05 am to 8:25 am on Jan 15, 2026 (UTC)
61
+ 2️⃣Following aspects may also be affected:
62
+ • Margin requirements (if you are using PM mode to trade this perpetual)
63
+ • Index & funding fee
64
+ • Trading bots & strategy orders
65
+ • OpenAPI & WebSocket
66
+ '''
67
+ ticker_change_map_file : str = 'ticker_change_map.json'
68
+
69
+ ticker_change_map : List[Dict[str, Union[str, int]]] = [
70
+ {
71
+ 'new_ticker' : 'XAU/USDT:USDT',
72
+ 'old_ticker' : 'XAUT/USDT:USDT',
73
+ 'cutoff_ms' : 1768464300000
74
+ }
75
+ ]
76
+
77
+ ticker : str = 'XAU/USDT:USDT'
78
+ old_ticker : Union[None, str] = get_old_ticker(ticker, ticker_change_map)
79
+ mapping : Union[None, Dict[str, Union[str, int]]] = get_ticker_map(ticker, ticker_change_map)
80
+ assert(old_ticker)
81
+ self.assertEqual(old_ticker, "XAUT/USDT:USDT")
82
+ assert(mapping)
83
+
84
+ ticker : str = '???/USDT:USDT'
85
+ old_ticker = get_old_ticker(ticker, ticker_change_map)
86
+ self.assertIsNone(old_ticker)
87
+
88
+ '''
89
+ with open(ticker_change_map_file, 'w', encoding='utf-8') as f:
90
+ json.dump(ticker_change_map, f, indent=2)
91
+
92
+ with open(ticker_change_map_file, 'r', encoding='utf-8') as f:
93
+ ticker_change_map_from_disk : List[Dict[str, Union[str, int]]] = json.load(f)
94
+ '''
95
+
96
+
@@ -0,0 +1,252 @@
1
+ import unittest
2
+ from typing import List, Dict, Union
3
+
4
+ from numpy import equal
5
+
6
+ from util.simple_math import generate_rand_nums, round_to_level, compute_adjacent_levels, bucket_series, bucketize_val
7
+
8
+ class SimpleMathTests(unittest.TestCase):
9
+
10
+ def test_generate_rand_nums(self):
11
+ range_min : float = 0
12
+ range_max : float = 1
13
+ size : int = 100
14
+ percentage_in_range : float = 91
15
+ abs_min : float = -0.5
16
+ abs_max : float = 1.1
17
+
18
+ rand_nums : List[float] = generate_rand_nums(
19
+ range_min = range_min,
20
+ range_max = range_max,
21
+ size = size,
22
+ percent_in_range = percentage_in_range,
23
+ abs_min = abs_min,
24
+ abs_max = abs_max
25
+ )
26
+
27
+ assert(len(rand_nums)==size)
28
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
29
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
30
+
31
+
32
+ range_min = -1
33
+ range_max = 1
34
+ percentage_in_range = 91
35
+ abs_min = -1.5
36
+ abs_max = 1.5
37
+
38
+ rand_nums : List[float] = generate_rand_nums(
39
+ range_min = range_min,
40
+ range_max = range_max,
41
+ size = size,
42
+ percent_in_range = percentage_in_range,
43
+ abs_min = abs_min,
44
+ abs_max = abs_max
45
+ )
46
+
47
+ assert(len(rand_nums)==size)
48
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
49
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
50
+
51
+
52
+ range_min = 0
53
+ range_max = 100
54
+ percentage_in_range = 91
55
+ abs_min = -150
56
+ abs_max = 150
57
+
58
+ rand_nums : List[float] = generate_rand_nums(
59
+ range_min = range_min,
60
+ range_max = range_max,
61
+ size = size,
62
+ percent_in_range = percentage_in_range,
63
+ abs_min = abs_min,
64
+ abs_max = abs_max
65
+ )
66
+
67
+ assert(len(rand_nums)==size)
68
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
69
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
70
+
71
+
72
+ range_min = -100
73
+ range_max = 100
74
+ percentage_in_range = 91
75
+ abs_min = -150
76
+ abs_max = 150
77
+
78
+ rand_nums : List[float] = generate_rand_nums(
79
+ range_min = range_min,
80
+ range_max = range_max,
81
+ size = size,
82
+ percent_in_range = percentage_in_range,
83
+ abs_min = abs_min,
84
+ abs_max = abs_max
85
+ )
86
+
87
+ assert(len(rand_nums)==size)
88
+ assert(len([x for x in rand_nums if x>=range_min and x<=range_max]) == (percentage_in_range/100) * size)
89
+ assert(len([x for x in rand_nums if x<abs_min or x>abs_max]) == 0)
90
+
91
+ def test_round_to_level(self):
92
+ prices = [
93
+ { 'price' : 15080, 'rounded' : 15000},
94
+ { 'price' : 15180, 'rounded' : 15200},
95
+ { 'price' : 25080, 'rounded' : 25200},
96
+ { 'price' : 25180, 'rounded' : 25200},
97
+ { 'price' : 25380, 'rounded' : 25500},
98
+ { 'price' : 95332, 'rounded' : 95000},
99
+ { 'price' : 95878, 'rounded' : 96000},
100
+ { 'price' : 103499, 'rounded' : 103000},
101
+ { 'price' : 103500, 'rounded' : 104000},
102
+ { 'price' : 150800, 'rounded' : 150000},
103
+ { 'price' : 151800, 'rounded' : 152000}
104
+ ]
105
+ for entry in prices:
106
+ price = entry['price']
107
+ expected = entry['rounded']
108
+ rounded_price = round_to_level(price, level_granularity=0.01)
109
+ print(f"{price} rounded to: {rounded_price}")
110
+ assert(rounded_price==expected)
111
+
112
+ def test_compute_adjacent_levels(self):
113
+ gold_price = 4450
114
+ level_granularity = 0.025 # So levels are $100 apart
115
+ adjacent_levels = compute_adjacent_levels(num=gold_price, level_granularity=level_granularity, num_levels_per_side=3)
116
+ assert(adjacent_levels)
117
+ assert(len(adjacent_levels)==7)
118
+ equal(adjacent_levels, [4100,4200,4300,4400,4500,4600,4700])
119
+
120
+ btc_price = 95000
121
+ level_granularity = 0.01 # So levels are $1000 apart
122
+ adjacent_levels = compute_adjacent_levels(num=btc_price, level_granularity=level_granularity, num_levels_per_side=3)
123
+ assert(adjacent_levels)
124
+ assert(len(adjacent_levels)==7)
125
+ equal(adjacent_levels, [92000,93000,94000,95000,96000,97000,98000])
126
+
127
+ def test_bucket_series(self):
128
+
129
+ level_granularity : float = 0.1
130
+
131
+ range_min : float = 0
132
+ range_max : float = 1
133
+ size : int = 100
134
+ percentage_in_range : float = 91
135
+ abs_min : float = -0.5
136
+ abs_max : float = 1.1
137
+
138
+ rand_nums : List[float] = generate_rand_nums(
139
+ range_min = range_min,
140
+ range_max = range_max,
141
+ size = size,
142
+ percent_in_range = percentage_in_range,
143
+ abs_min = abs_min,
144
+ abs_max = abs_max
145
+ )
146
+
147
+ buckets : Dict[
148
+ str,
149
+ Dict[str,Union[float, List[float]]]
150
+ ] = bucket_series(
151
+ values = rand_nums,
152
+ outlier_threshold_percent = 10,
153
+ level_granularity=level_granularity
154
+ )
155
+
156
+ bucketized = []
157
+ for num in rand_nums:
158
+ bucketized.append(
159
+ bucketize_val(num, buckets=buckets)
160
+ )
161
+
162
+
163
+ range_min = -1
164
+ range_max = 1
165
+ size : int = 100
166
+ percentage_in_range = 91
167
+ abs_min = -1.5
168
+ abs_max = 1.5
169
+
170
+ rand_nums : List[float] = generate_rand_nums(
171
+ range_min = range_min,
172
+ range_max = range_max,
173
+ size = size,
174
+ percent_in_range = percentage_in_range,
175
+ abs_min = abs_min,
176
+ abs_max = abs_max
177
+ )
178
+
179
+ buckets = bucket_series(
180
+ values = rand_nums,
181
+ outlier_threshold_percent = 10,
182
+ level_granularity=level_granularity
183
+ )
184
+
185
+
186
+ range_min = 0
187
+ range_max = 100
188
+ size : int = 100
189
+ percentage_in_range = 91
190
+ abs_min = -0.5
191
+ abs_max = 150
192
+
193
+ rand_nums : List[float] = generate_rand_nums(
194
+ range_min = range_min,
195
+ range_max = range_max,
196
+ size = size,
197
+ percent_in_range = percentage_in_range,
198
+ abs_min = abs_min,
199
+ abs_max = abs_max
200
+ )
201
+
202
+ buckets = bucket_series(
203
+ values = rand_nums,
204
+ outlier_threshold_percent = 10,
205
+ level_granularity=level_granularity
206
+ )
207
+
208
+
209
+ range_min = -100
210
+ range_max = 100
211
+ size : int = 100
212
+ percentage_in_range = 91
213
+ abs_min = -150
214
+ abs_max = 150
215
+
216
+ rand_nums : List[float] = generate_rand_nums(
217
+ range_min = range_min,
218
+ range_max = range_max,
219
+ size = size,
220
+ percent_in_range = percentage_in_range,
221
+ abs_min = abs_min,
222
+ abs_max = abs_max
223
+ )
224
+
225
+ buckets = bucket_series(
226
+ values = rand_nums,
227
+ outlier_threshold_percent = 10,
228
+ level_granularity=level_granularity
229
+ )
230
+
231
+
232
+ range_min = 20_000
233
+ range_max = 120_000
234
+ size : int = 100
235
+ percentage_in_range = 91
236
+ abs_min = 15_000
237
+ abs_max = 130_000
238
+
239
+ rand_nums : List[float] = generate_rand_nums(
240
+ range_min = range_min,
241
+ range_max = range_max,
242
+ size = size,
243
+ percent_in_range = percentage_in_range,
244
+ abs_min = abs_min,
245
+ abs_max = abs_max
246
+ )
247
+
248
+ buckets = bucket_series(
249
+ values = rand_nums,
250
+ outlier_threshold_percent = 10,
251
+ level_granularity=level_granularity
252
+ )
@@ -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