# Orders and execution (/features/backtesting/orders-and-execution)

## Portfolio from fills \[#portfolio-from-fills]

New in v2026.9.5

(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.

```python title="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)  # (1)
18.0
>>> pf.value.tolist()  # (2)
[999.0, 1009.0, 1018.0]
```

1.  The $20 price gain is reduced to $18 by the two $1 fees.
2.  Value includes unrealized profit between the fills.

## Contract multiplier \[#contract-multiplier]

New in v2026.4.7

✅ 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.

=== "Example 1: E-mini S&amp;P 500 futures"
    ```python title="SMA crossover on E-mini S&P 500 futures"
    >>> data = vbt.YFData.pull("ES=F", start="2023", end="2024")  # (1)

    >>> fast_sma = data.run("talib_func:sma", timeperiod=10)  # (2)
    >>> 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(  # (3)
    ...     data,
    ...     entries=entries,
    ...     exits=exits,
    ...     size=1,
    ...     init_cash=500_000,
    ... )

    >>> pf_futures = vbt.PF.from_signals(  # (4)
    ...     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)  # (5)
    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
    ```

    1.  Pull daily E-mini S\&P 500 futures data for 2023 from Yahoo Finance.
    2.  Generate long entry/exit signals from a 10/30 SMA crossover. The same logic works for
        any instrument; only the multiplier changes the dollar impact.
    3.  Simulate without a multiplier, as if trading a single share of a stock tracking the index.
        Each point move is worth exactly $1.
    4.  Add a multiplier to reflect the actual E-mini S\&P 500 contract spec, where
        one contract controls 50 times the index value, so each point move is worth $50.
    5.  Profit scales exactly 50x relative to the stock-like simulation.

=== "Example 2: Mixed futures portfolio"
    ```python title="SMA crossover on ES (x50) and NQ (x20) futures simultaneously"
    >>> data = vbt.YFData.pull(  # (1)
    ...     ["ES=F", "NQ=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 = vbt.PF.from_signals(  # (2)
    ...     data,
    ...     entries=entries,
    ...     exits=exits,
    ...     size=1,
    ...     multiplier=[50, 20],
    ...     init_cash=1_000_000,
    ... )

    >>> print(pf.total_profit)  # (3)
    symbol
    ES=F    31362.5
    NQ=F    55995.0
    dtype: float64
    ```

    1.  Pull daily data for E-mini S\&P 500 (ES, x50) and Micro E-mini Nasdaq-100 (NQ, x20) futures.
    2.  Pass multiplier as a list to assign the correct contract spec to each symbol.
        Signals and strategy logic are identical; only the dollar value of a point differs.
    3.  Each column's PnL reflects its own multiplier, letting you compare instruments on an
        equal-notional footing without any manual scaling.

## Negative price \[#negative-price]

New in v2026.4.7

✅ 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.

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

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

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

>>> print(data.close[data.close < 0])  # (4)
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))  # (5)
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
```

1.  Pull daily WTI crude oil futures.
2.  Enter long on March 3, which is the first trading day after the OPEC+ supply-cut talks collapsed,
    and exit on May 4 once prices begin recovering.
3.  One WTI contract controls 1,000 barrels.
4.  Confirm the negative settlement is in the data.
5.  Portfolio value drops significantly on the day of the negative settlement, but recovers as the price rebounds.

## Target price \[#target-price]

New in 1.10.0

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

```python title="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]

New in 1.9.0

✅ 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! 🏋️

```python title="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)](/assets/figures/features/portfolio/leverage.560bae250dca.json)

## Order delays \[#order-delays]

New in 1.4.1

✅ 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.

```python title="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)](/assets/figures/features/portfolio/order-delays.7247ed64175b.json)

## Limit orders \[#limit-orders]

New in 1.4.0

✅ 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.

```python title="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)),  # (1)
... )
>>> pf.orders.count().vbt.plot(
...     xaxis_title="Limit delta",
...     yaxis_title="Order count"
... ).show()
```

1.  Limit delta is the distance between the close (or any specified price) and the target limit price,
    expressed as a percentage. The higher the delta, the lower the chance it will eventually be hit.

Order count across limit-order delta values in a seeded random BTC-USD portfolio. [Figure data (JSON)](/assets/figures/features/portfolio/limit-delta.7987726a54d4.json)

## Delta formats \[#delta-formats]

New in 1.4.0

✅ 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.

```python title="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)](/assets/figures/features/portfolio/delta-formats.fdbb55d4608e.json)

## Bar skipping \[#bar-skipping]

New in 1.4.0

✅ 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.

```python title="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
```

```python
>>> %%timeit
>>> vbt.PF.from_orders(data, size, ffill_val_price=True)  # (1)
5.92 ms ± 300 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

1.  When enabled (default), this argument forces the simulation to process every bar.

```python
>>> %%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)
```
