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.
- siglab_py/constants.py +26 -1
- siglab_py/exchanges/binance.py +38 -0
- siglab_py/exchanges/deribit.py +83 -0
- siglab_py/exchanges/futubull.py +12 -2
- siglab_py/market_data_providers/candles_provider.py +2 -2
- siglab_py/market_data_providers/candles_ta_provider.py +3 -3
- siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +4 -4
- siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
- siglab_py/market_data_providers/google_monitor.py +320 -0
- siglab_py/market_data_providers/orderbooks_provider.py +15 -12
- siglab_py/market_data_providers/tg_monitor.py +428 -0
- siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
- siglab_py/ordergateway/client.py +172 -41
- siglab_py/ordergateway/encrypt_keys_util.py +1 -1
- siglab_py/ordergateway/gateway.py +456 -347
- siglab_py/ordergateway/test_ordergateway.py +8 -7
- siglab_py/tests/integration/market_data_util_tests.py +35 -1
- siglab_py/tests/unit/analytic_util_tests.py +47 -12
- siglab_py/tests/unit/simple_math_tests.py +235 -0
- siglab_py/tests/unit/trading_util_tests.py +65 -0
- siglab_py/util/analytic_util.py +478 -69
- siglab_py/util/market_data_util.py +487 -100
- siglab_py/util/notification_util.py +78 -0
- siglab_py/util/retry_util.py +11 -3
- siglab_py/util/simple_math.py +240 -0
- siglab_py/util/slack_notification_util.py +59 -0
- siglab_py/util/trading_util.py +118 -0
- {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/METADATA +5 -9
- siglab_py-0.6.12.dist-info/RECORD +44 -0
- {siglab_py-0.1.29.dist-info → siglab_py-0.6.12.dist-info}/WHEEL +1 -1
- siglab_py-0.1.29.dist-info/RECORD +0 -34
- {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
|
|
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/
|
|
94
|
+
ticker = 'BTC/USDC:USDC',
|
|
95
95
|
side = 'sell',
|
|
96
|
-
amount = 0.
|
|
96
|
+
amount = 0.00100,
|
|
97
97
|
leg_room_bps = 5,
|
|
98
|
-
reduce_only=
|
|
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=
|
|
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',
|
|
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
|
-
'
|
|
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',
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
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
|