# Realistic backtests (/features/backtesting/backtest-realism)

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.

```python title="Fill a limit order on the bar that created it, then on the next bar"
>>> data = vbt.BinanceData.pull(
...     "BTCUSDT", start="2020-01-01", end="2025-01-01", timeframe="1h"
... )
>>> dip = data.close < data.close.vbt.rolling_mean(24)  # (1)
>>> limit_price = data.close * 0.99
>>> exits = dict(
...     tp_stop=0.02,
...     td_stop=24,
...     time_delta_format="rows",
...     stop_entry_price="fillprice",  # (2)
...     fees=0.001,
... )

>>> same_bar = vbt.PF.from_signals(  # (3)
...     data, entries=dip & (data.low <= limit_price), price=limit_price, **exits
... )
>>> next_bar = vbt.PF.from_signals(  # (4)
...     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.01
```

1.  The signal uses each hour's close, so it exists only once that hour has closed. The buy limit
    sits 1% below that close.
2.  Exit with a 2% profit measured from the limit fill, or after 24 hours.
3.  A common mistake: treat the order as filled if the same hour's low touched the limit. That low
    happened before the close that produced the signal.
4.  A limit order created at a price below the close cannot fill on its own bar. `limit_tif=2` keeps
    it for the next hour only, so it gets the same single chance to fill as the naive version.

The 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 \[#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](/features/backtesting/orders-and-execution/) for every option,
[Order delays](/features/backtesting/orders-and-execution/#order-delays) for latency, and
[Realistic stop fills](/features/backtesting/stop-loss-and-take-profit/#realistic-stop-fills) for
what happens when both a stop and a target fall inside one bar.

## Stop fills and data resolution \[#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:

```python title="Test a 3% trailing stop on closes only and on full bars"
>>> 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.40
```

On 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 \[#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:

```python title="Align a daily trend filter to hourly bars, naively and safely"
>>> 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")  # (1)
>>> safe_trend = daily_trend.vbt.realign_closing(data.index, freq="1h")  # (2)
>>> 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.87
```

1.  Daily bars are labeled with the day's start, so each hour of the day gets the trend computed from
    that day's close.
2.  Moves each daily value to the last hourly bar of its day, when the daily close is known.

Equity of an hourly Bitcoin strategy using a daily trend filter aligned naively and safely, on a log scale from 2020 to 2024. [Figure data (JSON)](/assets/figures/features/backtesting/htf-alignment-equity.980edbc3a260.json)

The same 107 trades won 93% of the time with the naive alignment and 25% with the safe one. The
[Multi-timeframe analysis](/features/data/multi-timeframe-analysis/) page covers alignment and
resampling in detail.

## Point-in-time universes \[#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](/features/data/financial-data-pipelines/#universes-and-futures) page shows how to
build the membership mask.

## Trading costs and available capital \[#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](/features/backtesting/fees-slippage-and-leverage/) page covers these
settings and borrowing costs.

When several assets compete for the same money, use a
[shared cash balance](/features/backtesting/portfolio-accounting/#shared-cash). Size limits,
whole-share or lot rounding, and partial-fill settings let you model orders that fit the account.
See
[Fills, limits, and rejections](/features/backtesting/orders-and-execution/#fills-limits-and-rejections)
for an example of orders being capped, partially filled, and ignored.

## Backtest-to-live parity \[#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.readable` lists 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](/features/backtesting/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](/features/backtesting/live-simulation/) page continues a backtest bar by bar
on new data, and the
[Intraday and tick backtesting](/features/backtesting/intraday-and-tick-backtesting/) page tests
fills on finer data inside each bar.

!!! tip "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 execution](/features/backtesting/orders-and-execution/): Simulate limit and stop-entry orders, fill prices, timing, rejections, and real fills
*   [Intraday and tick backtesting](/features/backtesting/intraday-and-tick-backtesting/): Backtest minute bars, ticks, and sessions, and resolve stops on finer data
*   [Multi-timeframe analysis](/features/data/multi-timeframe-analysis/): Resample and realign data across timeframes by when each value became available
*   [Backtesting engine](/features/backtesting/backtesting-engine/): Simulate orders, signals, and callbacks across many assets and parameters at once