# 事件驱动回测 (/zh/features/backtesting/event-driven-backtesting)

某些交易规则依赖投资组合中已发生的事件：止损后冷却、未平仓数量上限、每日亏损限制。VBT 通过在模拟循环内部运行的编译回调，支持 Python 事件驱动回测。回调逐根查看现金、持仓和成交，同时保持
Numba 速度。

```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.  对每根 K 线和每列调用，返回多头入场、多头离场、空头入场、空头离场四个标志。离场始终放行，因此冷却不会困住已有持仓。
2.  每个已处理订单后调用。只有订单平掉持仓，且记录表明由止损导致时，才启动冷却。
3.  一次调用运行无冷却和有冷却规则。各变体有独立列及自己的 `blocked_until` 槽位。

5% 止损在 2022 年 3 月 4 日触发。无冷却时，趋势过滤器 3 月 9 日再次买入，次日又被止损；启用后，下次入场等到 3 月 16 日。样本中，冷却减少 10
笔交易并降低回撤。关键不在结果，而在于规则需要模拟自身输出，因此不能预先算成信号数组。

## 选择定制程度 \[#choose-how-much-to-customize]

既可给现有信号策略添加规则，也可控制订单生成。选择符合策略需求的层级：

| 您想做什么               | 使用                                |
| ------------------- | --------------------------------- |
| 保留信号数组，随账户变化调整数量或止损 | `from_signals` 的调整回调              |
| 根据当前持仓、成交或损益决定入场与离场 | `from_signals` 的信号回调              |
| 直接选择每笔订单，包括同根多个订单   | `from_order_func` 的订单函数，多订单使用灵活模式 |
| 自行管理账户状态和模拟循环       | 用 VBT 执行原语构建自定义模拟器                |

信号回调保留信号模拟器的数量、止损、限价与冲突处理。订单函数直接控制订单。两者都生成可用相同交易记录、度量与图表检查的投资组合。

这些回调可在一次调用中处理完整历史回测。新 K 线到达时延续模拟，参见[实时模拟](/zh/features/backtesting/live-simulation/)。

## 能查看投资组合的回调 \[#callbacks-that-see-the-portfolio]

`vbt.PF.from_signals` 接受 `signal_func_nb` 信号函数，替代四个信号数组。它接收包含当前 K 线 `c.i`、列 `c.col` 和实时模拟状态的上下文
`c`，返回本根四个信号。数量、止损、手续费等其他处理照常运行。

上下文公开常用状态：

| 状态                  | 上下文字段或辅助函数                                                               |
| ------------------- | ------------------------------------------------------------------------ |
| 持仓、现金和债务            | `c.last_position`、`c.last_cash`、`c.last_debt`                            |
| 投资组合净值              | `c.last_value`、`vbt.pf_nb.get_group_value_nb(c, c.group)`                |
| 未平仓头寸的入场根、价格、损益和收益率 | `c.last_pos_info[c.col]`                                                 |
| 待触发止损与限价单           | `c.last_sl_info`、`c.last_tsl_info`、`c.last_tp_info`、`c.last_limit_info`  |
| 自定义数组在当前根的值         | `vbt.pf_nb.select_nb(c, array)`                                          |
| 上一订单的操作             | `vbt.pf_nb.order_closed_position_nb(c)`、`vbt.pf_nb.get_last_order_nb(c)` |

模拟前计算指标，在回调内选取当前值，便于跨 K
线复用。如果指标本身需要在模拟期间更新，可用[流式指标](/zh/features/indicators/streaming-indicators/)。对于日历规则，`c.index[c.i]`
保存本根纳秒时间戳。

模拟器调用信号函数前按本根开盘估值，因此 `c.last_value` 和 `c.last_pos_info` 中的损益反映开盘而非收盘。若要按收盘判断持仓，可通过
`vbt.pf_nb.select_nb(c, c.close)` 计算。两个字段都会由模拟器重算，向它们写入不会改变结果。要改变结果，应返回不同信号，或修改待触发止损与限价记录。

!!! warning "回调中的时机"
    信号函数为调用它的 K 线决策。若要在本根开盘根据上一根数据操作，应在 `c.i - 1` 读取输入，并传入 `price="open"`。使用 `price="nextopen"`
    时，模拟器将返回信号标为上一根，并在当前开盘成交，因此读取当前收盘的回调会在该收盘存在前交易。

!!! tip "调试回调"
    编译回调的错误表现为 Numba 类型错误。常见原因包括函数没有 `@njit`、某分支未返回四个标志，以及 `signal_args` 不是元组，例如 `(0.1)` 而非
    `(0.1,)`。逐步检查逻辑时，移除 `@njit` 并传入 `jitted=False`：模拟器改用 Python 运行，可使用 `print` 和断点。

!!! info "信号函数或调整函数"
    `adjust_func_nb` 在内置信号函数前运行，信号仍来自数组，同时修改止损或数量。传入自定义 `signal_func_nb`
    后，调整函数不再自动调用，因此应从自己的信号函数内部调用调整逻辑。

## 有状态交易规则 \[#stateful-trading-rules]

多数有状态规则遵循相同模式：保留少量状态，在信号函数中读取，在订单成交后更新。这里将共享现金的 20 资产池限制为最多三个同时持仓：

```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.  计数本根此前批准的入场。订单在本根所有列信号函数运行后才执行，因此只统计未平仓头寸会遗漏同根入场。
2.  将全部 20 个资产放入同一组，让回调看到所有持仓。

无上限时，相同信号最多同时持有七个头寸；没有同根计数器时，上限会失效并达到五个。可按此方式构建的规则包括：

| 规则            | 读取的状态                        |
| ------------- | ---------------------------- |
| 止损、亏损或任意离场后冷却 | 上一订单及止损类型、已关闭持仓损益            |
| 最大同时持仓数量      | 组内未平仓头寸加本根已批准入场              |
| 最小持有期         | `c.last_pos_info` 中未平仓头寸的入场根 |
| 每日交易或亏损上限     | 每日首根重置的计数器和参考值               |
| 净值曲线过滤器       | 自定义数组保存的投资组合净值历史             |
| 投资组合级止损       | 组净值与阈值比较，再让所有列离场             |

信号函数和 `post_segment_func_nb` 每根运行。`post_order_func_nb`
仅在模拟器尝试执行订单时运行，包括拒单；信号被忽略或只创建限价挂单时不运行。每日计数或结算延迟等需在无交易 K 线上推进的记账逻辑，应放入 segment 回调；也可传入
`skip_empty=False`，让订单回调也逐根运行。

!!! info "教程"
    会员专享的[从 Python 到 Rust（英文）](https://members.vectorbt.pro/tutorials/from-python-to-rust/dynamic/)教程在四资产组合中构建止损冷却，并迁移到
    Numba 内核与原生 Rust。

## 订单函数 \[#order-functions]

信号回调仍产生信号，信号模拟器每列每根最多下一个订单。`vbt.PF.from_order_func` 去掉这一层，让回调直接返回订单。灵活订单函数还能选择列，并在同根返回多个订单。这里在向下跳空超过
0.5% 后开盘买入，当天收盘卖出：

```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.  同根内反复调用函数，`c.call_idx` 记录调用次数。
2.  返回列 `-1` 结束本根。
3.  为每根两个订单预留空间。

订单函数配有完整生命周期钩子。`pre_sim_func_nb` 和 `post_sim_func_nb` 包围整个运行，group 与 segment
钩子分别在各资产组与各根前后运行，`post_order_func_nb` 可看到每次执行结果。设置 `row_wise=True`
后，模拟器先处理所有组的同一时间戳，再进入下一时点，因此回调可协调各组。策略已无后续工作时，`vbt.pf_nb.stop_group_sim_nb` 可提前结束一组。

!!! warning "一根 K 线内的先后顺序"
    一根 K 线是黑箱，只有开盘与收盘顺序已知，因此在同根最高和最低价交易的回调，必须自行决定谁先发生。

## 记忆与自定义输出 \[#memory-and-custom-outputs]

回调通过传入数组共享状态。命名元组可将多个数组放入一个参数，`in_outputs` 更进一步：其中创建的数组在每个回调中可通过 `c.in_outputs`
访问，随投资组合返回，并按其索引和列封装。上例用它保存冷却和持仓计数，下方[原地输出](#in-place-outputs)则逐根记录债务。

也可记录结果背后的决策：风险规则阻止了哪些入场、何时处于冷却、或下单前策略看到什么敞口。将这些数组与组合保存在一起，可结合交易检查规则，而非只看到最终收益。

编译回调内应预分配数组并显式计数，不要不断扩容。未知大小的状态，可将 Numba 类型化列表作为回调参数。

## 自定义模拟器 \[#custom-simulators]

订单函数仍过于受限时，例如每个资产需多个账户状态或大量限价挂单，可自行编写模拟循环。VBT 公开自身模拟器使用的执行原语，因此结果仍是具备所有度量和图表的普通投资组合：

```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.  现金、持仓、债务、锁定现金、自由现金、估值价格和净值。
2.  根据现金和持仓验证订单，执行成交，写入订单记录，再返回新状态。

模拟器在价格从动态峰值下跌 10% 后买入，回升至距峰值 2% 以内时卖出。同一组基础组件还支持自行管理的限价单、同根多订单，以及跨持仓共享现金的账户。

!!! info "文档"
    会员专享的[模拟（英文）](https://members.vectorbt.pro/documentation/portfolio/simulation/)指南用这些原语逐步构建模拟器。

## 完整回调支持 \[#full-callback-support]

新增于 v2025.12.31

✅ 基于信号的投资组合模拟现已在各个步骤完整支持回调，包括整个模拟的前后处理、逐组和逐 segment 回调，甚至订单修改回调，可以深度定制模拟行为。

```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.  用枚举定义 DCA 模式，增强可读性。
2.  pre-group 回调初始化组级变量：累计入金和 DCA 模式。每组处理前调用；本例未分组，因此每个资产一组。变量以元组返回，传给 pre-segment 回调。
3.  pre-segment 回调在每月初处理定投存款。每组每根开始时、为组内各资产生成信号之前调用。信号或 post-order 回调需要的变量以元组返回。
4.  信号回调根据当前 segment 的定投金额生成买卖信号。DCA-in 模式生成买入，否则生成卖出。订单数量设为定投金额。
5.  post-order 回调处理止盈单并切换 DCA 模式。买单执行后，更新止盈单，使价格翻倍时收回初始投入；止盈执行后，切换至 DCA-out。
6.  指定 cash deposits 和 size 使用完整组合形状（K 线数 × 资产数），以便在回调中修改。

AAPL OHLC 与月度定投订单，收回初始投资后改为定期卖出. [Figure data (JSON)](/assets/figures/features/portfolio/dca-callbacks.fd5b757bbe5a.json)

!!! info "文档"
    会员专享的[回调指南（英文）](https://members.vectorbt.pro/documentation/portfolio/from-signals/#callbacks)介绍 `from_signals`
    接受的全部回调。

## 原地输出 \[#in-place-outputs]

新增于 1.0.0

✅ 投资组合现在可接受并返回在模拟期间填充的任意用户数组，例如信号。借助模板和可广播命名参数，原地输出数组可与普通数组一起广播。此外，VBT
会半自动确定各数组正确的封装与索引方式，例如从整个组合中选择一列时。

```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.  告诉投资组合类，等全部数组广播后，再创建最终形状的新浮点数组。
2.  投资组合实例知道如何正确将自定义 NumPy 数组封装为 pandas 对象。
3.  归约后的 NumPy 数组同样适用。

## 信号回调 \[#signal-callbacks]

新增于 1.4.0

✅
想定制信号模拟，甚至根据当前回测环境动态生成信号？两个新回调带来更强灵活性：一个逐根逐资产生成或覆盖信号，另一个在每根结束时为整个组计算自定义度量。两者都接受包含当前模拟状态的“上下文”，使交易决策可类似事件驱动回测器那样进行。

```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 迭代式 50 日与 200 日 SMA 交叉及买卖订单. [Figure data (JSON)](/assets/figures/features/portfolio/signal-callbacks.c33e37edbfeb.json)
