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