Features

Live simulation

Continue backtests on new bars, chain runs, and feed an external trading system

Can a backtest keep running as new bars arrive, without recomputing its history? VBT continues a portfolio from its last state: cash, positions, pending stops, and limit orders carry over, so each new bar advances the simulation from where it stopped. You can replay historical data, then keep the same simulated account running on new data for incremental backtests and paper trading in Python.

Replay the last 35 days of 2023 one bar at a time
data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")

def get_signals(data):
    sma = data.close.rolling(20).mean()
    return data.close.vbt.crossed_above(sma), data.close.vbt.crossed_below(sma)

history = data.iloc[:330]
entries, exits = get_signals(history)
pf = vbt.PF.from_signals(
    history,
    entries,
    exits,
    tsl_stop=0.05,
    attach_preparer=True,  
)

emitted = []
for i in range(330, len(data.index)):
    available = data.iloc[:i + 1]  
    entries, exits = get_signals(available)
    pf = pf.update(  
        available.iloc[-1:],
        entries=entries.iloc[-1:],
        exits=exits.iloc[-1:],
    )
    orders = pf.orders.readable
    emitted.append(orders[orders["Fill Index"] == available.index[-1]])  
pd.concat(emitted)[["Fill Index", "Side", "Price", "Stop Type"]]
                  Fill Index  Side         Price Stop Type
45 2023-12-11 00:00:00+00:00  Sell  42470.239844       TSL
46 2023-12-18 00:00:00+00:00   Buy  42623.539062      None
47 2023-12-26 00:00:00+00:00  Sell  42149.559180       TSL
48 2023-12-27 00:00:00+00:00   Buy  43442.855469      None
49 2023-12-28 00:00:00+00:00  Sell  42627.855469      None
entries, exits = get_signals(data)
full_pf = vbt.PF.from_signals(data, entries, exits, tsl_stop=0.05)  
pf.orders.records.equals(full_pf.orders.records), pf.value.equals(full_pf.value)
(True, True)

The trailing stop that fired on December 11 belonged to a position opened before the replay began. Its peak price was carried through every update. After 35 single-bar updates, the orders and the equity curve match the one-shot backtest exactly.

From research updates to live data

Keep your research moving as new data arrives. Continuation connects historical backtests, incremental analysis, and simulated trading:

What you want to doHow VBT handles it
Update a strategy after another day of pricesExtend the portfolio with pf.update
Replay a historical feed or monitor a simulated accountProcess one new bar or a batch of bars at a time
Retrain a model or change parameters between periodsChain simulations while carrying the account forward
Keep a native simulator running between incoming barsAdvance a Rust stepper and save checkpoints

The sections below cover both Python portfolio updates and native Rust stepping. Your data source and execution system can remain separate from the simulation.

Continuing from the last state

pf.update takes new data and new inputs and continues from the portfolio's last state. That state includes cash, positions, debt, the valuation price, entry prices, pending limit orders, and every stop, including trailing peaks and time-stop counters. By default the result contains the full history. With stack=False it contains only the new segment, which is cheaper when you only need the latest orders.

Continuation also works with custom trading rules. A cooldown after a loss, for example, can span several updates. Keep your callback's memory alongside the portfolio state and pass it into the next update. The Event-driven backtesting page introduces these rules, and the live tutorial shows how to carry their memory forward.

Resume after a restart

The state can also be stored. save_state=True keeps the cash and position series computed during the simulation, and pf.last_state holds the final state that a later run starts from. A portfolio built with attach_preparer=True can be written to disk with pf.save(path) and restored with vbt.PF.load(path) in a later session, ready for the next pf.update. For a complete live pipeline, also retain the indicator state and any custom strategy memory your application owns. The Portfolio continuation highlight below verifies a trailing stop and a time stop across updates.

Chaining and hybrid runs

Some workflows need to stop the simulation on purpose: retrain a model every month, reoptimize weights, or change parameters between periods. Run the backtest in segments, start each segment from the previous one, and stack the segments into one history with vbt.PF.row_stack. The Chaining simulations highlight below builds such a chain, and the same pattern lets a portfolio optimizer re-estimate weights at each rebalance with the account carried forward.

Bar-by-bar stepping

The native Rust engine adds steppers: a simulator object that stays in memory and advances one bar at a time, with strategy and execution state kept between steps. A stepper can be cloned or serialized as a checkpoint and restored later, for example after a restart, and its outputs can be saved as NumPy files that Python loads back into a portfolio.

Tutorial

The members-only From Python to Rust tutorial continues a stateful strategy in Python, in Numba, and with a native Rust stepper, and checks that all three agree.

Live pipelines

A live loop has three stages, and each one keeps its own state: the data, the indicators, and the portfolio. data.update() fetches new bars and, with return_meta=True, reports which rows were appended and which were revised. Indicators either recompute over the window they need or, for streaming indicators, update from their last state. The portfolio continues with pf.update. vbt.wait(timeframe, floor=True) sleeps until the next bar boundary, so the loop runs right after each bar closes:

Structure of a polling loop
while True:
    vbt.wait("1h", floor=True)
    update = data.update(return_meta=True)
    data = update["data"]
    for row in update["appended"]:
        ...  # compute signals for the closed bar, then call pf.update

Connecting to a broker

VBT does not send orders to a broker. It decides and simulates, and your execution layer routes the orders, handles fills, and reports them back. A few rules keep the two consistent:

  • Decide on closed bars. Use only completed bars for signals, so a decision cannot change after it was acted on. Many sources also return the bar that is still forming, so drop it before computing signals.
  • Plan before the fill. With next-open execution, the order to place comes from the signals of the last closed bar. The simulator records it only once the next bar arrives, and a stop or a conflicting signal can still change it.
  • Reconcile. Compare the broker's fills and positions with the simulated ones after each update, and avoid sending an order twice when a bar is revised. Real fills can be analyzed as a portfolio of their own, as shown on the Orders and execution page.

Order records hold only filled orders. Pending stops and limit orders live in the final state, so a broker-side stop can mirror the simulated one:

Read the trailing stop level to place at the broker
data = vbt.YFData.pull("ETH-USD", start="2024-09-01", end="2024-11-13")
sma = data.close.rolling(20).mean()
pf = vbt.PF.from_signals(
    data,
    data.close.vbt.crossed_above(sma),
    data.close.vbt.crossed_below(sma),
    tsl_stop=0.08,
)
state = pf.last_state  
position = float(state.last_position[0])
tsl_info = state.last_tsl_info[0]
position, float(tsl_info["peak_price"])
(0.0333110557990236, 3444.154052734375)
vbt.pf_nb.get_tsl_info_target_price_nb(tsl_info, position)  
3168.621728515625

The position opened on November 6 and is still open after the November 12 close. If the next bar trades through 3168.62, the backtest sells at that level, or at the open if the bar opens below it. Your execution layer can use the same stop level at the broker and reconcile the actual fill with the simulated one.

Portfolio continuation

✅ Pick up right where your simulation left off as new data arrives. VBT carries positions, order IDs, and stop state into each update, including entry timestamps and trailing highs. Your trailing stops remember their peaks, your time stops keep counting, and each update returns a new portfolio with the combined history.

Preserve a trailing stop across two updates
close = pd.Series(
    [100.0, 110.0, 108.0, 98.0, 103.0, 115.0],
    index=pd.date_range("2026-09-01", periods=6, freq="D")
)
entries = pd.Series([True, False, False, False, True, False], index=close.index)
pf_kwargs = dict(size=1, init_cash=1000, tsl_stop=0.05, freq="1D")

pf = vbt.PF.from_signals(
    close.iloc[:2],
    entries=entries.iloc[:2],
    attach_preparer=True,  
    **pf_kwargs,
)
for start in [2, 4]:
    pf = pf.update(  
        close.iloc[start:start + 2],
        entries=entries.iloc[start:start + 2],
    )

print(pf.orders.readable[["Order Id", "Fill Index", "Side", "Price", "Stop Type"]])
   Order Id Fill Index  Side  Price Stop Type
0         0 2026-09-01   Buy  100.0      None
1         1 2026-09-04  Sell   98.0       TSL
2         2 2026-09-05   Buy  103.0      None
full_pf = vbt.PF.from_signals(close, entries=entries, **pf_kwargs)  
pf.orders.records.equals(full_pf.orders.records) and pf.value.equals(full_pf.value)
True

Tutorial

Learn more in the Live simulation tutorial.

Chaining simulations

✅ Every built-in simulation function now returns the final simulation state after processing all data, including the last cash, position, pending order(s), and other relevant information. This state can be passed to the next simulation run, allowing you to continue from where the previous run ended. This enables seamless chaining of simulations across different time periods or datasets.

Simulate a SMA crossover strategy on a monthly basis
data = vbt.BinanceData.pull(  
    "BTCUSDT",
    start="one year ago",
    timeframe="5 minutes",
    cache=True
)

fast_sma = data.run("talib_func:sma", timeperiod=20)  
slow_sma = data.run("talib_func:sma", timeperiod=50)
long_entries = fast_sma.vbt.crossed_above(slow_sma)
short_entries = fast_sma.vbt.crossed_below(slow_sma)

single_pf = vbt.PF.from_signals(  
    data,
    long_entries=long_entries,
    short_entries=short_entries,
)

data_splits = data.split(by="month")  
long_entries_splits = long_entries.vbt.split(by="month", into=None)
short_entries_splits = short_entries.vbt.split(by="month", into=None)

pf_list = []
last_state = None
for i in range(len(data_splits)):  
    pf = vbt.PF.from_signals(
        data_splits.iloc[i],
        long_entries=long_entries_splits.iloc[i],
        short_entries=short_entries_splits.iloc[i],
        last_state=last_state,
    )
    pf_list.append(pf)
    last_state = pf.last_state

stacked_pf = vbt.PF.row_stack(*pf_list, chained=True)  
print(stacked_pf.returns.equals(single_pf.returns))
True

Copyright © 2021–2026 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.