Features

Signals and stops

Simulate signal-driven entries, exits, callbacks, and advanced stop logic

Stop laddering

✅ Stop laddering is a technique for incrementally moving out of a position. Instead of providing a single stop value to close a position, you can provide an array of stop values, with each one removing a certain amount of the position when triggered. You can control this amount by choosing a different ladder mode. Thanks to a new broadcasting feature that allows arrays to broadcast along just one axis, the stop values do not need to have the same shape as the data. You can even provide stop arrays of different shapes as parameters!

Test two TP ladders
data = vbt.YFData.pull("BTC-USD", end="2017-01")
pf = vbt.PF.from_holding(
    data,
    stop_ladder="uniform",
    tp_stop=vbt.Param([
        [0.1, 0.2, 0.3, 0.4, 0.5],
        [0.4, 0.5, 0.6],
    ], keys=["tp_ladder_1", "tp_ladder_2"])
)
pf.trades.plot(column="tp_ladder_1").show()
BTC-USD holding trade with five uniform take-profit ladder exits Figure data (JSON)

Time stops

✅ Joining other stop orders, time stop orders can close a position either after a certain period of time or on a specific date.

Enter randomly, exit before the end of the month
data = vbt.YFData.pull("BTC-USD", start="2022-01", end="2022-04")
entries = vbt.pd_acc.signals.generate_random(data.symbol_wrapper, n=10, seed=42)
pf = vbt.PF.from_signals(data, entries, dt_stop="M")  
pf.orders.readable[["Fill Index", "Side", "Stop Type"]]
                 Fill Index  Side Stop Type
0 2022-01-19 00:00:00+00:00   Buy      None
1 2022-01-31 00:00:00+00:00  Sell        DT
2 2022-02-25 00:00:00+00:00   Buy      None
3 2022-02-28 00:00:00+00:00  Sell        DT
4 2022-03-11 00:00:00+00:00   Buy      None
5 2022-03-31 00:00:00+00:00  Sell        DT

✅ Target size can be converted to signals using a special signal function, giving access to stop and limit order functionality. This is especially useful, for example, in portfolio optimization.

Perform the Mean-Variance Optimization with SL and TP
data = vbt.YFData.pull(
    ["SPY", "TLT", "XLF", "XLE", "XLU", "XLK", "XLB", "XLP", "XLY", "XLI", "XLV"],
    start="2022",
    end="2023",
    missing_index="drop"
)
pfo = vbt.PFO.from_riskfolio(data.returns, every="M")
pf = pfo.simulate(
    data,
    pf_method="from_signals",
    sl_stop=0.05,
    tp_stop=0.1,
    stop_exit_price="close"  
)
pf.plot_allocations().show()
Monthly Mean-Variance portfolio allocations with stop-loss and take-profit protection during 2022 Figure data (JSON)

Signal callbacks

✅ Want to customize your simulation based on signals, or even generate signals dynamically according to the current backtesting environment? Two new callbacks now bring simulator flexibility to the next level: one lets you generate or override signals for each asset at every bar, and another allows you to compute user-defined metrics for the entire group at the end of each bar. Both accept a "context" that contains information about the current simulation state, enabling trading decisions to be made in a way similar to event-driven backtesters.

Backtest SMA crossover iteratively
InOutputs = namedtuple("InOutputs", ["fast_sma", "slow_sma"])

def initialize_in_outputs(target_shape):
    return InOutputs(
        fast_sma=np.full(target_shape, np.nan),
        slow_sma=np.full(target_shape, np.nan)
    )

@njit
def signal_func_nb(ctx, fast_window, slow_window):
    fast_sma = ctx.in_outputs.fast_sma
    slow_sma = ctx.in_outputs.slow_sma
    fast_start_i = ctx.i - fast_window + 1
    slow_start_i = ctx.i - slow_window + 1
    if fast_start_i >= 0 and slow_start_i >= 0:
        fast_sma[ctx.i, ctx.col] = np.nanmean(ctx.close[fast_start_i : ctx.i + 1])
        slow_sma[ctx.i, ctx.col] = np.nanmean(ctx.close[slow_start_i : ctx.i + 1])
        is_entry = vbt.pf_nb.iter_crossed_above_nb(ctx, fast_sma, slow_sma)
        is_exit = vbt.pf_nb.iter_crossed_below_nb(ctx, fast_sma, slow_sma)
        return is_entry, is_exit, False, False
    return False, False, False, False

pf = vbt.PF.from_signals(
    vbt.YFData.pull("BTC-USD"),
    signal_func_nb=signal_func_nb,
    signal_args=(50, 200),
    in_outputs=vbt.RepFunc(initialize_in_outputs),
)
fig = pf.get_in_output("fast_sma").vbt.plot()
pf.get_in_output("slow_sma").vbt.plot(fig=fig)
pf.orders.plot(plot_ohlc=False, plot_close=False, fig=fig)
fig.show()
BTC-USD iterative 50-day and 200-day SMA crossover with buy and sell orders Figure data (JSON)

Signal contexts

✅ Signal generation functions have been redesigned to operate on contexts. This allows you to design more complex signal strategies with less namespace pollution.

Entry at the first bar of the week, exit at the last bar of the week
@njit
def entry_place_func_nb(ctx, index):
    for i in range(ctx.from_i, ctx.to_i):  
        if i == 0:
            return i - ctx.from_i  
        else:
            index_before = index[i - 1]
            index_now = index[i]
            index_next_week = vbt.dt_nb.future_weekday_nb(index_before, 0)
            if index_now >= index_next_week:  
                return i - ctx.from_i
    return -1

@njit
def exit_place_func_nb(ctx, index):
    for i in range(ctx.from_i, ctx.to_i):
        if i == len(index) - 1:
            return i - ctx.from_i
        else:
            index_now = index[i]
            index_after = index[i + 1]
            index_next_week = vbt.dt_nb.future_weekday_nb(index_now, 0)
            if index_after >= index_next_week:  
                return i - ctx.from_i
    return -1

data = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2020-01-14")
entries, exits = vbt.pd_acc.signals.generate_both(
    data.symbol_wrapper.shape,
    entry_place_func_nb=entry_place_func_nb,
    entry_place_args=(data.index.vbt.to_ns(),),
    exit_place_func_nb=exit_place_func_nb,
    exit_place_args=(data.index.vbt.to_ns(),),
    wrapper=data.symbol_wrapper
)
pd.concat((
    entries.rename("Entries"),
    exits.rename("Exits")
), axis=1).to_period("W")
                       Entries  Exits
Date
2020-01-06/2020-01-12     True  False
2020-01-06/2020-01-12    False  False
2020-01-06/2020-01-12    False  False
2020-01-06/2020-01-12    False  False
2020-01-06/2020-01-12    False  False
2020-01-06/2020-01-12    False  False
2020-01-06/2020-01-12    False   True
2020-01-13/2020-01-19     True  False

Tutorial

Learn more in the Signal development tutorial.

Copyright © 20212026 Oleg Polakow. All rights reserved.

Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.