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.
Files changed (45) hide show
  1. siglab_py/algo/__init__.py +0 -0
  2. siglab_py/algo/macdrsi_crosses_15m_tc_strategy.py +107 -0
  3. siglab_py/algo/strategy_base.py +122 -0
  4. siglab_py/algo/strategy_executor.py +1308 -0
  5. siglab_py/algo/tp_algo.py +529 -0
  6. siglab_py/backtests/__init__.py +0 -0
  7. siglab_py/backtests/backtest_core.py +2405 -0
  8. siglab_py/backtests/coinflip_15m_crypto.py +432 -0
  9. siglab_py/backtests/fibonacci_d_mv_crypto.py +541 -0
  10. siglab_py/backtests/macdrsi_crosses_15m_tc_crypto.py +473 -0
  11. siglab_py/constants.py +26 -1
  12. siglab_py/exchanges/binance.py +38 -0
  13. siglab_py/exchanges/deribit.py +83 -0
  14. siglab_py/exchanges/futubull.py +33 -3
  15. siglab_py/market_data_providers/candles_provider.py +11 -10
  16. siglab_py/market_data_providers/candles_ta_provider.py +5 -5
  17. siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py +238 -0
  18. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +224 -0
  19. siglab_py/market_data_providers/google_monitor.py +320 -0
  20. siglab_py/market_data_providers/orderbooks_provider.py +15 -12
  21. siglab_py/market_data_providers/tg_monitor.py +428 -0
  22. siglab_py/market_data_providers/{test_provider.py → trigger_provider.py} +9 -8
  23. siglab_py/ordergateway/client.py +172 -41
  24. siglab_py/ordergateway/encrypt_keys_util.py +1 -1
  25. siglab_py/ordergateway/gateway.py +456 -344
  26. siglab_py/ordergateway/test_ordergateway.py +8 -7
  27. siglab_py/tests/integration/market_data_util_tests.py +80 -6
  28. siglab_py/tests/unit/analytic_util_tests.py +67 -4
  29. siglab_py/tests/unit/market_data_util_tests.py +96 -0
  30. siglab_py/tests/unit/simple_math_tests.py +252 -0
  31. siglab_py/tests/unit/trading_util_tests.py +65 -0
  32. siglab_py/util/analytic_util.py +484 -66
  33. siglab_py/util/datetime_util.py +39 -0
  34. siglab_py/util/market_data_util.py +564 -74
  35. siglab_py/util/module_util.py +40 -0
  36. siglab_py/util/notification_util.py +78 -0
  37. siglab_py/util/retry_util.py +16 -3
  38. siglab_py/util/simple_math.py +262 -0
  39. siglab_py/util/slack_notification_util.py +59 -0
  40. siglab_py/util/trading_util.py +118 -0
  41. {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -13
  42. siglab_py-0.6.33.dist-info/RECORD +56 -0
  43. {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
  44. siglab_py-0.1.19.dist-info/RECORD +0 -31
  45. {siglab_py-0.1.19.dist-info → siglab_py-0.6.33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,541 @@
1
+ '''
2
+ Command line:
3
+ python fibonacci_d_mv_crypto.py --white_list_tickers BTC/USDT:USDT --reference_ticker BTC/USDT:USDT
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", "BTC/USDT:USDT",
18
+ "--reference_ticker", "BTC/USDT:USDT"
19
+ ]
20
+ }
21
+ ]
22
+ }
23
+ '''
24
+ import os
25
+ import sys
26
+ from datetime import datetime
27
+ from typing import Dict, List, Tuple, Callable, Union
28
+ import pandas as pd
29
+
30
+ from ccxt.base.exchange import Exchange
31
+ from ccxt.binance import binance # Use any crypto exchange that support the pair you want to trade
32
+
33
+ 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
34
+
35
+ PYPY_COMPAT : bool = False
36
+
37
+ sys.path.append('../gizmo')
38
+ # from market_data_gizmo import fetch_historical_price, fetch_candles, fix_column_types, compute_candles_stats, partition_sliding_window, estimate_fib_retracement
39
+ base_dir : str = f"{os.path.dirname(sys.path[0])}\\single_leg_ta"
40
+
41
+ REPORT_NAME : str = "backtest_fibonacci_strategy_d_mv_crypto"
42
+ CACHE_CANDLES : str = f"{os.path.dirname(sys.path[0])}\\cache\\candles"
43
+
44
+ white_list_tickers : List[str] = [ 'BTC/USDT' ]
45
+
46
+ force_reload : bool = True
47
+
48
+ '''
49
+ To reuse previously pulled candles or candles from other utility, specify file name here.
50
+ https://github.com/r0bbar/siglab/blob/master/siglab_py/market_data_providers/ccxt_candles_ta_to_csv.py
51
+ https://github.com/r0bbar/siglab/blob/master/siglab_py/market_data_providers/futu_candles_ta_to_csv.py
52
+ '''
53
+ reference_candles_file : Union[str, None] = None
54
+ hi_candles_file : Union[str, None] = None
55
+ lo_candles_file : Union[str, None] = None
56
+
57
+ num_candles_limit = 100 # Depends on exchange but generally 100 ok!
58
+ param = {
59
+ 'apiKey' : None,
60
+ 'secret' : None,
61
+ 'password' : None, # Other exchanges dont require this! This is saved in exchange.password!
62
+ 'subaccount' : None,
63
+ 'rateLimit' : 100, # In ms
64
+ 'options' : {
65
+ 'defaultType': 'spot',
66
+ 'leg_room_bps' : 5,
67
+ 'trade_fee_bps' : 10,
68
+
69
+ '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'].
70
+ }
71
+ }
72
+
73
+ exchanges : List[Exchange] = [
74
+ binance(param), # type: ignore
75
+ ]
76
+
77
+ exchanges[0].name='binance_linear' # type: ignore
78
+
79
+ commission_bps : float = 5
80
+
81
+ '''
82
+ ******** STRATEGY_SPECIFIC parameters ********
83
+ '''
84
+ target_fib_level : float = 0.618
85
+
86
+ additional_trade_fields : List[str] = [ # These are fields I wish to include in trade extract file for examination
87
+ 'lo_tm1_close',
88
+
89
+ 'hi_tm1_normalized_ema_long_slope',
90
+
91
+ 'hi_tm1_min_short_periods',
92
+ 'hi_tm1_idmin_short_periods',
93
+ 'hi_tm1_idmin_dt_short_periods',
94
+ 'hi_tm1_max_short_periods',
95
+ 'hi_tm1_idmax_short_periods',
96
+ 'hi_tm1_idmax_dt_short_periods',
97
+
98
+ 'hi_tm1_min_long_periods',
99
+ 'hi_tm1_idmin_long_periods',
100
+ 'hi_tm1_idmin_dt_long_periods',
101
+ 'hi_tm1_max_long_periods',
102
+ 'hi_tm1_idmax_long_periods',
103
+ 'hi_tm1_idmax_dt_long_periods',
104
+ ]
105
+
106
+
107
+ '''
108
+ ******** GENERIC parameters ********
109
+ '''
110
+ strategy_mode_values : List[str]= [ 'long_short'] # 'long_only', 'short_only', 'long_short'
111
+
112
+
113
+ start_dates : List[datetime] = [
114
+ # datetime(2024, 4, 1),
115
+ # datetime(2023, 1,1)
116
+ datetime(2021, 3, 1),
117
+ ]
118
+
119
+ # 'hi' refers to 'higher timeframe'
120
+ hi_how_many_candles_values : List[Tuple[str, int, int]] = [
121
+ # ('1d', 30, 478),
122
+ # ('1d', 30, 887),
123
+ ('1d', 30, 1595),
124
+ ]
125
+
126
+ '''
127
+ 'lo' refers to 'lower timeframe': Tuple (interval, sliding window size, total num candles to fetch)
128
+ - sliding window size: backtest_core. will parse into 'lo_stats_computed_over_how_many_candles', which will be passed as 'sliding_window_how_many_candles' to 'compute_candles_stats'. Things like EMAs depends on this.
129
+ - total num candles to fetch: It's just your test duration from start to end, how many candles are there?
130
+ '''
131
+ lo_how_many_candles_values : List[Tuple[str, int, int]] = [
132
+ # ('1h', 24, 24*478),
133
+ # ('1h', 24, 24*887),
134
+ ('1h', 24, 24*1595),
135
+ ]
136
+
137
+ # For 'lower timeframe' as well as 'higher timeframe', EMA's are evaluated with 'long periods' and 'short periods'. In example below, 'long' is 24 hours, 'short' is 8 hours.
138
+ hi_ma_short_vs_long_interval_values : List[Tuple[int, int]] = [ (15, 30) ]
139
+ lo_ma_short_vs_long_interval_values : List[Tuple[int, int]] = [ (8, 24) ]
140
+
141
+ # '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.
142
+ ref_ema_num_days_fast : int = 30
143
+ ref_ema_num_days_slow : int = 90
144
+ long_above_ref_ema_short_below : bool = False
145
+
146
+ '''
147
+ For example, Monday's are weird. Entries, SL adjustments ...etc may have STRATEGY_SPECIFIC logic around day of week, or not.
148
+ '''
149
+ CAUTIOUS_DAYOFWEEK : List[int] = [0,1,2,3,4]
150
+ how_many_last_candles : int = 3 # How many candles to be included in Reversal Check? Note the last is always 'current' candle.
151
+ last_candles_timeframe : str = 'hi' # Either hi or lo (default)
152
+ enable_sliced_entry : bool = True # This relates to param 'entry_percent_initial_cash_values' as well: If you entry notional say 33% of total equity. If enable_sliced_entry=True, your entries can be done in slices.
153
+
154
+ initial_cash_values : List[float] = [ 100000 ]
155
+ entry_percent_initial_cash_values : List[float] = [ 70 ]
156
+ target_position_size_percent_total_equity_values : List[float] = [ 100 ]
157
+ min_volume_usdt_threshold_values : List[float] = [ 100000 ]
158
+ clip_order_notional_to_best_volumes : bool = False
159
+ 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
160
+
161
+ '''
162
+ Economic events comes from 'economic_calanedar_archive.csv' in same folder.
163
+
164
+ Block entries if pending economic event in next x-intervals (applied on lo timeframe)
165
+ Set to -1 to disable this.
166
+ '''
167
+ adj_sl_on_ecoevents = False
168
+ block_entries_on_impacting_ecoevents = False
169
+ num_intervals_block_pending_ecoevents = 24
170
+ ECOEVENTS_MAPPED_REGIONS = [ 'united_states' ]
171
+
172
+ mapped_event_codes = [
173
+ 'core_inflation_rate_mom', 'core_inflation_rate_yoy',
174
+ 'inflation_rate_mom', 'inflation_rate_yoy',
175
+ 'fed_interest_rate_decision',
176
+ 'fed_chair_speech',
177
+ 'core_pce_price_index_mom',
178
+ 'core_pce_price_index_yoy',
179
+ 'unemployment_rate',
180
+ 'non_farm_payrolls',
181
+ 'gdp_growth_rate_qoq_adv',
182
+ 'gdp_growth_rate_qoq_final',
183
+ 'gdp_growth_rate_yoy'
184
+ ]
185
+
186
+ num_intervals_current_ecoevents = 8
187
+
188
+ sl_num_intervals_delay_values : List[float] = [ 0 ] # Delay before re-entry after SL in num intervals (lo timeframe)
189
+ sl_hard_percent_values : List[float] = [ 3 ] # Hard SL (Evaluated 'pessimistically': For example if you long and candle spike down -3%, even close -1%, SL will be triggered)
190
+ sl_percent_trailing_values : List[float] = [ 50 ] # How much give back to street for trailing stops to fire?
191
+ '''
192
+ References: 'calc_eff_trailing_sl' in backtest_core
193
+ https://github.com/r0bbar/siglab/blob/master/siglab_py/tests/manual/trading_util_tests.ipynb
194
+ https://medium.com/@norman-lm-fung/gradually-tightened-trailing-stops-f7854bf1e02b
195
+ '''
196
+ use_gradual_tightened_trailing_stops : bool = True
197
+ trailing_stop_mode : str = "linear" # linear or parabolic
198
+ tp_min_percent = 0.5
199
+ tp_max_percent = 5
200
+
201
+ dayofweek_adj_map_order_notional : Dict = {
202
+ 0 : 1,
203
+ 1 : 1,
204
+ 2 : 1,
205
+ 3 : 1,
206
+ 4 : 1,
207
+ 5 : 1,
208
+ 6 : 1
209
+ }
210
+
211
+ dayofweek_sl_adj_map : Dict = {
212
+ 0 : 1,
213
+ 1 : 1,
214
+ 2 : 1,
215
+ 3 : 1,
216
+ 4 : 1,
217
+ 5 : 1,
218
+ 6 : 1
219
+ }
220
+
221
+ POST_MOVE_NUM_INTERVALS : int = 24*2
222
+ POST_MOVE_PERCENT_THRESHOLD : int = 3
223
+
224
+ enable_hi_timeframe_confirm : bool = True
225
+
226
+ boillenger_std_multiples_values : List[float] = [ 2 ] # Even if your strategy don't use it, compute_candles_stats takes this as argument.
227
+ rsi_upper_threshold_values : List[float] = [ 70 ]
228
+ rsi_lower_threshold_values : List[float] = [ 30 ]
229
+ rsi_sliding_window_how_many_candles : int = 14 # For RSI, 14 is standard. If you want see spikes >70 and <30, use this config.
230
+ rsi_trend_sliding_window_how_many_candles : int = 24*7 # 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'.
231
+ hurst_exp_window_how_many_candles : int = 125 # For hurst, at least 125.
232
+
233
+ # Segmentation related parameters https://norman-lm-fung.medium.com/time-series-slicer-and-price-pattern-extractions-81f9dd1108fd
234
+ sliding_window_ratio : float = 16
235
+ smoothing_window_size_ratio : int = 3
236
+ linregress_stderr_threshold : float = 10
237
+ max_recur_depth : int = 2
238
+ min_segment_size_how_many_candles : int = 15
239
+ segment_consolidate_slope_ratio_threshold : float = 2
240
+ sideway_price_condition_threshold : float = 0.05 # i.e. Price if stay within 5% between start and close it's considered 'Sideway' market.
241
+
242
+ ECONOMIC_CALENDARS_FILE : str = "economic_calanedar_archive.csv"
243
+
244
+ args = parseargs()
245
+ force_reload = args['force_reload']
246
+ white_list_tickers : List[str] = args['white_list_tickers']
247
+ reference_ticker : str = args['reference_ticker']
248
+ asymmetric_tp_bps : int = args['asymmetric_tp_bps']
249
+
250
+ full_report_name = f"{REPORT_NAME}_{start_dates[0].strftime('%Y%m%d')}"
251
+ trade_extract_filename : str = f"{full_report_name}_{white_list_tickers[0].replace(':','').replace('/','')}_trades.csv"
252
+
253
+ logger = get_logger(full_report_name)
254
+
255
+ import inspect
256
+ import builtins
257
+ def is_external(obj):
258
+ if inspect.ismodule(obj):
259
+ return True
260
+ module = getattr(obj, '__module__', None)
261
+ return module and not module.startswith('__') # Exclude built-in/dunder modules
262
+
263
+ local_vars = {
264
+ k: v
265
+ for k, v in locals().items()
266
+ if not (k.startswith('__') and k.endswith('__')) # Exclude dunders
267
+ and not is_external(v) # Exclude anything from external modules
268
+ }
269
+
270
+ algo_params : List[Dict] = spawn_parameters(local_vars)
271
+
272
+ logger.info(f"#algo_params: {len(algo_params)}")
273
+
274
+
275
+ '''
276
+ ******** STRATEGY_SPECIFIC Logic here ********
277
+ a. order_notional_adj
278
+ Specific logic to adjust order sizes based on market condition(s) for example.
279
+ b. entry (initial + final)
280
+ 'allow_entry_initial' is first pass entry conditions determination.
281
+ If 'allow_entry_initial' allow entry, 'allow_entry_final' will perform the second pass entry condition determinations.
282
+ 'allow_entry_final' is generally for more expensive operations, keep 'allow_entry_initial' fast and nimble.
283
+ c. 'pnl_eval' (You may wish to use specific prices to mark your TPs)
284
+ d. 'tp_eval' (Logic to fire TP)
285
+ e. 'sl_adj'
286
+ Adjustment to sl_percent_hard
287
+ f. 'trailing_stop_threshold_eval'
288
+ g. 'sort_filter_universe' (optional, if 'white_list_tickers' only has one ticker for example, then you don't need bother)
289
+ h. 'additional_trade_fields' to be included in trade extract file (Entries only. Not applicable to exit trades.)
290
+ '''
291
+ def order_notional_adj(
292
+ algo_param : Dict,
293
+ ) -> Dict[str, float]:
294
+ initial_cash : float = algo_param['initial_cash']
295
+ entry_percent_initial_cash : float = algo_param['entry_percent_initial_cash']
296
+ '''
297
+ Slicing: If 100% target_order_notional on first entry, essentially you'd have disabled slicing. Alternative is adjust entry cash check.
298
+ '''
299
+ target_order_notional = initial_cash * entry_percent_initial_cash/100
300
+
301
+ return {
302
+ 'target_order_notional' : target_order_notional
303
+ }
304
+
305
+ def allow_entry_initial(
306
+ key : str,
307
+ lo_row_tm1,
308
+ hi_row_tm1,
309
+ hi_fib_eval_result
310
+ ) -> Dict[str, bool]:
311
+ return {
312
+ 'long' : _allow_entry_initial(key, 'long', lo_row_tm1, hi_row_tm1, hi_fib_eval_result),
313
+ 'short' : _allow_entry_initial(key, 'short', lo_row_tm1, hi_row_tm1, hi_fib_eval_result)
314
+ }
315
+ def _allow_entry_initial(
316
+ key : str,
317
+ long_or_short : str, # long or short
318
+ lo_row_tm1,
319
+ hi_row_tm1,
320
+ hi_fib_eval_result,
321
+ ) -> Dict[str, bool]:
322
+ if (
323
+ hi_row_tm1 is None
324
+ or (not hi_row_tm1['normalized_ema_long_slope'])
325
+ or not hi_fib_eval_result
326
+ ):
327
+ return False
328
+
329
+ if long_or_short == "long":
330
+ if (
331
+ lo_row_tm1['close']<hi_row_tm1['min_long_periods']
332
+ and hi_row_tm1['normalized_ema_long_slope']>0
333
+ and hi_row_tm1['close']>hi_row_tm1['open']
334
+ ):
335
+ return True
336
+ else:
337
+ return False
338
+ elif long_or_short == "short":
339
+ if (
340
+ lo_row_tm1['close']>hi_row_tm1['max_long_periods']
341
+ and hi_row_tm1['normalized_ema_long_slope']<0
342
+ and hi_row_tm1['close']<hi_row_tm1['open']
343
+ ):
344
+ return True
345
+ else:
346
+ return False
347
+ else:
348
+ raise ValueError("Long or Short?")
349
+
350
+ def allow_entry_final(
351
+ key : str,
352
+ exchange : Exchange,
353
+ lo_row,
354
+ hi_row_tm1,
355
+ hi_fib_eval_result,
356
+ reversal_camp_cache,
357
+ fetch_historical_price_func : Callable[..., float],
358
+ pd_reference_price_cache : pd.DataFrame,
359
+ algo_param : Dict
360
+ ) -> Dict[str, Union[bool, float, None]]:
361
+ reference_ticker = algo_param['reference_ticker']
362
+ timestamp_ms : int = lo_row['timestamp_ms']
363
+
364
+ entry_price_long = lo_row['open']
365
+ entry_price_short = lo_row['open']
366
+
367
+ reference_price = None
368
+ if algo_param['long_above_ref_ema_short_below']:
369
+ reference_price = fetch_historical_price_func(
370
+ exchange=exchange,
371
+ normalized_symbol=reference_ticker,
372
+ pd_reference_price_cache=pd_reference_price_cache,
373
+ timestamp_ms=timestamp_ms,
374
+ ref_timeframe='1m')
375
+
376
+ allow_long = True if (
377
+ hi_row_tm1['normalized_ema_long_slope']>0
378
+ and entry_price_long<hi_fib_eval_result['long_periods']['fib_target']
379
+ ) else False
380
+
381
+ allow_short = True if (
382
+ hi_row_tm1['normalized_ema_long_slope']<0
383
+ and entry_price_short>hi_fib_eval_result['long_periods']['fib_target']
384
+ ) else False
385
+
386
+ return {
387
+ 'long' : allow_long,
388
+ 'short' : allow_short,
389
+
390
+ # In additional to allow or not, allow_entry_final also calculate a few things which you may need to mark the entry trades.
391
+ 'entry_price_long' : entry_price_long,
392
+ 'entry_price_short' : entry_price_short,
393
+ 'target_price_long' : hi_fib_eval_result['long_periods']['fib_target'],
394
+ 'target_price_short' : hi_fib_eval_result['long_periods']['fib_target'],
395
+
396
+ 'reference_price' : reference_price
397
+ }
398
+
399
+ def allow_slice_entry(
400
+ lo_row,
401
+ last_candles,
402
+ algo_param : Dict,
403
+ pnl_percent_notional : float
404
+ ):
405
+ enable_sliced_entry = algo_param['enable_sliced_entry']
406
+
407
+ allow_slice_entry_long = enable_sliced_entry and (
408
+ (pnl_percent_notional>0)
409
+ or (last_candles[0]['is_green'] and last_candles[-2]['is_green'])
410
+ )
411
+ allow_slice_entry_short = enable_sliced_entry and (
412
+ (pnl_percent_notional>0)
413
+ or (not last_candles[0]['is_green'] and not last_candles[-2]['is_green'])
414
+ )
415
+
416
+ return {
417
+ 'long' : allow_slice_entry_long,
418
+ 'short' : allow_slice_entry_short
419
+ }
420
+
421
+ def sl_adj(
422
+ lo_row,
423
+ hi_row,
424
+ post_move_candles,
425
+ pos_side,
426
+ avg_entry_price,
427
+ this_ticker_open_trades : List[Dict],
428
+ algo_param : Dict
429
+ ):
430
+ running_sl_percent_hard = algo_param['sl_hard_percent']
431
+
432
+ if pos_side=='buy':
433
+ target_price = max([ trade['target_price'] for trade in this_ticker_open_trades]) # Most aggressive target
434
+ pnl_potential_bps = (target_price/avg_entry_price -1) *10000
435
+ else:
436
+ target_price = min([ trade['target_price'] for trade in this_ticker_open_trades]) # Most aggressive target
437
+ pnl_potential_bps = (avg_entry_price/target_price -1) *10000
438
+
439
+ running_sl_percent_hard = min(
440
+ running_sl_percent_hard,
441
+ (pnl_potential_bps/100) * 1.5
442
+ )
443
+
444
+ return {
445
+ 'running_sl_percent_hard' : running_sl_percent_hard
446
+ }
447
+
448
+
449
+ def trailing_stop_threshold_eval(
450
+ lo_row,
451
+ pos_side : str,
452
+ avg_entry_price : float,
453
+ this_ticker_open_trades : List[Dict],
454
+ algo_param : Dict
455
+ ) -> Dict[str, float]:
456
+ tp_min_percent = algo_param['tp_min_percent']
457
+ tp_max_percent = algo_param['tp_max_percent']
458
+
459
+ if pos_side=='buy':
460
+ target_price = max([ trade['target_price'] for trade in this_ticker_open_trades]) # Most aggressive target
461
+ pnl_potential_bps = (target_price/avg_entry_price -1) *10000
462
+
463
+ else:
464
+ target_price = min([ trade['target_price'] for trade in this_ticker_open_trades]) # Most aggressive target
465
+ pnl_potential_bps = (avg_entry_price/target_price -1) *10000
466
+
467
+ tp_max_percent = min((pnl_potential_bps/100), tp_max_percent)
468
+ tp_min_percent = max(tp_max_percent/3, tp_min_percent)
469
+
470
+ return {
471
+ 'tp_min_percent' : tp_min_percent,
472
+ 'tp_max_percent' : tp_max_percent
473
+ }
474
+
475
+ def pnl_eval (
476
+ this_candle,
477
+ lo_row_tm1,
478
+ running_sl_percent_hard : float,
479
+ this_ticker_open_trades : List[Dict],
480
+ algo_param : Dict
481
+ ) -> Dict[str, float]:
482
+ return generic_pnl_eval(
483
+ this_candle,
484
+ running_sl_percent_hard,
485
+ this_ticker_open_trades,
486
+ algo_param,
487
+ long_tp_indicator_name=None, # type: ignore
488
+ short_tp_indicator_name=None # type: ignore
489
+ )
490
+
491
+ def tp_eval (
492
+ this_ticker_open_positions_side : str,
493
+ lo_row,
494
+ this_ticker_open_trades : List[Dict],
495
+ algo_param : Dict
496
+ ) -> bool:
497
+ '''
498
+ 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'.
499
+ '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.
500
+ '''
501
+ return generic_tp_eval(lo_row, this_ticker_open_trades)
502
+
503
+ def sort_filter_universe(
504
+ tickers : List[str],
505
+ exchange : Exchange,
506
+
507
+ # Use "i" (row index) to find current/last interval's market data or TAs from "all_exchange_candles"
508
+ i,
509
+ all_exchange_candles : Dict[str, Dict[str, Dict[str, pd.DataFrame]]],
510
+
511
+ max_num_tickers : int = 10
512
+ ) -> List[str]:
513
+ return generic_sort_filter_universe(
514
+ tickers=tickers,
515
+ exchange=exchange,
516
+ i=i,
517
+ all_exchange_candles=all_exchange_candles,
518
+ max_num_tickers=max_num_tickers
519
+ )
520
+
521
+ algo_results : List[Dict] = run_all_scenario(
522
+ algo_params=algo_params,
523
+ exchanges=exchanges,
524
+ order_notional_adj_func=order_notional_adj, # type: ignore
525
+ allow_entry_initial_func=allow_entry_initial, # type: ignore
526
+ allow_entry_final_func=allow_entry_final, # type: ignore
527
+ allow_slice_entry_func=allow_slice_entry, # type: ignore
528
+ sl_adj_func=sl_adj,
529
+ trailing_stop_threshold_eval_func=trailing_stop_threshold_eval,
530
+ pnl_eval_func=pnl_eval,
531
+ tp_eval_func=tp_eval,
532
+ sort_filter_universe_func=sort_filter_universe,
533
+
534
+ logger=logger
535
+ )
536
+
537
+ dump_trades_to_disk(
538
+ algo_results,
539
+ trade_extract_filename,
540
+ logger
541
+ )