def breakout_labels_nb(close: tp.Array2d, window: int, pos_th: tp.MaybeArray[float], neg_th: tp.MaybeArray[float], wait: int = 1, flex_2d: bool = True) -> tp.Array2d: """For each value, return 1 if any value in the next period is greater than the positive threshold (in %), -1 if less than the negative threshold, and 0 otherwise. First hit wins.""" pos_th = np.asarray(pos_th) neg_th = np.asarray(neg_th) out = np.full_like(close, 0, dtype=np.float_) for col in range(close.shape[1]): for i in range(close.shape[0]): _pos_th = abs(flex_select_auto_nb(pos_th, i, col, flex_2d)) _neg_th = abs(flex_select_auto_nb(neg_th, i, col, flex_2d)) for j in range(i + wait, min(i + window + wait, close.shape[0])): if _pos_th > 0 and close[j, col] >= close[i, col] * (1 + _pos_th): out[i, col] = 1 break if _neg_th > 0 and close[j, col] <= close[i, col] * (1 - _neg_th): out[i, col] = -1 break return out
def local_extrema_apply_nb(close: tp.Array2d, pos_th: tp.MaybeArray[float], neg_th: tp.MaybeArray[float], flex_2d: bool = True) -> tp.Array2d: """Get array of local extrema denoted by 1 (peak) or -1 (trough), otherwise 0. Two adjacent peak and trough points should exceed the given threshold parameters. If any threshold is given element-wise, it will be applied per new/updated extremum. Inspired by https://www.mdpi.com/1099-4300/22/10/1162/pdf""" pos_th = np.asarray(pos_th) neg_th = np.asarray(neg_th) out = np.full(close.shape, 0, dtype=np.int_) for col in range(close.shape[1]): prev_i = 0 direction = 0 for i in range(1, close.shape[0]): _pos_th = abs(flex_select_auto_nb(pos_th, prev_i, col, flex_2d)) _neg_th = abs(flex_select_auto_nb(neg_th, prev_i, col, flex_2d)) if _pos_th == 0: raise ValueError("Positive threshold cannot be 0") if _neg_th == 0: raise ValueError("Negative threshold cannot be 0") if direction == 1: # Find next high while updating current lows if close[i, col] < close[prev_i, col]: prev_i = i elif close[i, col] >= close[prev_i, col] * (1 + _pos_th): out[prev_i, col] = -1 prev_i = i direction = -1 elif direction == -1: # Find next low while updating current highs if close[i, col] > close[prev_i, col]: prev_i = i elif close[i, col] <= close[prev_i, col] * (1 - _neg_th): out[prev_i, col] = 1 prev_i = i direction = 1 else: # Find first high/low if close[i, col] >= close[prev_i, col] * (1 + _pos_th): out[prev_i, col] = -1 prev_i = i direction = -1 elif close[i, col] <= close[prev_i, col] * (1 - _neg_th): out[prev_i, col] = 1 prev_i = i direction = 1 if i == close.shape[0] - 1: # Find last high/low if direction != 0: out[prev_i, col] = -direction return out
def bn_cont_sat_trend_labels_nb(close, local_extrema, pos_th, neg_th, flex_2d=True): """Similar to `bn_cont_trend_labels_nb` but sets each close value to 0/1 if the percentage change to the next extremum exceeds a threshold set for this value. """ pos_th = np.asarray(pos_th) neg_th = np.asarray(neg_th) out = np.empty_like(close, dtype=np.float_) for col in range(close.shape[1]): idxs = np.flatnonzero(local_extrema[:, col]) if idxs.shape[0] == 0: out[:, col] = np.nan continue for k in range(1, idxs.shape[0]): start_i = prev_i = idxs[k - 1] end_i = next_i = idxs[k] if k == idxs.shape[0] - 1: end_i += 1 _pos_th = abs(flex_select_auto_nb(prev_i, col, pos_th, flex_2d)) _neg_th = abs(flex_select_auto_nb(prev_i, col, neg_th, flex_2d)) if _pos_th == 0: raise ValueError("Positive threshold cannot be 0") if _neg_th == 0: raise ValueError("Negative threshold cannot be 0") _min = np.min(close[prev_i:next_i + 1, col]) _max = np.max(close[prev_i:next_i + 1, col]) for i in range(start_i, end_i): if close[next_i, col] > close[prev_i, col]: _start = _max / (1 + _pos_th) _end = _min * (1 + _pos_th) if _max >= _end and close[i, col] <= _start: out[i, col] = 1 else: out[i, col] = 1 - (close[i, col] - _start) / (_max - _start) else: _start = _min / (1 - _neg_th) _end = _max * (1 - _neg_th) if _min <= _end and close[i, col] >= _start: out[i, col] = 0 else: out[i, col] = 1 - (close[i, col] - _min) / (_start - _min) return out
def bn_cont_sat_trend_labels_nb(close: tp.Array2d, local_extrema: tp.Array2d, pos_th: tp.MaybeArray[float], neg_th: tp.MaybeArray[float], flex_2d: bool = True) -> tp.Array2d: """Similar to `bn_cont_trend_labels_nb` but sets each close value to 0 or 1 if the percentage change to the next extremum exceeds the threshold set for this range. """ pos_th = np.asarray(pos_th) neg_th = np.asarray(neg_th) out = np.full_like(close, np.nan, dtype=np.float_) for col in range(close.shape[1]): idxs = np.flatnonzero(local_extrema[:, col]) if idxs.shape[0] == 0: continue for k in range(1, idxs.shape[0]): prev_i = idxs[k - 1] next_i = idxs[k] _pos_th = abs(flex_select_auto_nb(pos_th, prev_i, col, flex_2d)) _neg_th = abs(flex_select_auto_nb(neg_th, prev_i, col, flex_2d)) if _pos_th == 0: raise ValueError("Positive threshold cannot be 0") if _neg_th == 0: raise ValueError("Negative threshold cannot be 0") _min = np.min(close[prev_i:next_i + 1, col]) _max = np.max(close[prev_i:next_i + 1, col]) for i in range(prev_i, next_i): if close[next_i, col] > close[prev_i, col]: _start = _max / (1 + _pos_th) _end = _min * (1 + _pos_th) if _max >= _end and close[i, col] <= _start: out[i, col] = 1 else: out[i, col] = 1 - (close[i, col] - _start) / (_max - _start) else: _start = _min / (1 - _neg_th) _end = _max * (1 - _neg_th) if _min <= _end and close[i, col] >= _start: out[i, col] = 0 else: out[i, col] = 1 - (close[i, col] - _min) / (_start - _min) return out
def omega_ratio_nb(returns, ann_factor, risk_free, required_return): """2-dim version of `omega_ratio_1d_nb`. `risk_free_arr` and `required_return_arr` should be arrays of shape `returns.shape[1]`.""" risk_free_arr = np.asarray(risk_free) required_return_arr = np.asarray(required_return) out = np.empty(returns.shape[1], dtype=np.float_) for col in range(returns.shape[1]): _risk_free = flex_select_auto_nb(0, col, risk_free_arr, True) _required_return = flex_select_auto_nb(0, col, required_return_arr, True) out[col] = omega_ratio_1d_nb(returns[:, col], ann_factor, risk_free=_risk_free, required_return=_required_return) return out
def rand_choice_nb(from_i: int, to_i: int, col: int, n: tp.MaybeArray[int]) -> tp.Array1d: """`choice_func_nb` to randomly pick `n` values from range `[from_i, to_i)`. `n` uses flexible indexing.""" ns = np.asarray(n) size = min(to_i - from_i, flex_select_auto_nb(ns, 0, col, True)) return from_i + np.random.choice(to_i - from_i, size=size, replace=False)
def rand_choice_nb(from_i, to_i, col, n): """`choice_func_nb` to randomly pick `n` values from range `[from_i, to_i)`. `n` uses flexible indexing.""" ns = np.asarray(n) return from_i + np.random.choice(to_i - from_i, size=flex_select_auto_nb( 0, col, ns, True), replace=False)
def cum_returns_nb(returns, start_value): """2-dim version of `cum_returns_1d_nb`.""" start_value_arr = np.asarray(start_value) out = np.empty_like(returns, dtype=np.float_) for col in range(returns.shape[1]): _start_value = flex_select_auto_nb(0, col, start_value_arr, True) out[:, col] = cum_returns_1d_nb(returns[:, col], start_value=_start_value) return out
def value_at_risk_nb(returns, cutoff): """2-dim version of `value_at_risk_1d_nb`. `cutoff_arr` should be an array of shape `returns.shape[1]`.""" cutoff_arr = np.asarray(cutoff) out = np.empty(returns.shape[1], dtype=np.float_) for col in range(returns.shape[1]): _cutoff = flex_select_auto_nb(0, col, cutoff_arr, True) out[col] = value_at_risk_1d_nb(returns[:, col], cutoff=_cutoff) return out
def cum_returns_final_nb(returns, start_value): """2-dim version of `cum_returns_final_1d_nb`. `start_value_arr` should be an array of shape `returns.shape[1]`.""" start_value_arr = np.asarray(start_value) out = np.empty(returns.shape[1], dtype=np.float_) for col in range(returns.shape[1]): _start_value = flex_select_auto_nb(0, col, start_value_arr, True) out[col] = cum_returns_final_1d_nb(returns[:, col], start_value=_start_value) return out
def sharpe_ratio_nb(returns, ann_factor, risk_free): """2-dim version of `sharpe_ratio_1d_nb`. `risk_free_arr` should be an array of shape `returns.shape[1]`.""" risk_free_arr = np.asarray(risk_free) out = np.empty(returns.shape[1], dtype=np.float_) for col in range(returns.shape[1]): _risk_free = flex_select_auto_nb(0, col, risk_free_arr, True) out[col] = sharpe_ratio_1d_nb(returns[:, col], ann_factor, risk_free=_risk_free) return out
def annualized_volatility_nb(returns, ann_factor, levy_alpha): """2-dim version of `annualized_volatility_1d_nb`. `levy_alpha_arr` should be an array of shape `returns.shape[1]`.""" levy_alpha_arr = np.asarray(levy_alpha) out = np.empty(returns.shape[1], dtype=np.float_) for col in range(returns.shape[1]): _levy_alpha = flex_select_auto_nb(0, col, levy_alpha_arr, True) out[col] = annualized_volatility_1d_nb(returns[:, col], ann_factor, levy_alpha=_levy_alpha) return out
def breakout_labels_nb(close, window, pos_th, neg_th, wait=1, flex_2d=True): """For each value, return 1 if any value in the next period is greater than the positive threshold (in %), -1 if less than the negative threshold, and 0 otherwise. First hit wins.""" out = np.full_like(close, 0, dtype=np.float_) for col in range(close.shape[1]): for i in range(close.shape[0]): _pos_th = abs(flex_select_auto_nb(i, col, pos_th, flex_2d)) _neg_th = abs(flex_select_auto_nb(i, col, neg_th, flex_2d)) for j in range(i + wait, min(i + window + wait, close.shape[0])): if _pos_th > 0 and close[j, col] >= close[i, col] * (1 + _pos_th): out[i, col] = 1 break if _neg_th > 0 and close[j, col] <= close[i, col] * (1 - _neg_th): out[i, col] = -1 break return out
def sortino_ratio_nb(returns, ann_factor, required_return): """2-dim version of `sortino_ratio_1d_nb`. `required_return_arr` should be an array of shape `returns.shape[1]`.""" required_return_arr = np.asarray(required_return) out = np.empty(returns.shape[1], dtype=np.float_) for col in range(returns.shape[1]): _required_return = flex_select_auto_nb(0, col, required_return_arr, True) out[col] = sortino_ratio_1d_nb(returns[:, col], ann_factor, required_return=_required_return) return out
def rand_by_prob_choice_nb(col, from_i, to_i, prob, first, temp_idx_arr, flex_2d): """`choice_func_nb` to randomly pick values from range `[from_i, to_i)` with probability `prob`. `prob` uses flexible indexing.""" probs = np.asarray(prob) j = 0 for i in range(from_i, to_i): if np.random.uniform(0, 1) < flex_select_auto_nb(i, col, probs, flex_2d): # [0, 1) temp_idx_arr[j] = i j += 1 if first: break return temp_idx_arr[:j]
def rand_by_prob_choice_nb(from_i: int, to_i: int, col: int, prob: tp.MaybeArray[float], pick_first: bool, temp_idx_arr: tp.Array1d, flex_2d: bool) -> tp.Array1d: """`choice_func_nb` to randomly pick values from range `[from_i, to_i)` with probability `prob`. `prob` uses flexible indexing.""" probs = np.asarray(prob) j = 0 for i in range(from_i, to_i): if np.random.uniform(0, 1) < flex_select_auto_nb( probs, i, col, flex_2d): # [0, 1) temp_idx_arr[j] = i j += 1 if pick_first: break return temp_idx_arr[:j]
def stop_choice_nb(from_i: int, to_i: int, col: int, ts: tp.ArrayLike, stop: tp.MaybeArray[float], trailing: tp.MaybeArray[bool], wait: int, pick_first: bool, temp_idx_arr: tp.Array1d, flex_2d: bool) -> tp.Array1d: """`choice_func_nb` that returns the indices of the stop being hit. Args: from_i (int): Index to start generation from (inclusive). to_i (int): Index to run generation to (exclusive). col (int): Current column. ts (array of float): 2-dim time series array such as price. stop (float or array_like): Stop value for stop loss. Can be per frame, column, row, or element-wise. Set to `np.nan` to disable. trailing (bool or array_like): Whether to use trailing stop. Can be per frame, column, row, or element-wise. Set to False to disable. wait (int): Number of ticks to wait before placing exits. Setting False or 0 may result in two signals at one bar. !!! note If `wait` is greater than 0, trailing stop won't update at bars that come before `from_i`. pick_first (bool): Whether to stop as soon as the first exit signal is found. temp_idx_arr (array of int): Empty integer array used to temporarily store indices. flex_2d (bool): See `vectorbt.base.reshape_fns.flex_select_auto_nb`.""" j = 0 init_i = from_i - wait init_ts = flex_select_auto_nb(ts, init_i, col, flex_2d) init_stop = flex_select_auto_nb(np.asarray(stop), init_i, col, flex_2d) init_trailing = flex_select_auto_nb(np.asarray(trailing), init_i, col, flex_2d) max_high = min_low = init_ts for i in range(from_i, to_i): if not np.isnan(init_stop): if init_trailing: if init_stop >= 0: # Trailing stop buy curr_stop_price = min_low * (1 + abs(init_stop)) else: # Trailing stop sell curr_stop_price = max_high * (1 - abs(init_stop)) else: curr_stop_price = init_ts * (1 + init_stop) # Check if stop price is within bar curr_ts = flex_select_auto_nb(ts, i, col, flex_2d) if not np.isnan(init_stop): if init_stop >= 0: exit_signal = curr_ts >= curr_stop_price else: exit_signal = curr_ts <= curr_stop_price if exit_signal: temp_idx_arr[j] = i j += 1 if pick_first: return temp_idx_arr[:1] # Keep track of lowest low and highest high if trailing if init_trailing: if curr_ts < min_low: min_low = curr_ts elif curr_ts > max_high: max_high = curr_ts return temp_idx_arr[:j]
def adv_stop_choice_nb(col, from_i, to_i, open, high, low, close, hit_price_out, stop_type_out, sl_stop, ts_stop, tp_stop, is_open_safe, wait, first, temp_idx_arr, flex_2d): """`choice_func_nb` that returns the indices of the stop price being reached. Compared to `stop_choice_nb`, takes into account the whole bar, can check for both (trailing) stop loss and take profit simultaneously, and tracks hit price and stop type. !!! note We don't have intra-candle data. If there was a huge price fluctuation in both directions, we can't determine whether SL was triggered before TP and vice versa. So some assumptions need to be made: 1) trailing stop can only be based on previous close/high, and 2) we pessimistically assume that SL comes before TS and TP. Args: col (int): Current column. from_i (int): Index to start generation from (inclusive). to_i (int): Index to run generation to (exclusive). open (array_like of float): Entry price such as open or previous close. high (array_like of float): High price. low (array_like of float): Low price. close (array_like of float): Close price. hit_price_out (array_like of float): Array where hit price of each exit will be stored. stop_type_out (array_like of int): Array where stop type of each exit will be stored. 0 for stop loss, 1 for take profit. sl_stop (float or array_like): Percentage value for stop loss. Can be per frame, column, row, or element-wise. Set to 0. to disable. ts_stop (bool or array_like): Percentage value for trailing stop. Can be per frame, column, row, or element-wise. tp_stop (float or array_like): Percentage value for take profit. Can be per frame, column, row, or element-wise. Set to 0. to disable. is_open_safe (bool): Whether entry price comes right at or before open. If True and wait is 0, can use high/low at entry tick. Otherwise uses close. wait (bool or int): Number of ticks to wait before placing exits. Setting False or 0 may result in two signals at one tick. first (bool): Whether to stop as soon as the first exit signal is found. temp_idx_arr (array_like of int): Empty integer array used to temporarily store indices. flex_2d (bool): See `vectorbt.base.reshape_fns.flex_choose_i_and_col_nb`. """ sl_stops = np.asarray(sl_stop) ts_stops = np.asarray(ts_stop) tp_stops = np.asarray(tp_stop) init_i = from_i - wait init_open = flex_select_auto_nb(init_i, col, open, flex_2d) init_sl_stop = abs(flex_select_auto_nb(init_i, col, sl_stops, flex_2d)) init_ts_stop = abs(flex_select_auto_nb(init_i, col, ts_stops, flex_2d)) init_tp_stop = abs(flex_select_auto_nb(init_i, col, tp_stops, flex_2d)) max_i = init_i max_p = init_open j = 0 for i in range(from_i, to_i): # Calculate stop price if init_sl_stop > 0: curr_sl_stop_price = init_open * (1 - init_sl_stop) if init_ts_stop > 0: max_ts_stop = abs(flex_select_auto_nb(max_i, col, ts_stops, flex_2d)) curr_ts_stop_price = max_p * (1 - max_ts_stop) if init_tp_stop > 0: curr_tp_stop_price = init_open * (1 + init_tp_stop) # Check if stop price is within bar if i > init_i or is_open_safe: # is_open_safe means open is either open or any other price before it # so it's safe to use high/low at entry tick curr_high = flex_select_auto_nb(i, col, high, flex_2d) curr_low = flex_select_auto_nb(i, col, low, flex_2d) else: # Otherwise, we can only use close price at entry tick curr_close = flex_select_auto_nb(i, col, close, flex_2d) curr_high = curr_low = curr_close exit_signal = False if init_sl_stop > 0: if curr_low <= curr_sl_stop_price: exit_signal = True hit_price_out[i, col] = curr_sl_stop_price stop_type_out[i, col] = StopType.StopLoss if not exit_signal and init_ts_stop > 0: if curr_low <= curr_ts_stop_price: exit_signal = True hit_price_out[i, col] = curr_ts_stop_price stop_type_out[i, col] = StopType.TrailStop if not exit_signal and init_tp_stop > 0: if curr_high >= curr_tp_stop_price: exit_signal = True hit_price_out[i, col] = curr_tp_stop_price stop_type_out[i, col] = StopType.TakeProfit if exit_signal: temp_idx_arr[j] = i j += 1 if first: return temp_idx_arr[:1] # Keep track of highest high if trailing if init_ts_stop > 0: if curr_high > max_p: max_i = i max_p = curr_high return temp_idx_arr[:j]
def stop_choice_nb(col, from_i, to_i, ts, stop, trailing, wait, first, temp_idx_arr, flex_2d): """`choice_func_nb` that returns the indices of the stop being reached. Args: col (int): Current column. from_i (int): Index to start generation from (inclusive). to_i (int): Index to run generation to (exclusive). ts (array_like): 2-dim time series array such as price. stop (float or array_like): Stop value for stop loss. Can be per frame, column, row, or element-wise. Set to 0. to disable. trailing (bool or array_like): Whether to use trailing stop. Can be per frame, column, row, or element-wise. wait (bool or int): Number of ticks to wait before placing exits. Setting False or 0 may result in two signals at one tick. first (bool): Whether to stop as soon as the first exit signal is found. temp_idx_arr (int): Empty integer array used to temporarily store indices. flex_2d (bool): See `vectorbt.base.reshape_fns.flex_choose_i_and_col_nb`.""" stops = np.asarray(stop) trailings = np.asarray(trailing) j = 0 min_i = max_i = init_i = from_i - wait init_ts = flex_select_auto_nb(init_i, col, ts, flex_2d) init_stop = flex_select_auto_nb(init_i, col, stops, flex_2d) init_trailing = flex_select_auto_nb(init_i, col, trailings, flex_2d) max_high = min_low = init_ts for i in range(from_i, to_i): if init_trailing: if init_stop > 0: # Trailing stop buy last_stop = flex_select_auto_nb(min_i, col, stops, flex_2d) curr_stop_price = min_low * (1 + abs(last_stop)) elif init_stop < 0: # Trailing stop sell last_stop = flex_select_auto_nb(max_i, col, stops, flex_2d) curr_stop_price = max_high * (1 - abs(last_stop)) else: curr_stop_price = init_ts * (1 + init_stop) # Check if stop price is within bar curr_ts = flex_select_auto_nb(i, col, ts, flex_2d) exit_signal = False if init_stop > 0: exit_signal = curr_ts >= curr_stop_price elif init_stop < 0: exit_signal = curr_ts <= curr_stop_price if exit_signal: temp_idx_arr[j] = i j += 1 if first: return temp_idx_arr[:1] # Keep track of lowest low and highest high if trailing if init_trailing: if curr_ts < min_low: min_i = i min_low = curr_ts elif curr_ts > max_high: max_i = i max_high = curr_ts return temp_idx_arr[:j]
def generate_rand_enex_nb(shape, n, entry_wait, exit_wait, seed=None): """Pick a number of entries and the same number of exits one after another. Respects `entry_wait` and `exit_wait` constraints through a number of tricks. Tries to mimic a uniform distribution as much as possible. The idea is the following: with constraints, there is some fixed amount of total space required between first entry and last exit. Upscale this space in a way that distribution of entries and exit is similar to a uniform distribution. This means randomizing the position of first entry, last exit, and all signals between them. `n` uses flexible indexing. Specify `seed` to make output deterministic.""" if seed is not None: np.random.seed(seed) entries = np.full(shape, False) exits = np.full(shape, False) if entry_wait == 0 and exit_wait == 0: raise ValueError("entry_wait and exit_wait cannot be both 0") if entry_wait == 1 and exit_wait == 1: # Basic case both = generate_rand_nb(shape, n * 2, seed=None) for col in range(both.shape[1]): both_idxs = np.flatnonzero(both[:, col]) entries[both_idxs[0::2], col] = True exits[both_idxs[1::2], col] = True else: ns = np.asarray(n) for col in range(shape[1]): _n = flex_select_auto_nb(0, col, ns, True) if _n == 1: entry_idx = np.random.randint(0, shape[0] - exit_wait) entries[entry_idx, col] = True else: # Minimum range between two entries min_range = entry_wait + exit_wait # Minimum total range between first and last entry min_total_range = min_range * (_n - 1) if shape[0] < min_total_range + exit_wait + 1: raise ValueError("Cannot take a larger sample than population") # We should decide how much space should be allocate before first and after last entry # Maximum space outside of min_total_range max_free_space = shape[0] - min_total_range - 1 # If min_total_range is tiny compared to max_free_space, limit it # otherwise we would have huge space before first and after last entry # Limit it such as distribution of entries mimics uniform free_space = min(max_free_space, 3 * shape[0] // (_n + 1)) # What about last exit? it requires exit_wait space free_space -= exit_wait # Now we need to distribute free space among three ranges: # 1) before first, 2) between first and last added to min_total_range, 3) after last # We do 2) such that min_total_range can freely expand to maximum # We allocate twice as much for 3) as for 1) because an exit is missing rand_floats = uniform_summing_to_one_nb(6) chosen_spaces = rescale_float_to_int_nb(rand_floats, (0, free_space), free_space) first_idx = chosen_spaces[0] last_idx = shape[0] - np.sum(chosen_spaces[-2:]) - exit_wait - 1 # Selected range between first and last entry total_range = last_idx - first_idx # Maximum range between two entries within total_range max_range = total_range - (_n - 2) * min_range # Select random ranges within total_range rand_floats = uniform_summing_to_one_nb(_n - 1) chosen_ranges = rescale_float_to_int_nb(rand_floats, (min_range, max_range), total_range) # Translate them into entries entry_idxs = np.empty(_n, dtype=np.int_) entry_idxs[0] = first_idx entry_idxs[1:] = chosen_ranges entry_idxs = np.cumsum(entry_idxs) entries[entry_idxs, col] = True # Generate exits for col in range(shape[1]): entry_idxs = np.flatnonzero(entries[:, col]) for j in range(len(entry_idxs)): entry_i = entry_idxs[j] + exit_wait if j < len(entry_idxs) - 1: exit_i = entry_idxs[j + 1] - entry_wait else: exit_i = entries.shape[0] - 1 i = np.random.randint(exit_i - entry_i + 1) exits[entry_i + i, col] = True return entries, exits
def ohlc_stop_choice_nb(from_i: int, to_i: int, col: int, open: tp.ArrayLike, high: tp.ArrayLike, low: tp.ArrayLike, close: tp.ArrayLike, stop_price_out: tp.Array2d, stop_type_out: tp.Array2d, sl_stop: tp.MaybeArray[float], sl_trail: tp.MaybeArray[bool], tp_stop: tp.MaybeArray[float], reverse: tp.MaybeArray[bool], is_open_safe: bool, wait: int, pick_first: bool, temp_idx_arr: tp.Array1d, flex_2d: bool) -> tp.Array1d: """`choice_func_nb` that returns the indices of the stop price being hit within OHLC. Compared to `stop_choice_nb`, takes into account the whole bar, can check for both (trailing) stop loss and take profit simultaneously, and tracks hit price and stop type. !!! note We don't have intra-candle data. If there was a huge price fluctuation in both directions, we can't determine whether SL was triggered before TP and vice versa. So some assumptions need to be made: 1) trailing stop can only be based on previous close/high, and 2) we pessimistically assume that SL comes before TP. Args: col (int): Current column. from_i (int): Index to start generation from (inclusive). to_i (int): Index to run generation to (exclusive). open (array of float): Entry price such as open or previous close. high (array of float): High price. low (array of float): Low price. close (array of float): Close price. stop_price_out (array of float): Array where hit price of each exit will be stored. stop_type_out (array of int): Array where stop type of each exit will be stored. 0 for stop loss, 1 for take profit. sl_stop (float or array_like): Percentage value for stop loss. Can be per frame, column, row, or element-wise. Set to `np.nan` to disable. sl_trail (bool or array_like): Whether `sl_stop` is trailing. Can be per frame, column, row, or element-wise. Set to False to disable. tp_stop (float or array_like): Percentage value for take profit. Can be per frame, column, row, or element-wise. Set to `np.nan` to disable. reverse (bool or array_like): Whether to do the opposite, i.e.: prices are followed downwards. is_open_safe (bool): Whether entry price comes right at or before open. If True and wait is 0, can use high/low at entry bar. Otherwise uses only close. wait (int): Number of ticks to wait before placing exits. Setting False or 0 may result in entry and exit signal at one bar. !!! note If `wait` is greater than 0, even with `is_open_safe` set to True, trailing stop won't update at bars that come before `from_i`. pick_first (bool): Whether to stop as soon as the first exit signal is found. temp_idx_arr (array of int): Empty integer array used to temporarily store indices. flex_2d (bool): See `vectorbt.base.reshape_fns.flex_select_auto_nb`. """ init_i = from_i - wait init_open = flex_select_auto_nb(open, init_i, col, flex_2d) init_sl_stop = flex_select_auto_nb(np.asarray(sl_stop), init_i, col, flex_2d) if init_sl_stop < 0: raise ValueError("Stop value must be 0 or greater") init_sl_trail = flex_select_auto_nb(np.asarray(sl_trail), init_i, col, flex_2d) init_tp_stop = flex_select_auto_nb(np.asarray(tp_stop), init_i, col, flex_2d) if init_tp_stop < 0: raise ValueError("Stop value must be 0 or greater") init_reverse = flex_select_auto_nb(np.asarray(reverse), init_i, col, flex_2d) max_p = min_p = init_open j = 0 for i in range(from_i, to_i): # Resolve current bar _open = flex_select_auto_nb(open, i, col, flex_2d) _high = flex_select_auto_nb(high, i, col, flex_2d) _low = flex_select_auto_nb(low, i, col, flex_2d) _close = flex_select_auto_nb(close, i, col, flex_2d) if np.isnan(_open): _open = _close if np.isnan(_low): _low = min(_open, _close) if np.isnan(_high): _high = max(_open, _close) # Calculate stop price if not np.isnan(init_sl_stop): if init_sl_trail: if init_reverse: curr_sl_stop_price = min_p * (1 + init_sl_stop) else: curr_sl_stop_price = max_p * (1 - init_sl_stop) else: if init_reverse: curr_sl_stop_price = init_open * (1 + init_sl_stop) else: curr_sl_stop_price = init_open * (1 - init_sl_stop) if not np.isnan(init_tp_stop): if init_reverse: curr_tp_stop_price = init_open * (1 - init_tp_stop) else: curr_tp_stop_price = init_open * (1 + init_tp_stop) # Check if stop price is within bar if i > init_i or is_open_safe: # is_open_safe means open is either open or any other price before it # so it's safe to use high/low at entry bar curr_high = _high curr_low = _low else: # Otherwise, we can only use close price at entry bar curr_high = curr_low = _close exit_signal = False if not np.isnan(init_sl_stop): if (not init_reverse and curr_low <= curr_sl_stop_price) or \ (init_reverse and curr_high >= curr_sl_stop_price): exit_signal = True stop_price_out[i, col] = curr_sl_stop_price if init_sl_trail: stop_type_out[i, col] = StopType.TrailStop else: stop_type_out[i, col] = StopType.StopLoss if not exit_signal and not np.isnan(init_tp_stop): if (not init_reverse and curr_high >= curr_tp_stop_price) or \ (init_reverse and curr_low <= curr_tp_stop_price): exit_signal = True stop_price_out[i, col] = curr_tp_stop_price stop_type_out[i, col] = StopType.TakeProfit if exit_signal: temp_idx_arr[j] = i j += 1 if pick_first: return temp_idx_arr[:1] # Keep track of highest high if trailing if init_sl_trail: if curr_low < min_p: min_p = curr_low if curr_high > max_p: max_p = curr_high return temp_idx_arr[:j]