Features
Orders and execution
Simulate fills, order types, leverage, prices, delays, and execution behavior
✅ 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.
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.0pf.value.tolist() [999.0, 1009.0, 1018.0]✅ 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.
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.25print(pf_futures.total_profit) 31362.5print(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✅ 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.
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: float64print(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.567825print(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✅ Limit and stop orders can now be defined using a target price instead of a delta.
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_afterTimedeltaIndex([ '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 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! 🏋️
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()✅ 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.
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()✅ 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.
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()✅ 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.
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()✅ 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.
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 © 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.