# Signals and stops (/features/backtesting/signals-and-stops)

## Stop laddering \[#stop-laddering]

New in 1.12.0

✅ 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!

```python title="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)](/assets/figures/features/portfolio/stop-laddering.e265b961a2c9.json)

## Time stops \[#time-stops]

New in 1.11.0

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

```python title="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")  # (1)
>>> 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
```

1.  Use `dt_stop` for datetime-based stops and `td_stop` for timedelta-based stops.
    Datetime-based stops can be periods ("D"), timestamps ("2023-01-01"), and even specific times
    ("18:00").

## Target size to signals \[#target-size-to-signals]

New in 1.10.0

✅ 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.

```python title="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"  # (1)
... )
>>> pf.plot_allocations().show()
```

1.  Otherwise, user and stop signals will occur at different times within the same bar,
    making it impossible to establish the correct order of execution.

Monthly Mean-Variance portfolio allocations with stop-loss and take-profit protection during 2022. [Figure data (JSON)](/assets/figures/features/portfolio/target-size-to-signals.8f527b79be15.json)

## Signal callbacks \[#signal-callbacks]

New in 1.4.0

✅ 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.

```python title="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)](/assets/figures/features/portfolio/signal-callbacks.c33e37edbfeb.json)

## Signal contexts \[#signal-contexts]

New in 1.2.3

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

```python title="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):  # (1)
...         if i == 0:
...             return i - ctx.from_i  # (2)
...         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:  # (3)
...                 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:  # (4)
...                 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
```

1.  Iterate over the bars in the current period segment.
2.  If a signal should be placed, return an index relative to the segment.
3.  Place a signal if the current bar crosses Monday.
4.  Place a signal if the next bar crosses Monday.

!!! info "Tutorial"
    Learn more in the [Signal development](/tutorials/signal-development) tutorial.
