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.
- siglab_py/algo/__init__.py +0 -0
- siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
- siglab_py/algo/strategy_base.py +122 -0
- siglab_py/algo/strategy_executor.py +1308 -0
- siglab_py/algo/tp_algo.py +529 -0
- siglab_py/backtests/__init__.py +0 -0
- siglab_py/backtests/backtest_core.py +2405 -0
- siglab_py/backtests/coinflip_15m_crypto.py +432 -0
- siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
- siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
- 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 +11 -10
- siglab_py/market_data_providers/candles_ta_provider.py +5 -5
- 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 +75 -2
- siglab_py/tests/unit/analytic_util_tests.py +47 -12
- siglab_py/tests/unit/market_data_util_tests.py +45 -1
- siglab_py/tests/unit/simple_math_tests.py +252 -0
- siglab_py/tests/unit/trading_util_tests.py +65 -0
- siglab_py/util/analytic_util.py +476 -67
- siglab_py/util/datetime_util.py +39 -0
- siglab_py/util/market_data_util.py +528 -98
- siglab_py/util/module_util.py +40 -0
- siglab_py/util/notification_util.py +78 -0
- siglab_py/util/retry_util.py +16 -3
- siglab_py/util/simple_math.py +262 -0
- siglab_py/util/slack_notification_util.py +59 -0
- siglab_py/util/trading_util.py +118 -0
- {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -9
- siglab_py-0.6.33.dist-info/RECORD +56 -0
- {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
- siglab_py-0.1.30.dist-info/RECORD +0 -34
- {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
|
|
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
|
|
|
@@ -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',
|
|
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
|
+
|
|
@@ -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
|