siglab-py 0.1.19__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 +33 -3
- 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 +238 -0
- siglab_py/market_data_providers/futu_candles_ta_to_csv.py +224 -0
- 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 -344
- siglab_py/ordergateway/test_ordergateway.py +8 -7
- siglab_py/tests/integration/market_data_util_tests.py +80 -6
- siglab_py/tests/unit/analytic_util_tests.py +67 -4
- siglab_py/tests/unit/market_data_util_tests.py +96 -0
- 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 +484 -66
- siglab_py/util/datetime_util.py +39 -0
- siglab_py/util/market_data_util.py +564 -74
- 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.19.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -13
- siglab_py-0.6.33.dist-info/RECORD +56 -0
- {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
- siglab_py-0.1.19.dist-info/RECORD +0 -31
- {siglab_py-0.1.19.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 *
|
|
@@ -36,7 +38,7 @@ class MarketDataUtilTests(unittest.TestCase):
|
|
|
36
38
|
|
|
37
39
|
if pd_candles is not None:
|
|
38
40
|
assert len(pd_candles) > 0, "No candles returned."
|
|
39
|
-
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute'}
|
|
41
|
+
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute', 'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr'}
|
|
40
42
|
assert set(pd_candles.columns) >= expected_columns, "Missing expected columns."
|
|
41
43
|
assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
|
|
42
44
|
assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
|
|
@@ -81,7 +83,7 @@ class MarketDataUtilTests(unittest.TestCase):
|
|
|
81
83
|
|
|
82
84
|
if pd_candles is not None:
|
|
83
85
|
assert len(pd_candles) > 0, "No candles returned."
|
|
84
|
-
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute'}
|
|
86
|
+
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute', 'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr'}
|
|
85
87
|
assert set(pd_candles.columns) >= expected_columns, "Missing expected columns."
|
|
86
88
|
assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
|
|
87
89
|
assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
|
|
@@ -100,26 +102,98 @@ 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
|
|
114
117
|
|
|
115
118
|
if pd_candles is not None:
|
|
116
119
|
assert len(pd_candles) > 0, "No candles returned."
|
|
117
|
-
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute'}
|
|
120
|
+
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute', 'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr'}
|
|
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):
|
|
196
|
+
# You need Futu OpenD running and you need entitlements
|
|
123
197
|
end_date : datetime = datetime.today()
|
|
124
198
|
start_date : datetime = end_date - timedelta(days=365*3)
|
|
125
199
|
|
|
@@ -148,7 +222,7 @@ class MarketDataUtilTests(unittest.TestCase):
|
|
|
148
222
|
|
|
149
223
|
if pd_candles is not None:
|
|
150
224
|
assert len(pd_candles) > 0, "No candles returned."
|
|
151
|
-
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute'}
|
|
225
|
+
expected_columns = {'exchange', 'symbol', 'timestamp_ms', 'open', 'high', 'low', 'close', 'volume', 'datetime_utc', 'datetime', 'year', 'month', 'day', 'hour', 'minute', 'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr'}
|
|
152
226
|
assert set(pd_candles.columns) >= expected_columns, "Missing expected columns."
|
|
153
227
|
assert pd_candles['timestamp_ms'].notna().all(), "timestamp_ms column contains NaN values."
|
|
154
228
|
assert pd_candles['timestamp_ms'].is_monotonic_increasing, "Timestamps are not in ascending order."
|
|
@@ -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,8 +38,71 @@ 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
|
-
expected_columns : List[str] = [
|
|
45
|
-
|
|
44
|
+
expected_columns : List[str] = [
|
|
45
|
+
'exchange', 'symbol', 'timestamp_ms',
|
|
46
|
+
'open', 'high', 'low', 'close', 'volume',
|
|
47
|
+
'datetime', 'datetime_utc', 'year', 'month', 'day', 'hour', 'minute', 'dayofweek',
|
|
48
|
+
'pct_chg_on_close', 'candle_height', 'candle_body_height',
|
|
49
|
+
'week_of_month', 'apac_trading_hr', 'emea_trading_hr', 'amer_trading_hr',
|
|
50
|
+
'is_green', 'candle_class', 'pct_change_close',
|
|
51
|
+
'sma_short_periods', 'sma_long_periods', 'ema_short_periods', 'ema_long_periods', 'ema_close',
|
|
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',
|
|
56
|
+
'chop_against_ema',
|
|
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',
|
|
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',
|
|
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',
|
|
65
|
+
'hurst_exp',
|
|
66
|
+
'boillenger_upper', 'boillenger_lower', 'boillenger_channel_height', 'boillenger_upper_agg', 'boillenger_lower_agg', 'boillenger_channel_height_agg',
|
|
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',
|
|
68
|
+
'fvg_low', 'fvg_high', 'fvg_gap', 'fvg_mitigated',
|
|
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',
|
|
76
|
+
'close_above_or_below_ema',
|
|
77
|
+
'close_vs_ema_inflection'
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
missing_columns = [ expected for expected in expected_columns if expected not in pd_candles.columns.to_list() ]
|
|
81
|
+
unexpected_columns = [ actual for actual in pd_candles.columns.to_list() if actual not in expected_columns ]
|
|
82
|
+
|
|
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,96 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Union
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from util.market_data_util import *
|
|
8
|
+
|
|
9
|
+
from futu import *
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# @unittest.skip("Skip all integration tests.")
|
|
13
|
+
class MarketDataUtilTests(unittest.TestCase):
|
|
14
|
+
def test_timestamp_to_week_of_month(self):
|
|
15
|
+
test_timestamps = [
|
|
16
|
+
1672531200000, # 2023-01-01 (Week 0)
|
|
17
|
+
1673136000000, # 2023-01-08 (Week 1)
|
|
18
|
+
1673740800000, # 2023-01-15 (Week 2)
|
|
19
|
+
1674345600000, # 2023-01-22 (Week 3)
|
|
20
|
+
1674950400000, # 2023-01-29 (Week 4)
|
|
21
|
+
1675468800000, # 2023-02-01 (Week 0)
|
|
22
|
+
1676073600000, # 2023-02-08 (Week 1)
|
|
23
|
+
1676678400000, # 2023-02-15 (Week 2)
|
|
24
|
+
1677283200000, # 2023-02-22 (Week 3)
|
|
25
|
+
1677888000000, # 2023-03-01 (Week 0)
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
expectations = [0, 1, 2, 3, 4, 0, 1, 2, 3, 0]
|
|
29
|
+
|
|
30
|
+
for i, ts in enumerate(test_timestamps):
|
|
31
|
+
expectation = expectations[i]
|
|
32
|
+
actual = timestamp_to_week_of_month(ts)
|
|
33
|
+
assert expectation == actual, f"Test failed for timestamp {ts}. Expected: {expectation}, Actual: {actual}"
|
|
34
|
+
|
|
35
|
+
def test_timestamp_to_active_trading_regions(self):
|
|
36
|
+
test_timestamps = [
|
|
37
|
+
1672531200000, # 2023-01-01 00:00:00 UTC (APAC)
|
|
38
|
+
1672563600000, # 2023-01-01 09:00:00 UTC (APAC, EMEA)
|
|
39
|
+
1672574400000, # 2023-01-01 12:00:00 UTC (EMEA)
|
|
40
|
+
1672588800000, # 2023-01-01 16:00:00 UTC (EMEA, AMER)
|
|
41
|
+
1672599600000, # 2023-01-01 19:00:00 UTC (AMER)
|
|
42
|
+
1672610400000, # 2023-01-01 22:00:00 UTC (APAC)
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
expectations = [ ['APAC'], ['APAC', 'EMEA'], ['EMEA'], ['EMEA','AMER'], ['AMER'], ['APAC']]
|
|
46
|
+
|
|
47
|
+
i = 0
|
|
48
|
+
for ts in test_timestamps:
|
|
49
|
+
expectation = expectations[i]
|
|
50
|
+
actual = timestamp_to_active_trading_regions(ts)
|
|
51
|
+
assert(expectation==actual)
|
|
52
|
+
i+=1
|
|
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
|
+
)
|