Features
Realistic backtests
Avoid look-ahead bias, impossible fills, and survivorship bias in your backtests
Why does a backtest look better than live trading? An order may have filled before its signal existed, a daily value arrived before the day ended, or trading costs ate the profit. VBT lets you control order timing, align data by when it became available, model costs, and inspect individual fills. Here's how to use those tools to avoid look-ahead bias and build more realistic backtests in Python.
data = vbt.BinanceData.pull(
"BTCUSDT", start="2020-01-01", end="2025-01-01", timeframe="1h"
)
dip = data.close < data.close.vbt.rolling_mean(24)
limit_price = data.close * 0.99
exits = dict(
tp_stop=0.02,
td_stop=24,
time_delta_format="rows",
stop_entry_price="fillprice",
fees=0.001,
)
same_bar = vbt.PF.from_signals(
data, entries=dip & (data.low <= limit_price), price=limit_price, **exits
)
next_bar = vbt.PF.from_signals(
data, entries=dip, price=limit_price, order_type="limit", limit_tif=2, **exits
)
metrics = ["total_trades", "win_rate", "total_return", "sharpe_ratio"]
stats = {"same bar": same_bar.stats(metrics), "next bar": next_bar.stats(metrics)}
pd.DataFrame(stats).astype(float).round(2) same bar next bar
Total Trades 802.00 663.00
Win Rate [%] 83.29 66.37
Total Return [%] 87489.57 -31.97
Sharpe Ratio 3.88 0.01The same-bar version always buys the dip that already happened and then sells the rebound, so its trades reach the target sooner and it trades more often. It shows an 87,000% gain where the honest version loses 32%. The gap is a reason to inspect the order timing before spending time on a parameter search.
Causal order timing
A decision can use only what was known when it was made, and an order can fill only then or later.
In vbt.PF.from_signals, the price argument says where in the bar each order executes:
| Setting | Signal uses | Order fills |
|---|---|---|
price="close" (default) | The bar's close | At that close |
price="nextopen" | The bar's close | At the next bar's open |
from_ago=1 | The previous bar | At this bar's price |
order_type="limit" with price="close" | The bar's close | From the next bar, when the limit is reached |
Limit orders follow the same rule. Placed at the open, they can fill anywhere in the bar. Placed at
the close, they wait for the next bar. Placed in between, they can only check the close. Stops are
checked against each later bar's high and low, and limit_delay or order delays push fills further
out. See Orders and execution for every option,
Order delays for latency, and
Realistic stop fills for
what happens when both a stop and a target fall inside one bar.
Stop fills and data resolution
The high and the low have to be there to be checked. Passing only a close series, instead of the
data object or open, high, and low, makes every stop wait for a close beyond its level:
btc = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
entries = btc.close.vbt.crossed_above(btc.close.vbt.rolling_mean(20))
pfs = {
"close only": vbt.PF.from_signals(btc.close, entries, tsl_stop=0.03),
"OHLC": vbt.PF.from_signals(btc, entries, tsl_stop=0.03),
}
stop_metrics = ["total_trades", "win_rate", "total_return", "max_dd"]
pd.DataFrame({k: pf.stats(stop_metrics) for k, pf in pfs.items()}).astype(float).round(2) close only OHLC
Total Trades 90.00 96.00
Win Rate [%] 38.89 41.67
Total Return [%] 530.06 213.53
Max Drawdown [%] 33.67 21.40On closes alone, a trade survives every intraday dip that ends the day above the stop, so it rides more rebounds and the return more than doubles. When the stop finally fires, the fill is the close, below the level, which deepens the drawdown. Neither number is what a stop order at the exchange would have done.
Higher-timeframe alignment
Data from a longer timeframe has the same trap. A daily value is known only after the day closes, but aligning it to hourly bars with a plain forward fill hands each hour the close of its own day:
daily_close = data.close.resample("1D").last()
daily_trend = daily_close > daily_close.vbt.rolling_mean(20)
naive_trend = daily_trend.reindex(data.index, method="ffill")
safe_trend = daily_trend.vbt.realign_closing(data.index, freq="1h")
safe_trend = safe_trend.fillna(False).astype(bool)
pfs = {
"naive": vbt.PF.from_signals(data, naive_trend, ~naive_trend, fees=0.001),
"safe": vbt.PF.from_signals(data, safe_trend, ~safe_trend, fees=0.001),
}
pd.DataFrame({k: pf.stats(metrics) for k, pf in pfs.items()}).astype(float).round(2) naive safe
Total Trades 107.00 107.00
Win Rate [%] 93.46 25.23
Total Return [%] 854502.66 315.56
Sharpe Ratio 4.41 0.87The same 107 trades won 93% of the time with the naive alignment and 25% with the safe one. The Multi-timeframe analysis page covers alignment and resampling in detail.
Point-in-time universes
A strategy tested only on stocks that are listed today never holds the companies that failed or left the index. To avoid this survivorship bias, keep every symbol that was ever part of the universe, and mask each one with NaN outside the dates it belonged, so the backtest sees the universe that existed on each date. VBT ignores signals on bars without a price, including exits, so close positions on the last bar a symbol belongs to. The Data pipelines page shows how to build the membership mask.
Trading costs and available capital
A strategy can use the right prices and still spend money it would not have. VBT applies fees and slippage when processing orders, so costs affect how much the portfolio can buy as well as its final return. Use percentage commissions, fixed fees, and slippage to test whether the strategy still works after trading costs. The Fees, slippage, and leverage page covers these settings and borrowing costs.
When several assets compete for the same money, use a shared cash balance. Size limits, whole-share or lot rounding, and partial-fill settings let you model orders that fit the account. See Fills, limits, and rejections for an example of orders being capped, partially filled, and ignored.
Backtest-to-live parity
When live results differ from the backtest, compare them step by step. With the same inputs, settings, and random seed where randomness is used, you can reproduce a VBT simulation and find the first difference.
- Data: check that the live bars match the historical ones, including the last, still-forming bar.
- Indicators and signals: recompute them on the live data and compare values and timestamps. Platforms also define indicators differently, for example the smoothing and warm-up of RSI, so compare values bar by bar before comparing trades.
- Orders:
pf.orders.readablelists every order with its signal, creation, and fill bars, and stop type, which pinpoints where two runs diverge. - Costs: fees, slippage, and spreads that the backtest left out, covered on the Fees, slippage, and leverage page.
For orders that reached execution but did not fill as requested, enable log=True and inspect
pf.logs.readable. The logs show the requested order, the execution result, and the account state
before and after it. That helps distinguish an unaffordable order from one reduced by a size limit,
instead of guessing from the equity curve.
The Live simulation page continues a backtest bar by bar on new data, and the Intraday and tick backtesting page tests fills on finer data inside each bar.
When a result looks too good
Look at a few trades on a chart first. For fills at the exact low or high of a bar, check that the order existed before that price was reached. Win rates above 80% or returns that dwarf the market deserve a closer look, but neither alone proves the strategy saw the future.
Related pages
- Orders and executionSimulate limit and stop-entry orders, fill prices, timing, rejections, and real fills
- Intraday and tick backtestingBacktest minute bars, ticks, and sessions, and resolve stops on finer data
- Data › Multi-timeframe analysisResample and realign data across timeframes by when each value became available
- Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
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.