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.
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 +12 -2
  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 +4 -4
  18. siglab_py/market_data_providers/futu_candles_ta_to_csv.py +7 -2
  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 -347
  26. siglab_py/ordergateway/test_ordergateway.py +8 -7
  27. siglab_py/tests/integration/market_data_util_tests.py +75 -2
  28. siglab_py/tests/unit/analytic_util_tests.py +47 -12
  29. siglab_py/tests/unit/market_data_util_tests.py +45 -1
  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 +476 -67
  33. siglab_py/util/datetime_util.py +39 -0
  34. siglab_py/util/market_data_util.py +528 -98
  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.30.dist-info → siglab_py-0.6.33.dist-info}/METADATA +5 -9
  42. siglab_py-0.6.33.dist-info/RECORD +56 -0
  43. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/WHEEL +1 -1
  44. siglab_py-0.1.30.dist-info/RECORD +0 -34
  45. {siglab_py-0.1.30.dist-info → siglab_py-0.6.33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2405 @@
1
+ # type: ignore Sorry sorry
2
+ import os
3
+ import logging
4
+ import argparse
5
+ import arrow
6
+ from datetime import datetime, timedelta, timezone
7
+ import time
8
+ from typing import List, Dict, Any, Union, Callable
9
+ import uuid
10
+ import math
11
+ import json
12
+ import inspect
13
+ import pandas as pd
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib.dates as mdates
16
+
17
+ from ccxt.base.exchange import Exchange
18
+
19
+ from siglab_py.util.retry_util import retry
20
+ from siglab_py.util.market_data_util import fetch_candles, fix_column_types, fetch_historical_price, timestamp_to_week_of_month
21
+ from siglab_py.util.trading_util import calc_eff_trailing_sl
22
+ from siglab_py.util.analytic_util import compute_candles_stats, lookup_fib_target, partition_sliding_window
23
+ from siglab_py.util.simple_math import bucket_series, bucketize_val
24
+
25
+ def get_logger(report_name : str):
26
+ logging.Formatter.converter = time.gmtime
27
+ logger = logging.getLogger(report_name)
28
+ log_level = logging.INFO # DEBUG --> INFO --> WARNING --> ERROR
29
+ logger.setLevel(log_level)
30
+ format_str = '%(asctime)s %(message)s'
31
+ formatter = logging.Formatter(format_str)
32
+
33
+ sh = logging.StreamHandler()
34
+ sh.setLevel(log_level)
35
+ sh.setFormatter(formatter)
36
+ logger.addHandler(sh)
37
+ fh = logging.FileHandler(f"{report_name}.log", mode='w')
38
+ fh.setLevel(log_level)
39
+ fh.setFormatter(formatter)
40
+ logger.addHandler(fh)
41
+
42
+ return logger
43
+
44
+ def spawn_parameters(
45
+ flattened_parameters
46
+ ) -> List[Dict[str, Any]]:
47
+ algo_params : List[Dict[str, Any]] = []
48
+ for key in flattened_parameters:
49
+ _key = key.lower()
50
+ if _key in [ 'exchanges' ]:
51
+ continue
52
+
53
+ val = flattened_parameters[key]
54
+ if not algo_params:
55
+ assert(_key=="pypy_compat")
56
+ param_dict = {_key : val}
57
+ algo_params.append(param_dict)
58
+
59
+ else:
60
+ cloned_algo_params = None
61
+
62
+ for existing_algo_param in algo_params:
63
+ if type(val) not in [list, List]:
64
+ existing_algo_param[_key] = val
65
+ else:
66
+ if _key == 'hi_how_many_candles_values':
67
+ for x in val:
68
+ existing_algo_param['hi_stats_computed_over_how_many_candles'] = x[1]
69
+ existing_algo_param['hi_candle_size'] = x[0]
70
+ existing_algo_param['hi_how_many_candles'] = x[2]
71
+ elif _key == 'hi_ma_short_vs_long_interval_values':
72
+ for x in val:
73
+ existing_algo_param['hi_ma_short_interval'] = x[0]
74
+ existing_algo_param['hi_ma_long_interval'] = x[1]
75
+
76
+ elif _key == 'lo_how_many_candles_values':
77
+ for x in val:
78
+ existing_algo_param['lo_stats_computed_over_how_many_candles'] = x[1]
79
+ existing_algo_param['lo_candle_size'] = x[0]
80
+ existing_algo_param['lo_how_many_candles'] = x[2]
81
+
82
+ elif _key == 'lo_ma_short_vs_long_interval_values':
83
+ for x in val:
84
+ existing_algo_param['lo_ma_short_interval'] = x[0]
85
+ existing_algo_param['lo_ma_long_interval'] = x[1]
86
+
87
+ elif _key in [ 'white_list_tickers', 'additional_trade_fields', 'cautious_dayofweek', 'allow_entry_dayofweek', 'mapped_event_codes', 'ecoevents_mapped_regions' ]:
88
+ existing_algo_param[_key] = val
89
+
90
+ else:
91
+ if len(val)>1:
92
+ cloned_algo_params = []
93
+
94
+ if _key not in [ 'start_dates']:
95
+ _key = _key.replace("_values","")
96
+ elif _key == 'start_dates':
97
+ _key = 'start_date'
98
+
99
+ i = 0
100
+ for x in val:
101
+
102
+ if i==0:
103
+ existing_algo_param[_key] = x
104
+ else:
105
+ cloned_algo_param = existing_algo_param.copy()
106
+ cloned_algo_param[_key] = x
107
+ cloned_algo_params.append(cloned_algo_param)
108
+ i+=1
109
+
110
+ if cloned_algo_params:
111
+ algo_params = algo_params + cloned_algo_params
112
+ cloned_algo_params.clear()
113
+ cloned_algo_params = None
114
+
115
+ param_id : int = 0
116
+ for algo_param in algo_params:
117
+ start_date = algo_param.pop('start_date')
118
+ name_exclude_start_date = ""
119
+ for key in algo_param:
120
+ name_exclude_start_date += f"{key}: {algo_param[key]}|"
121
+ name = "start_date: {start_date}|" + name_exclude_start_date
122
+ algo_param['param_id'] = param_id
123
+ algo_param['start_date'] = start_date
124
+ algo_param['name'] = name
125
+ algo_param['name_exclude_start_date'] = name_exclude_start_date
126
+
127
+ # Purpose is to avoid snowball effect in equity curves in long dated back tests.
128
+ if 'constant_order_notional' not in algo_param:
129
+ algo_param['constant_order_notional'] = True
130
+ algo_param['target_order_notional'] = None
131
+ if algo_param['constant_order_notional']:
132
+ algo_param['target_order_notional'] = algo_param['initial_cash'] * algo_param['entry_percent_initial_cash']/100
133
+
134
+ param_id+=1
135
+ return algo_params
136
+
137
+ def create_plot_canvas(key : str, pd_hi_candles : pd.DataFrame, pd_lo_candles : pd.DataFrame):
138
+ SMALL_SIZE = 7
139
+ DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
140
+
141
+ plt.rc('figure', figsize=(25, 25))
142
+ plt.ion()
143
+ plt.rc('font', size=SMALL_SIZE)
144
+ plt.rc('axes', titlesize=SMALL_SIZE)
145
+ plt.rc('axes', labelsize=SMALL_SIZE)
146
+ plt.rc('xtick', labelsize=SMALL_SIZE)
147
+ plt.rc('ytick', labelsize=SMALL_SIZE)
148
+ plt.rc('legend', fontsize=SMALL_SIZE)
149
+ plt.rc('figure', titlesize=SMALL_SIZE)
150
+
151
+ fig, axes = plt.subplots(5, 1, gridspec_kw={'height_ratios': [3, 1, 1, 1, 1]})
152
+
153
+ date_numbers_hi = mdates.date2num(pd_hi_candles['datetime'])
154
+ date_numbers_lo = mdates.date2num(pd_lo_candles['datetime'])
155
+ major_locator = mdates.AutoDateLocator(minticks=3, maxticks=7)
156
+
157
+ for ax in axes:
158
+ ax.set_xticklabels([])
159
+ ax.minorticks_on()
160
+ ax.grid()
161
+ ax.xaxis.set_major_locator(major_locator)
162
+ ax.xaxis.set_major_formatter(mdates.DateFormatter(DATE_FORMAT))
163
+ ax.tick_params(axis="x", which='major', labelbottom=True, rotation=45)
164
+
165
+ time_series_canvas = axes[0]
166
+ time_series_canvas.minorticks_on()
167
+ time_series_canvas.grid()
168
+ time_series_canvas.set_ylabel(f'Close px and boillenger band {key}')
169
+ time_series_canvas.tick_params(axis="x", which='major', labelbottom=True, rotation=45)
170
+ time_series_canvas.plot(date_numbers_lo, pd_lo_candles['close'], color='darkblue', linewidth=2, label=f"close")
171
+ time_series_canvas.plot(date_numbers_hi, pd_hi_candles['boillenger_upper'], color='lightblue', linestyle='--', linewidth=0.5, label=f"upper boillenger (hi)")
172
+ time_series_canvas.plot(date_numbers_hi, pd_hi_candles['boillenger_lower'], color='lightblue', linestyle='--', linewidth=0.5, label=f"lower boillenger (hi)")
173
+ time_series_canvas.plot(date_numbers_lo, pd_lo_candles['boillenger_upper'], color='gray', linestyle='-', linewidth=1, label=f"upper boillenger (lo)")
174
+ time_series_canvas.plot(date_numbers_lo, pd_lo_candles['boillenger_lower'], color='gray', linestyle='-', linewidth=1, label=f"lower boillenger (lo)")
175
+ time_series_canvas.legend()
176
+
177
+ boillenger_channel_height_canvas = axes[1]
178
+ boillenger_channel_height_canvas.minorticks_on()
179
+ boillenger_channel_height_canvas.grid()
180
+ boillenger_channel_height_canvas.set_ylabel(f'boillenger channel height vs ATR band {key}')
181
+ boillenger_channel_height_canvas.tick_params(axis="x", which='major', labelbottom=True, rotation=45)
182
+ boillenger_channel_height_canvas.plot(date_numbers_hi, pd_hi_candles['boillenger_channel_height'], color='lightblue', linewidth=0.5, label=f"boillenger channel height (hi)")
183
+ boillenger_channel_height_canvas.plot(date_numbers_hi, pd_hi_candles['atr'], color='lightblue', linestyle='dashed', linewidth=0.5, label=f"ATR (hi)")
184
+ boillenger_channel_height_canvas.plot(date_numbers_lo, pd_lo_candles['boillenger_channel_height'], color='gray', linewidth=0.5, label=f"boillenger channel height (lo)")
185
+ boillenger_channel_height_canvas.plot(date_numbers_lo, pd_lo_candles['atr'], color='gray', linestyle='dashed', linewidth=0.5, label=f"ATR (lo)")
186
+ boillenger_channel_height_canvas.legend()
187
+
188
+ rsi_canvas = axes[2]
189
+ rsi_canvas.minorticks_on()
190
+ rsi_canvas.grid()
191
+ rsi_canvas.set_ylabel(f'RSI {key}')
192
+ rsi_canvas.tick_params(axis="x", which='major', labelbottom=True, rotation=45)
193
+ rsi_canvas.plot(date_numbers_hi, pd_hi_candles['rsi'], color='lightblue', linewidth=2, label=f"RSI (hi)")
194
+ rsi_canvas.plot(date_numbers_lo, pd_lo_candles['rsi'], color='gray', linestyle='dashed', linewidth=2, label=f"RSI (lo)")
195
+
196
+ macd_canvas_hi = axes[3]
197
+ macd_canvas_hi.minorticks_on()
198
+ macd_canvas_hi.grid()
199
+ macd_canvas_hi.set_ylabel(f'MACD hi {key}')
200
+ macd_canvas_hi.tick_params(axis="x", which='major', labelbottom=True, rotation=45)
201
+ macd_canvas_hi.plot(date_numbers_hi, pd_hi_candles['macd'], color='lightblue', linewidth=0.5, label=f"MACD (hi)")
202
+ macd_canvas_hi.plot(date_numbers_hi, pd_hi_candles['signal'], color='lightblue', linewidth=0.5, label=f"signal (hi)")
203
+ bar_colors = ['red' if value < 0 else 'green' for value in pd_hi_candles['macd_minus_signal']]
204
+ macd_canvas_hi.bar(date_numbers_hi, pd_hi_candles['macd_minus_signal'], width=0.005, color=bar_colors, label="MACD Histogram (hi)")
205
+
206
+ macd_canvas_lo = axes[4]
207
+ macd_canvas_lo.minorticks_on()
208
+ macd_canvas_lo.grid()
209
+ macd_canvas_lo.set_ylabel(f'MACD lo {key}')
210
+ macd_canvas_lo.tick_params(axis="x", which='major', labelbottom=True, rotation=45)
211
+ macd_canvas_lo.plot(date_numbers_lo, pd_lo_candles['macd'], color='gray', linewidth=0.5, label=f"MACD (lo)")
212
+ macd_canvas_lo.plot(date_numbers_lo, pd_lo_candles['signal'], color='gray', linewidth=0.5, label=f"signal (lo)")
213
+ bar_colors_lo = ['red' if value < 0 else 'green' for value in pd_lo_candles['macd_minus_signal']]
214
+ macd_canvas_lo.bar(date_numbers_lo, pd_lo_candles['macd_minus_signal'], width=0.005, color=bar_colors_lo, label="MACD Histogram (lo)")
215
+
216
+ return {
217
+ 'plt' : plt,
218
+ 'time_series_canvas' : time_series_canvas
219
+ }
220
+
221
+ def plot_segments(
222
+ pd_candles : pd.DataFrame,
223
+ ts_partitions : Dict,
224
+ jpg_filename : str = None
225
+ ):
226
+ import matplotlib.pyplot as plt
227
+ import matplotlib.gridspec as gridspec
228
+
229
+ minima = ts_partitions['minima']
230
+ maxima = ts_partitions['maxima']
231
+ segments = ts_partitions['segments']
232
+
233
+ fig = plt.figure(figsize=(15, 8), facecolor='black')
234
+ gs = gridspec.GridSpec(1, 1, height_ratios=[1])
235
+
236
+ # Price Chart
237
+ ax0 = plt.subplot(gs[0])
238
+ ax0.plot(pd_candles['datetime'], pd_candles['close'], label='Close', color='dodgerblue')
239
+ ax0.plot(pd_candles['datetime'], pd_candles['smoothed_close'], label='Smoothed Close', color='yellow')
240
+ ax0.plot(pd_candles['datetime'], pd_candles['ema_close'], label='3m EMA', linestyle='--', color='orange')
241
+ ax0.fill_between(pd_candles['datetime'], pd_candles['close'], pd_candles['ema_close'], where=(pd_candles['close'] > pd_candles['ema_close']), interpolate=True, color='dodgerblue', alpha=0.3, label='Bull Market')
242
+ ax0.fill_between(pd_candles['datetime'], pd_candles['close'], pd_candles['ema_close'], where=(pd_candles['close'] <= pd_candles['ema_close']), interpolate=True, color='red', alpha=0.3, label='Bear Market')
243
+
244
+ ax0.set_title('Close vs EMA', color='white')
245
+ ax0.set_xlabel('Date', color='white')
246
+ ax0.set_ylabel('Price', color='white')
247
+ legend = ax0.legend()
248
+ legend.get_frame().set_facecolor('black')
249
+ legend.get_frame().set_edgecolor('white')
250
+ for text in legend.get_texts():
251
+ text.set_color('white')
252
+
253
+ # @CRITICAL close vs smoothed_close and merge_distance
254
+ for maxima_index in maxima:
255
+ ax0.plot(pd_candles['datetime'][maxima_index], pd_candles['close'][maxima_index], marker='+', markersize=8, color='yellow', label='maxima')
256
+ for minima_index in minima:
257
+ ax0.plot(pd_candles['datetime'][minima_index], pd_candles['close'][minima_index], marker='o', markersize=5, color='yellow', label='minima')
258
+
259
+ for segment in segments:
260
+
261
+ ax0.axvline(x=pd_candles['datetime'][segment['end']], color='gray', linewidth=2, linestyle='--')
262
+
263
+ if 'maxima_idx_boillenger' in segment and segment['maxima_linregress_boillenger'] is not None:
264
+ '''
265
+ We don't need to compute y_series like this:
266
+ slope_maxima = segment['maxima_linregress_boillenger'].slope
267
+ intercept_maxima = segment['maxima_linregress_boillenger'].intercept
268
+ segment_maxima_dates = pd_candles['datetime'][segment['maxima_idx_boillenger']]
269
+ y_series = [ slope_maxima * idx + intercept_maxima for idx in segment['maxima_idx_boillenger'] ]
270
+ But, syntax is just for reference.
271
+ '''
272
+ x_series = [pd_candles.loc[idx]['datetime'] for idx in segment['maxima_idx_boillenger'] if idx in pd_candles.index] # x = dates
273
+ y_series = [segment['maxima_close_boillenger'][i] for i, idx in enumerate(segment['maxima_idx_boillenger']) if idx in pd_candles.index] # y = boillenger upper
274
+ ax0.plot(
275
+ x_series,
276
+ y_series,
277
+ color='green', linestyle='--', label='Maxima Linear Regression')
278
+
279
+ if 'minima_idx_boillenger' in segment and segment['minima_linregress_boillenger'] is not None:
280
+ x_series = [pd_candles.loc[idx]['datetime'] for idx in segment['minima_idx_boillenger'] if idx in pd_candles.index] # x = dates
281
+ y_series = [segment['minima_close_boillenger'][i] for i, idx in enumerate(segment['minima_idx_boillenger']) if idx in pd_candles.index] # y = boillenger lower
282
+ ax0.plot(
283
+ x_series,
284
+ y_series,
285
+ color='red', linestyle='--', label='Minima Linear Regression')
286
+
287
+ ax0.set_facecolor('black')
288
+
289
+ ax0.tick_params(axis='x', colors='white')
290
+ ax0.tick_params(axis='y', colors='white')
291
+
292
+ # Show the plot
293
+ plt.grid(True)
294
+ plt.tight_layout()
295
+
296
+ if jpg_filename:
297
+ plt.savefig(jpg_filename, format='jpg', dpi=300)
298
+
299
+ def segments_to_df(segments : List[Dict]) -> pd.DataFrame:
300
+ segments = [
301
+ {
302
+ 'start' : segment['start'],
303
+ 'end' : segment['end'],
304
+ 'start_datetime' : segment['start_datetime'] if not type(segment['start_datetime']) is str else arrow.get(segment['start_datetime']).datetime.replace(tzinfo=None),
305
+ 'end_datetime' : segment['end_datetime'] if not type(segment['end_datetime']) is str else arrow.get(segment['end_datetime']).datetime.replace(tzinfo=None),
306
+ 'start_close' : segment['start_close'],
307
+ 'end_close' : segment['end_close'],
308
+ 'window_size_num_intervals' : segment['window_size_num_intervals'],
309
+ 'cur_recur_depth' : segment['cur_recur_depth'],
310
+ 'up_or_down' : segment['up_or_down'],
311
+ 'class' : segment['class'],
312
+ 'maxima_linregress_slope' : segment['maxima_linregress_full'].slope,
313
+ 'maxima_linregress_intercept' : segment['maxima_linregress_full'].intercept,
314
+ 'maxima_linregress_std_err' : segment['maxima_linregress_full'].stderr,
315
+ 'minima_linregress_slope' : segment['minima_linregress_full'].slope,
316
+ 'minima_linregress_intercept' : segment['minima_linregress_full'].intercept,
317
+ 'minima_linregress_std_err' : segment['minima_linregress_full'].stderr
318
+
319
+ }
320
+ for segment in segments ]
321
+ for segment in segments:
322
+ segment['start_ts'] = int(segment['start_datetime'].timestamp())
323
+ segment['end_ts'] = int(segment['end_datetime'].timestamp())
324
+ pd_segments = pd.DataFrame(segments)
325
+ return pd_segments
326
+
327
+ def generic_check_signal_thresholds(
328
+ signal_thresholds : List[Dict[str, Any]],
329
+ this_candle : Dict[str, Any],
330
+ adj_bps : float = 0
331
+ ) -> bool:
332
+ '''
333
+ WARNING!!! Do not put any strategy specific logic here!!!
334
+ Thanks.
335
+ '''
336
+ return all([
337
+ this_candle[signal['lhs']] > (this_candle[signal['rhs']] + adj_bps/10000)
338
+ if signal['op'] == '>'
339
+ else this_candle[signal['lhs']] < (this_candle[signal['rhs']] + adj_bps/10000)
340
+ for signal in signal_thresholds
341
+ ])
342
+
343
+ def generic_pnl_eval (
344
+ this_candle,
345
+ running_sl_percent_hard : float,
346
+ this_ticker_open_trades : List[Dict],
347
+ algo_param : Dict,
348
+ long_tp_indicator_name : str = None,
349
+ short_tp_indicator_name : str = None
350
+ ) -> Dict[str, float]:
351
+ '''
352
+ WARNING!!! Do not put any strategy specific logic here!!!
353
+ Thanks.
354
+ '''
355
+ unrealized_pnl_interval, unrealized_pnl_open, unrealized_pnl_live_optimistic, unrealized_pnl_live_pessimistic, unrealized_pnl_tp, unrealized_pnl_sl, unrealized_pnl_close_approx = 0, 0, 0, 0, 0, 0, 0
356
+ assert(len(set([ trade['side'] for trade in this_ticker_open_trades]))==1) # open trades should be in same direction
357
+ this_ticker_open_positions_side = this_ticker_open_trades[-1]['side']
358
+
359
+ lo_dayofweek = this_candle['dayofweek']
360
+ cautious_dayofweek : List[int] = algo_param['cautious_dayofweek']
361
+
362
+ lo_close = this_candle['close']
363
+ lo_open = this_candle['open']
364
+ lo_high = this_candle['high']
365
+ lo_low = this_candle['low']
366
+
367
+ # ATR, Fib618, bollengers are price levels. RSI/MFI..etc are not prices. Be careful.
368
+ long_tp_price = this_candle[long_tp_indicator_name] if long_tp_indicator_name else None
369
+ short_tp_price = this_candle[short_tp_indicator_name] if short_tp_indicator_name else None
370
+
371
+ _asymmetric_tp_bps = algo_param['asymmetric_tp_bps'] if lo_dayofweek in cautious_dayofweek else 0
372
+
373
+ for trade in this_ticker_open_trades:
374
+ target_price = trade['target_price'] if 'target_price' in trade else None
375
+ if not long_tp_indicator_name and not short_tp_indicator_name:
376
+ assert(target_price)
377
+
378
+ if this_ticker_open_positions_side=='buy':
379
+ unrealized_pnl_interval += (lo_close - trade['entry_price']) * trade['size']
380
+ unrealized_pnl_open += (lo_open - trade['entry_price']) * trade['size']
381
+ unrealized_pnl_live_optimistic += (lo_high - trade['entry_price']) * trade['size']
382
+ unrealized_pnl_live_pessimistic += (lo_low - trade['entry_price']) * trade['size']
383
+ unrealized_pnl_close_approx += (min(lo_close*(1+_asymmetric_tp_bps/10000), lo_high) - trade['entry_price']) * trade['size'] # Less accurate to use close price
384
+ if (
385
+ long_tp_indicator_name
386
+ and not target_price # If entry trades are tagged target_price, it should take precedence over indicator
387
+ ):
388
+ unrealized_pnl_tp += (min(long_tp_price*(1+_asymmetric_tp_bps/10000), lo_high) - trade['entry_price']) * trade['size']
389
+ else:
390
+ if target_price:
391
+ if (lo_high>target_price and lo_low<target_price):
392
+ unrealized_pnl_tp += (target_price - trade['entry_price']) * trade['size']
393
+ else:
394
+ unrealized_pnl_tp += unrealized_pnl_close_approx # This is worst, try not to estimate pnl with close price!
395
+ else:
396
+ unrealized_pnl_tp += unrealized_pnl_close_approx # This is worst, try not to estimate pnl with close price!
397
+ unrealized_pnl_sl += -1 * (trade['entry_price'] * trade['size'] * (running_sl_percent_hard/100))
398
+
399
+ else:
400
+ unrealized_pnl_interval += (trade['entry_price'] - lo_close) * trade['size']
401
+ unrealized_pnl_open += (trade['entry_price'] - lo_open) * trade['size']
402
+ unrealized_pnl_live_optimistic += (trade['entry_price'] - lo_low) * trade['size']
403
+ unrealized_pnl_live_pessimistic += (trade['entry_price'] - lo_high) * trade['size']
404
+ unrealized_pnl_close_approx += (trade['entry_price'] - max(lo_close*(1-_asymmetric_tp_bps/10000), lo_low)) * trade['size']
405
+ if (
406
+ short_tp_indicator_name
407
+ and not target_price # If entry trades are tagged target_price, it should take precedence over indicator
408
+ ):
409
+ unrealized_pnl_tp += (trade['entry_price'] - max(short_tp_price*(1-_asymmetric_tp_bps/10000), lo_low)) * trade['size']
410
+ else:
411
+ if target_price:
412
+ if (lo_high>target_price and lo_low<target_price):
413
+ unrealized_pnl_tp += (trade['entry_price'] - target_price) * trade['size']
414
+ else:
415
+ unrealized_pnl_tp += unrealized_pnl_close_approx # This is worst, try not to estimate pnl with close price!
416
+ else:
417
+ unrealized_pnl_tp += unrealized_pnl_close_approx # This is worst, try not to estimate pnl with close price!
418
+
419
+ unrealized_pnl_sl += -1 * (trade['entry_price'] * trade['size'] * (running_sl_percent_hard/100))
420
+
421
+ return {
422
+ 'unrealized_pnl_interval' : unrealized_pnl_interval,
423
+ 'unrealized_pnl_open' : unrealized_pnl_open,
424
+ 'unrealized_pnl_live_optimistic' : unrealized_pnl_live_optimistic,
425
+ 'unrealized_pnl_live_pessimistic' : unrealized_pnl_live_pessimistic,
426
+ 'unrealized_pnl_tp' : unrealized_pnl_tp,
427
+ 'unrealized_pnl_sl' : unrealized_pnl_sl
428
+ }
429
+
430
+ def generic_tp_eval (
431
+ lo_row,
432
+ this_ticker_open_trades : List[Dict]
433
+ ) -> bool:
434
+ low : float = lo_row['low']
435
+ high : float = lo_row['high']
436
+
437
+ for trade in this_ticker_open_trades:
438
+ if trade['target_price']<=high and trade['target_price']>=low:
439
+ return True
440
+ return False
441
+
442
+ def generic_sort_filter_universe(
443
+ tickers : List[str],
444
+ exchange : Exchange,
445
+
446
+ # Use "i" (row index) to find current/last interval's market data or TAs from "all_exchange_candles"
447
+ i,
448
+ all_exchange_candles : Dict[str, Dict[str, Dict[str, pd.DataFrame]]],
449
+
450
+ max_num_tickers : int = 10
451
+ ) -> List[str]:
452
+ if not tickers:
453
+ return None
454
+
455
+ sorted_filtered_tickers : List[str] = tickers.copy()
456
+
457
+ # Custom strategy specific sort logic here. Sort first before you filter!
458
+ sorted_filtered_tickers.sort()
459
+
460
+ # Custom filtering logic
461
+ if len(sorted_filtered_tickers)>max_num_tickers:
462
+ sorted_filtered_tickers = sorted_filtered_tickers[:max_num_tickers]
463
+
464
+ return sorted_filtered_tickers
465
+
466
+ @retry(num_attempts=3)
467
+ def fetch_price(
468
+ exchange,
469
+ normalized_symbol : str,
470
+ pd_reference_price_cache : pd.DataFrame,
471
+ timestamp_ms : int,
472
+ ref_timeframe : str = '1m'
473
+ ) -> float:
474
+ cached_row = pd_reference_price_cache[pd_reference_price_cache.timestamp_ms==timestamp_ms]
475
+ if cached_row.shape[0]>0:
476
+ reference_price = cached_row.iloc[-1]['price']
477
+ else:
478
+ reference_price = fetch_historical_price(
479
+ exchange=exchange,
480
+ normalized_symbol=normalized_symbol,
481
+ timestamp_ms=timestamp_ms,
482
+ ref_timeframe=ref_timeframe)
483
+ cached_row = {
484
+ 'exchange' : exchange,
485
+ 'ticker' : normalized_symbol,
486
+ 'datetime' : datetime.fromtimestamp(int(timestamp_ms/1000)),
487
+ 'datetime_utc' : datetime.fromtimestamp(int(timestamp_ms/1000), tz=timezone.utc),
488
+ 'timestamp_ms' : timestamp_ms,
489
+ 'price' : reference_price
490
+ }
491
+ # pd_reference_price_cache = pd.concat([pd_reference_price_cache, pd.DataFrame([cached_row])], axis=0, ignore_index=True)
492
+ pd_reference_price_cache.loc[len(pd_reference_price_cache)] = cached_row
493
+ return reference_price
494
+
495
+ def fetch_cycle_ath_atl(
496
+ exchange,
497
+ symbol,
498
+ timeframe,
499
+ start_date : datetime,
500
+ end_date : datetime
501
+ ):
502
+ ath = float('-inf')
503
+ atl = float('inf')
504
+ all_ohlcv = []
505
+
506
+ start_ts = int(start_date.timestamp() * 1000)
507
+ end_ts = int(end_date.timestamp() * 1000)
508
+
509
+ while start_ts < end_ts:
510
+ try:
511
+ ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=start_ts, limit=100)
512
+ if not ohlcv:
513
+ break
514
+ all_ohlcv.extend(ohlcv)
515
+ start_ts = ohlcv[-1][0] + 1
516
+ time.sleep(0.1)
517
+ except Exception as e:
518
+ print(f"fetch_cycle_ath_atl Oops: {e}")
519
+
520
+ for candle in all_ohlcv:
521
+ high = candle[2]
522
+ low = candle[3]
523
+ ath = max(ath, high)
524
+ atl = min(atl, low)
525
+
526
+ return {
527
+ 'ath' : ath,
528
+ 'atl' : atl
529
+ }
530
+
531
+ '''
532
+ ******** THE_LOOP ********
533
+
534
+ This is the loop which replay candles to back tests. No STRATEGY_SPECIFIC logic should be here!!!
535
+ '''
536
+ def run_scenario(
537
+ algo_param : Dict,
538
+ exchanges : List[Exchange],
539
+ all_exchange_candles : Dict[str, Dict[str, Dict[str, pd.DataFrame]]],
540
+ pd_ref_candles_fast : pd.DataFrame,
541
+ pd_ref_candles_slow : pd.DataFrame,
542
+ tickers : List[str],
543
+ ref_candles_partitions : Dict,
544
+ pd_hi_candles_partitions : pd.DataFrame,
545
+ pd_lo_candles_partitions : pd.DataFrame,
546
+ economic_calendars_loaded : bool,
547
+ pd_economic_calendars : pd.DataFrame,
548
+
549
+ order_notional_adj_func : Callable[..., float],
550
+ allow_entry_initial_func : Callable[..., bool],
551
+ allow_entry_final_func : Callable[..., bool],
552
+ allow_slice_entry_func : Callable[..., bool],
553
+ sl_adj_func : Callable[..., Dict[str, float]],
554
+ trailing_stop_threshold_eval_func : Callable[..., Dict[str, float]],
555
+ pnl_eval_func : Callable[..., Dict[str, float]],
556
+ tp_eval_func : Callable[..., bool],
557
+ sort_filter_universe_func : Callable[..., List[str]],
558
+
559
+ logger,
560
+
561
+ pypy_compat : bool = False,
562
+ plot_timeseries : bool = True,
563
+ ):
564
+ exceptions : Dict = {}
565
+
566
+ if not pypy_compat:
567
+ pd_ref_candles_segments = segments_to_df(ref_candles_partitions['segments'])
568
+ pd_hi_candles_segments = segments_to_df(pd_hi_candles_partitions['segments'])
569
+ pd_lo_candles_segments = segments_to_df(pd_lo_candles_partitions['segments'])
570
+
571
+ min_sl_age_ms : int = 0
572
+ if algo_param['lo_candle_size'][-1]=="m":
573
+ one_interval_ms = 60*1000
574
+ min_sl_age_ms = algo_param['sl_num_intervals_delay'] * one_interval_ms
575
+ num_intervals_block_pending_ecoevents_ms = one_interval_ms*algo_param['num_intervals_block_pending_ecoevents']
576
+ elif algo_param['lo_candle_size'][-1]=="h":
577
+ one_interval_ms = 60*60*1000
578
+ min_sl_age_ms = algo_param['sl_num_intervals_delay'] * one_interval_ms
579
+ num_intervals_block_pending_ecoevents_ms = one_interval_ms*algo_param['num_intervals_block_pending_ecoevents']
580
+ elif algo_param['lo_candle_size'][-1]=="d":
581
+ one_interval_ms = 60*60*24*1000
582
+ min_sl_age_ms = algo_param['sl_num_intervals_delay'] * one_interval_ms
583
+ num_intervals_block_pending_ecoevents_ms = one_interval_ms*algo_param['num_intervals_block_pending_ecoevents']
584
+
585
+ commission_bps = algo_param['commission_bps']
586
+
587
+ initial_cash : float = algo_param['initial_cash']
588
+ entry_percent_initial_cash : float = algo_param['entry_percent_initial_cash']
589
+ target_position_size_percent_total_equity : float = algo_param['target_position_size_percent_total_equity']
590
+
591
+ class GlobalState:
592
+ def __init__(self, initial_cash) -> None:
593
+ self.cash = initial_cash
594
+ self.total_equity = self.cash
595
+ self.total_commission = 0
596
+
597
+ self.num_sl = 0
598
+ self.num_trades = 0
599
+
600
+ gloabl_state = GlobalState(initial_cash=initial_cash) # This cash position is shared across all tickers in universe
601
+ current_position_usdt = 0
602
+
603
+ # This is for trade extract export, include entries, TP/SL.
604
+ all_trades : List = []
605
+
606
+ # This is for performance enhancements, trade memory for speed. Reduce list comprehension. These are duplicated trade cache.
607
+ sl_by_ticker : Dict[str, List[Dict[str, Any]]] = {}
608
+ open_trades_by_ticker : Dict[str, List[Dict[str, Any]]] = {}
609
+
610
+ compiled_candles_by_exchange_pairs : List[Dict[str, pd.DataFrame]]= {}
611
+ hi_num_intervals, lo_num_intervals = 99999999, 99999999
612
+
613
+ for exchange in exchanges:
614
+ for ticker in tickers:
615
+ key : str = f"{exchange.name}-{ticker}"
616
+ pd_hi_candles : pd.DataFrame = all_exchange_candles[exchange.name][ticker]['hi_candles']
617
+ pd_lo_candles : pd.DataFrame = all_exchange_candles[exchange.name][ticker]['lo_candles']
618
+
619
+ # market_data_gizmo sometimes insert dummy row(s) between start_date and actual first candle fetched
620
+ if pd_hi_candles[~pd_hi_candles.close.notna()].shape[0]>0:
621
+ pd_hi_candles.drop(pd_hi_candles[~pd_hi_candles.close.notna()].index[0], inplace=True)
622
+
623
+ hi_num_intervals = min(hi_num_intervals, pd_hi_candles.shape[0])
624
+ lo_num_intervals = min(lo_num_intervals, pd_lo_candles.shape[0])
625
+
626
+ compiled_candles_by_exchange_pairs[key] = {}
627
+ compiled_candles_by_exchange_pairs[key]['hi_candles'] = pd_hi_candles
628
+ compiled_candles_by_exchange_pairs[key]['lo_candles'] = pd_lo_candles
629
+
630
+ all_canvas = {}
631
+ if plot_timeseries:
632
+ for exchange in exchanges:
633
+ for ticker in tickers:
634
+ key = f"{exchange.name}-{ticker}"
635
+ pd_hi_candles = compiled_candles_by_exchange_pairs[key]['hi_candles']
636
+ pd_lo_candles = compiled_candles_by_exchange_pairs[key]['lo_candles']
637
+
638
+ canvas = create_plot_canvas(key, pd_hi_candles, pd_lo_candles)
639
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"] = canvas
640
+
641
+ order_notional_adj_func_sig = inspect.signature(order_notional_adj_func)
642
+ order_notional_adj_func_params = order_notional_adj_func_sig.parameters.keys()
643
+ allow_entry_initial_func_sig = inspect.signature(allow_entry_initial_func)
644
+ allow_entry_initial_func_params = allow_entry_initial_func_sig.parameters.keys()
645
+ allow_entry_final_func_sig = inspect.signature(allow_entry_final_func)
646
+ allow_entry_final_func_params = allow_entry_final_func_sig.parameters.keys()
647
+ allow_slice_entry_func_sig = inspect.signature(allow_slice_entry_func)
648
+ allow_slice_entry_func_params = allow_slice_entry_func_sig.parameters.keys()
649
+ sl_adj_func_sig = inspect.signature(sl_adj_func)
650
+ sl_adj_func_params = sl_adj_func_sig.parameters.keys()
651
+ trailing_stop_threshold_eval_func_sig = inspect.signature(trailing_stop_threshold_eval_func)
652
+ trailing_stop_threshold_eval_func_params = trailing_stop_threshold_eval_func_sig.parameters.keys()
653
+ tp_eval_func_sig = inspect.signature(tp_eval_func)
654
+ tp_eval_func_params = tp_eval_func_sig.parameters.keys()
655
+ sort_filter_universe_func_sig = inspect.signature(sort_filter_universe_func)
656
+ sort_filter_universe_func_params = sort_filter_universe_func_sig.parameters.keys()
657
+
658
+ BUCKETS_m100_100 = bucket_series(
659
+ values=list([i for i in range(-100,100)]),
660
+ outlier_threshold_percent=10,
661
+ level_granularity=algo_param['default_level_granularity'] if 'default_level_granularity' in algo_param else 0.01
662
+ )
663
+
664
+ REFERENCE_PRICE_CACHE_COLUMNS = [
665
+ 'exchange', 'ticker', 'datetime', 'datetime_utc', 'timestamp_ms', 'price'
666
+ ]
667
+ reference_price_cache = {}
668
+
669
+ def _max_camp(
670
+ camp1 : bool,
671
+ camp2 : bool,
672
+ camp3 : bool
673
+ ) -> int:
674
+ camp : int = 1 if camp1 else 0
675
+ if camp2:
676
+ camp = 2
677
+ if camp3:
678
+ camp =3
679
+ return camp
680
+ REVERSAL_CAMP_ITEM = {
681
+ 'camp1' : False,
682
+ 'camp2' : False,
683
+ 'camp3' : False,
684
+ 'camp1_price' : None,
685
+ 'camp2_price' : None,
686
+ 'camp3_price' : None,
687
+
688
+ 'datetime' : None # Last update
689
+ }
690
+ reversal_camp_cache = {}
691
+ lo_boillenger_lower_breached_cache = {}
692
+ lo_boillenger_upper_breached_cache = {}
693
+ ath, atl = None, None
694
+ target_order_notional = 0
695
+ for i in range(algo_param['how_many_last_candles'], lo_num_intervals):
696
+ for exchange in exchanges:
697
+
698
+ kwargs = {k: v for k, v in locals().items() if k in sort_filter_universe_func_params}
699
+ sorted_filtered_tickers = sort_filter_universe_func(**kwargs)
700
+
701
+ for ticker in sorted_filtered_tickers:
702
+ key = f"{exchange.name}-{ticker}"
703
+ if key not in reversal_camp_cache:
704
+ reversal_camp_cache[key] = REVERSAL_CAMP_ITEM.copy()
705
+
706
+ if ticker not in open_trades_by_ticker:
707
+ open_trades_by_ticker[ticker] = []
708
+
709
+ if ticker not in sl_by_ticker:
710
+ sl_by_ticker[ticker] = []
711
+
712
+ pd_reference_price_cache : pd.DataFrame = None
713
+ reference_price_cache_file : str = f"refpx_{ticker.replace('/','').replace(':','')}.csv"
714
+ if reference_price_cache_file not in reference_price_cache:
715
+ if os.path.isfile(reference_price_cache_file):
716
+ pd_reference_price_cache = pd.read_csv(reference_price_cache_file)
717
+ pd_reference_price_cache.drop(pd_reference_price_cache.columns[pd_reference_price_cache.columns.str.contains('unnamed',case = False)],axis = 1, inplace = True)
718
+ reference_price_cache[reference_price_cache_file] = pd_reference_price_cache
719
+ else:
720
+ pd_reference_price_cache = reference_price_cache[reference_price_cache_file]
721
+ if reference_price_cache_file not in reference_price_cache:
722
+ pd_reference_price_cache = pd.DataFrame(columns=REFERENCE_PRICE_CACHE_COLUMNS)
723
+ reference_price_cache[reference_price_cache_file] = pd_reference_price_cache
724
+
725
+ pd_candles = compiled_candles_by_exchange_pairs[key]
726
+ pd_hi_candles = pd_candles['hi_candles']
727
+ pd_lo_candles = pd_candles['lo_candles']
728
+
729
+ lo_row = pd_lo_candles.iloc[i]
730
+ lo_row_tm1 = pd_lo_candles.iloc[i-1]
731
+
732
+ lo_datetime = lo_row['datetime']
733
+ tm1 = lo_row_tm1['datetime']
734
+
735
+ lo_year = lo_row['year']
736
+ lo_month = lo_row['month']
737
+ lo_day = lo_row['day']
738
+ lo_hour = lo_row['hour']
739
+ lo_minute = lo_row['minute']
740
+ lo_timestamp_ms = lo_row['timestamp_ms']
741
+ lo_dayofweek = lo_row['dayofweek']
742
+ lo_open = lo_row['open']
743
+ lo_high = lo_row['high']
744
+ lo_low = lo_row['low']
745
+ lo_mid = (lo_high + lo_low)/2
746
+ lo_close = lo_row['close']
747
+ lo_candle_open_close = lo_open - lo_close
748
+ lo_candle_hi_lo = lo_high - lo_low
749
+ lo_volume = lo_row['volume']
750
+ lo_atr = lo_row['atr']
751
+ lo_rsi = lo_row['rsi']
752
+ lo_rsi_bucket = lo_row['rsi_bucket']
753
+ lo_rsi_trend = lo_row['rsi_trend']
754
+ lo_mfi = lo_row['mfi']
755
+ lo_mfi_bucket = lo_row['mfi_bucket']
756
+ lo_macd_minus_signal = lo_row['macd_minus_signal']
757
+ lo_boillenger_upper = lo_row['boillenger_upper']
758
+ lo_boillenger_lower = lo_row['boillenger_lower']
759
+ lo_boillenger_mid = (lo_boillenger_upper + lo_boillenger_lower) / 2
760
+ lo_boillenger_height = lo_boillenger_upper - lo_boillenger_lower
761
+ lo_boillenger_channel_height = lo_row['boillenger_channel_height']
762
+ lo_aggressive_up = lo_row['aggressive_up']
763
+ lo_aggressive_down = lo_row['aggressive_down']
764
+ lo_fvg_high = lo_row['fvg_high']
765
+ lo_fvg_low = lo_row['fvg_low']
766
+ lo_hurst_exp = lo_row['hurst_exp']
767
+ lo_ema_volume_short_periods = lo_row['ema_volume_short_periods']
768
+ lo_ema_short_slope = lo_row['ema_short_slope'] if 'ema_short_slope' in pd_lo_candles.columns else 0
769
+ lo_normalized_ema_short_slope = lo_row['normalized_ema_short_slope'] if 'normalized_ema_short_slope' in pd_lo_candles.columns else 0
770
+ lo_ema_long_slope = lo_row['ema_long_slope'] if 'ema_long_slope' in pd_lo_candles.columns else 0
771
+ lo_normalized_ema_long_slope = lo_row['normalized_ema_long_slope'] if 'normalized_ema_long_slope' in pd_lo_candles.columns else 0
772
+ lo_tm1_normalized_ema_long_slope = lo_row_tm1['normalized_ema_long_slope'] if 'normalized_ema_long_slope' in pd_lo_candles.columns else 0
773
+
774
+ lo_tm1_close = lo_row_tm1['close']
775
+ lo_tm1_rsi = lo_row_tm1['rsi']
776
+ lo_tm1_rsi_bucket = lo_row_tm1['rsi_bucket']
777
+ lo_tm1_rsi_trend = lo_row_tm1['rsi_trend']
778
+
779
+ lo_max_short_periods = lo_row['max_short_periods']
780
+ lo_idmax_short_periods = int(lo_row['idmax_short_periods']) if not math.isnan(lo_row['idmax_short_periods']) else None
781
+ lo_idmax_dt_short_periods = pd_lo_candles.at[lo_idmax_short_periods, 'datetime'] if not (lo_idmax_short_periods is None or pd.isna(lo_idmax_short_periods)) else None
782
+ lo_max_long_periods = lo_row['max_long_periods']
783
+ lo_idmax_long_periods = int(lo_row['idmax_long_periods']) if not math.isnan(lo_row['idmax_long_periods']) else None
784
+ lo_idmax_dt_long_periods = pd_lo_candles.at[lo_idmax_long_periods, 'datetime'] if not (lo_idmax_long_periods is None or pd.isna(lo_idmax_long_periods)) else None
785
+
786
+ lo_tm1_max_short_periods = lo_row_tm1['max_short_periods']
787
+ lo_tm1_idmax_short_periods = int(lo_row_tm1['idmax_short_periods']) if not math.isnan(lo_row_tm1['idmax_short_periods']) else None
788
+ lo_tm1_idmax_dt_short_periods = pd_lo_candles.at[lo_tm1_idmax_short_periods, 'datetime'] if not (lo_tm1_idmax_short_periods is None or pd.isna(lo_tm1_idmax_short_periods)) else None
789
+ lo_tm1_max_long_periods = lo_row_tm1['max_long_periods']
790
+ lo_tm1_idmax_long_periods = int(lo_row_tm1['idmax_long_periods']) if not math.isnan(lo_row_tm1['idmax_long_periods']) else None
791
+ lo_tm1_idmax_dt_long_periods = pd_lo_candles.at[lo_tm1_idmax_long_periods, 'datetime'] if not (lo_tm1_idmax_long_periods is None or pd.isna(lo_tm1_idmax_long_periods)) else None
792
+
793
+ lo_min_short_periods = lo_row['min_short_periods']
794
+ lo_idmin_short_periods = int(lo_row['idmin_short_periods']) if not math.isnan(lo_row['idmin_short_periods']) else None
795
+ lo_idmin_dt_short_periods = pd_lo_candles.at[lo_idmin_short_periods,'datetime'] if not (lo_idmin_short_periods is None or pd.isna(lo_idmin_short_periods)) else None
796
+ lo_min_long_periods = lo_row['min_long_periods']
797
+ lo_idmin_long_periods = int(lo_row['idmin_long_periods']) if not math.isnan(lo_row['idmin_long_periods']) else None
798
+ lo_idmin_dt_long_periods = pd_lo_candles.at[lo_idmin_long_periods,'datetime'] if not (lo_idmin_long_periods is None or pd.isna(lo_idmin_long_periods)) else None
799
+
800
+ lo_tm1_min_short_periods = lo_row_tm1['min_short_periods']
801
+ lo_tm1_idmin_short_periods = int(lo_row_tm1['idmin_short_periods']) if not math.isnan(lo_row_tm1['idmin_short_periods']) else None
802
+ lo_tm1_idmin_dt_short_periods = pd_lo_candles.at[lo_tm1_idmin_short_periods,'datetime'] if not (lo_tm1_idmin_short_periods is None or pd.isna(lo_tm1_idmin_short_periods)) else None
803
+ lo_tm1_min_long_periods = lo_row_tm1['min_long_periods']
804
+ lo_tm1_idmin_long_periods = int(lo_row_tm1['idmin_long_periods']) if not math.isnan(lo_row_tm1['idmin_long_periods']) else None
805
+ lo_tm1_idmin_dt_long_periods = pd_lo_candles.at[lo_tm1_idmin_long_periods,'datetime'] if not (lo_tm1_idmin_long_periods is None or pd.isna(lo_tm1_idmin_long_periods)) else None
806
+
807
+ if not ath or not atl:
808
+ ath_atl = fetch_cycle_ath_atl(exchange=exchange, symbol=ticker, timeframe='1d', start_date=(algo_param['start_date'] - timedelta(days=365*4)), end_date=algo_param['start_date'])
809
+ ath = ath_atl['ath']
810
+ atl = ath_atl['atl']
811
+
812
+ if lo_close>ath:
813
+ ath = lo_close
814
+ if lo_close<atl:
815
+ atl = lo_close
816
+
817
+ # Incoming economic calendars? num_incoming_economic_calendars is used to Block entries if incoming events (total_num_ecoevents==0 to make entries).
818
+ num_impacting_economic_calendars : int = 0
819
+ num_bullish_ecoevents, num_bearish_ecoevents, total_num_ecoevents = 0, 0, 0
820
+ if economic_calendars_loaded and algo_param['block_entries_on_impacting_ecoevents']:
821
+ pd_impacting_economic_calendars = pd_economic_calendars[pd_economic_calendars.event_code.isin(algo_param['mapped_event_codes'])]
822
+ pd_impacting_economic_calendars = pd_impacting_economic_calendars[
823
+ (
824
+ (
825
+ pd_impacting_economic_calendars.calendar_item_timestamp_ms>=lo_timestamp_ms) # Incoming
826
+ & (lo_timestamp_ms>=(pd_impacting_economic_calendars.calendar_item_timestamp_ms - num_intervals_block_pending_ecoevents_ms)
827
+ )
828
+ )
829
+ |
830
+ (
831
+ (
832
+ pd_impacting_economic_calendars.calendar_item_timestamp_ms<lo_timestamp_ms) # Passed
833
+ & (lo_timestamp_ms<=(pd_impacting_economic_calendars.calendar_item_timestamp_ms + num_intervals_block_pending_ecoevents_ms/3)
834
+ )
835
+ )
836
+ ]
837
+ num_impacting_economic_calendars = pd_impacting_economic_calendars.shape[0]
838
+
839
+ if num_impacting_economic_calendars>0:
840
+ pd_passed_economic_calendars = pd_impacting_economic_calendars[pd_impacting_economic_calendars.calendar_item_timestamp_ms>(lo_timestamp_ms+one_interval_ms)] # Careful with look ahead bias
841
+ num_bullish_ecoevents = pd_passed_economic_calendars[pd_passed_economic_calendars.pos_neg=='bullish'].shape[0]
842
+ num_bearish_ecoevents = pd_passed_economic_calendars[pd_passed_economic_calendars.pos_neg=='bearish'].shape[0]
843
+ num_neutral_ecoevents = pd_passed_economic_calendars[pd_passed_economic_calendars.pos_neg=='neutral'].shape[0]
844
+
845
+ # If adj_sl_on_ecoevents==True, total_num_ecoevents is used to set sl_percent_adj
846
+ total_num_ecoevents = num_bullish_ecoevents + num_bearish_ecoevents + num_neutral_ecoevents
847
+
848
+ lo_fib_eval_result = lookup_fib_target(lo_row_tm1, pd_lo_candles)
849
+ lo_fib_short_periods_fib_target, lo_fib_short_periods_price_swing, lo_fib_long_periods_fib_target, lo_fib_long_periods_price_swing = None, None, None, None
850
+ if lo_fib_eval_result:
851
+ lo_fib_short_periods_fib_target = lo_fib_eval_result['short_periods']['fib_target']
852
+ lo_fib_long_periods_fib_target = lo_fib_eval_result['long_periods']['fib_target']
853
+
854
+ current_ref_candles_segment_index, last_ref_candles_segmment_index = -1, -1
855
+ current_ref_candles_segment, last_ref_candles_segment = None, None
856
+ current_ref_candles_segment_class, last_ref_candles_segment_class = None, None
857
+ if not pypy_compat:
858
+ if pd_ref_candles_segments[(pd_ref_candles_segments.start_ts<=lo_datetime.timestamp()) & (pd_ref_candles_segments.end_ts>lo_datetime.timestamp()) ].shape[0]>0:
859
+ current_ref_candles_segment_index = pd_ref_candles_segments[(pd_ref_candles_segments.start_ts<=lo_datetime.timestamp()) & (pd_ref_candles_segments.end_ts>lo_datetime.timestamp()) ].index.to_list()[0] # Take first
860
+ current_ref_candles_segment = pd_ref_candles_segments.iloc[current_ref_candles_segment_index]
861
+ if current_ref_candles_segment is not None and not current_ref_candles_segment.empty:
862
+ current_ref_candles_segment_class = current_ref_candles_segment['class']
863
+ last_ref_candles_segmment_index = current_ref_candles_segment_index
864
+ last_ref_candles_segment = current_ref_candles_segment
865
+ if current_ref_candles_segment_index>0:
866
+ last_ref_candles_segmment_index = current_ref_candles_segment_index-1
867
+ last_ref_candles_segment = pd_ref_candles_segments.iloc[current_ref_candles_segment_index]
868
+ if last_ref_candles_segment is not None and not last_ref_candles_segment.empty:
869
+ last_ref_candles_segment_class = last_ref_candles_segment['class']
870
+
871
+ current_hi_candles_segment_index, last_hi_candles_segmment_index = -1, -1
872
+ current_hi_candles_segment, last_hi_candles_segment = None, None
873
+ current_hi_candles_segment_class, last_hi_candles_segment_class = None, None
874
+ if not pypy_compat:
875
+ if pd_hi_candles_segments[(pd_hi_candles_segments.start_ts<=lo_datetime.timestamp()) & (pd_hi_candles_segments.end_ts>lo_datetime.timestamp()) ].shape[0]>0:
876
+ current_hi_candles_segment_index = pd_hi_candles_segments[(pd_hi_candles_segments.start_ts<=lo_datetime.timestamp()) & (pd_hi_candles_segments.end_ts>lo_datetime.timestamp()) ].index.to_list()[0] # Take first
877
+ current_hi_candles_segment = pd_hi_candles_segments.iloc[current_hi_candles_segment_index]
878
+ if current_hi_candles_segment is not None and not current_hi_candles_segment.empty:
879
+ current_hi_candles_segment_class = current_hi_candles_segment['class']
880
+ last_hi_candles_segmment_index = current_hi_candles_segment_index
881
+ last_hi_candles_segment = current_hi_candles_segment
882
+ if current_hi_candles_segment_index>0:
883
+ last_hi_candles_segmment_index = current_hi_candles_segment_index-1
884
+ last_hi_candles_segment = pd_hi_candles_segments.iloc[current_hi_candles_segment_index]
885
+ if last_hi_candles_segment is not None and not last_hi_candles_segment.empty:
886
+ last_hi_candles_segment_class = last_hi_candles_segment['class']
887
+
888
+ current_lo_candles_segment_index, last_lo_candles_segmment_index = -1, -1
889
+ current_lo_candles_segment, last_lo_candles_segment = None, None
890
+ current_lo_candles_segment_class, last_lo_candles_segment_class = None, None
891
+ if not pypy_compat:
892
+ if pd_lo_candles_segments[(pd_lo_candles_segments.start_ts<=lo_datetime.timestamp()) & (pd_lo_candles_segments.end_ts>lo_datetime.timestamp()) ].shape[0]>0:
893
+ current_lo_candles_segment_index = pd_lo_candles_segments[(pd_lo_candles_segments.start_ts<=lo_datetime.timestamp()) & (pd_lo_candles_segments.end_ts>lo_datetime.timestamp()) ].index.to_list()[0] # Take first
894
+ current_lo_candles_segment = pd_lo_candles_segments.iloc[current_lo_candles_segment_index]
895
+ if current_lo_candles_segment is not None and not current_lo_candles_segment.empty:
896
+ current_lo_candles_segment_class = current_lo_candles_segment['class']
897
+ last_lo_candles_segmment_index = current_lo_candles_segment_index
898
+ last_lo_candles_segment = current_lo_candles_segment
899
+ if current_lo_candles_segment_index>0:
900
+ last_lo_candles_segmment_index = current_lo_candles_segment_index-1
901
+ last_lo_candles_segment = pd_lo_candles_segments.iloc[current_lo_candles_segment_index]
902
+ if last_lo_candles_segment is not None and not last_lo_candles_segment.empty:
903
+ last_lo_candles_segment_class = last_lo_candles_segment['class']
904
+
905
+ # Find corresponding row in pd_hi_candles
906
+ def _find_ref_row(lo_year, lo_month, lo_day, pd_ref_candles):
907
+ ref_row = None
908
+ ref_matching_rows = pd_ref_candles[(pd_ref_candles.year==lo_year) & (pd_ref_candles.month==lo_month) & (pd_ref_candles.day==lo_day)]
909
+ if not ref_matching_rows.empty:
910
+ ref_row = ref_matching_rows.iloc[0]
911
+ ref_row.has_inflection_point = False
912
+
913
+ recent_rows = pd_ref_candles[(pd_ref_candles['datetime'] <= lo_datetime)].tail(3)
914
+ if not recent_rows['close_above_or_below_ema'].isna().all():
915
+ ref_row.has_inflection_point = True
916
+
917
+ else:
918
+ logger.warning(f"{key} ref_row not found for year: {lo_year}, month: {lo_month}, day: {lo_day}")
919
+
920
+ return ref_row
921
+
922
+ def _search_hi_tm1(hi_row, lo_row, pd_hi_candles):
923
+ row_index = hi_row.name -1
924
+ hi_row_tm1 = pd_hi_candles.iloc[row_index] if hi_row is not None else None
925
+ hi_row_tm1 = hi_row_tm1 if hi_row_tm1['timestamp_ms'] < lo_row['timestamp_ms'] else None
926
+ if row_index>1:
927
+ while hi_row_tm1['timestamp_ms'] >= lo_row['timestamp_ms']:
928
+ row_index = row_index -1
929
+ hi_row_tm1 = pd_hi_candles.iloc[row_index]
930
+ return hi_row_tm1
931
+
932
+ hi_row, hi_row_tm1 = None, None
933
+ if lo_datetime>=algo_param['start_date']:
934
+ if algo_param['lo_candle_size'][-1]=="m":
935
+ matching_rows = pd_hi_candles[(pd_hi_candles.year==lo_year) & (pd_hi_candles.month==lo_month) & (pd_hi_candles.day==lo_day) & (pd_hi_candles.hour==lo_hour)]
936
+ if not matching_rows.empty:
937
+ hi_row = matching_rows.iloc[0]
938
+
939
+ else:
940
+ logger.warning(f"{key} hi_row not found for year: {lo_year}, month: {lo_month}, day: {lo_day}, hour: {lo_hour}")
941
+ continue
942
+
943
+ hi_row_tm1 = _search_hi_tm1(hi_row, lo_row, pd_hi_candles)
944
+ if hi_row_tm1 is not None:
945
+ assert(hi_row_tm1['timestamp_ms'] < lo_row['timestamp_ms']) # No look ahead bias!!!
946
+ else:
947
+ continue
948
+
949
+ # Be careful with look ahead bias!!!
950
+ target_ref_candle_date = lo_datetime + timedelta(days=-1)
951
+ ref_row_fast = _find_ref_row(
952
+ target_ref_candle_date.year,
953
+ target_ref_candle_date.month,
954
+ target_ref_candle_date.day,
955
+ pd_ref_candles_fast)
956
+ ref_row_slow = _find_ref_row(
957
+ target_ref_candle_date.year,
958
+ target_ref_candle_date.month,
959
+ target_ref_candle_date.day,
960
+ pd_ref_candles_slow)
961
+
962
+ elif algo_param['lo_candle_size'][-1]=="h":
963
+ matching_rows = pd_hi_candles[(pd_hi_candles.year==lo_year) & (pd_hi_candles.month==lo_month) & (pd_hi_candles.day==lo_day)]
964
+ if not matching_rows.empty:
965
+ hi_row = matching_rows.iloc[0]
966
+
967
+ else:
968
+ logger.warning(f"{key} hi_row not found for year: {lo_year}, month: {lo_month}, day: {lo_day}")
969
+ continue
970
+
971
+ hi_row_tm1 = _search_hi_tm1(hi_row, lo_row, pd_hi_candles)
972
+ if hi_row_tm1 is not None:
973
+ assert(hi_row_tm1['timestamp_ms'] < lo_row['timestamp_ms']) # No look ahead bias!!!
974
+ else:
975
+ continue
976
+
977
+ # Be careful with look ahead bias!!!
978
+ target_ref_candle_date = lo_datetime + timedelta(days=-1)
979
+ ref_row_fast = _find_ref_row(
980
+ target_ref_candle_date.year,
981
+ target_ref_candle_date.month,
982
+ target_ref_candle_date.day,
983
+ pd_ref_candles_fast)
984
+ ref_row_slow = _find_ref_row(
985
+ target_ref_candle_date.year,
986
+ target_ref_candle_date.month,
987
+ target_ref_candle_date.day,
988
+ pd_ref_candles_slow)
989
+
990
+ elif algo_param['lo_candle_size'][-1]=="d":
991
+ # Not supported atm
992
+ hi_row, hi_row_tm1 = None, None
993
+
994
+ hi_datetime, hi_year, hi_month, hi_day, hi_hour, hi_minute, hi_timestamp_ms = None, None, None, None, None, None, None
995
+ hi_open, hi_high, hi_low, hi_close, hi_volume = None, None, None, None, None
996
+ hi_atr, hi_rsi, hi_rsi_bucket, hi_rsi_trend, hi_mfi, hi_mfi_bucket, hi_macd_minus_signal, hi_boillenger_upper, hi_boillenger_lower, hi_boillenger_channel_height = None, None, None, None, None, None, None, None, None, None
997
+ hi_hurst_exp, hi_ema_volume_long_periods = None, None
998
+ hi_tm1_rsi, hi_tm1_rsi_bucket, hi_tm1_rsi_trend = None, None, None
999
+ hi_fib_eval_result = None
1000
+ if hi_row is not None:
1001
+ hi_datetime = hi_row['datetime']
1002
+ hi_year = hi_row['year']
1003
+ hi_month = hi_row['month']
1004
+ hi_day = hi_row['day']
1005
+ hi_hour = hi_row['hour']
1006
+ hi_minute = hi_row['minute']
1007
+ hi_timestamp_ms = hi_row['timestamp_ms']
1008
+ hi_open = hi_row['open']
1009
+ hi_high = hi_row['high']
1010
+ hi_low = hi_row['low']
1011
+ hi_close = hi_row['close']
1012
+ hi_volume = hi_row['volume']
1013
+ hi_atr = hi_row['atr']
1014
+ hi_rsi = hi_row['rsi']
1015
+ hi_rsi_bucket = hi_row['rsi_bucket']
1016
+ hi_rsi_trend = hi_row['rsi_trend']
1017
+ hi_mfi = hi_row['mfi']
1018
+ hi_mfi_bucket = hi_row['mfi_bucket']
1019
+ hi_macd_minus_signal = hi_row['macd_minus_signal']
1020
+ hi_boillenger_upper = hi_row['boillenger_upper']
1021
+ hi_boillenger_lower = hi_row['boillenger_lower']
1022
+ hi_boillenger_channel_height = hi_row['boillenger_channel_height']
1023
+ hi_hurst_exp = hi_row['hurst_exp']
1024
+
1025
+ hi_tm1_rsi = hi_row_tm1['rsi']
1026
+ hi_tm1_rsi_bucket = hi_row_tm1['rsi_bucket']
1027
+ hi_tm1_rsi_trend = hi_row_tm1['rsi_trend']
1028
+
1029
+ hi_ema_volume_long_periods = hi_row['ema_volume_long_periods']
1030
+ hi_ema_short_slope = hi_row['ema_short_slope'] if 'ema_short_slope' in pd_hi_candles.columns else 0
1031
+ hi_normalized_ema_short_slope = hi_row['normalized_ema_short_slope'] if 'normalized_ema_short_slope' in pd_hi_candles.columns else 0
1032
+ hi_ema_long_slope = hi_row['ema_long_slope'] if 'ema_long_slope' in pd_hi_candles.columns else 0
1033
+ hi_normalized_ema_long_slope = hi_row['normalized_ema_long_slope'] if 'normalized_ema_long_slope' in pd_hi_candles.columns else 0
1034
+ hi_tm1_normalized_ema_long_slope = hi_row_tm1['normalized_ema_long_slope'] if 'normalized_ema_long_slope' in pd_hi_candles.columns else 0
1035
+
1036
+ hi_max_short_periods = hi_row['max_short_periods']
1037
+ hi_idmax_short_periods = int(hi_row['idmax_short_periods']) if not math.isnan(hi_row['idmax_short_periods']) else None
1038
+ hi_idmax_dt_short_periods = pd_hi_candles.at[hi_idmax_short_periods,'datetime'] if not(hi_idmax_short_periods is None or pd.isna(hi_idmax_short_periods)) else None
1039
+ hi_max_long_periods = hi_row['max_long_periods']
1040
+ hi_idmax_long_periods = int(hi_row['idmax_long_periods']) if not math.isnan(hi_row['idmax_long_periods']) else None
1041
+ hi_idmax_dt_long_periods = pd_hi_candles.at[hi_idmax_long_periods,'datetime'] if not(hi_idmax_long_periods is None or pd.isna(hi_idmax_long_periods)) else None
1042
+
1043
+ hi_tm1_max_short_periods = hi_row_tm1['max_short_periods']
1044
+ hi_tm1_idmax_short_periods = int(hi_row_tm1['idmax_short_periods']) if not math.isnan(hi_row_tm1['idmax_short_periods']) else None
1045
+ hi_tm1_idmax_dt_short_periods = pd_hi_candles.at[hi_tm1_idmax_short_periods,'datetime'] if not(hi_tm1_idmax_short_periods is None or pd.isna(hi_tm1_idmax_short_periods)) else None
1046
+ hi_tm1_max_long_periods = hi_row_tm1['max_long_periods']
1047
+ hi_tm1_idmax_long_periods = int(hi_row_tm1['idmax_long_periods']) if not math.isnan(hi_row_tm1['idmax_long_periods']) else None
1048
+ hi_tm1_idmax_dt_long_periods = pd_hi_candles.at[hi_tm1_idmax_long_periods,'datetime'] if not(hi_tm1_idmax_long_periods is None or pd.isna(hi_tm1_idmax_long_periods)) else None
1049
+
1050
+ hi_min_short_periods = hi_row['min_short_periods']
1051
+ hi_idmin_short_periods = int(hi_row['idmin_short_periods']) if not math.isnan(hi_row['idmin_short_periods']) else None
1052
+ hi_idmin_dt_short_periods = pd_hi_candles.at[hi_idmin_short_periods,'datetime'] if not (hi_idmin_short_periods is None or pd.isna(hi_idmin_short_periods)) else None
1053
+ hi_min_long_periods = hi_row['min_long_periods']
1054
+ hi_idmin_long_periods = int(hi_row['idmin_long_periods']) if not math.isnan(hi_row['idmin_long_periods']) else None
1055
+ hi_idmin_dt_long_periods = pd_hi_candles.at[hi_idmin_long_periods,'datetime'] if not (hi_idmin_long_periods is None or pd.isna(hi_idmin_long_periods)) else None
1056
+
1057
+ hi_tm1_min_short_periods = hi_row_tm1['min_short_periods']
1058
+ hi_tm1_idmin_short_periods = int(hi_row_tm1['idmin_short_periods']) if not math.isnan(hi_row_tm1['idmin_short_periods']) else None
1059
+ hi_tm1_idmin_dt_short_periods = pd_hi_candles.at[hi_tm1_idmin_short_periods,'datetime'] if not (hi_tm1_idmin_short_periods is None or pd.isna(hi_tm1_idmin_short_periods)) else None
1060
+ hi_tm1_min_long_periods = hi_row_tm1['min_long_periods']
1061
+ hi_tm1_idmin_long_periods = int(hi_row_tm1['idmin_long_periods']) if not math.isnan(hi_row_tm1['idmin_long_periods']) else None
1062
+ hi_tm1_idmin_dt_long_periods = pd_hi_candles.at[hi_tm1_idmin_long_periods,'datetime'] if not (hi_tm1_idmin_long_periods is None or pd.isna(hi_tm1_idmin_long_periods)) else None
1063
+
1064
+ hi_fib_eval_result = lookup_fib_target(hi_row_tm1, pd_hi_candles)
1065
+ hi_fib_short_periods_fib_target, hi_fib_short_periods_price_swing, hi_fib_long_periods_fib_target, hi_fib_long_periods_price_swing = None, None, None, None
1066
+ if hi_fib_eval_result:
1067
+ hi_fib_short_periods_fib_target = hi_fib_eval_result['short_periods']['fib_target']
1068
+ hi_fib_long_periods_fib_target = hi_fib_eval_result['long_periods']['fib_target']
1069
+
1070
+ last_candles, post_move_candles, post_move_price_change, post_move_price_change_percent = None, None, None, None
1071
+ if algo_param['last_candles_timeframe']=='lo':
1072
+ last_candles = pd_lo_candles[pd_lo_candles['timestamp_ms']<=lo_timestamp_ms].tail(algo_param['how_many_last_candles']).to_dict('records')
1073
+ assert(all([ candle['timestamp_ms']<=lo_timestamp_ms for candle in last_candles ]))
1074
+ post_move_candles = pd_lo_candles[pd_lo_candles['timestamp_ms']<=lo_timestamp_ms].tail(algo_param['post_move_num_intervals']).to_dict('records')
1075
+
1076
+ elif algo_param['last_candles_timeframe']=='hi' and hi_row is not None:
1077
+ last_candles = pd_hi_candles[pd_hi_candles['timestamp_ms']<=hi_timestamp_ms].tail(algo_param['how_many_last_candles']).to_dict('records')
1078
+ assert(all([ candle['timestamp_ms']<=hi_timestamp_ms for candle in last_candles ]))
1079
+ post_move_candles = pd_hi_candles[pd_hi_candles['timestamp_ms']<=hi_timestamp_ms].tail(algo_param['post_move_num_intervals']).to_dict('records')
1080
+
1081
+ post_move_price_change, post_move_price_change_percent = 0, 0
1082
+ if post_move_candles and len(post_move_candles)>=2:
1083
+ post_move_price_change = post_move_candles[-1]['close'] - post_move_candles[0]['open']
1084
+ post_move_price_change_percent = 0
1085
+ if post_move_price_change>0:
1086
+ post_move_price_change_percent = (post_move_candles[-1]['close']/post_move_candles[0]['open'] -1) * 100
1087
+ else:
1088
+ post_move_price_change_percent = -(post_move_candles[0]['close']/post_move_candles[-1]['open'] -1) * 100
1089
+
1090
+ ref_close_fast, ref_ema_close_fast = None, None
1091
+ if ref_row_fast is not None:
1092
+ ref_close_fast = ref_row_fast['close']
1093
+ ref_ema_close_fast = ref_row_fast['ema_close']
1094
+
1095
+ ref_close_slow, ref_ema_close_slow = None, None
1096
+ if ref_row_slow is not None:
1097
+ ref_close_slow = ref_row_slow['close']
1098
+ ref_ema_close_slow = ref_row_slow['ema_close']
1099
+
1100
+ # POSITION NOTIONAL MARKING lo_low, lo_high. pessimistic!
1101
+ def _refresh_current_position(timestamp_ms):
1102
+ this_ticker_open_trades = open_trades_by_ticker[ticker]
1103
+ current_position_usdt_buy = sum([x['size'] * lo_close for x in this_ticker_open_trades if x['side']=='buy'])
1104
+ current_position_usdt_sell = sum([x['size'] * lo_close for x in this_ticker_open_trades if x['side']=='sell'])
1105
+ current_position_usdt = current_position_usdt_buy + current_position_usdt_sell
1106
+
1107
+ this_ticker_current_position_usdt_buy = sum([x['size'] * lo_close for x in this_ticker_open_trades if x['side']=='buy'])
1108
+ this_ticker_current_position_usdt_sell = sum([x['size'] * lo_close for x in this_ticker_open_trades if x['side']=='sell'])
1109
+
1110
+ this_ticker_historical_stops = sl_by_ticker[ticker]
1111
+
1112
+ entries_since_sl : Union[int, None] = -1
1113
+
1114
+ avg_entry_price = None
1115
+ pos_side = '---'
1116
+ max_trade_age_ms = timestamp_ms
1117
+ if this_ticker_open_trades:
1118
+ max_trade_age_ms = timestamp_ms - max([trade['timestamp_ms'] for trade in this_ticker_open_trades ])
1119
+
1120
+ avg_entry_price = sum([ trade['entry_price']*trade['size'] for trade in this_ticker_open_trades]) / sum([ trade['size'] for trade in this_ticker_open_trades])
1121
+
1122
+ sides = [ x['side'] for x in this_ticker_open_trades ]
1123
+ if len(set(sides))==1:
1124
+ if sides[0]=='buy':
1125
+ pos_side = 'buy'
1126
+ else:
1127
+ pos_side = 'sell'
1128
+
1129
+ max_sl_trade_age_ms = None
1130
+ if this_ticker_historical_stops:
1131
+ last_sl_timestamp_ms = this_ticker_historical_stops[-1]['timestamp_ms']
1132
+ max_sl_trade_age_ms = timestamp_ms - last_sl_timestamp_ms
1133
+
1134
+ # In single legged trading, we either long or short for a particular ticker at any given moment
1135
+ assert(
1136
+ (this_ticker_current_position_usdt_buy>=0 and this_ticker_current_position_usdt_sell==0)
1137
+ or (this_ticker_current_position_usdt_buy==0 and this_ticker_current_position_usdt_sell>=0))
1138
+
1139
+ if this_ticker_current_position_usdt_buy>0:
1140
+ this_ticker_open_positions_side = 'buy'
1141
+ this_ticker_current_position_usdt = this_ticker_current_position_usdt_buy
1142
+ elif this_ticker_current_position_usdt_sell>0:
1143
+ this_ticker_open_positions_side = 'sell'
1144
+ this_ticker_current_position_usdt = this_ticker_current_position_usdt_sell
1145
+ else:
1146
+ this_ticker_open_positions_side = 'flat'
1147
+ this_ticker_current_position_usdt = 0
1148
+
1149
+ return {
1150
+ 'avg_entry_price' : avg_entry_price,
1151
+ 'side' : pos_side,
1152
+ 'current_position_usdt_buy' : current_position_usdt_buy,
1153
+ 'current_position_usdt_sell' : current_position_usdt_sell,
1154
+ 'current_position_usdt' : current_position_usdt,
1155
+ 'this_ticker_open_trades' : this_ticker_open_trades,
1156
+ 'this_ticker_current_position_usdt_buy' : this_ticker_current_position_usdt_buy,
1157
+ 'this_ticker_current_position_usdt_sell' : this_ticker_current_position_usdt_sell,
1158
+ 'this_ticker_open_positions_side' : this_ticker_open_positions_side,
1159
+ 'this_ticker_current_position_usdt' : this_ticker_current_position_usdt,
1160
+ 'max_trade_age_ms' : max_trade_age_ms,
1161
+ 'max_sl_trade_age_ms' : max_sl_trade_age_ms
1162
+ }
1163
+
1164
+ current_positions_info = _refresh_current_position(lo_timestamp_ms)
1165
+ avg_entry_price = current_positions_info['avg_entry_price']
1166
+ pos_side = current_positions_info['side']
1167
+ current_position_usdt_buy = current_positions_info['current_position_usdt_buy']
1168
+ current_position_usdt_sell = current_positions_info['current_position_usdt_sell']
1169
+ current_position_usdt = current_positions_info['current_position_usdt']
1170
+ this_ticker_open_trades = current_positions_info['this_ticker_open_trades']
1171
+ this_ticker_current_position_usdt_buy = current_positions_info['this_ticker_current_position_usdt_buy']
1172
+ this_ticker_current_position_usdt_sell = current_positions_info['this_ticker_current_position_usdt_sell']
1173
+ this_ticker_open_positions_side = current_positions_info['this_ticker_open_positions_side']
1174
+ this_ticker_current_position_usdt = current_positions_info['this_ticker_current_position_usdt']
1175
+ max_trade_age_ms = current_positions_info['max_trade_age_ms']
1176
+ max_sl_trade_age_ms = current_positions_info['max_sl_trade_age_ms']
1177
+ block_entry_since_last_sl = True if max_sl_trade_age_ms and max_sl_trade_age_ms<=min_sl_age_ms else False
1178
+
1179
+ def _close_open_positions(
1180
+ key, ticker,
1181
+ this_ticker_current_position_usdt,
1182
+ this_ticker_open_positions_side,
1183
+ current_position_usdt,
1184
+ trade_pnl,
1185
+ effective_tp_trailing_percent,
1186
+ row,
1187
+ reason,
1188
+ reason2,
1189
+ gloabl_state,
1190
+ all_trades,
1191
+ sl_by_ticker,
1192
+ open_trades_by_ticker,
1193
+ all_canvas,
1194
+ algo_param,
1195
+ standard_pnl_percent_buckets=BUCKETS_m100_100
1196
+ ):
1197
+ def _gains_losses_to_label(gains_losses_percent):
1198
+ gains_losses_percent_label = bucketize_val(gains_losses_percent, buckets=standard_pnl_percent_buckets)
1199
+
1200
+ if gains_losses_percent>=0:
1201
+ return f"gain {gains_losses_percent_label}%"
1202
+ else:
1203
+ return f"loss {gains_losses_percent_label}%"
1204
+
1205
+ def _how_long_before_closed_sec_to_label(how_long_before_closed_sec):
1206
+ how_long_before_closed_sec_label = None
1207
+ how_long_before_closed_hr = how_long_before_closed_sec/(60*60)
1208
+ if how_long_before_closed_hr<=1:
1209
+ how_long_before_closed_sec_label = "<=1hr"
1210
+ elif how_long_before_closed_hr>1 and how_long_before_closed_hr<=8:
1211
+ how_long_before_closed_sec_label = ">1hr <=8hr"
1212
+ elif how_long_before_closed_hr>8 and how_long_before_closed_hr<=24:
1213
+ how_long_before_closed_sec_label = ">8hr <=24hr"
1214
+ elif how_long_before_closed_hr>24 and how_long_before_closed_hr<=24*7:
1215
+ how_long_before_closed_sec_label = ">24hr <=7days"
1216
+ elif how_long_before_closed_hr>24*7 and how_long_before_closed_hr<=24*7*2:
1217
+ how_long_before_closed_sec_label = ">7days <=14days"
1218
+ else:
1219
+ how_long_before_closed_sec_label = ">14days"
1220
+ return how_long_before_closed_sec_label
1221
+
1222
+ this_ticker_open_trades = open_trades_by_ticker[ticker]
1223
+
1224
+ entry_dt = min([ trade['trade_datetime'] for trade in this_ticker_open_trades ])
1225
+ entry_dayofweek = entry_dt.dayofweek
1226
+ entry_hour = entry_dt.hour
1227
+
1228
+ this_datetime = row['datetime']
1229
+ this_timestamp_ms = row['timestamp_ms']
1230
+ dayofweek = row['dayofweek']
1231
+ high = row['high']
1232
+ low = row['low']
1233
+ close = row['close']
1234
+ ema_short_slope = row['ema_short_slope'] if 'ema_short_slope' in row else None
1235
+ ema_long_slope = row['ema_long_slope'] if 'ema_long_slope' in row else None
1236
+
1237
+ # Step 1. mark open trades as closed first
1238
+ entry_commission, exit_commission = 0, 0
1239
+ for trade in this_ticker_open_trades:
1240
+ entry_commission += trade['commission']
1241
+ if this_ticker_open_positions_side=='buy':
1242
+ exit_commission += close * trade['size'] * commission_bps / 10000
1243
+
1244
+ else:
1245
+ exit_commission += close * trade['size'] * commission_bps / 10000
1246
+ trade['trade_pnl'] = 0 # trade_pnl parked under closing trade
1247
+ trade['trade_pnl_bps'] = 0
1248
+ trade['closed'] = True
1249
+ max_pain = min([ trade['max_pain'] for trade in this_ticker_open_trades])
1250
+ max_pain_percent = max_pain/this_ticker_current_position_usdt * 100
1251
+ max_pain_percent_label = _gains_losses_to_label(max_pain_percent)
1252
+
1253
+ timestamp_ms_from_closed_trades = min([ trade['timestamp_ms'] for trade in this_ticker_open_trades])
1254
+ num_impacting_economic_calendars = min([ trade['num_impacting_economic_calendars'] if 'num_impacting_economic_calendars' in trade else 0 for trade in this_ticker_open_trades])
1255
+ max_camp = max([ trade['max_camp'] for trade in this_ticker_open_trades])
1256
+ entry_post_move_price_change_percent = max([ trade['post_move_price_change_percent'] if 'post_move_price_change_percent' in trade else 0 for trade in this_ticker_open_trades ])
1257
+
1258
+ # Step 2. Update global_state
1259
+ trade_pnl_less_comm = trade_pnl - (entry_commission + exit_commission)
1260
+ gains_losses_percent = trade_pnl_less_comm/this_ticker_current_position_usdt * 100
1261
+ gains_losses_percent_label = _gains_losses_to_label(gains_losses_percent)
1262
+ how_long_before_closed_sec = (this_timestamp_ms - timestamp_ms_from_closed_trades) / 1000
1263
+ how_long_before_closed_sec_label = _how_long_before_closed_sec_to_label(how_long_before_closed_sec)
1264
+
1265
+ gloabl_state.total_equity += trade_pnl_less_comm
1266
+ gloabl_state.total_commission += exit_commission
1267
+ cash_before = gloabl_state.cash
1268
+ gloabl_state.cash = gloabl_state.total_equity
1269
+ cash_after = gloabl_state.cash
1270
+ running_total_num_positions : int = len(open_trades_by_ticker)
1271
+
1272
+ # Step 3. closing trade
1273
+ # closing_price = low if this_ticker_open_positions_side=='buy' else high # pessimistic!
1274
+ closing_price = close
1275
+ closing_trade = {
1276
+ 'trade_datetime' : this_datetime,
1277
+ 'timestamp_ms' : this_timestamp_ms,
1278
+ 'dayofweek' : dayofweek,
1279
+ 'entry_dt' : entry_dt,
1280
+ 'entry_dayofweek' : entry_dayofweek,
1281
+ 'entry_hour' : entry_hour,
1282
+ 'exchange' : exchange.name,
1283
+ 'symbol' : ticker,
1284
+ 'side' : 'sell' if this_ticker_open_positions_side=='buy' else 'buy',
1285
+ 'size' : this_ticker_current_position_usdt / closing_price, # in base ccy
1286
+ 'entry_price' : closing_price, # pessimistic!
1287
+ 'closed' : True,
1288
+ 'reason' : reason,
1289
+ 'reason2' : reason2,
1290
+ 'total_equity' : gloabl_state.total_equity,
1291
+ 'this_ticker_current_position_usdt' : this_ticker_current_position_usdt,
1292
+ 'current_position_usdt' : current_position_usdt,
1293
+ 'running_total_num_positions' : running_total_num_positions,
1294
+ 'cash_before' : cash_before,
1295
+ 'cash_after' : cash_after,
1296
+ 'order_notional' : this_ticker_current_position_usdt,
1297
+ 'trade_pnl' : trade_pnl,
1298
+ 'commission' : exit_commission,
1299
+ 'max_pain' : max_pain,
1300
+ 'trade_pnl_less_comm': trade_pnl_less_comm,
1301
+ 'trade_pnl_bps' : (trade_pnl / this_ticker_current_position_usdt) * 100 * 100 if this_ticker_current_position_usdt!=0 else 0,
1302
+ 'gains_losses_percent' : gains_losses_percent,
1303
+ 'gains_losses_percent_label' : gains_losses_percent_label,
1304
+ 'how_long_before_closed_sec' : how_long_before_closed_sec,
1305
+ 'how_long_before_closed_sec_label' : how_long_before_closed_sec_label,
1306
+ 'max_pain_percent' : max_pain_percent,
1307
+ 'max_pain_percent_label' : max_pain_percent_label,
1308
+ 'ema_short_slope' : ema_short_slope,
1309
+ 'ema_long_slope' : ema_long_slope,
1310
+ 'num_impacting_economic_calendars' : num_impacting_economic_calendars,
1311
+ 'max_camp' : max_camp,
1312
+ 'entry_post_move_price_change_percent' : entry_post_move_price_change_percent
1313
+ }
1314
+ _last_open_trade = this_ticker_open_trades[-1]
1315
+ additional_fields = {field: _last_open_trade[field] if field in _last_open_trade else None for field in algo_param['additional_trade_fields']}
1316
+ closing_trade.update(additional_fields)
1317
+ all_trades.append(closing_trade)
1318
+ if reason=='SL':
1319
+ sl_by_ticker[ticker].append(closing_trade)
1320
+ open_trades_by_ticker[ticker].clear()
1321
+
1322
+ if plot_timeseries:
1323
+ '''
1324
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.axvline.html
1325
+ linestyle='-' means solid line. If you don't supply linestyle, the vertical line wont show!!!
1326
+ '''
1327
+ color = 'green' if reason=='TP' or (reason=='HC' and trade_pnl_less_comm>0) else 'red'
1328
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"]['time_series_canvas'].axvline(x=this_datetime, color=color, linewidth=2, linestyle='--')
1329
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"]['time_series_canvas'].scatter([this_datetime, this_datetime], [low, high], color=color)
1330
+
1331
+ # UNREAL EVALUATION. We're being pessimistic! We use low/high for estimating unrealized_pnl for buys and sells respectively here.
1332
+ pnl_percent_notional = 0
1333
+ if current_position_usdt>0:
1334
+ unrealized_pnl, unrealized_pnl_interval, unrealized_pnl_open, unrealized_pnl_live_optimistic, unrealized_pnl_live_pessimistic, unrealized_pnl_live, max_pnl_percent_notional, unrealized_pnl_boillenger, unrealized_pnl_sl, max_unrealized_pnl_live, max_pain, recovered_pnl_optimistic, recovered_pnl_pessimistic, max_recovered_pnl = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 # USDT
1335
+ _asymmetric_tp_bps = algo_param['asymmetric_tp_bps'] if lo_dayofweek in algo_param['cautious_dayofweek'] else 0
1336
+
1337
+ max_unrealized_pnl_live = max([ trade['max_unrealized_pnl_live'] if 'max_unrealized_pnl_live' in trade else 0 for trade in this_ticker_open_trades ])
1338
+ # 'min' max_pain becaues max_pain is a negative number. It's a loss!
1339
+ max_pain = min([ trade['max_pain'] if 'max_pain' in trade else 0 for trade in this_ticker_open_trades ])
1340
+ max_recovered_pnl = max([ trade['max_recovered_pnl'] if 'max_recovered_pnl' in trade else 0 for trade in this_ticker_open_trades ])
1341
+ trade_datetime = max([ trade['trade_datetime'] if 'trade_datetime' in trade else 0 for trade in this_ticker_open_trades ])
1342
+ entry_post_move_price_change_percent = max([ trade['post_move_price_change_percent'] if 'post_move_price_change_percent' in trade else 0 for trade in this_ticker_open_trades ])
1343
+ max_camp = max([ trade['max_camp'] for trade in this_ticker_open_trades])
1344
+ running_sl_percent_hard = max([ trade['running_sl_percent_hard'] for trade in this_ticker_open_trades])
1345
+
1346
+ max_pnl_potential_percent = None
1347
+ if any([ trade for trade in this_ticker_open_trades if 'target_price' in trade and trade['target_price'] ]):
1348
+ max_pnl_potential_percent = max([ (trade['target_price']/trade['entry_price'] -1) *100 if trade['side']=='buy' else (trade['entry_price']/trade['target_price'] -1) *100 for trade in this_ticker_open_trades if 'target_price' in trade ])
1349
+
1350
+ kwargs = {k: v for k, v in locals().items() if k in sl_adj_func_params}
1351
+ sl_adj_func_result = sl_adj_func(**kwargs)
1352
+ running_sl_percent_hard = sl_adj_func_result['running_sl_percent_hard']
1353
+
1354
+ # this_ticker_open_trades should be updated after SL adj eval
1355
+ for trade in this_ticker_open_trades:
1356
+ trade['running_sl_percent_hard'] = running_sl_percent_hard
1357
+
1358
+ kwargs = {k: v for k, v in locals().items() if k in trailing_stop_threshold_eval_func_params}
1359
+ trailing_stop_threshold_eval_func_result = trailing_stop_threshold_eval_func(**kwargs)
1360
+ tp_min_percent = trailing_stop_threshold_eval_func_result['tp_min_percent']
1361
+ tp_max_percent = trailing_stop_threshold_eval_func_result['tp_max_percent']
1362
+ recover_min_percent = algo_param['recover_min_percent'] if 'recover_min_percent' in algo_param else None
1363
+ recover_max_pain_percent = algo_param['recover_max_pain_percent'] if 'recover_max_pain_percent' in algo_param else None
1364
+
1365
+ # tp_min_percent adj: Strategies where target_price not based on tp_max_percent, but variable
1366
+ if max_pnl_potential_percent and max_pnl_potential_percent<tp_max_percent:
1367
+ tp_minmax_ratio = tp_min_percent/tp_max_percent
1368
+ tp_max_percent = max_pnl_potential_percent
1369
+ tp_min_percent = tp_minmax_ratio * tp_max_percent
1370
+
1371
+ unrealized_pnl_eval_result = pnl_eval_func(lo_row, lo_row_tm1, running_sl_percent_hard, this_ticker_open_trades, algo_param)
1372
+ unrealized_pnl_interval = unrealized_pnl_eval_result['unrealized_pnl_interval']
1373
+ unrealized_pnl_open = unrealized_pnl_eval_result['unrealized_pnl_open']
1374
+ unrealized_pnl_live_optimistic = unrealized_pnl_eval_result['unrealized_pnl_live_optimistic']
1375
+ unrealized_pnl_live_pessimistic = unrealized_pnl_eval_result['unrealized_pnl_live_pessimistic']
1376
+ unrealized_pnl_tp = unrealized_pnl_eval_result['unrealized_pnl_tp']
1377
+ unrealized_pnl_sl = unrealized_pnl_eval_result['unrealized_pnl_sl']
1378
+ unrealized_pnl_live = unrealized_pnl_live_pessimistic
1379
+
1380
+ if unrealized_pnl_live>0 and unrealized_pnl_live_optimistic>max_unrealized_pnl_live:
1381
+ max_unrealized_pnl_live = unrealized_pnl_live_optimistic
1382
+ for trade in this_ticker_open_trades:
1383
+ trade['max_unrealized_pnl_live'] = max_unrealized_pnl_live # Evaluated optimistically!!!
1384
+
1385
+ # Do this before max_pain updated
1386
+ if unrealized_pnl_live<0 and unrealized_pnl_live_optimistic>max_pain:
1387
+ recovered_pnl_optimistic = unrealized_pnl_live_optimistic - max_pain
1388
+ recovered_pnl_pessimistic = unrealized_pnl_live_pessimistic - max_pain
1389
+ if recovered_pnl_optimistic>max_recovered_pnl:
1390
+ max_recovered_pnl = recovered_pnl_optimistic
1391
+ for trade in this_ticker_open_trades:
1392
+ trade['max_recovered_pnl'] = max_recovered_pnl
1393
+
1394
+ if unrealized_pnl_live<0:
1395
+ if unrealized_pnl_live<max_pain:
1396
+ max_pain = unrealized_pnl_live
1397
+ for trade in this_ticker_open_trades:
1398
+ trade['max_pain'] = max_pain # unrealized_pnl_live is set to unrealized_pnl_live_pessimistic!
1399
+
1400
+ if unrealized_pnl_live<0 and max_unrealized_pnl_live>0:
1401
+ # If out unrealized_pnl_live already fell from positive to negative, reset max_unrealized_pnl_live back to zero
1402
+ max_unrealized_pnl_live = 0
1403
+ for trade in this_ticker_open_trades:
1404
+ trade['max_unrealized_pnl_live'] = max_unrealized_pnl_live
1405
+
1406
+ unrealized_pnl = unrealized_pnl_live
1407
+ pnl_percent_notional = unrealized_pnl_open / current_position_usdt * 100 # This is evaluated using open (Don't use close, that's forward bias!)
1408
+ max_pnl_percent_notional = max_unrealized_pnl_live / current_position_usdt * 100
1409
+ max_pain_percent_notional = max_pain / current_position_usdt * 100
1410
+ max_recovered_pnl_percent_notional = max_recovered_pnl / current_position_usdt * 100
1411
+
1412
+ if (
1413
+ (pnl_percent_notional>0 and pnl_percent_notional>=tp_min_percent)
1414
+ or (
1415
+ recover_max_pain_percent
1416
+ and pnl_percent_notional<0
1417
+ and max_recovered_pnl_percent_notional>=recover_min_percent
1418
+ and abs(max_pain_percent_notional)>=recover_max_pain_percent
1419
+ ) # Taking 'abs': Trailing stop can fire if trade moves in either direction - if your trade is losing trade.
1420
+ ):
1421
+ '''
1422
+
1423
+ 'effective_tp_trailing_percent' is initialized to float('inf') on entries. Whenever 'pnl_percent_notional' crosses 'tp_min_percent', trailing stop mechanism kicks in.
1424
+
1425
+ https://norman-lm-fung.medium.com/gradually-tightened-trailing-stops-f7854bf1e02b
1426
+
1427
+ 'effective_tp_trailing_percent' is used to TRIGGER trailing stop.
1428
+ Please be careful if you're marking closing trade with candle close.
1429
+ '''
1430
+ if algo_param['use_gradual_tightened_trailing_stops']:
1431
+ effective_tp_trailing_percent = calc_eff_trailing_sl(
1432
+ tp_min_percent = tp_min_percent,
1433
+ tp_max_percent = tp_max_percent,
1434
+ sl_percent_trailing = algo_param['sl_percent_trailing'],
1435
+ pnl_percent_notional = max_pnl_percent_notional if pnl_percent_notional>0 else max_recovered_pnl_percent_notional,
1436
+ default_effective_tp_trailing_percent = float('inf'),
1437
+ linear=True if algo_param['trailing_stop_mode']=='linear' else False, # trailing_stop_mode: linear vs parabolic
1438
+ pow=5
1439
+ )
1440
+ else:
1441
+ effective_tp_trailing_percent = algo_param['sl_percent_trailing']
1442
+
1443
+ # 1. SL
1444
+ if (
1445
+ unrealized_pnl_live < 0
1446
+ or (
1447
+ (unrealized_pnl_live>0 and unrealized_pnl_live<max_unrealized_pnl_live)
1448
+ or (
1449
+ unrealized_pnl_live<0
1450
+ and recovered_pnl_pessimistic<max_recovered_pnl
1451
+ and abs(max_recovered_pnl_percent_notional)>=recover_min_percent
1452
+ and abs(max_pain_percent_notional)>=recover_max_pain_percent
1453
+ )
1454
+ )
1455
+ ):
1456
+ # unrealized_pnl_live is set to unrealized_pnl_live_pessimistic!
1457
+ loss_hard = abs(unrealized_pnl_live)/this_ticker_current_position_usdt * 100 if unrealized_pnl_live<0 else 0
1458
+
1459
+ if unrealized_pnl_live>0:
1460
+ loss_trailing = (1 - unrealized_pnl_live/max_unrealized_pnl_live) * 100 if unrealized_pnl_live>0 and unrealized_pnl_live<max_unrealized_pnl_live else 0
1461
+ elif unrealized_pnl_live<0:
1462
+ loss_trailing = (1 - recovered_pnl_pessimistic/max_recovered_pnl) * 100 if unrealized_pnl_live<0 and recovered_pnl_pessimistic<max_recovered_pnl else 0
1463
+
1464
+ if loss_hard>=running_sl_percent_hard:
1465
+ unrealized_pnl = (running_sl_percent_hard/algo_param['sl_hard_percent']) * unrealized_pnl_sl
1466
+ reason2 = "sl_hard_percent"
1467
+ elif (
1468
+ loss_trailing>=effective_tp_trailing_percent # loss_trailing is evaluated pessimistically.
1469
+ # and pnl_percent_notional>tp_min_percent
1470
+ # and unrealized_pnl_live >= sl_trailing_min_threshold_usdt
1471
+ ):
1472
+ '''
1473
+ If you're using 'effective_tp_trailing_percent' to approx unrealised pnl, make sure "loss_trailing>=effective_tp_trailing_percent" is the only condition.
1474
+ Don't AND this with other condition. Otherwise use close price to approx unrealised pnl instead!!!
1475
+ '''
1476
+ if unrealized_pnl_live>0:
1477
+ unrealized_pnl = min(
1478
+ ((100-effective_tp_trailing_percent)/100) * max_unrealized_pnl_live,
1479
+ this_ticker_current_position_usdt * algo_param['tp_max_percent']/100
1480
+ )
1481
+ else:
1482
+ unrealized_pnl = max_pain + ((100-effective_tp_trailing_percent)/100) * max_recovered_pnl
1483
+ # unrealized_pnl = unrealized_pnl_interval # less accurate
1484
+ reason2 = "sl_percent_trailing"
1485
+
1486
+ if (
1487
+ (loss_hard>=running_sl_percent_hard)
1488
+ or (
1489
+ loss_trailing>=effective_tp_trailing_percent
1490
+ # and pnl_percent_notional>tp_min_percent
1491
+ # and unrealized_pnl_live >= sl_trailing_min_threshold_usdt
1492
+ )
1493
+ ):
1494
+ block_entry_since_last_sl = True
1495
+ reason = 'SL' if unrealized_pnl<0 else 'TP'
1496
+ _close_open_positions(
1497
+ key,
1498
+ ticker,
1499
+ this_ticker_current_position_usdt,
1500
+ this_ticker_open_positions_side,
1501
+ current_position_usdt,
1502
+ unrealized_pnl,
1503
+ effective_tp_trailing_percent,
1504
+ lo_row, reason, reason2, gloabl_state,
1505
+ all_trades, sl_by_ticker, open_trades_by_ticker,
1506
+ all_canvas,
1507
+ algo_param
1508
+ )
1509
+ current_positions_info = _refresh_current_position(lo_timestamp_ms)
1510
+ avg_entry_price = current_positions_info['avg_entry_price']
1511
+ pos_side = current_positions_info['side']
1512
+ current_position_usdt_buy = current_positions_info['current_position_usdt_buy']
1513
+ current_position_usdt_sell = current_positions_info['current_position_usdt_sell']
1514
+ current_position_usdt = current_positions_info['current_position_usdt']
1515
+ this_ticker_open_trades = current_positions_info['this_ticker_open_trades']
1516
+ this_ticker_current_position_usdt_buy = current_positions_info['this_ticker_current_position_usdt_buy']
1517
+ this_ticker_current_position_usdt_sell = current_positions_info['this_ticker_current_position_usdt_sell']
1518
+ this_ticker_open_positions_side = current_positions_info['this_ticker_open_positions_side']
1519
+ this_ticker_current_position_usdt = current_positions_info['this_ticker_current_position_usdt']
1520
+ max_sl_trade_age_ms = current_positions_info['max_sl_trade_age_ms']
1521
+
1522
+ # sl_percent_trailing = algo_param['sl_percent_trailing'] # Reset! Remember!
1523
+ this_ticker_open_positions_side='flat' # Reset!
1524
+ reversal_camp_cache[key] = REVERSAL_CAMP_ITEM.copy()
1525
+
1526
+ # 2. TP: Trigger by unrealized_pnl_live_optimistic, not unrealized_pnl_live (which is unrealized_pnl_live_pessimistic). Pnl estimation from unrealized_pnl_boillenger however!!!
1527
+ if this_ticker_current_position_usdt>0 and unrealized_pnl_live_optimistic>0:
1528
+ kwargs = {k: v for k, v in locals().items() if k in tp_eval_func_params}
1529
+ tp_eval_func_result = tp_eval_func(**kwargs)
1530
+
1531
+ if tp_eval_func_result:
1532
+ unrealized_pnl_tp = min(
1533
+ unrealized_pnl_tp,
1534
+ this_ticker_current_position_usdt * algo_param['tp_max_percent']/100
1535
+ )
1536
+ _close_open_positions(
1537
+ key, ticker,
1538
+ this_ticker_current_position_usdt,
1539
+ this_ticker_open_positions_side,
1540
+ current_position_usdt,
1541
+ unrealized_pnl_tp,
1542
+ effective_tp_trailing_percent,
1543
+ lo_row, 'TP', '', gloabl_state,
1544
+ all_trades, sl_by_ticker, open_trades_by_ticker,
1545
+ all_canvas,
1546
+ algo_param
1547
+ )
1548
+ current_position_usdt -= this_ticker_current_position_usdt
1549
+ this_ticker_current_position_usdt = 0
1550
+ this_ticker_open_positions_side='flat' # Reset!
1551
+ reversal_camp_cache[key] = REVERSAL_CAMP_ITEM.copy()
1552
+
1553
+ def _position_size_and_cash_check(
1554
+ current_position_usdt : float, # All positions added together (include other tickers)
1555
+ this_ticker_current_position_usdt : float, # This position only
1556
+ target_order_notional : float,
1557
+ total_equity : float,
1558
+ target_position_size_percent_total_equity : float,
1559
+ cash : float
1560
+ ) -> bool:
1561
+ return (
1562
+ (current_position_usdt + target_order_notional <= total_equity * (target_position_size_percent_total_equity/100))
1563
+ and (this_ticker_current_position_usdt + target_order_notional <= total_equity * (target_position_size_percent_total_equity/100))
1564
+ and cash >= target_order_notional
1565
+ )
1566
+
1567
+ entry_adj_bps = 0 # Essentially disable this for time being.
1568
+
1569
+ if(
1570
+ lo_low<=(lo_boillenger_lower*(1-entry_adj_bps/10000))
1571
+ ):
1572
+ lo_boillenger_lower_breached_history = lo_boillenger_lower_breached_cache.get(key, [])
1573
+ lo_boillenger_upper_breached_history = lo_boillenger_upper_breached_cache.get(key, [])
1574
+ lo_boillenger_lower_breached_cache[key] = lo_boillenger_lower_breached_history
1575
+ lo_boillenger_upper_breached_cache[key] = lo_boillenger_upper_breached_history
1576
+ lo_boillenger_upper_breached_history.clear()
1577
+ lo_boillenger_lower_breached_history.append(lo_datetime)
1578
+ reversal_camp_cache[key] = REVERSAL_CAMP_ITEM.copy()
1579
+ elif(
1580
+ lo_high>=(lo_boillenger_upper*(1+entry_adj_bps/10000))
1581
+ ):
1582
+ lo_boillenger_lower_breached_history = lo_boillenger_lower_breached_cache.get(key, [])
1583
+ lo_boillenger_upper_breached_history = lo_boillenger_upper_breached_cache.get(key, [])
1584
+ lo_boillenger_lower_breached_cache[key] = lo_boillenger_lower_breached_history
1585
+ lo_boillenger_upper_breached_cache[key] = lo_boillenger_upper_breached_history
1586
+ lo_boillenger_lower_breached_history.clear()
1587
+ lo_boillenger_upper_breached_history.append(lo_datetime)
1588
+ reversal_camp_cache[key] = REVERSAL_CAMP_ITEM.copy()
1589
+
1590
+ if algo_param['constant_order_notional']:
1591
+ target_order_notional = algo_param['target_order_notional']
1592
+ else:
1593
+ kwargs = {k: v for k, v in locals().items() if k in order_notional_adj_func_params}
1594
+ order_notional_adj_func_result = order_notional_adj_func(**kwargs)
1595
+ target_order_notional = order_notional_adj_func_result['target_order_notional']
1596
+
1597
+ order_notional_long, order_notional_short = target_order_notional, target_order_notional
1598
+ if algo_param['clip_order_notional_to_best_volumes']:
1599
+ order_notional_long = min(lo_volume * lo_low, target_order_notional)
1600
+ order_notional_short = min(lo_volume * lo_high, target_order_notional)
1601
+
1602
+ kwargs = {k: v for k, v in locals().items() if k in allow_entry_initial_func_params}
1603
+ allow_entry_initial_func_result = allow_entry_initial_func(**kwargs)
1604
+ allow_entry_initial_long = allow_entry_initial_func_result['long']
1605
+ allow_entry_initial_short = allow_entry_initial_func_result['short']
1606
+ allow_entry_final_long = False
1607
+ allow_entry_final_short = False
1608
+
1609
+ if algo_param['enable_sliced_entry']:
1610
+ kwargs = {k: v for k, v in locals().items() if k in allow_slice_entry_func_params}
1611
+ allow_slice_entry_func_result = allow_slice_entry_func(**kwargs)
1612
+
1613
+ # 3. Entries
1614
+ if (
1615
+ algo_param['strategy_mode'] in [ 'long_only', 'long_short']
1616
+ and order_notional_long>0
1617
+ and (not algo_param['block_entries_on_impacting_ecoevents'] or num_impacting_economic_calendars==0)
1618
+ and not block_entry_since_last_sl
1619
+ and (
1620
+ (
1621
+ this_ticker_open_positions_side=='flat'
1622
+ and allow_entry_initial_long
1623
+ and _position_size_and_cash_check(current_position_usdt, this_ticker_current_position_usdt, order_notional_long, gloabl_state.total_equity, target_position_size_percent_total_equity, gloabl_state.cash)
1624
+ ) or (
1625
+ this_ticker_open_positions_side=='buy'
1626
+ and _position_size_and_cash_check(current_position_usdt, this_ticker_current_position_usdt, order_notional_long, gloabl_state.total_equity, target_position_size_percent_total_equity, gloabl_state.cash)
1627
+ and (algo_param['enable_sliced_entry'] and allow_slice_entry_func_result['long'])
1628
+ )
1629
+ )
1630
+ ):
1631
+ # Long
1632
+ order_notional = order_notional_long
1633
+
1634
+ if not reversal_camp_cache[key]['camp1'] and not reversal_camp_cache[key]['camp2'] and not reversal_camp_cache[key]['camp3']:
1635
+ reversal_camp_cache[key]['camp1'] = True
1636
+ reversal_camp_cache[key]['camp1_price'] = lo_close
1637
+ elif reversal_camp_cache[key]['camp1'] and not reversal_camp_cache[key]['camp2'] and not reversal_camp_cache[key]['camp3']:
1638
+ reversal_camp_cache[key]['camp2'] = True
1639
+ reversal_camp_cache[key]['camp2_price'] = lo_close
1640
+ elif reversal_camp_cache[key]['camp1'] and reversal_camp_cache[key]['camp2'] and not reversal_camp_cache[key]['camp3']:
1641
+ reversal_camp_cache[key]['camp3'] = True
1642
+ reversal_camp_cache[key]['camp3_price'] = lo_close
1643
+ reversal_camp_cache[key]['datetime'] = lo_datetime
1644
+
1645
+ fetch_historical_price_func = fetch_price
1646
+ kwargs = {k: v for k, v in locals().items() if k in allow_entry_final_func_params}
1647
+ allow_entry_final_func_result = allow_entry_final_func(**kwargs)
1648
+
1649
+ allow_entry_final_long = allow_entry_final_func_result['long']
1650
+ if (allow_entry_final_long):
1651
+ order_notional_adj_factor = algo_param['dayofweek_adj_map_order_notional'][lo_dayofweek]
1652
+ if order_notional>0 and order_notional_adj_factor>0:
1653
+ max_camp = _max_camp(reversal_camp_cache[key]['camp1'], reversal_camp_cache[key]['camp2'], reversal_camp_cache[key]['camp3'])
1654
+ target_price = allow_entry_final_func_result['target_price_long']
1655
+ reference_price = allow_entry_final_func_result['reference_price']
1656
+ sitting_on_boillenger_band = allow_entry_final_func_result['sitting_on_boillenger_band'] if 'sitting_on_boillenger_band' in allow_entry_final_func_result else None
1657
+ _additional_trade_fields = {k: v for k, v in locals().items() if k in algo_param['additional_trade_fields']}
1658
+
1659
+ commission = order_notional_adj_factor*order_notional * commission_bps / 10000
1660
+ gloabl_state.total_commission += commission
1661
+
1662
+ cash_before = gloabl_state.cash
1663
+ gloabl_state.cash = gloabl_state.cash - order_notional_adj_factor*order_notional - commission
1664
+ cash_after = gloabl_state.cash
1665
+
1666
+ running_total_num_positions : int = len(open_trades_by_ticker)
1667
+
1668
+ entry_price = allow_entry_final_func_result['entry_price_long']
1669
+ reversal_camp_cache[key]['price'] = entry_price
1670
+
1671
+ pnl_potential_bps = (target_price/entry_price - 1) *10000 if target_price else None
1672
+
1673
+ new_trade_0 = {
1674
+ 'trade_datetime' : lo_datetime,
1675
+ 'timestamp_ms' : lo_timestamp_ms,
1676
+ 'dayofweek' : lo_dayofweek,
1677
+ 'exchange' : exchange.name,
1678
+ 'symbol' : ticker,
1679
+ 'side' : 'buy',
1680
+ 'size' : order_notional_adj_factor*order_notional/lo_close, # in base ccy.
1681
+ 'entry_price' : entry_price,
1682
+ 'target_price' : target_price,
1683
+ 'pnl_potential_bps' : pnl_potential_bps,
1684
+ 'ref_ema_close_fast' : ref_ema_close_fast,
1685
+ 'running_sl_percent_hard' : algo_param['sl_hard_percent'],
1686
+ 'closed' : False,
1687
+ 'reason' : 'entry',
1688
+ 'reason2' : '',
1689
+ 'total_equity' : gloabl_state.total_equity,
1690
+ 'this_ticker_current_position_usdt' : this_ticker_current_position_usdt,
1691
+ 'current_position_usdt' : current_position_usdt,
1692
+ 'running_total_num_positions' : running_total_num_positions,
1693
+ 'cash_before' : cash_before,
1694
+ 'cash_after' : cash_after,
1695
+ 'order_notional' : order_notional,
1696
+ 'commission' : commission,
1697
+ 'max_pain' : 0,
1698
+ 'num_impacting_economic_calendars' : num_impacting_economic_calendars,
1699
+ 'max_camp': max_camp,
1700
+ 'post_move_price_change_percent' : post_move_price_change_percent
1701
+ }
1702
+ all_trades.append(new_trade_0)
1703
+ open_trades_by_ticker[ticker].append(new_trade_0)
1704
+ new_trade_0.update(_additional_trade_fields)
1705
+
1706
+ # Resets!
1707
+ effective_tp_trailing_percent = float('inf')
1708
+ lo_boillenger_lower_breached_history = lo_boillenger_lower_breached_cache.get(key, [])
1709
+ lo_boillenger_lower_breached_history.clear()
1710
+
1711
+ if plot_timeseries:
1712
+ '''
1713
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.axvline.html
1714
+ linestyle='-' means solid line. If you don't supply linestyle, the vertical line wont show!!!
1715
+ '''
1716
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"]['time_series_canvas'].axvline(x=lo_datetime, color='gray', linewidth=1, linestyle='-')
1717
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"]['time_series_canvas'].scatter([lo_datetime, lo_datetime], [lo_low, lo_high], color='gray')
1718
+
1719
+ elif (
1720
+ algo_param['strategy_mode'] in [ 'short_only', 'long_short']
1721
+ and order_notional_short>0
1722
+ and (not algo_param['block_entries_on_impacting_ecoevents'] or num_impacting_economic_calendars==0)
1723
+ and not block_entry_since_last_sl
1724
+ and (
1725
+ (
1726
+ this_ticker_open_positions_side=='flat'
1727
+ and allow_entry_initial_short
1728
+ and _position_size_and_cash_check(current_position_usdt, this_ticker_current_position_usdt, order_notional_short, gloabl_state.total_equity, target_position_size_percent_total_equity, gloabl_state.cash)
1729
+ ) or (
1730
+ this_ticker_open_positions_side=='sell'
1731
+ and _position_size_and_cash_check(current_position_usdt, this_ticker_current_position_usdt, order_notional_short, gloabl_state.total_equity, target_position_size_percent_total_equity, gloabl_state.cash)
1732
+ and (algo_param['enable_sliced_entry'] and allow_slice_entry_func_result['short'])
1733
+ )
1734
+ )
1735
+ ):
1736
+ # Short
1737
+ order_notional = order_notional_short
1738
+
1739
+ if not reversal_camp_cache[key]['camp1'] and not reversal_camp_cache[key]['camp2'] and not reversal_camp_cache[key]['camp3']:
1740
+ reversal_camp_cache[key]['camp1'] = True
1741
+ reversal_camp_cache[key]['camp1_price'] = lo_close
1742
+ elif reversal_camp_cache[key]['camp1'] and not reversal_camp_cache[key]['camp2'] and not reversal_camp_cache[key]['camp3']:
1743
+ reversal_camp_cache[key]['camp2'] = True
1744
+ reversal_camp_cache[key]['camp2_price'] = lo_close
1745
+ elif reversal_camp_cache[key]['camp1'] and reversal_camp_cache[key]['camp2'] and not reversal_camp_cache[key]['camp3']:
1746
+ reversal_camp_cache[key]['camp3'] = True
1747
+ reversal_camp_cache[key]['camp3_price'] = lo_close
1748
+ reversal_camp_cache[key]['datetime'] = lo_datetime
1749
+
1750
+ fetch_historical_price_func = fetch_price
1751
+ kwargs = {k: v for k, v in locals().items() if k in allow_entry_final_func_params}
1752
+ allow_entry_final_func_result = allow_entry_final_func(**kwargs)
1753
+
1754
+ allow_entry_final_short = allow_entry_final_func_result['short']
1755
+ if (allow_entry_final_short):
1756
+ order_notional_adj_factor = algo_param['dayofweek_adj_map_order_notional'][lo_dayofweek]
1757
+ if order_notional>0 and order_notional_adj_factor>0:
1758
+ max_camp = _max_camp(reversal_camp_cache[key]['camp1'], reversal_camp_cache[key]['camp2'], reversal_camp_cache[key]['camp3'])
1759
+ target_price = allow_entry_final_func_result['target_price_short']
1760
+ reference_price = allow_entry_final_func_result['reference_price']
1761
+ sitting_on_boillenger_band = allow_entry_final_func_result['sitting_on_boillenger_band'] if 'sitting_on_boillenger_band' in allow_entry_final_func_result else None
1762
+ _additional_trade_fields = {k: v for k, v in locals().items() if k in algo_param['additional_trade_fields']}
1763
+
1764
+ commission = order_notional_adj_factor*order_notional * commission_bps / 10000
1765
+ gloabl_state.total_commission += commission
1766
+
1767
+ cash_before = gloabl_state.cash
1768
+ gloabl_state.cash = gloabl_state.cash - order_notional_adj_factor*order_notional - commission
1769
+ cash_after = gloabl_state.cash
1770
+
1771
+ running_total_num_positions : int = len(open_trades_by_ticker)
1772
+
1773
+ entry_price = allow_entry_final_func_result['entry_price_short']
1774
+ reversal_camp_cache[key]['price'] = entry_price
1775
+
1776
+ pnl_potential_bps = (entry_price/target_price - 1) *10000 if target_price else None
1777
+
1778
+ new_trade_0 = {
1779
+ 'trade_datetime' : lo_datetime,
1780
+ 'timestamp_ms' : lo_timestamp_ms,
1781
+ 'dayofweek' : lo_dayofweek,
1782
+ 'exchange' : exchange.name,
1783
+ 'symbol' : ticker,
1784
+ 'side' : 'sell',
1785
+ 'size' : order_notional_adj_factor*order_notional/lo_close, # in base ccy
1786
+ 'entry_price' : entry_price,
1787
+ 'target_price' : target_price,
1788
+ 'pnl_potential_bps' : pnl_potential_bps,
1789
+ 'ref_ema_close_fast' : ref_ema_close_fast,
1790
+ 'running_sl_percent_hard' : algo_param['sl_hard_percent'],
1791
+ 'closed' : False,
1792
+ 'reason' : 'entry',
1793
+ 'reason2' : '',
1794
+ 'total_equity' : gloabl_state.total_equity,
1795
+ 'this_ticker_current_position_usdt' : this_ticker_current_position_usdt,
1796
+ 'current_position_usdt' : current_position_usdt,
1797
+ 'running_total_num_positions' : running_total_num_positions,
1798
+ 'cash_before' : cash_before,
1799
+ 'cash_after' : cash_after,
1800
+ 'order_notional' : order_notional,
1801
+ 'commission' : commission,
1802
+ 'max_pain' : 0,
1803
+ 'num_impacting_economic_calendars' : num_impacting_economic_calendars,
1804
+ 'max_camp': max_camp,
1805
+ 'post_move_price_change_percent' : post_move_price_change_percent
1806
+ }
1807
+ all_trades.append(new_trade_0)
1808
+ open_trades_by_ticker[ticker].append(new_trade_0)
1809
+ new_trade_0.update(_additional_trade_fields)
1810
+
1811
+ # Resets!
1812
+ effective_tp_trailing_percent = float('inf')
1813
+ lo_boillenger_upper_breached_history = lo_boillenger_upper_breached_cache.get(key, [])
1814
+ lo_boillenger_upper_breached_history.clear()
1815
+
1816
+ if plot_timeseries:
1817
+ '''
1818
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.axvline.html
1819
+ linestyle='-' means solid line. If you don't supply linestyle, the vertical line wont show!!!
1820
+ '''
1821
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"]['time_series_canvas'].axvline(x=lo_datetime, color='gray', linewidth=1, linestyle='-')
1822
+ all_canvas[f"{key}-param_id{algo_param['param_id']}"]['time_series_canvas'].scatter([lo_datetime, lo_datetime], [lo_low, lo_high], color='gray')
1823
+
1824
+ iter_info = f"param_id: {algo_param['param_id']}, {key} i: {i} {lo_datetime}, # trades: {len(all_trades)}, equity: {round(gloabl_state.total_equity,2)}"
1825
+ if i%100==0 and i%1000!=0:
1826
+ print(iter_info)
1827
+ elif i%1000==0:
1828
+ logger.info(iter_info)
1829
+
1830
+ if i==pd_lo_candles.shape[0]-1:
1831
+ # HC
1832
+ if this_ticker_current_position_usdt>0:
1833
+ _close_open_positions(
1834
+ key, ticker,
1835
+ this_ticker_current_position_usdt, this_ticker_open_positions_side, current_position_usdt,
1836
+ unrealized_pnl,
1837
+ None, lo_row, 'HC', '',
1838
+ gloabl_state,
1839
+ all_trades, sl_by_ticker, open_trades_by_ticker,
1840
+ all_canvas,
1841
+ algo_param
1842
+ )
1843
+
1844
+ sorted_filtered_tickers.clear()
1845
+ sorted_filtered_tickers = None
1846
+
1847
+ if gloabl_state.total_equity<target_order_notional:
1848
+ logger.warning(f"total_equity {gloabl_state.total_equity} < target_order_notional {target_order_notional} exiting prematurely on {lo_datetime}!!!")
1849
+ break
1850
+
1851
+ if plot_timeseries:
1852
+ for exchange in exchanges:
1853
+ for ticker in tickers:
1854
+ key = f"{exchange.name}-{ticker}-param_id{algo_param['param_id']}"
1855
+ canvas = all_canvas[key]
1856
+ canvas['plt'].savefig(f"ts_{key.replace('/','').replace(':','')}.jpg", format='jpg', dpi=300)
1857
+
1858
+ for reference_price_cache_file in reference_price_cache:
1859
+ reference_price_cache[reference_price_cache_file].sort_values("timestamp_ms", inplace=True)
1860
+ reference_price_cache[reference_price_cache_file].to_csv(reference_price_cache_file)
1861
+
1862
+ num_tp = len([ x for x in all_trades if x['reason']=='TP'])
1863
+ num_sl = len([ x for x in all_trades if x['reason']=='SL'])
1864
+ num_hc_tp = len([ x for x in all_trades if x['reason']=='HC' and x['trade_pnl']>0 ] )
1865
+ num_hc_sl = len([ x for x in all_trades if x['reason']=='HC' and x['trade_pnl']<=0 ] )
1866
+ num_hc = num_hc_tp + num_hc_sl
1867
+
1868
+ return {
1869
+ 'realized_pnl' : sum([x['trade_pnl'] for x in all_trades if 'trade_pnl' in x]) - gloabl_state.total_commission,
1870
+ 'total_commission' : gloabl_state.total_commission,
1871
+ 'hit_ratio' : (num_tp + num_hc_tp) / (num_tp + num_sl + num_hc),
1872
+ 'num_tp' : num_tp,
1873
+ 'num_sl' : num_sl,
1874
+ 'num_hc' : num_hc,
1875
+ 'num_entry' : num_tp + num_sl + num_hc,
1876
+ 'trades' : all_trades,
1877
+ 'exceptions' : exceptions
1878
+ }
1879
+
1880
+ def run_all_scenario(
1881
+ algo_params : List[Dict[str, Any]],
1882
+ exchanges : List[Exchange],
1883
+
1884
+ order_notional_adj_func : Callable[..., float],
1885
+ allow_entry_initial_func : Callable[..., bool],
1886
+ allow_entry_final_func : Callable[..., bool],
1887
+ allow_slice_entry_func : Callable[..., bool],
1888
+ sl_adj_func : Callable[..., Dict[str, float]],
1889
+ trailing_stop_threshold_eval_func : Callable[..., Dict[str, float]],
1890
+ pnl_eval_func : Callable[..., Dict[str, float]],
1891
+ tp_eval_func : Callable[..., bool],
1892
+ sort_filter_universe_func : Callable[..., List[str]],
1893
+
1894
+ logger,
1895
+
1896
+ reference_start_dt : datetime = datetime(2021,1,1, tzinfo=timezone.utc),
1897
+ ) -> List[Dict]:
1898
+ all_exceptions = []
1899
+
1900
+ start = datetime.now()
1901
+ max_test_end_date = start
1902
+
1903
+ economic_calendars_file = algo_params[0]['economic_calendars_file']
1904
+ ecoevents_mapped_regions = algo_params[0]['ecoevents_mapped_regions']
1905
+ pd_economic_calendars = None
1906
+ economic_calendars_loaded : bool = False
1907
+ if os.path.isfile(economic_calendars_file):
1908
+ pd_economic_calendars = pd.read_csv(economic_calendars_file)
1909
+ pd_economic_calendars = pd_economic_calendars[pd_economic_calendars.region.isin(ecoevents_mapped_regions)]
1910
+ economic_calendars_loaded = True if pd_economic_calendars.shape[0]>0 else False
1911
+
1912
+ i : int = 1
1913
+ algo_results : List[Dict] = []
1914
+ best_realized_pnl, best_algo_result = 0, None
1915
+ for algo_param in algo_params:
1916
+
1917
+ algo_result : Dict = {
1918
+ 'param' : algo_param
1919
+ }
1920
+ algo_results.append(algo_result)
1921
+
1922
+ # We calc test_end_date with 'lo' (not with 'hi'), we assume it'd be the same.
1923
+ test_start_date = algo_param['start_date']
1924
+ test_fetch_start_date = test_start_date
1925
+ lo_candle_size = algo_param['lo_candle_size']
1926
+ lo_num_intervals = int(lo_candle_size[0])
1927
+ lo_interval = lo_candle_size[-1]
1928
+ lo_how_many_candles = algo_param['lo_how_many_candles']
1929
+ if lo_interval=="m":
1930
+ test_end_date = test_start_date + timedelta(minutes=lo_num_intervals*lo_how_many_candles)
1931
+ test_fetch_start_date = test_fetch_start_date - timedelta(minutes=algo_param['lo_stats_computed_over_how_many_candles']*2)
1932
+ test_end_date_ref = test_end_date + timedelta(minutes=algo_param['lo_stats_computed_over_how_many_candles']*4)
1933
+ elif lo_interval=="h":
1934
+ test_end_date = test_start_date + timedelta(hours=lo_num_intervals*lo_how_many_candles)
1935
+ test_fetch_start_date = test_fetch_start_date - timedelta(hours=algo_param['lo_stats_computed_over_how_many_candles']*2)
1936
+ test_end_date_ref = test_end_date + timedelta(hours=algo_param['lo_stats_computed_over_how_many_candles']*4)
1937
+ elif lo_interval=="d":
1938
+ test_end_date = test_start_date + timedelta(days=lo_num_intervals*lo_how_many_candles)
1939
+ test_fetch_start_date = test_fetch_start_date - timedelta(days=algo_param['lo_stats_computed_over_how_many_candles']*2)
1940
+ test_end_date_ref = test_end_date + timedelta(days=algo_param['lo_stats_computed_over_how_many_candles']*4)
1941
+ test_end_date = test_end_date if test_end_date < max_test_end_date else max_test_end_date
1942
+ test_end_date_ref = test_end_date_ref if test_end_date_ref < max_test_end_date else max_test_end_date
1943
+ cutoff_ts = int(test_fetch_start_date.timestamp()) # in seconds
1944
+
1945
+
1946
+ ####################################### STEP 1. Fetch candles (Because each test may have diff test_end_date, you need re-fetch candles for each algo_param) #######################################
1947
+ '''
1948
+ cutoff_ts in seconds, example '1668135382'
1949
+
1950
+ exchanges[0].fetch_ohlcv('ETHUSDT', "1m", cutoff_ts)
1951
+
1952
+ Candles format, first field is timestamp in ms:
1953
+ [
1954
+ [1502942400000, 301.13, 301.13, 301.13, 301.13, 0.42643],
1955
+ [1502942460000, 301.13, 301.13, 301.13, 301.13, 2.75787],
1956
+ [1502942520000, 300.0, 300.0, 300.0, 300.0, 0.0993],
1957
+ [1502942580000, 300.0, 300.0, 300.0, 300.0, 0.31389],
1958
+ ...
1959
+ ]
1960
+ '''
1961
+ delisted : List[str] = []
1962
+
1963
+ data_fetch_start : float = time.time()
1964
+
1965
+ # Fetch BTC
1966
+ reference_ticker : str = algo_param['reference_ticker']
1967
+ target_candle_file_name_fast : str = f'{reference_ticker.replace("^","").replace("/","").replace(":","")}_fast_candles_{datetime(2021,1,1, tzinfo=timezone.utc).strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date_ref.strftime("%Y-%m-%d-%H-%M-%S")}_1d.csv'
1968
+ target_candle_file_name_slow : str = f'{reference_ticker.replace("^","").replace("/","").replace(":","")}_slow_candles_{datetime(2021,1,1, tzinfo=timezone.utc).strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date_ref.strftime("%Y-%m-%d-%H-%M-%S")}_1d.csv'
1969
+ logger.info(f"reference_ticker: {reference_ticker}, target_candle_file_name_fast: {target_candle_file_name_fast}, target_candle_file_name_slow: {target_candle_file_name_slow}, reference_candles_file: {algo_param['reference_candles_file'] if 'reference_candles_file' in algo_param else '---'}")
1970
+ if algo_param['force_reload'] or not os.path.isfile(target_candle_file_name_fast):
1971
+ if algo_param['force_reload'] and 'reference_candles_file' in algo_param and algo_param['reference_candles_file'] and os.path.isfile(algo_param['reference_candles_file']):
1972
+ pd_ref_candles_fast = pd.read_csv(algo_param['reference_candles_file'])
1973
+ pd_ref_candles_slow : pd.DataFrame = pd_ref_candles_fast.copy(deep=True)
1974
+ logger.info(f"reference candles loaded from {algo_param['reference_candles_file']}")
1975
+
1976
+ else:
1977
+ ref_candles : Dict[str, pd.DataFrame] = fetch_candles(
1978
+ start_ts=int(reference_start_dt.timestamp()),
1979
+ end_ts=int(test_end_date_ref.timestamp()),
1980
+ exchange=exchanges[0],
1981
+ normalized_symbols=[reference_ticker],
1982
+ candle_size = '1d',
1983
+ num_candles_limit=algo_param['num_candles_limit'],
1984
+ logger=logger,
1985
+ cache_dir=algo_param['cache_candles'],
1986
+ list_ts_field=exchanges[0].options['list_ts_field'] if 'list_ts_field' in exchanges[0].options else None
1987
+ )
1988
+ logger.info(f"Reference candles fetched: {reference_ticker}, start: {reference_start_dt}, end: {test_end_date_ref}")
1989
+ pd_ref_candles_fast : pd.DataFrame = ref_candles[reference_ticker]
1990
+ pd_ref_candles_slow : pd.DataFrame = pd_ref_candles_fast.copy(deep=True)
1991
+
1992
+ compute_candles_stats(pd_candles=pd_ref_candles_fast, boillenger_std_multiples=2, sliding_window_how_many_candles=algo_param['ref_ema_num_days_fast'], slow_fast_interval_ratio=int(algo_param['ref_ema_num_days_fast']/2), rsi_sliding_window_how_many_candles=algo_param['rsi_sliding_window_how_many_candles'], rsi_trend_sliding_window_how_many_candles=algo_param['rsi_trend_sliding_window_how_many_candles'], hurst_exp_window_how_many_candles=algo_param['hurst_exp_window_how_many_candles'], target_fib_level=algo_param['target_fib_level'], pypy_compat=algo_param['pypy_compat'])
1993
+ compute_candles_stats(pd_candles=pd_ref_candles_slow, boillenger_std_multiples=2, sliding_window_how_many_candles=algo_param['ref_ema_num_days_slow'], slow_fast_interval_ratio=int(algo_param['ref_ema_num_days_slow']/2), rsi_sliding_window_how_many_candles=algo_param['rsi_sliding_window_how_many_candles'], rsi_trend_sliding_window_how_many_candles=algo_param['rsi_trend_sliding_window_how_many_candles'], hurst_exp_window_how_many_candles=algo_param['hurst_exp_window_how_many_candles'], target_fib_level=algo_param['target_fib_level'], pypy_compat=algo_param['pypy_compat'])
1994
+ logger.info(f"Reference candles {reference_ticker} compute_candles_stats done.")
1995
+
1996
+ pd_ref_candles_fast.to_csv(target_candle_file_name_fast)
1997
+ pd_ref_candles_slow.to_csv(target_candle_file_name_slow)
1998
+
1999
+ else:
2000
+ pd_ref_candles_fast : pd.DataFrame = pd.read_csv(target_candle_file_name_fast)
2001
+ pd_ref_candles_slow : pd.DataFrame = pd.read_csv(target_candle_file_name_slow)
2002
+ fix_column_types(pd_ref_candles_fast)
2003
+ fix_column_types(pd_ref_candles_slow)
2004
+ logger.info(f"Reference candles {reference_ticker} loaded from target_candle_file_name_fast: {target_candle_file_name_fast}, target_candle_file_name_slow: {target_candle_file_name_slow}")
2005
+
2006
+ total_seconds = (test_end_date_ref - test_start_date).total_seconds()
2007
+ total_hours = total_seconds / 3600
2008
+ total_days = total_hours / 24
2009
+ sliding_window_how_many_candles : int = 0
2010
+ sliding_window_how_many_candles = int(total_days / algo_param['sliding_window_ratio'])
2011
+
2012
+ ref_candles_partitions, pd_hi_candles_partitions, pd_lo_candles_partitions = None, None, None
2013
+ if not algo_param['pypy_compat']:
2014
+ ref_candles_partitions = partition_sliding_window(
2015
+ pd_candles = pd_ref_candles_fast,
2016
+ sliding_window_how_many_candles = sliding_window_how_many_candles,
2017
+ smoothing_window_size_ratio = algo_param['smoothing_window_size_ratio'],
2018
+ linregress_stderr_threshold = algo_param['linregress_stderr_threshold'],
2019
+ max_recur_depth = algo_param['max_recur_depth'],
2020
+ min_segment_size_how_many_candles = algo_param['min_segment_size_how_many_candles'],
2021
+ segment_consolidate_slope_ratio_threshold = algo_param['segment_consolidate_slope_ratio_threshold'],
2022
+ sideway_price_condition_threshold = algo_param['sideway_price_condition_threshold']
2023
+ )
2024
+ candle_segments_jpg_file_name : str = f'{reference_ticker.replace("^","").replace("/","").replace(":","")}_refcandles_w_segments_{datetime(2021,1,1, tzinfo=timezone.utc).strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date_ref.strftime("%Y-%m-%d-%H-%M-%S")}_1d.jpg'
2025
+ plot_segments(pd_ref_candles_fast, ref_candles_partitions, candle_segments_jpg_file_name)
2026
+
2027
+ candle_segments_file_name : str = f'{reference_ticker.replace("^","").replace("/","").replace(":","")}_refcandles_w_segments_{datetime(2021,1,1, tzinfo=timezone.utc).strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date_ref.strftime("%Y-%m-%d-%H-%M-%S")}_1d.csv'
2028
+ pd_ref_candles_segments = segments_to_df(ref_candles_partitions['segments'])
2029
+ pd_ref_candles_segments.to_csv(candle_segments_file_name)
2030
+
2031
+ all_exchange_candles : Dict[str, Dict[str, Dict[str, pd.DataFrame]]] = {}
2032
+ for exchange in exchanges:
2033
+ markets = exchange.load_markets()
2034
+ if exchange.name not in all_exchange_candles:
2035
+ all_exchange_candles[exchange.name] = {}
2036
+
2037
+ if algo_param['white_list_tickers']:
2038
+ tickers = algo_param['white_list_tickers']
2039
+ else:
2040
+ tickers = list(markets.keys())
2041
+
2042
+ for ticker in tickers:
2043
+ if ticker not in markets:
2044
+ err_msg = f"{ticker}: {'no longer in markets'}"
2045
+ logger.error(err_msg)
2046
+ delisted.append(ticker)
2047
+ else:
2048
+ all_exchange_candles[exchange.name][ticker] = {}
2049
+
2050
+ _ticker = ticker.split(":")[0].replace("/","")
2051
+ total_seconds = (test_end_date - test_fetch_start_date).total_seconds()
2052
+ total_hours = total_seconds / 3600
2053
+ total_days = total_hours / 24
2054
+ sliding_window_how_many_candles : int = 0
2055
+ sliding_window_how_many_candles = int(total_days / algo_param['sliding_window_ratio'])
2056
+
2057
+ pd_hi_candles = None
2058
+ target_candle_file_name : str = f'{_ticker}_candles_{test_fetch_start_date.strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date.strftime("%Y-%m-%d-%H-%M-%S")}_{algo_param["hi_candle_size"]}.csv'
2059
+ if algo_param['force_reload'] or not os.path.isfile(target_candle_file_name):
2060
+ if algo_param['force_reload'] and 'hi_candles_file' in algo_param and algo_param['hi_candles_file'] and os.path.isfile(algo_param['hi_candles_file']):
2061
+ pd_hi_candles : pd.DataFrame = pd.read_csv(algo_param['hi_candles_file'])
2062
+
2063
+ else:
2064
+ hi_candles : Dict[str, pd.DataFrame] = fetch_candles(
2065
+ start_ts=cutoff_ts,
2066
+ end_ts=int(test_end_date.timestamp()),
2067
+ exchange=exchange, normalized_symbols=[ ticker ],
2068
+ candle_size = algo_param['hi_candle_size'],
2069
+ num_candles_limit=algo_param['num_candles_limit'],
2070
+ logger=logger,
2071
+ cache_dir=algo_param['cache_candles'],
2072
+ list_ts_field=exchange.options['list_ts_field']
2073
+ )
2074
+ pd_hi_candles : pd.DataFrame = hi_candles[ticker]
2075
+ logger.info(f"pd_hi_candles fetched: {ticker} {pd_hi_candles.shape}, start: {cutoff_ts}, end: {int(test_end_date.timestamp())}")
2076
+ compute_candles_stats(pd_candles=pd_hi_candles, boillenger_std_multiples=algo_param['boillenger_std_multiples'], sliding_window_how_many_candles=algo_param['hi_stats_computed_over_how_many_candles'], slow_fast_interval_ratio=(algo_param['hi_stats_computed_over_how_many_candles']/algo_param['hi_ma_short_interval']), rsi_sliding_window_how_many_candles=algo_param['rsi_sliding_window_how_many_candles'], rsi_trend_sliding_window_how_many_candles=algo_param['rsi_trend_sliding_window_how_many_candles'], hurst_exp_window_how_many_candles=algo_param['hurst_exp_window_how_many_candles'], target_fib_level=algo_param['target_fib_level'], pypy_compat=algo_param['pypy_compat'])
2077
+ logger.info(f"pd_hi_candles {ticker} compute_candles_stats done: {target_candle_file_name}")
2078
+ pd_hi_candles.to_csv(target_candle_file_name)
2079
+
2080
+ if pd_hi_candles is not None and pd_hi_candles.shape[0]>0:
2081
+ first_candle_datetime = datetime.fromtimestamp(pd_hi_candles.iloc[0]['timestamp_ms']/1000)
2082
+ last_candle_datetime = datetime.fromtimestamp(pd_hi_candles.iloc[-1]['timestamp_ms']/1000)
2083
+
2084
+ assert(last_candle_datetime>first_candle_datetime)
2085
+ else:
2086
+ err_msg = f"{ticker} no hi candles?"
2087
+ logger.error(err_msg)
2088
+ else:
2089
+ pd_hi_candles : pd.DataFrame = pd.read_csv(target_candle_file_name)
2090
+ fix_column_types(pd_hi_candles)
2091
+ logger.info(f"pd_hi_candles {ticker} {pd_hi_candles.shape} loaded from {target_candle_file_name}")
2092
+
2093
+ if not algo_param['pypy_compat']:
2094
+ pd_hi_candles_partitions = partition_sliding_window(
2095
+ pd_candles = pd_hi_candles,
2096
+ sliding_window_how_many_candles = sliding_window_how_many_candles,
2097
+ smoothing_window_size_ratio = algo_param['smoothing_window_size_ratio'],
2098
+ linregress_stderr_threshold = algo_param['linregress_stderr_threshold'],
2099
+ max_recur_depth = algo_param['max_recur_depth'],
2100
+ min_segment_size_how_many_candles = algo_param['min_segment_size_how_many_candles'],
2101
+ segment_consolidate_slope_ratio_threshold = algo_param['segment_consolidate_slope_ratio_threshold'],
2102
+ sideway_price_condition_threshold = algo_param['sideway_price_condition_threshold']
2103
+ )
2104
+ candle_segments_jpg_file_name : str = f'{_ticker}_hicandles_w_segments_{test_fetch_start_date.strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date.strftime("%Y-%m-%d-%H-%M-%S")}_{algo_param["hi_candle_size"]}.jpg'
2105
+ plot_segments(pd_hi_candles, pd_hi_candles_partitions, candle_segments_jpg_file_name)
2106
+
2107
+ candle_segments_file_name : str = f'{_ticker}_hicandles_w_segments_{test_fetch_start_date.strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date.strftime("%Y-%m-%d-%H-%M-%S")}_{algo_param["hi_candle_size"]}.csv'
2108
+ pd_hi_candles_segments = segments_to_df(pd_hi_candles_partitions['segments'])
2109
+ pd_hi_candles_segments.to_csv(candle_segments_file_name)
2110
+
2111
+ pd_lo_candles = None
2112
+ _ticker = ticker.split(":")[0].replace("/","")
2113
+ target_candle_file_name : str = f'{_ticker}_candles_{test_fetch_start_date.strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date.strftime("%Y-%m-%d-%H-%M-%S")}_{algo_param["lo_candle_size"]}.csv'
2114
+ if algo_param['force_reload'] or not os.path.isfile(target_candle_file_name):
2115
+ if algo_param['force_reload'] and 'lo_candles_file' in algo_param and algo_param['lo_candles_file'] and os.path.isfile(algo_param['lo_candles_file']):
2116
+ pd_lo_candles : pd.DataFrame = pd.read_csv(algo_param['lo_candles_file'])
2117
+
2118
+ else:
2119
+ lo_candles : Dict[str, pd.DataFrame] = fetch_candles(
2120
+ start_ts=cutoff_ts,
2121
+ end_ts=int(test_end_date.timestamp()),
2122
+ exchange=exchange, normalized_symbols=[ ticker ],
2123
+ candle_size = algo_param['lo_candle_size'],
2124
+ num_candles_limit=algo_param['num_candles_limit'],
2125
+ logger=logger,
2126
+ cache_dir=algo_param['cache_candles'],
2127
+ list_ts_field=exchange.options['list_ts_field']
2128
+ )
2129
+ pd_lo_candles : pd.DataFrame = lo_candles[ticker]
2130
+ logger.info(f"pd_lo_candles fetched: {ticker} {pd_lo_candles.shape}, start: {cutoff_ts}, end: {int(test_end_date.timestamp())}")
2131
+ compute_candles_stats(pd_candles=pd_lo_candles, boillenger_std_multiples=algo_param['boillenger_std_multiples'], sliding_window_how_many_candles=algo_param['lo_stats_computed_over_how_many_candles'], slow_fast_interval_ratio=(algo_param['lo_stats_computed_over_how_many_candles']/algo_param['lo_ma_short_interval']), rsi_sliding_window_how_many_candles=algo_param['rsi_sliding_window_how_many_candles'], rsi_trend_sliding_window_how_many_candles=algo_param['rsi_trend_sliding_window_how_many_candles'], hurst_exp_window_how_many_candles=algo_param['hurst_exp_window_how_many_candles'], target_fib_level=algo_param['target_fib_level'], pypy_compat=algo_param['pypy_compat'])
2132
+ logger.info(f"pd_lo_candles {ticker} compute_candles_stats done. {target_candle_file_name}")
2133
+ pd_lo_candles.to_csv(target_candle_file_name)
2134
+
2135
+ if pd_lo_candles is not None and pd_lo_candles.shape[0]>0:
2136
+ first_candle_datetime = datetime.fromtimestamp(pd_lo_candles.iloc[0]['timestamp_ms']/1000)
2137
+ last_candle_datetime = datetime.fromtimestamp(pd_lo_candles.iloc[-1]['timestamp_ms']/1000)
2138
+
2139
+ assert(last_candle_datetime>first_candle_datetime)
2140
+ else:
2141
+ err_msg = f"{ticker} no lo candles?"
2142
+ logger.error(err_msg)
2143
+ else:
2144
+ pd_lo_candles : pd.DataFrame = pd.read_csv(target_candle_file_name)
2145
+ fix_column_types(pd_lo_candles)
2146
+ logger.info(f"pd_lo_candles {ticker} {pd_lo_candles.shape} loaded from {target_candle_file_name}")
2147
+
2148
+ if not algo_param['pypy_compat']:
2149
+ pd_lo_candles_partitions = partition_sliding_window(
2150
+ pd_candles = pd_lo_candles,
2151
+ sliding_window_how_many_candles = sliding_window_how_many_candles,
2152
+ smoothing_window_size_ratio = algo_param['smoothing_window_size_ratio'],
2153
+ linregress_stderr_threshold = algo_param['linregress_stderr_threshold'],
2154
+ max_recur_depth = algo_param['max_recur_depth'],
2155
+ min_segment_size_how_many_candles = algo_param['min_segment_size_how_many_candles'],
2156
+ segment_consolidate_slope_ratio_threshold = algo_param['segment_consolidate_slope_ratio_threshold'],
2157
+ sideway_price_condition_threshold = algo_param['sideway_price_condition_threshold']
2158
+ )
2159
+ candle_segments_jpg_file_name : str = f'{_ticker}_locandles_w_segments_{test_fetch_start_date.strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date.strftime("%Y-%m-%d-%H-%M-%S")}_{algo_param["lo_candle_size"]}.jpg'
2160
+ plot_segments(pd_lo_candles, pd_lo_candles_partitions, candle_segments_jpg_file_name)
2161
+
2162
+ candle_segments_file_name : str = f'{_ticker}_locandles_w_segments_{test_fetch_start_date.strftime("%Y-%m-%d-%H-%M-%S")}_{test_end_date.strftime("%Y-%m-%d-%H-%M-%S")}_{algo_param["hi_candle_size"]}.csv'
2163
+ pd_lo_candles_segments = segments_to_df(pd_lo_candles_partitions['segments'])
2164
+ pd_lo_candles_segments.to_csv(candle_segments_file_name)
2165
+
2166
+ all_exchange_candles[exchange.name][ticker]['hi_candles'] = pd_hi_candles
2167
+ all_exchange_candles[exchange.name][ticker]['lo_candles'] = pd_lo_candles
2168
+
2169
+ data_fetch_finish : float = time.time()
2170
+
2171
+ ####################################### STEP 2. Trade simulation #######################################
2172
+ logger.info(f"Start run_scenario")
2173
+ scenario_start : float = time.time()
2174
+ result = run_scenario(
2175
+ algo_param=algo_param,
2176
+ exchanges=exchanges,
2177
+ all_exchange_candles=all_exchange_candles,
2178
+ pd_ref_candles_fast=pd_ref_candles_fast,
2179
+ pd_ref_candles_slow=pd_ref_candles_slow,
2180
+ ref_candles_partitions=ref_candles_partitions,
2181
+ pd_hi_candles_partitions=pd_hi_candles_partitions,
2182
+ pd_lo_candles_partitions=pd_lo_candles_partitions,
2183
+ economic_calendars_loaded=economic_calendars_loaded,
2184
+ pd_economic_calendars=pd_economic_calendars,
2185
+ tickers=tickers,
2186
+
2187
+ order_notional_adj_func=order_notional_adj_func,
2188
+ allow_entry_initial_func=allow_entry_initial_func,
2189
+ allow_entry_final_func=allow_entry_final_func,
2190
+ allow_slice_entry_func=allow_slice_entry_func,
2191
+ sl_adj_func=sl_adj_func,
2192
+ trailing_stop_threshold_eval_func=trailing_stop_threshold_eval_func,
2193
+ pnl_eval_func=pnl_eval_func,
2194
+ tp_eval_func=tp_eval_func,
2195
+ sort_filter_universe_func=sort_filter_universe_func,
2196
+
2197
+ logger=logger,
2198
+
2199
+ pypy_compat=algo_param['pypy_compat'],
2200
+ plot_timeseries=True
2201
+ )
2202
+ scenario_finish = time.time()
2203
+
2204
+ data_fetch_elapsed_ms = (data_fetch_finish - data_fetch_start) * 1000
2205
+ scenario_elapsed_ms = (scenario_finish - scenario_start) * 1000
2206
+
2207
+ logger.info(f"Done run_scenario. data_fetch_elapsed_ms: {data_fetch_elapsed_ms} ms, scenario_elapsed_ms: {scenario_elapsed_ms}")
2208
+
2209
+ algo_result['orders'] = result['trades']
2210
+ result.pop('trades')
2211
+ algo_result['summary'] = {
2212
+ # Key parameters
2213
+ 'initial_cash' : algo_param['initial_cash'],
2214
+ 'entry_percent_initial_cash' : algo_param['entry_percent_initial_cash'],
2215
+ 'strategy_mode' : algo_param['strategy_mode'],
2216
+ 'ref_ema_num_days_fast' : algo_param['ref_ema_num_days_fast'],
2217
+ 'ref_ema_num_days_slow' : algo_param['ref_ema_num_days_slow'],
2218
+ 'long_above_ref_ema_short_below' : algo_param['long_above_ref_ema_short_below'],
2219
+ 'ref_price_vs_ema_percent_threshold' : algo_param['ref_price_vs_ema_percent_threshold'] if 'ref_price_vs_ema_percent_threshold' in algo_param else None,
2220
+ 'rsi_upper_threshold' : algo_param['rsi_upper_threshold'],
2221
+ 'rsi_lower_threshold' : algo_param['rsi_lower_threshold'],
2222
+ 'boillenger_std_multiples' : algo_param['boillenger_std_multiples'],
2223
+ 'ema_short_slope_threshold' : algo_param['ema_short_slope_threshold'] if 'ema_short_slope_threshold' in algo_param else None,
2224
+ 'num_intervals_block_pending_ecoevents' : algo_param['num_intervals_block_pending_ecoevents'],
2225
+ 'num_intervals_current_ecoevents' : algo_param['num_intervals_current_ecoevents'],
2226
+ 'sl_hard_percent' : algo_param['sl_hard_percent'],
2227
+ 'sl_percent_trailing' : algo_param['sl_percent_trailing'],
2228
+ 'use_gradual_tightened_trailing_stops' : algo_param['use_gradual_tightened_trailing_stops'],
2229
+ 'sl_num_intervals_delay' : algo_param['sl_num_intervals_delay'],
2230
+ 'tp_min_percent' : algo_param['tp_min_percent'],
2231
+ 'tp_max_percent' : algo_param['tp_max_percent'],
2232
+ 'asymmetric_tp_bps' : algo_param['asymmetric_tp_bps'],
2233
+
2234
+ # Key output
2235
+ 'realized_pnl' : result['realized_pnl'], # Commission already taken out
2236
+ 'total_commission' : result['total_commission'],
2237
+ 'hit_ratio' : result['hit_ratio'],
2238
+ 'num_tp' : result['num_tp'],
2239
+ 'num_sl' : result['num_sl'],
2240
+ 'num_hc' : result['num_hc'],
2241
+ 'num_entry' : result['num_entry'],
2242
+ 'data_fetch_elapsed_ms' : data_fetch_elapsed_ms,
2243
+ 'scenario_elapsed_ms' : scenario_elapsed_ms,
2244
+ 'num_exceptions' : len(result['exceptions'])
2245
+ }
2246
+
2247
+ all_exceptions = all_exceptions + list(result['exceptions'].items())
2248
+ logger.error(list(result['exceptions'].items()))
2249
+
2250
+ logger.info(f"Done ({i}/{len(algo_params)}) {algo_param['name_exclude_start_date']}")
2251
+ logger.info(json.dumps(algo_result['summary'], indent=4))
2252
+
2253
+ if result['realized_pnl']>best_realized_pnl or not best_algo_result:
2254
+ best_algo_result = algo_result['summary']
2255
+
2256
+ i = i + 1
2257
+
2258
+ finish = datetime.now()
2259
+ elapsed = (finish-start).seconds
2260
+
2261
+
2262
+ logger.info(f"Backtest done in {elapsed}sec over {len(algo_params)} scenario's with start_date {test_start_date} over {len(exchanges)} exchange(s) and {len(tickers)} tickers.")
2263
+
2264
+ logger.info(f"*** Best result realized_pnl: {best_algo_result['realized_pnl']}")
2265
+ logger.info(json.dumps(best_algo_result, indent=4))
2266
+
2267
+ pd_results = pd.DataFrame([ x['summary'] for x in algo_results])
2268
+ pd_results.loc['avg', 'realized_pnl'] = pd_results['realized_pnl'].mean(numeric_only=True, axis=0)
2269
+ pd_results.loc['avg', 'total_commission'] = pd_results['total_commission'].mean(numeric_only=True, axis=0)
2270
+ pd_results.loc['avg', 'hit_ratio'] = pd_results['hit_ratio'].mean(numeric_only=True, axis=0)
2271
+
2272
+ return algo_results
2273
+
2274
+ def parseargs():
2275
+ parser = argparse.ArgumentParser()
2276
+ parser.add_argument("--force_reload", help="Reload candles? Both candles and TA previously computed will be loaded from disk. Y or N (default)", default=False)
2277
+ parser.add_argument("--white_list_tickers", help="Comma seperated list, example: BTC/USDT:USDT,ETH/USDT:USDT,XRP/USDT:USDT ", default="BTC/USDT:USDT")
2278
+ parser.add_argument("--reference_ticker", help="This is ticker for bull / bear determination. The Northstar.", default="BTC/USDT:USDT")
2279
+ parser.add_argument("--block_entries_on_impacting_ecoevents", help="Block entries on economic event? Y (default) or N", default=True)
2280
+ parser.add_argument("--enable_sliced_entry", help="Block entries on economic event? Y or N (default)", default=False)
2281
+ parser.add_argument("--asymmetric_tp_bps", help="A positive asymmetric_tp_bps means you are taking deeper TPs. A negative asymmetric_tp_bps means shallower", default=0)
2282
+ args = parser.parse_args()
2283
+
2284
+ if args.force_reload:
2285
+ if args.force_reload=='Y':
2286
+ force_reload = True
2287
+ else:
2288
+ force_reload = False
2289
+ else:
2290
+ force_reload = False
2291
+
2292
+ if args.white_list_tickers:
2293
+ white_list_tickers = args.white_list_tickers.split(',')
2294
+
2295
+ reference_ticker = args.reference_ticker if args.reference_ticker else white_list_tickers[0]
2296
+
2297
+ if args.block_entries_on_impacting_ecoevents:
2298
+ if args.block_entries_on_impacting_ecoevents=='Y':
2299
+ block_entries_on_impacting_ecoevents = True
2300
+ else:
2301
+ block_entries_on_impacting_ecoevents = False
2302
+ else:
2303
+ block_entries_on_impacting_ecoevents = True
2304
+
2305
+ if args.enable_sliced_entry:
2306
+ if args.enable_sliced_entry=='Y':
2307
+ enable_sliced_entry = True
2308
+ else:
2309
+ enable_sliced_entry = False
2310
+ else:
2311
+ enable_sliced_entry = False
2312
+
2313
+ asymmetric_tp_bps = int(args.asymmetric_tp_bps)
2314
+
2315
+ return {
2316
+ 'force_reload': force_reload,
2317
+ 'white_list_tickers' : white_list_tickers,
2318
+ 'reference_ticker' : reference_ticker,
2319
+ 'block_entries_on_impacting_ecoevents' : block_entries_on_impacting_ecoevents,
2320
+ 'enable_sliced_entry' : enable_sliced_entry,
2321
+ 'asymmetric_tp_bps' : asymmetric_tp_bps
2322
+ }
2323
+
2324
+ def dump_trades_to_disk(
2325
+ algo_results,
2326
+ filename,
2327
+ logger
2328
+ ):
2329
+ flattenned_trades : List[Dict[str, Any]]= []
2330
+ for algo_result in algo_results:
2331
+ for order in algo_result['orders']:
2332
+ try:
2333
+ order['name'] = algo_result['param']['name']
2334
+ order['name_exclude_start_date'] = algo_result['param']['name_exclude_start_date']
2335
+
2336
+ order['initial_cash'] = algo_result['param']['initial_cash']
2337
+ order['entry_percent_initial_cash'] = algo_result['param']['entry_percent_initial_cash']
2338
+ order['clip_order_notional_to_best_volumes'] = algo_result['param']['clip_order_notional_to_best_volumes']
2339
+ order['target_position_size_percent_total_equity'] = algo_result['param']['target_position_size_percent_total_equity']
2340
+
2341
+ order['reference_ticker'] = algo_result['param']['reference_ticker']
2342
+ order['strategy_mode'] = algo_result['param']['strategy_mode']
2343
+ order['boillenger_std_multiples'] = algo_result['param']['boillenger_std_multiples']
2344
+ order['ema_short_slope_threshold'] = algo_result['param']['ema_short_slope_threshold'] if 'ema_short_slope_threshold' in algo_result['param'] else None
2345
+ order['how_many_last_candles'] = algo_result['param']['how_many_last_candles']
2346
+ order['last_candles_timeframe'] = algo_result['param']['last_candles_timeframe']
2347
+ order['enable_wait_entry'] = algo_result['param']['enable_wait_entry'] if 'enable_wait_entry' in algo_result['param'] else None
2348
+ order['allow_entry_sit_bb'] = algo_result['param']['allow_entry_sit_bb'] if 'allow_entry_sit_bb' in algo_result['param'] else None
2349
+ order['enable_sliced_entry'] = algo_result['param']['enable_sliced_entry']
2350
+ order['adj_sl_on_ecoevents'] = algo_result['param']['adj_sl_on_ecoevents']
2351
+ order['block_entries_on_impacting_ecoevents'] = algo_result['param']['block_entries_on_impacting_ecoevents']
2352
+ order['num_intervals_block_pending_ecoevents'] = algo_result['param']['num_intervals_block_pending_ecoevents']
2353
+ order['num_intervals_current_ecoevents'] = algo_result['param']['num_intervals_current_ecoevents']
2354
+ order['enable_hi_timeframe_confirm'] = algo_result['param']['enable_hi_timeframe_confirm'] if 'enable_hi_timeframe_confirm' in algo_result['param'] else None
2355
+ order['sl_num_intervals_delay'] = algo_result['param']['sl_num_intervals_delay']
2356
+ order['sl_hard_percent'] = algo_result['param']['sl_hard_percent']
2357
+ order['sl_percent_trailing'] = algo_result['param']['sl_percent_trailing']
2358
+ order['use_gradual_tightened_trailing_stops'] = algo_result['param']['use_gradual_tightened_trailing_stops']
2359
+ order['tp_min_percent'] = algo_result['param']['tp_min_percent']
2360
+ order['tp_max_percent'] = algo_result['param']['tp_max_percent']
2361
+ order['asymmetric_tp_bps'] = algo_result['param']['asymmetric_tp_bps']
2362
+
2363
+ order['hi_candle_size'] = algo_result['param']['hi_candle_size']
2364
+ order['hi_stats_computed_over_how_many_candles'] = algo_result['param']['hi_stats_computed_over_how_many_candles']
2365
+ order['hi_how_many_candles'] = algo_result['param']['hi_how_many_candles']
2366
+ order['hi_ma_short_interval'] = algo_result['param']['hi_ma_short_interval']
2367
+ order['hi_ma_long_interval'] = algo_result['param']['hi_ma_long_interval']
2368
+
2369
+ order['lo_candle_size'] = algo_result['param']['lo_candle_size']
2370
+ order['lo_stats_computed_over_how_many_candles'] = algo_result['param']['lo_stats_computed_over_how_many_candles']
2371
+ order['lo_how_many_candles'] = algo_result['param']['lo_how_many_candles']
2372
+ order['lo_ma_short_interval'] = algo_result['param']['lo_ma_short_interval']
2373
+ order['lo_ma_long_interval'] = algo_result['param']['lo_ma_long_interval']
2374
+
2375
+ order['target_fib_level'] = algo_result['param']['target_fib_level']
2376
+ order['rsi_sliding_window_how_many_candles'] = algo_result['param']['rsi_sliding_window_how_many_candles']
2377
+ order['rsi_trend_sliding_window_how_many_candles'] = algo_result['param']['rsi_trend_sliding_window_how_many_candles']
2378
+ order['hurst_exp_window_how_many_candles'] = algo_result['param']['hurst_exp_window_how_many_candles']
2379
+
2380
+ order['ref_ema_num_days_fast'] = algo_result['param']['ref_ema_num_days_fast']
2381
+ order['re_ema_num_days_slow'] = algo_result['param']['ref_ema_num_days_slow']
2382
+ order['long_above_ref_ema_short_below'] = algo_result['param']['long_above_ref_ema_short_below']
2383
+ order['ref_price_vs_ema_percent_threshold'] = algo_result['param']['ref_price_vs_ema_percent_threshold'] if 'ref_price_vs_ema_percent_threshold' in algo_result['param'] else None
2384
+ order['rsi_upper_threshold'] = algo_result['param']['rsi_upper_threshold']
2385
+ order['rsi_lower_threshold'] = algo_result['param']['rsi_lower_threshold']
2386
+
2387
+ order['id'] = str(uuid.uuid4())
2388
+ order['trade_year'] = order['trade_datetime'].year
2389
+ order['trade_month'] = order['trade_datetime'].month
2390
+ order['trade_day'] = order['trade_datetime'].day
2391
+ order['trade_dayofweek'] = order['dayofweek']
2392
+ order['trade_week_of_month'] = timestamp_to_week_of_month(
2393
+ int(order['trade_datetime'].timestamp() * 1000)
2394
+ )
2395
+
2396
+ flattenned_trades.append(order)
2397
+
2398
+ except Exception as error:
2399
+ logger.error(f"Error while processing flattenned trades! {error}")
2400
+
2401
+ if len(flattenned_trades)>0:
2402
+ pd_flattenned_trades = pd.DataFrame(flattenned_trades)
2403
+ pd_flattenned_trades.to_csv(filename)
2404
+
2405
+ logger.info(f"Trade extract: {filename}")