Features

Orders and execution

Simulate fills, order types, leverage, prices, delays, and execution behavior

Portfolio from fills

Recently added

✅ Turn your execution records into a VBT portfolio and explore trades, drawdowns, and performance with the same tools you use for backtests. Missing record fields receive defaults, and readable side names are mapped automatically, keeping the setup light.

Calculate portfolio value and profit from two fills
close = pd.Series(
    [100.0, 105.0, 110.0],
    index=pd.date_range("2026-09-01", periods=3, freq="D")
)
fills = [
    dict(idx=0, size=2, price=100, fees=1, side="Buy"),
    dict(idx=2, size=2, price=110, fees=1, side="Sell"),
]
pf = vbt.PF(close, order_records=fills, init_cash=1000, freq="1D")

print(pf.total_profit)  
18.0
pf.value.tolist()  
[999.0, 1009.0, 1018.0]

Contract multiplier

✅ All simulation entry points now accept a multiplier that scales every order by a constant factor, making it straightforward to model futures contracts and other derivatives where one contract represents multiple units of the underlying asset.

SMA crossover on E-mini S&P 500 futures
data = vbt.YFData.pull("ES=F", start="2023", end="2024")  

fast_sma = data.run("talib_func:sma", timeperiod=10)  
slow_sma = data.run("talib_func:sma", timeperiod=30)
entries = fast_sma.vbt.crossed_above(slow_sma)
exits = fast_sma.vbt.crossed_below(slow_sma)

pf_stock = vbt.PF.from_signals(  
    data,
    entries=entries,
    exits=exits,
    size=1,
    init_cash=500_000,
)

pf_futures = vbt.PF.from_signals(  
    data,
    entries=entries,
    exits=exits,
    size=1,
    multiplier=50,
    init_cash=500_000,
)

print(pf_stock.total_profit)
627.25
print(pf_futures.total_profit)  
31362.5
print(pf_futures.trades.readable[["Avg Entry Price", "Avg Exit Price", "PnL", "Return"]])
   Avg Entry Price  Avg Exit Price      PnL    Return
0          4057.50         4138.00   4025.0  0.019840
1          4212.00         4480.75  13437.5  0.063806
2          4490.25         4378.75  -5575.0 -0.024832
3          4430.50         4820.00  19475.0  0.087913

Negative price

✅ The simulation engine previously rejected negative prices at validation time, making it impossible to model instruments whose price can legally go below zero (such as the infamous April 2020 WTI crude oil event). Negative prices are now fully supported across the entire pipeline.

Long WTI crude oil through the April 2020
data = vbt.YFData.pull("CL=F", start="2020-02-01", end="2020-07-01")  

entries = pd.Series(False, index=data.index)  
exits = pd.Series(False, index=data.index)
entries["2020-03-03"] = True
exits["2020-05-04"] = True

pf = vbt.PF.from_signals(  
    data,
    entries=entries,
    exits=exits,
    size=1,
    multiplier=1000,
    init_cash=200_000,
)

print(data.close[data.close < 0])  
Date
2020-04-20 00:00:00-04:00   -37.630001
Name: Close, dtype: float64
print(pf.trades.readable[["Avg Entry Price", "Avg Exit Price", "PnL", "Return"]])
   Avg Entry Price  Avg Exit Price           PnL    Return
0            47.18       20.389999 -26790.000916 -0.567825
print(pf.value[["2020-03-03", "2020-03-09", "2020-04-20", "2020-05-04"]].round(2))  
Date
2020-03-03 00:00:00-05:00    200000.0
2020-03-09 00:00:00-04:00    183950.0
2020-04-20 00:00:00-04:00    115190.0
2020-05-04 00:00:00-04:00    173210.0
Name: value, dtype: float64

Target price

✅ Limit and stop orders can now be defined using a target price instead of a delta.

Set the SL to the previous low in a random portfolio
data = vbt.YFData.pull("BTC-USD")
pf = vbt.PF.from_random_signals(
    data,
    n=100,
    seed=42,
    sl_stop=data.low.vbt.ago(1),
    delta_format="target"
)
sl_orders = pf.orders.stop_type_sl
signal_index = pf.wrapper.index[sl_orders.signal_idx.values]
hit_index = pf.wrapper.index[sl_orders.idx.values]
hit_after = hit_index - signal_index
hit_after
TimedeltaIndex([ '7 days',  '3 days',  '1 days',  '5 days',  '4 days',
                 '1 days', '28 days',  '1 days',  '1 days',  '1 days',
                 '1 days',  '1 days', '13 days', '10 days',  '5 days',
                 '1 days',  '3 days',  '4 days',  '1 days',  '9 days',
                 '5 days',  '1 days',  '1 days',  '2 days',  '1 days',
                 '1 days',  '1 days',  '3 days',  '1 days',  '1 days',
                 '1 days',  '1 days',  '1 days',  '2 days',  '2 days',
                 '1 days', '12 days',  '3 days',  '1 days',  '1 days',
                 '1 days',  '1 days',  '1 days',  '3 days',  '1 days',
                 '1 days',  '1 days',  '4 days',  '1 days',  '1 days',
                 '2 days',  '6 days', '11 days',  '1 days',  '2 days',
                 '1 days',  '1 days',  '1 days',  '1 days',  '1 days',
                 '4 days', '10 days',  '1 days',  '1 days',  '1 days',
                 '2 days',  '3 days',  '1 days'],
               dtype='timedelta64[ns]', name='Date', freq=None)

Leverage

✅ Leverage is now an integral part of portfolio simulation. Supports two leverage modes: lazy (enables leverage only if there is not enough cash) and eager (enables leverage while using only part of the available cash). Allows setting leverage per order, and can also determine the optimal leverage value automatically to fulfill any order requirement! 🏋️

Explore how leverage affects the equity curve in a random portfolio
data = vbt.YFData.pull("BTC-USD", start="2020", end="2022")
pf = vbt.PF.from_random_signals(
    data,
    n=100,
    seed=42,
    leverage=vbt.Param([0.5, 1, 2, 3]),
)
pf.value.vbt.plot().show()
BTC-USD random portfolio equity curves at four leverage levels from 2020 through 2021 Figure data (JSON)

Order delays

✅ By default, VBT executes every order at the end of the current bar. Previously, if you wanted to delay execution to the next bar, you had to manually shift all order-related arrays by one bar, which made the process error-prone. Now, you can simply specify how many bars in the past should be used to take order information from. In addition, the price argument now supports "nextopen" and "nextclose" as options, providing a one-line solution.

Compare orders without and with a delay in a random portfolio
pf = vbt.PF.from_random_signals(
    vbt.YFData.pull("BTC-USD", start="2021-01", end="2021-02"),
    n=3,
    seed=42,
    price=vbt.Param(["close", "nextopen"])
)
fig = pf.orders["close"].plot(
    buy_trace_kwargs=dict(name="Buy (close)", marker=dict(symbol="triangle-up-open")),
    sell_trace_kwargs=dict(name="Buy (close)", marker=dict(symbol="triangle-down-open"))
)
pf.orders["nextopen"].plot(
    plot_ohlc=False,
    plot_close=False,
    buy_trace_kwargs=dict(name="Buy (nextopen)"),
    sell_trace_kwargs=dict(name="Sell (nextopen)"),
    fig=fig
)
fig.show()
BTC-USD OHLC with random orders executed at the close or delayed until the next open Figure data (JSON)

Limit orders

✅ Long-awaited support for limit orders is now available for signal-based simulation! Includes time-in-force (TIF) orders such as DAY, GTC, GTD, LOO, and FOK ⏰ You can also reverse a limit order or create it using a delta for easier testing.

Explore how limit delta affects number of orders in a random portfolio
pf = vbt.PF.from_random_signals(
    vbt.YFData.pull("BTC-USD"),
    n=100,
    seed=42,
    order_type="limit",
    limit_delta=vbt.Param(np.arange(0.001, 0.1, 0.001)),  
)
pf.orders.count().vbt.plot(
    xaxis_title="Limit delta",
    yaxis_title="Order count"
).show()
Order count across limit-order delta values in a seeded random BTC-USD portfolio Figure data (JSON)

Delta formats

✅ Previously, stop orders could only be provided as percentages. While this worked for single values, it often required extra transformations for arrays. For example, setting SL to ATR meant you needed to know the entry price. More generally, to lock in a specific dollar amount of a trade, you might want to use a fixed price trailing stop. To address this, VBT now offers multiple stop value formats ("delta formats") to choose from.

Use ATR as SL
data = vbt.YFData.pull("BTC-USD")
atr = vbt.talib("ATR").run(data.high, data.low, data.close).real
pf = vbt.PF.from_holding(
    data.loc["2022-01-01":"2022-01-07"],
    sl_stop=atr.loc["2022-01-01":"2022-01-07"],
    delta_format="absolute"
)
pf.orders.plot().show()
BTC-USD OHLC with a holding trade stopped using ATR as an absolute stop-loss Figure data (JSON)

Bar skipping

✅ Simulation based on orders and signals can now (partially) skip bars that do not define any orders, often resulting in significant speedups for strategies with sparsely distributed orders.

Benchmark bar skipping in a buy-and-hold portfolio
data = vbt.BinanceData.pull("BTCUSDT", start="one month ago UTC", timeframe="minute")
size = data.symbol_wrapper.fill(np.nan)
size[0] = np.inf
%%timeit
vbt.PF.from_orders(data, size, ffill_val_price=True)  
5.92 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
vbt.PF.from_orders(data, size, ffill_val_price=False)
2.75 ms ± 16 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

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.