# Event-driven backtesting (/features/backtesting/event-driven-backtesting)

Some trading rules depend on what already happened in the portfolio: a cooldown after a stop, a cap
on open positions, a daily loss limit. VBT supports event-driven backtesting in Python through
compiled callbacks that run inside the simulation loop, see cash, positions, and fills on every bar,
and still run at Numba speed.

```python title="Block re-entries for 10 bars after a stop loss"
>>> @njit
... def cooldown_signal_nb(c, entries, exits, cooldown):  # (1)
...     is_exit = vbt.pf_nb.select_nb(c, exits)
...     if c.i < c.in_outputs.blocked_until[c.col]:
...         return False, is_exit, False, False
...     return vbt.pf_nb.select_nb(c, entries), is_exit, False, False

>>> @njit
... def cooldown_post_order_nb(c, cooldown):  # (2)
...     if vbt.pf_nb.order_closed_position_nb(c):
...         if vbt.pf_nb.get_last_order_nb(c)["stop_type"] >= 0:
...             wait = vbt.pf_nb.select_nb(c, cooldown)
...             c.in_outputs.blocked_until[c.col] = c.i + 1 + wait

>>> data = vbt.YFData.pull("BTC-USD", start="2022-01-01", end="2024-01-01")
>>> sma = data.close.rolling(20).mean()
>>> pf = vbt.PF.from_signals(
...     data,
...     signal_func_nb=cooldown_signal_nb,
...     signal_args=(vbt.Rep("entries"), vbt.Rep("exits"), vbt.Rep("cooldown")),
...     post_order_func_nb=cooldown_post_order_nb,
...     post_order_args=(vbt.Rep("cooldown"),),
...     broadcast_named_args=dict(
...         entries=data.close > sma,
...         exits=data.close < sma,
...         cooldown=vbt.Param([0, 10]),  # (3)
...     ),
...     in_outputs=dict(
...         blocked_until=vbt.RepEval("np.zeros(wrapper.shape_2d[1], dtype=np.int_)")
...     ),
...     sl_stop=0.05,
... )
>>> pf.stats(["total_trades", "win_rate", "total_return", "max_dd"], agg_func=None)
          Total Trades  Win Rate [%]  Total Return [%]  Max Drawdown [%]
cooldown
0                   44     22.727273          4.394750         38.506912
10                  34     29.411765         34.063625         31.897484

>>> pf[10].orders.readable[["Fill Index", "Side", "Stop Type"]].iloc[2:6]
                 Fill Index  Side Stop Type
2 2022-02-28 00:00:00+00:00   Buy      None
3 2022-03-04 00:00:00+00:00  Sell        SL
4 2022-03-16 00:00:00+00:00   Buy      None
5 2022-04-06 00:00:00+00:00  Sell      None
```

1.  Called for every bar and column. It returns four flags: long entry, long exit, short entry, and
    short exit. Exits always pass, so a cooldown can never trap an open position.
2.  Called after each processed order. It arms the cooldown only when this order closed the position
    and its record says a stop caused it.
3.  Run the rule without and with the cooldown in one call. Each variant gets its own column and its
    own `blocked_until` slot.

The 5% stop fired on March 4, 2022. Without the cooldown, the trend filter bought again on March 9
and was stopped out the next day. With it, the next entry waits until March 16. In this sample, the
cooldown removed 10 trades and reduced the drawdown. The point is not the result but that the rule
needs the simulation's own output, so it cannot be precomputed as a signal array.

## Choose how much to customize \[#choose-how-much-to-customize]

You can add a rule to an existing signal strategy or take control of order generation. Choose the
level that matches what your strategy needs:

| What you want to do                                                    | Use                                                                               |
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Keep signal arrays and adjust sizes or stops as the account changes    | An adjustment callback with `from_signals`                                        |
| Decide whether to enter or exit using current positions, fills, or PnL | A signal callback with `from_signals`                                             |
| Choose each order directly, including several orders on one bar        | An order function with `from_order_func`, using flexible mode for multiple orders |
| Manage your own account states and simulation loop                     | A custom simulator built from VBT's execution primitives                          |

Signal callbacks retain the signal simulator's sizing, stops, limits, and conflict handling. Order
functions give you direct control over the orders. Both produce a portfolio you can inspect with the
same trade records, metrics, and charts.

These callbacks can process a full historical backtest in one call. To continue a simulation as new
bars arrive, see [Live simulation](/features/backtesting/live-simulation/).

## Callbacks that see the portfolio \[#callbacks-that-see-the-portfolio]

`vbt.PF.from_signals` accepts a signal function, `signal_func_nb`, that replaces the four signal
arrays. It receives a context `c` with the current bar `c.i`, the column `c.col`, and the live
simulation state, then returns the four signals for that bar. Everything else, from order sizing to
stops and fees, works as usual.

The context exposes the state you usually need:

| State                                        | Context field or helper                                                    |
| -------------------------------------------- | -------------------------------------------------------------------------- |
| Position, cash, and debt                     | `c.last_position`, `c.last_cash`, `c.last_debt`                            |
| Portfolio value                              | `c.last_value`, `vbt.pf_nb.get_group_value_nb(c, c.group)`                 |
| Open position: entry bar, price, PnL, return | `c.last_pos_info[c.col]`                                                   |
| Pending stops and limit orders               | `c.last_sl_info`, `c.last_tsl_info`, `c.last_tp_info`, `c.last_limit_info` |
| Your own arrays at the current bar           | `vbt.pf_nb.select_nb(c, array)`                                            |
| What the last order did                      | `vbt.pf_nb.order_closed_position_nb(c)`, `vbt.pf_nb.get_last_order_nb(c)`  |

Compute indicators before the simulation and select their current value inside the callback to reuse
the work across bars. If the indicator itself needs to update during simulation, you can use
[streaming indicators](/features/indicators/streaming-indicators/). For calendar rules,
`c.index[c.i]` holds the bar's timestamp in nanoseconds.

The simulator values the portfolio at the bar's open before it calls the signal function, so
`c.last_value` and the PnL in `c.last_pos_info` reflect the open, not the close. To judge a position
on the close, compute it from `vbt.pf_nb.select_nb(c, c.close)`. Both fields are recomputed by the
simulator, so writing to them changes nothing. To change the outcome, return different signals or
edit the pending stop and limit records.

!!! warning "Timing inside callbacks"
    A signal function decides on the bar it is called for. To act on the previous bar's data at this
    bar's open, read your inputs at `c.i - 1` and pass `price="open"`. With `price="nextopen"`, the
    simulator labels each returned signal as the previous bar's and fills it at the current open, so a
    callback that reads the current bar's close trades before that close exists.

!!! tip "Debugging a callback"
    Mistakes in compiled callbacks surface as Numba typing errors. The usual causes are a function
    without `@njit`, a branch that returns nothing instead of four flags, and `signal_args` that is
    not a tuple, such as `(0.1)` instead of `(0.1,)`. To step through the logic, remove `@njit` and
    pass `jitted=False`: the simulator then runs in Python, so `print` and breakpoints work.

!!! info "Signal function or adjustment function"
    `adjust_func_nb` runs before the built-in signal functions to change stops or sizes while signals
    still come from arrays. Once you pass your own `signal_func_nb`, that call no longer happens
    automatically, so call the adjustment logic from inside your signal function.

## Stateful trading rules \[#stateful-trading-rules]

Most stateful rules follow the same pattern: keep a little state, read it in the signal function,
and update it after orders fill. Here is a limit of three concurrent positions across a 20-asset
universe that shares one cash balance:

```python title="Hold at most three positions across 20 assets"
>>> symbols = [f"S{i:02d}" for i in range(20)]
>>> data = vbt.GBMOHLCData.pull(symbols, start="2024-01-01", end="2025-01-01", seed=42)
>>> sma = data.close.rolling(20).mean()

>>> @njit
... def max_positions_nb(c, entries, exits, max_positions):
...     if c.in_outputs.bar_entries[0] != c.i:  # (1)
...         c.in_outputs.bar_entries[:] = (c.i, 0)
...     if c.last_position[c.col] > 0:
...         return False, vbt.pf_nb.select_nb(c, exits), False, False
...     if vbt.pf_nb.select_nb(c, entries):
...         n_open = vbt.pf_nb.get_n_active_positions_nb(c)
...         if n_open + c.in_outputs.bar_entries[1] < max_positions:
...             c.in_outputs.bar_entries[1] += 1
...             return True, False, False, False
...     return False, False, False, False

>>> pf = vbt.PF.from_signals(
...     data,
...     signal_func_nb=max_positions_nb,
...     signal_args=(vbt.Rep("entries"), vbt.Rep("exits"), 3),
...     broadcast_named_args=dict(
...         entries=data.close.vbt.crossed_above(sma),
...         exits=data.close.vbt.crossed_below(sma),
...     ),
...     in_outputs=dict(bar_entries=vbt.RepEval("np.full(2, -1, dtype=np.int_)")),
...     size=0.3,
...     size_type="valuepercent",
...     group_by=True,  # (2)
...     cash_sharing=True,
... )
>>> print((pf.assets > 0).sum(axis=1).max())
3
```

1.  Count entries approved earlier on this bar. Orders execute after the signal function has run for
    every column of the bar, so open positions alone miss same-bar entries.
2.  Put all 20 assets into one group so the callback sees all of their positions.

Without the limit, the same signals hold up to seven positions at once. Without the same-bar
counter, the limit leaks and reaches five. Rules built this way include:

| Rule                                       | State it reads                                                     |
| ------------------------------------------ | ------------------------------------------------------------------ |
| Cooldown after a stop, a loss, or any exit | Last order record and its stop type, closed position PnL           |
| Maximum concurrent positions               | Open positions in the group plus entries approved this bar         |
| Minimum holding period                     | Entry bar of the open position in `c.last_pos_info`                |
| Daily trade or loss limit                  | Counters and a reference value reset on the first bar of each day  |
| Equity curve filter                        | Portfolio value history kept in your own array                     |
| Portfolio-level stop                       | Group value compared with a threshold, then exits for every column |

The signal function and `post_segment_func_nb` run on every bar. `post_order_func_nb` runs only when
the simulator tries to execute an order, including one that is rejected, and not when a signal is
ignored or only places a limit order. Bookkeeping that must advance on quiet bars, such as a daily
counter or a settlement delay, belongs in the segment callback, or pass `skip_empty=False` to run
the order callback on every bar too.

!!! info "Tutorial"
    The members-only [From Python to Rust](https://members.vectorbt.pro/tutorials/from-python-to-rust/dynamic/)
    tutorial builds the stop cooldown on a four-asset portfolio and carries it into Numba kernels and
    native Rust.

## Order functions \[#order-functions]

Signal callbacks still produce signals, and the signal simulator places at most one order per column
and bar. `vbt.PF.from_order_func` removes that layer: your callback returns orders directly. With a
flexible order function, it also picks the column and can return several orders on one bar. This one
buys at the open after a gap down of more than 0.5% and sells at the close of the same day:

```python title="Buy the open and sell the close on gap-down days"
>>> @njit
... def open_to_close_nb(c, gap_down):
...     col = c.from_col
...     if not gap_down[c.i, col]:
...         return -1, vbt.pf_enums.NoOrder
...     if c.call_idx == 0:  # (1)
...         return col, vbt.pf_nb.order_nb(size=1, price=c.open[c.i, col])
...     if c.call_idx == 1:
...         return col, vbt.pf_nb.close_position_nb(price=c.close[c.i, col])
...     return -1, vbt.pf_enums.NoOrder  # (2)

>>> data = vbt.YFData.pull("SPY", start="2024-01-01", end="2024-02-01")
>>> gap_down = data.open < data.close.shift(1) * 0.995
>>> pf = vbt.PF.from_order_func(
...     data,
...     flex_order_func_nb=open_to_close_nb,
...     flex_order_args=(vbt.Rep("gap_down"),),
...     broadcast_named_args=dict(gap_down=gap_down),
...     max_order_records=len(data.index) * 2,  # (3)
... )
>>> pf.orders.readable[["Index", "Side", "Price"]]
                      Index  Side       Price
0 2024-01-09 00:00:00-05:00   Buy  456.917453
1 2024-01-09 00:00:00-05:00  Sell  458.863770
2 2024-01-17 00:00:00-05:00   Buy  456.869019
3 2024-01-17 00:00:00-05:00  Sell  457.324127
```

1.  The function is called repeatedly within a bar, and `c.call_idx` counts the calls.
2.  Returning column `-1` ends the bar.
3.  Reserve room for two orders per bar.

Order functions come with a full set of lifecycle hooks. `pre_sim_func_nb` and `post_sim_func_nb`
wrap the whole run, the group and segment hooks run around each asset group and each bar, and
`post_order_func_nb` sees every execution result. With `row_wise=True`, the simulator finishes one
timestamp across all groups before moving to the next, so callbacks can coordinate groups.
`vbt.pf_nb.stop_group_sim_nb` ends a group early once a strategy has nothing left to do.

!!! warning "Order within a bar"
    A bar is a black box. Only its open and close have a known order in time, so a callback that
    trades on the high and the low of the same bar has to decide which came first.

## Memory and custom outputs \[#memory-and-custom-outputs]

Callbacks share state through arrays you pass in. Named tuples keep several arrays in one argument,
and `in_outputs` goes one step further: arrays created there are available as `c.in_outputs` in
every callback and come back with the portfolio, wrapped with its index and columns. The examples
above use them for the cooldown and the position counter, and the
[In-place outputs](#in-place-outputs) section below records debt on every bar.

You can also record the decisions behind a result: which entries a risk rule blocked, when a
cooldown was active, or what exposure the strategy saw before an order. Keeping those arrays with
the portfolio lets you inspect the rule alongside its trades instead of only seeing the final
return.

Inside compiled callbacks, preallocate arrays and keep an explicit count instead of growing them.
For state with an unknown size, pass a Numba typed list as a callback argument.

## Custom simulators \[#custom-simulators]

When even order functions are too rigid, for example for several account states per asset or many
pending limit orders, write the simulation loop yourself. VBT exposes the same execution primitives
its own simulators use, so the result is a regular portfolio with every metric and chart:

```python title="Write a dip-buying simulator from execution primitives"
>>> @njit
... def dip_buyer_nb(close, init_cash, dip, rebound):
...     order_records = np.empty(close.shape, dtype=vbt.pf_enums.order_dt)
...     order_counts = np.zeros(close.shape[1], dtype=np.int_)
...     for col in range(close.shape[1]):
...         state = vbt.pf_enums.ExecState(  # (1)
...             init_cash, 0.0, 0.0, 0.0, init_cash, np.nan, np.nan
...         )
...         peak = close[0, col]
...         for i in range(close.shape[0]):
...             price = close[i, col]
...             peak = max(peak, price)
...             if state.position == 0 and price <= peak * (1 - dip):
...                 order = vbt.pf_nb.order_nb(size=np.inf, price=price)
...             elif state.position > 0 and price >= peak * (1 - rebound):
...                 order = vbt.pf_nb.close_position_nb(price=price)
...                 peak = price
...             else:
...                 continue
...             _, state = vbt.pf_nb.process_order_nb(  # (2)
...                 col, col, i, state, order,
...                 order_records=order_records, order_counts=order_counts
...             )
...     return vbt.nb.repartition_nb(order_records, order_counts)

>>> data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")
>>> records = dip_buyer_nb(data.close.values[:, None], 100.0, 0.1, 0.02)
>>> pf = vbt.PF(
...     data.symbol_wrapper,
...     close=data.close,
...     order_records=records,
...     init_cash=100.0
... )
>>> pf.orders.readable[["Index", "Side", "Price"]]
                      Index  Side         Price
0 2023-03-07 00:00:00+00:00   Buy  22219.769531
1 2023-03-14 00:00:00+00:00  Sell  24746.074219
2 2023-04-21 00:00:00+00:00   Buy  27276.910156
3 2023-06-21 00:00:00+00:00  Sell  30027.296875
4 2023-08-17 00:00:00+00:00   Buy  26664.550781
5 2023-10-23 00:00:00+00:00  Sell  33086.234375
```

1.  Cash, position, debt, locked cash, free cash, valuation price, and value.
2.  Validates the order against cash and position, fills it, writes the order record, and returns the
    new state.

The simulator buys after a 10% drop from the running peak and sells once the price is back within 2%
of it. The same building blocks support limit orders you manage yourself, multiple orders per bar,
and accounts that share cash across positions.

!!! info "Documentation"
    The members-only [Simulation](https://members.vectorbt.pro/documentation/portfolio/simulation/) guide builds a
    simulator from these primitives step by step.

## Full callback support \[#full-callback-support]

New in v2025.12.31

✅ Portfolio simulation method based on signals now fully supports callbacks at every step of the
process. This includes pre-processing and post-processing callbacks for the simulation as a whole,
as well as per-group and per-segment callbacks, and even an order modification callback. This allows
you to customize the simulation behavior to a great extent.

```python title="DCA in $100 every month until 2x in profit, take out initial investment, and DCA out"
>>> DCAMode = namedtuple("DCAMode", ["In", "Out"])(0, 1)  # (1)

>>> @njit
... def pre_group_func_nb(ctx):  # (2)
...     total_deposited = np.full(1, 0.0)
...     dca_mode = np.full(1, DCAMode.In)
...     return (total_deposited, dca_mode)

>>> @njit
... def pre_segment_func_nb(ctx, total_deposited, dca_mode, cash_deposits, dca_amount):  # (3)
...     if ctx.i == 0 or vbt.dt_nb.month_nb(ctx.index[ctx.i - 1]) != vbt.dt_nb.month_nb(ctx.index[ctx.i]):
...         dca_amount_now = vbt.pf_nb.select_from_group_nb(ctx, ctx.group, dca_amount)
...         if dca_mode[0] == DCAMode.In and ctx.track_cash_deposits:
...             cash_deposits[ctx.i, ctx.group] = dca_amount_now
...             total_deposited[0] += dca_amount_now
...     else:
...         dca_amount_now = 0.0
...     return (total_deposited, dca_mode, dca_amount_now)

>>> @njit
... def signal_func_nb(ctx, total_deposited, dca_mode, dca_amount_now, size):  # (4)
...     if dca_amount_now > 0:
...         size[ctx.i, ctx.col] = dca_amount_now
...         if dca_mode[0] == DCAMode.In:
...             return True, False, False, False
...         return False, True, False, False
...     return False, False, False, False

>>> @njit
... def post_order_func_nb(ctx, total_deposited, dca_mode, dca_amount_now):  # (5)
...     if dca_mode[0] == DCAMode.In:
...         if vbt.pf_nb.order_increased_position_nb(ctx):
...             tp_info = ctx.last_tp_info[ctx.col]
...             tp_info["stop"] = 1.0
...             tp_info["init_price"] = ctx.last_pos_info[ctx.col]["entry_price"]
...             tp_info["exit_size"] = total_deposited[0]
...             tp_info["exit_size_type"] = vbt.pf_enums.SizeType.Value
...         elif vbt.pf_nb.get_last_order_nb(ctx)["stop_type"] == vbt.pf_enums.StopType.TP:
...             dca_mode[0] = DCAMode.Out

>>> pf = vbt.PF.from_signals(
...     vbt.YFData.pull("AAPL", start="2018"),
...     pre_group_func_nb=pre_group_func_nb,
...     pre_segment_func_nb=pre_segment_func_nb,
...     pre_segment_args=(
...         vbt.Rep("cash_deposits"),
...         vbt.Rep("dca_amount")
...     ),
...     signal_func_nb=signal_func_nb,
...     signal_args=(
...         vbt.Rep("size"),
...     ),
...     post_order_func_nb=post_order_func_nb,
...     broadcast_named_args=dict(dca_amount=100),
...     arg_config=dict(
...         cash_deposits=dict(full_shape=True),  # (6)
...         size=dict(full_shape=True)
...     ),
...     accumulate=True,
...     size_type="value",
...     cash_sharing=True
... )
>>> pf.plot_orders().show()
```

1.  Define DCA modes as an enum for better readability.
2.  Pre-group callback to initialize group-level variables: total cash deposited and DCA mode. This
    callback is called before processing each group (asset in this case because grouping is
    disabled). The variables are returned as a tuple to be passed to the pre-segment callback.
3.  Pre-segment callback to handle DCA deposits at the start of each month. This callback is called
    for each group at the beginning of each bar, before generating signals for each asset in the
    group. The variables that are required by either the signal or post-order callbacks are returned
    as a tuple.
4.  Signal callback to generate buy or sell signals based on the DCA amount for the current segment.
    If in DCA-in mode, a buy signal is generated. Otherwise, a sell signal is generated. The order
    size is set to the DCA amount.
5.  Post-order callback to handle take-profit orders and switch DCA modes. If a buy order was
    executed, our take-profit order is updated to exit the initial investment when the position
    doubles in price. If the take-profit order was executed, the DCA mode is switched to DCA-out.
6.  Specify that cash deposits and size should have the full shape of the portfolio (number of bars x
    number of assets) such that they can be modified in the callbacks.

AAPL OHLC with monthly DCA orders that recover the initial investment and then DCA out. [Figure data (JSON)](/assets/figures/features/portfolio/dca-callbacks.fd5b757bbe5a.json)

!!! info "Documentation"
    The members-only [callbacks guide](https://members.vectorbt.pro/documentation/portfolio/from-signals/#callbacks)
    covers every callback that `from_signals` accepts.

## In-place outputs \[#in-place-outputs]

New in 1.0.0

✅ The portfolio can now accept and return any user-defined arrays filled during simulation, such as
signals. In-place output arrays can broadcast together with regular arrays using templates and
broadcastable named arguments. Additionally, VBT will (semi-)automatically determine how to
correctly wrap and index each array, for example, whenever you select a column from the entire
portfolio.

```python title="Track the debt of a random portfolio during the simulation"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
>>> size = data.symbol_wrapper.fill(np.nan)
>>> np.random.seed(42)
>>> rand_indices = np.random.choice(np.arange(len(size)), 10)
>>> size.iloc[rand_indices[0::2]] = -np.inf
>>> size.iloc[rand_indices[1::2]] = np.inf

>>> @njit
... def post_segment_func_nb(ctx):
...     for col in range(ctx.from_col, ctx.to_col):
...         col_debt = ctx.last_debt[col]
...         ctx.in_outputs.debt[ctx.i, col] = col_debt
...         if col_debt > ctx.in_outputs.max_debt[col]:
...             ctx.in_outputs.max_debt[col] = col_debt

>>> pf = vbt.PF.from_def_order_func(
...     data.close,
...     size=size,
...     post_segment_func_nb=post_segment_func_nb,
...     in_outputs=dict(
...         debt=vbt.RepEval("np.empty_like(close)"),
...         max_debt=vbt.RepEval("np.full(close.shape[1], 0.)")
...     )  # (1)
... )
>>> pf.get_in_output("debt")  # (2)
symbol                       BTC-USD    ETH-USD
Date
2017-11-09 00:00:00+00:00   0.000000   0.000000
2017-11-10 00:00:00+00:00   0.000000   0.000000
2017-11-11 00:00:00+00:00   0.000000   0.000000
2017-11-12 00:00:00+00:00   0.000000   0.000000
2017-11-13 00:00:00+00:00   0.000000   0.000000
...                              ...        ...
2023-02-08 00:00:00+00:00  43.746892  25.054571
2023-02-09 00:00:00+00:00  43.746892  25.054571
2023-02-10 00:00:00+00:00  43.746892  25.054571
2023-02-11 00:00:00+00:00  43.746892  25.054571
2023-02-12 00:00:00+00:00  43.746892  25.054571

[1922 rows x 2 columns]

>>> pf.get_in_output("max_debt")  # (3)
symbol
BTC-USD    75.890464
ETH-USD    25.926328
Name: max_debt, dtype: float64
```

1.  Instruct the portfolio class to wait until all arrays are broadcast and create a new floating
    array of the final shape.
2.  The portfolio instance knows how to properly wrap a custom NumPy array as a pandas object.
3.  The same applies to reduced NumPy arrays.

## 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)


## Related pages

*   [Backtesting engine](/features/backtesting/backtesting-engine/): Simulate orders, signals, and callbacks across many assets and parameters at once
*   [Signal backtesting](/features/backtesting/signal-backtesting/): Turn entry and exit signals into long, short, reversing, and pyramided positions
*   [Orders and execution](/features/backtesting/orders-and-execution/): Simulate limit and stop-entry orders, fill prices, timing, rejections, and real fills
*   [Live simulation](/features/backtesting/live-simulation/): Continue backtests on new bars, chain runs, and feed an external trading system