siglab-py 0.5.30__py3-none-any.whl → 0.6.18__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (34) hide show
  1. siglab_py/backtests/__init__.py +0 -0
  2. siglab_py/backtests/backtest_core.py +2371 -0
  3. siglab_py/backtests/coinflip_15m_crypto.py +432 -0
  4. siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
  5. siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +468 -0
  6. siglab_py/constants.py +5 -0
  7. siglab_py/exchanges/binance.py +38 -0
  8. siglab_py/exchanges/deribit.py +83 -0
  9. siglab_py/exchanges/futubull.py +11 -2
  10. siglab_py/market_data_providers/candles_provider.py +2 -2
  11. siglab_py/market_data_providers/candles_ta_provider.py +3 -3
  12. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +6 -4
  13. siglab_py/market_data_providers/google_monitor.py +320 -0
  14. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  15. siglab_py/market_data_providers/tg_monitor.py +6 -2
  16. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  17. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  18. siglab_py/ordergateway/gateway.py +97 -35
  19. siglab_py/tests/integration/market_data_util_tests.py +37 -1
  20. siglab_py/tests/unit/analytic_util_tests.py +37 -10
  21. siglab_py/tests/unit/simple_math_tests.py +252 -0
  22. siglab_py/tests/unit/trading_util_tests.py +0 -21
  23. siglab_py/util/analytic_util.py +195 -33
  24. siglab_py/util/datetime_util.py +39 -0
  25. siglab_py/util/market_data_util.py +184 -65
  26. siglab_py/util/notification_util.py +1 -1
  27. siglab_py/util/retry_util.py +6 -1
  28. siglab_py/util/simple_math.py +262 -0
  29. siglab_py/util/trading_util.py +0 -12
  30. {siglab_py-0.5.30.dist-info → siglab_py-0.6.18.dist-info}/METADATA +1 -1
  31. siglab_py-0.6.18.dist-info/RECORD +50 -0
  32. {siglab_py-0.5.30.dist-info → siglab_py-0.6.18.dist-info}/WHEEL +1 -1
  33. siglab_py-0.5.30.dist-info/RECORD +0 -39
  34. {siglab_py-0.5.30.dist-info → siglab_py-0.6.18.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,432 @@
1
+ '''
2
+ Command line:
3
+ python coinflip_15m_crypto.py --white_list_tickers SOL/USDT:USDT --reference_ticker SOL/USDT:USDT --force_reload N --block_entries_on_impacting_ecoevents N
4
+
5
+ Debug from vscode, Launch.json:
6
+ {
7
+ "version": "0.2.0",
8
+ "configurations": [
9
+ {
10
+ "name": "Python: Current File",
11
+ "type": "python",
12
+ "request": "launch",
13
+ "program": "${file}",
14
+ "console": "integratedTerminal",
15
+ "justMyCode": true,
16
+ "args" : [
17
+ "--white_list_tickers", "SOL/USDT:USDT",
18
+ "--reference_ticker", "SOL/USDT:USDT",
19
+ "--force_reload", "N",
20
+ "--block_entries_on_impacting_ecoevents", "N"
21
+ ]
22
+ }
23
+ ]
24
+ }
25
+ '''
26
+ import os
27
+ import sys
28
+ import argparse
29
+ import json
30
+ from datetime import datetime, timedelta, timezone
31
+ import time
32
+ from typing import Dict, List, Tuple, Any, Callable
33
+ import pandas as pd
34
+
35
+ from ccxt.base.exchange import Exchange
36
+ from ccxt.bybit import bybit
37
+
38
+ from backtest_core import parseargs, get_logger, spawn_parameters, generic_pnl_eval, generic_tp_eval, generic_sort_filter_universe, run_all_scenario, dump_trades_to_disk
39
+
40
+ PYPY_COMPAT : bool = True
41
+
42
+ sys.path.append('../gizmo')
43
+ # from market_data_gizmo import fetch_historical_price, fetch_candles, fix_column_types, compute_candles_stats, partition_sliding_window, estimate_fib_retracement
44
+ base_dir : str = f"{os.path.dirname(sys.path[0])}\\single_leg_ta"
45
+
46
+ REPORT_NAME : str = "coinflip_15m_crypto"
47
+ CACHE_CANDLES : str = f"{os.path.dirname(sys.path[0])}\\cache\\candles"
48
+
49
+ white_list_tickers : List[str] = [ "SOL/USDT:USDT" ]
50
+
51
+ force_reload : bool = False
52
+
53
+ num_candles_limit = 100 # Depends on exchange but generally 100 ok!
54
+ param = {
55
+ 'apiKey' : None,
56
+ 'secret' : None,
57
+ 'password' : None, # Other exchanges dont require this! This is saved in exchange.password!
58
+ 'subaccount' : None,
59
+ 'rateLimit' : 100, # In ms
60
+ 'options' : {
61
+ 'defaultType': 'linear',
62
+ 'leg_room_bps' : 5,
63
+ 'trade_fee_bps' : 10,
64
+
65
+ 'list_ts_field' : 'listTime' # list_ts_field: Response field in exchange.markets[symbol] to indiate timestamp of symbol's listing date in ms. For bybit, markets['launchTime'] is list date. For okx, it's markets['listTime'].
66
+ }
67
+ }
68
+
69
+ exchanges = [
70
+ bybit(param),
71
+ ]
72
+
73
+ exchanges[0].name='bybit_linear'
74
+
75
+ commission_bps : float = 5
76
+
77
+ '''
78
+ ******** STRATEGY_SPECIFIC parameters ********
79
+ '''
80
+ additional_trade_fields : List[str] = [
81
+ # Add fields you want to include in trade extract
82
+ ]
83
+
84
+
85
+ '''
86
+ ******** GENERIC parameters ********
87
+ '''
88
+ strategy_mode_values : List[str]= [ 'long_short'] # 'long_only', 'short_only', 'long_short'
89
+
90
+ '''
91
+ For example, Monday's are weird. Entries, SL adjustments ...etc may have STRATEGY_SPECIFIC logic around this.
92
+ '''
93
+ CAUTIOUS_DAYOFWEEK : List[int] = [ 0 ]
94
+ how_many_last_candles : int = 3
95
+ last_candles_timeframe : str = 'lo' # Either hi or lo (default)
96
+ enable_wait_entry : bool = True
97
+ enable_sliced_entry : bool = False
98
+ enable_athatl_logic : bool = False # If you have special logic in 'allow_entry_initial' or 'allow_entry_final'.
99
+
100
+ '''
101
+ Economic events comes from 'economic_calanedar.csv' in same folder.
102
+
103
+ Block entries if pending economic event in next x-intervals (applied on lo timeframe)
104
+ Set to -1 to disable this.
105
+ '''
106
+ adj_sl_on_ecoevents = False
107
+ block_entries_on_impacting_ecoevents = True
108
+ num_intervals_block_pending_ecoevents = 3
109
+ ECOEVENTS_MAPPED_REGIONS = [ 'united_states' ]
110
+
111
+ mapped_event_codes = [
112
+ 'core_inflation_rate_mom', 'core_inflation_rate_yoy',
113
+ 'inflation_rate_mom', 'inflation_rate_yoy',
114
+ 'fed_interest_rate_decision',
115
+ 'fed_chair_speech',
116
+ 'core_pce_price_index_mom',
117
+ 'core_pce_price_index_yoy',
118
+ 'unemployment_rate',
119
+ 'non_farm_payrolls',
120
+ 'gdp_growth_rate_qoq_adv',
121
+ 'gdp_growth_rate_qoq_final',
122
+ 'gdp_growth_rate_yoy'
123
+ ]
124
+
125
+ num_intervals_current_ecoevents = 8
126
+
127
+ sl_num_intervals_delay_values : List[float] = [ 15*4*8 ]
128
+ sl_hard_percent_values : List[float] = [ 2.5 ]
129
+ sl_percent_trailing_values : List[float] = [ 35 ]
130
+ use_gradual_tightened_trailing_stops : bool = True
131
+ trailing_stop_mode : str = "linear" # linear or parabolic
132
+
133
+ '''
134
+ This is for trailing stops slope calc.
135
+ Say if your trade's max profit potential is tp_max_percent=3%=300bps.
136
+ tp_min_percent = 0.3 means you will NOT TP until at least pnl > 0.3% or 30bps.
137
+ '''
138
+ tp_min_percent = 3
139
+ tp_max_percent = 5
140
+
141
+ POST_MOVE_NUM_INTERVALS : int = 24*3
142
+ POST_MOVE_PERCENT_THRESHOLD : int = 3
143
+
144
+ enable_hi_timeframe_confirm : bool = True
145
+
146
+ start_dates : List[datetime] = [
147
+ datetime(2024, 4, 1)
148
+ ]
149
+
150
+ hi_how_many_candles_values : List[Tuple[str, int, int]] = [
151
+ ('1h', 24*7, 24*572)
152
+ ]
153
+
154
+ lo_how_many_candles_values : List[Tuple[str, int, int]] = [
155
+ ('15m', 15 *10, 15*4*24 *572)
156
+ ]
157
+
158
+ hi_ma_short_vs_long_interval_values : List[Tuple[int, int]] = [ (12, 30) ]
159
+ lo_ma_short_vs_long_interval_values : List[Tuple[int, int]] = [ (5, 10) ]
160
+
161
+ rsi_sliding_window_how_many_candles : int = 14 # For RSI, 14 is standard. If you want see spikes >70 and <30, use this config.
162
+ rsi_trend_sliding_window_how_many_candles : int = 30 # This is for purpose of RSI trend identification (Locating local peaks/troughs in RSI). This should typically be multiples of 'rsi_sliding_window_how_many_candles'.
163
+ rsi_upper_threshold_values : List[float] = [ 60 ]
164
+ rsi_lower_threshold_values : List[float] = [ 40 ]
165
+ rsi_midrangeonly : bool = False
166
+
167
+ target_fib_level : float = 0.618
168
+ boillenger_std_multiples_values : List[float] = [ 2 ]
169
+ allow_entry_sit_bb : bool = True
170
+ hurst_exp_window_how_many_candles : int = 125 # For hurst, at least 125.
171
+
172
+
173
+ # 'strategy_mode' decides if strategy can long_only, short_only, long_short at get go of back test. If long_above_btc_ema_short_below==True, strategy can long at bottom only if BTC (General market) stands above say 90d EMA. Or short only if BTC below 90d EMA for the given point in time.
174
+ ref_ema_num_days_fast : int = 5
175
+ ref_ema_num_days_slow : int = 90
176
+ long_above_ref_ema_short_below : bool = True
177
+ ref_price_vs_ema_percent_threshold : float = 2
178
+ ath_atl_close_gap_threshold_percent : float = 3
179
+
180
+ ema_short_slope_threshold_values : List[float] = [ 999 ] # 999 essentially turn it off
181
+
182
+ initial_cash_values : List[float] = [ 100000 ]
183
+
184
+ entry_percent_initial_cash_values : List[float] = [ 70 ]
185
+ target_position_size_percent_total_equity_values : List[float] = [ 100 ]
186
+ min_volume_usdt_threshold_values : List[float] = [ 100000 ]
187
+ clip_order_notional_to_best_volumes : bool = False
188
+ constant_order_notional : bool = True if min(start_dates) <= datetime(2024,1,1) else False # This is avoid snowball effect in long dated back tests
189
+
190
+ dayofweek_adj_map_order_notional : Dict = {
191
+ 0 : 1,
192
+ 1 : 1,
193
+ 2 : 1,
194
+ 3 : 1,
195
+ 4 : 1,
196
+ 5 : 1,
197
+ 6 : 1
198
+ }
199
+
200
+ dayofweek_sl_adj_map : Dict = {
201
+ 0 : 1,
202
+ 1 : 1,
203
+ 2 : 1,
204
+ 3 : 1,
205
+ 4 : 1,
206
+ 5 : 1,
207
+ 6 : 0.5
208
+ }
209
+
210
+ # Segmentation related parameters https://norman-lm-fung.medium.com/time-series-slicer-and-price-pattern-extractions-81f9dd1108fd
211
+ sliding_window_ratio : float = 16
212
+ smoothing_window_size_ratio : int = 3
213
+ linregress_stderr_threshold : float = 10
214
+ max_recur_depth : int = 2
215
+ min_segment_size_how_many_candles : int = 15
216
+ segment_consolidate_slope_ratio_threshold : float = 2
217
+ sideway_price_condition_threshold : float = 0.05 # i.e. Price if stay within 5% between start and close it's considered 'Sideway' market.
218
+
219
+ ECONOMIC_CALENDARS_FILE : str = "economic_calanedar_archive.csv"
220
+
221
+ default_level_granularity : float = 0.001
222
+
223
+ args = parseargs()
224
+ force_reload = args['force_reload']
225
+ white_list_tickers : List[str] = args['white_list_tickers']
226
+ reference_ticker : str = args['reference_ticker']
227
+ block_entries_on_impacting_ecoevents = args['block_entries_on_impacting_ecoevents']
228
+ enable_sliced_entry = args['enable_sliced_entry']
229
+ asymmetric_tp_bps : int = args['asymmetric_tp_bps']
230
+
231
+ full_report_name = f"{REPORT_NAME}_{start_dates[0].strftime('%Y%m%d')}"
232
+ trade_extract_filename : str = f"{full_report_name}_{white_list_tickers[0].replace(':','').replace('/','')}_trades.csv"
233
+
234
+ logger = get_logger(full_report_name)
235
+
236
+ import inspect
237
+ import builtins
238
+ def is_external(obj):
239
+ if inspect.ismodule(obj):
240
+ return True
241
+ module = getattr(obj, '__module__', None)
242
+ return module and not module.startswith('__') # Exclude built-in/dunder modules
243
+
244
+ local_vars = {
245
+ k: v
246
+ for k, v in locals().items()
247
+ if not (k.startswith('__') and k.endswith('__')) # Exclude dunders
248
+ and not is_external(v) # Exclude anything from external modules
249
+ }
250
+
251
+ algo_params : List[Dict] = spawn_parameters(local_vars)
252
+
253
+ logger.info(f"#algo_params: {len(algo_params)}")
254
+
255
+
256
+ '''
257
+ ******** STRATEGY_SPECIFIC Logic here ********
258
+ a. order_notional_adj
259
+ Specific logic to adjust order sizes based on market condition(s) for example.
260
+ b. entry (initial + final)
261
+ 'allow_entry_initial' is first pass entry conditions determination.
262
+ If 'allow_entry_initial' allow entry, 'allow_entry_final' will perform the second pass entry condition determinations.
263
+ 'allow_entry_final' is generally for more expensive operations, keep 'allow_entry_initial' fast and nimble.
264
+ c. 'pnl_eval' (You may wish to use specific prices to mark your TPs)
265
+ d. 'tp_eval' (Logic to fire TP)
266
+ e. 'sl_adj'
267
+ Adjustment to sl_percent_hard
268
+ f. 'trailing_stop_threshold_eval'
269
+ g. 'sort_filter_universe' (optional, if 'white_list_tickers' only has one ticker for example, then you don't need bother)
270
+ h. 'additional_trade_fields' to be included in the trade extract file
271
+ '''
272
+ def order_notional_adj(
273
+ algo_param : Dict,
274
+ ) -> Dict[str, float]:
275
+ initial_cash : float = algo_param['initial_cash']
276
+ entry_percent_initial_cash : float = algo_param['entry_percent_initial_cash']
277
+ target_order_notional = initial_cash * entry_percent_initial_cash/100
278
+ return {
279
+ 'target_order_notional' : target_order_notional
280
+ }
281
+
282
+ def allow_entry_initial(
283
+ lo_row_tm1,
284
+ hi_row_tm1
285
+ ) -> Dict[str, bool]:
286
+ class KOL:
287
+ def scream(self):
288
+ import random
289
+ x = random.uniform(-1,1)
290
+ if x>=0:
291
+ return "bullish"
292
+ else:
293
+ return "bearish"
294
+
295
+ influencer = KOL()
296
+
297
+ allow_long, allow_short = False, False
298
+ if influencer.scream()=="bullish":
299
+ allow_long = (
300
+ True
301
+ # and hi_row_tm1['close']>hi_row_tm1['ema_close'] # Can add a little something, for example a trend filter, to see if it can change things up?
302
+ )
303
+ elif influencer.scream()=="bearish":
304
+ allow_short = (
305
+ True
306
+ # and hi_row_tm1['close']<hi_row_tm1['ema_close']
307
+ )
308
+
309
+ return {
310
+ 'long' : allow_long,
311
+ 'short' : allow_short
312
+ }
313
+
314
+ def allow_entry_final(
315
+ lo_row,
316
+ algo_param : Dict
317
+
318
+ ) -> bool:
319
+ open : float = lo_row['open']
320
+
321
+ entry_price_long, entry_price_short = open, open
322
+ allow_long, allow_short = True, True
323
+ reference_price = None
324
+
325
+ pnl_potential_bps = algo_param['tp_max_percent']*100
326
+
327
+ target_price_long = entry_price_long * (1 + pnl_potential_bps/10000)
328
+ target_price_short = entry_price_short * (1 - pnl_potential_bps/10000)
329
+
330
+ return {
331
+ 'long' : allow_long,
332
+ 'short' : allow_short,
333
+
334
+ # In additional to allow or not, allow_entry_final also calculate a few things which you may need to mark the entry trades.
335
+ 'entry_price_long' : entry_price_long,
336
+ 'entry_price_short' : entry_price_short,
337
+ 'target_price_long' : target_price_long,
338
+ 'target_price_short' : target_price_short,
339
+ 'reference_price' : reference_price
340
+ }
341
+
342
+ allow_slice_entry = allow_entry_initial
343
+
344
+ def sl_adj(
345
+ max_unrealized_pnl_live : float,
346
+ current_position_usdt : float,
347
+ algo_param : Dict
348
+ ):
349
+ tp_min_percent = algo_param['tp_min_percent']
350
+ max_pnl_percent_notional = max_unrealized_pnl_live / current_position_usdt * 100
351
+ running_sl_percent_hard = algo_param['sl_hard_percent']
352
+ return {
353
+ 'running_sl_percent_hard' : running_sl_percent_hard
354
+ }
355
+
356
+ def trailing_stop_threshold_eval(
357
+ algo_param : Dict
358
+ ) -> Dict[str, float]:
359
+ tp_min_percent = algo_param['tp_min_percent']
360
+ tp_max_percent = algo_param['tp_max_percent']
361
+ return {
362
+ 'tp_min_percent' : tp_min_percent,
363
+ 'tp_max_percent' : tp_max_percent
364
+ }
365
+
366
+ def pnl_eval (
367
+ this_candle,
368
+ lo_row_tm1,
369
+ running_sl_percent_hard : float,
370
+ this_ticker_open_trades : List[Dict],
371
+ algo_param : Dict
372
+ ) -> Dict[str, float]:
373
+ return generic_pnl_eval(
374
+ this_candle,
375
+ running_sl_percent_hard,
376
+ this_ticker_open_trades,
377
+ algo_param,
378
+ long_tp_indicator_name=None,
379
+ short_tp_indicator_name=None
380
+ )
381
+
382
+ def tp_eval (
383
+ this_ticker_open_positions_side : str,
384
+ lo_row,
385
+ this_ticker_open_trades : List[Dict],
386
+ algo_param : Dict
387
+ ) -> bool:
388
+ '''
389
+ Be very careful, backtest_core 'generic_pnl_eval' may use a) some indicator (tp_indicator_name), or b) target_price to evaluate 'unrealized_pnl_tp'.
390
+ 'tp_eval' only return True or False but it needs be congruent with backtest_core 'generic_pnl_eval', otherwise incorrect rosy pnl may be reported.
391
+ '''
392
+ return generic_tp_eval(lo_row, this_ticker_open_trades)
393
+
394
+ def sort_filter_universe(
395
+ tickers : List[str],
396
+ exchange : Exchange,
397
+
398
+ # Use "i" (row index) to find current/last interval's market data or TAs from "all_exchange_candles"
399
+ i,
400
+ all_exchange_candles : Dict[str, Dict[str, Dict[str, pd.DataFrame]]],
401
+
402
+ max_num_tickers : int = 10
403
+ ) -> List[str]:
404
+ return generic_sort_filter_universe(
405
+ tickers=tickers,
406
+ exchange=exchange,
407
+ i=i,
408
+ all_exchange_candles=all_exchange_candles,
409
+ max_num_tickers=max_num_tickers
410
+ )
411
+
412
+ algo_results : List[Dict] = run_all_scenario(
413
+ algo_params=algo_params,
414
+ exchanges=exchanges,
415
+ order_notional_adj_func=order_notional_adj,
416
+ allow_entry_initial_func=allow_entry_initial,
417
+ allow_entry_final_func=allow_entry_final,
418
+ allow_slice_entry_func=allow_slice_entry,
419
+ sl_adj_func=sl_adj,
420
+ trailing_stop_threshold_eval_func=trailing_stop_threshold_eval,
421
+ pnl_eval_func=pnl_eval,
422
+ tp_eval_func=tp_eval,
423
+ sort_filter_universe_func=sort_filter_universe,
424
+
425
+ logger=logger
426
+ )
427
+
428
+ dump_trades_to_disk(
429
+ algo_results,
430
+ trade_extract_filename,
431
+ logger
432
+ )