# Fees, slippage, and leverage (/features/backtesting/fees-slippage-and-leverage)

A strategy that looks good before costs can fail after them. VBT lets you backtest with fees and
slippage, fixed and per-share commissions, leverage, and futures contract multipliers, and test
several cost levels in one call to see how much margin your edge really has.

```python title="Test one strategy at four fee levels in one call"
>>> data = vbt.YFData.pull("SPY", start="2015-01-01", end="2025-01-01")
>>> fast = data.close.rolling(10).mean()
>>> slow = data.close.rolling(30).mean()

>>> pf = vbt.PF.from_signals(
...     data,
...     fast.vbt.crossed_above(slow),
...     fast.vbt.crossed_below(slow),
...     fees=vbt.Param([0, 0.0005, 0.001, 0.002]),  # (1)
...     init_cash=10_000,
... )
>>> metrics = ["total_return", "total_fees_paid", "sharpe_ratio", "total_trades"]
>>> pf.stats(metrics, agg_func=None).round(2)
        Total Return [%]  Total Fees Paid  Sharpe Ratio  Total Trades
fees
0.0000             99.33             0.00          0.83            37
0.0005             92.09           476.16          0.78            37
0.0010             85.11           933.33          0.74            37
0.0020             71.91          1793.57          0.66            37

>>> pf.value.vbt.plot().show()
```

1.  0, 5, 10, and 20 basis points of each order's value.

SPY moving average crossover portfolio value from 2015 to 2024 at fees of 0, 5, 10, and 20 basis points. [Figure data (JSON)](/assets/figures/features/backtesting/fees-cost-sensitivity.6e3bd01c30da.json)

With only 37 trades in ten years, 20 basis points per order still take 27 points of total return. A
strategy that trades daily would lose far more, which is why every backtest should include realistic
costs from the start.

## Fees and commissions \[#fees-and-commissions]

`fees` is a fraction of each order's value and `fixed_fees` a flat amount per order. A per-share
commission becomes a fraction by dividing it by the price:

```python title="Compare three commission models on a 100-share order"
>>> close = pd.Series([50.0], index=pd.date_range("2026-01-01", periods=1))
>>> pf = vbt.PF.from_orders(
...     close,
...     size=100,
...     fees=vbt.Param(
...         [0.001, 0.0, 0.005 / 50],  # (1)
...         keys=["0.1% of value", "$1 per order", "$0.005 per share"],
...         level=0,
...     ),
...     fixed_fees=vbt.Param([0.0, 1.0, 0.0], level=0),
...     init_cash=10_000,
... )
>>> pf.orders.fees.to_pd().iloc[0].droplevel("fixed_fees")
fees
0.1% of value       5.0
$1 per order        1.0
$0.005 per share    0.5
Name: 2026-01-01 00:00:00, dtype: float64
```

1.  With real data, pass `0.005 / close` to apply the per-share rate on every bar.

Fees are arrays like every other argument, so they can differ per asset and per bar. Negative fees
model rebates. To charge maker and taker rates, fill the fee array by signal type, for example a
lower fee where the entry is a limit order. When the order type that fills is not known in advance,
as with stops, or when fees depend on recent volume tiers, set the fee in a callback before the
order executes.

## Slippage \[#slippage]

`slippage` moves market-order fills against you by a fraction of the price: buys fill higher and
sells fill lower. In the signal simulator, limit orders use their
[limit fill-price rules](/features/backtesting/orders-and-execution/#pending-limit-and-stop-entry-orders)
instead.

```python title="Apply 10 basis points of slippage"
>>> index = pd.date_range("2026-01-01", periods=2)
>>> pf = vbt.PF.from_signals(
...     pd.Series([100.0, 110.0], index=index),
...     pd.Series([True, False], index=index),
...     pd.Series([False, True], index=index),
...     slippage=vbt.Param([0, 0.001]),
... )
>>> pf.orders.readable[["Column", "Side", "Price"]]
   Column  Side   Price
0   0.000   Buy  100.00
1   0.000  Sell  110.00
2   0.001   Buy  100.10
3   0.001  Sell  109.89
```

Slippage is applied after the simulator has decided that an order fills. A stop at $100 with 10
basis points of slippage triggers when the price reaches $100 and fills at $99.90. Slippage can push
a price outside the bar's range, which `price_area_vio_mode="cap"` prevents.

You can pass slippage as an array, with different estimates for each asset and bar, or update it in
a callback as conditions change. For example, use larger estimates for less liquid assets or
volatile periods, then compare the strategy across several assumptions. If you have bid and ask
data, the
[execution-price example](/features/backtesting/orders-and-execution/#execution-price-and-timing)
shows how to buy at the ask and sell at the bid.

## Leverage and borrowing \[#leverage-and-borrowing]

Leverage decides how much of a position may be funded with borrowed money. It does not change the
size of an order by itself, so a leveraged strategy asks for more than 100% of its equity and allows
the account to borrow the rest:

```python title="Hold 200% of equity in SPY, rebalanced daily"
>>> data = vbt.YFData.pull("SPY", start="2020-01-01", end="2025-01-01")
>>> pf = vbt.PF.from_orders(
...     data,
...     size=2.0,
...     size_type="targetpercent",
...     leverage=vbt.Param([1, 2]),
...     init_cash=10_000,
... )
>>> pd.DataFrame({
...     "shares": pf.assets.iloc[0],
...     "total return": pf.total_return,
...     "max drawdown": pf.max_drawdown,
... }).round(3)
          shares  total return  max drawdown
leverage
1         33.853         0.946        -0.337
2         67.707         1.206        -0.586
```

Without leverage, the account can only buy with its own cash and the 200% target is capped at 100%.
With leverage 2, it holds twice as many shares, rebalanced every day like a leveraged ETF. Total
return rose from 95% to 121%, and the maximum drawdown from 34% to 59%.

The size type decides how much of the borrowing capacity an order asks for. With `leverage=3` and
$1,000 of cash at $100 a share, `size=1.0` with `size_type="percent"` buys 30 shares, all of the
leveraged buying power, while `"valuepercent"` buys 10 shares, 100% of equity.

The default `leverage_mode="lazy"` borrows only once own cash runs out, while `"eager"` applies
leverage to every order and uses only part of the available cash. Fees are paid from own cash. The
account tracks debt, locked cash, and free cash on every bar. The engine does not liquidate
positions when equity reaches zero. Orders stop filling, and you decide how to treat that point in
the analysis.

## Borrowing costs and funding payments \[#borrowing-costs-and-funding-payments]

Financing costs depend on how much you borrow and how long you hold the position. A callback can
read the debt and charge interest as negative `cash_earnings`, even on days without a trade. Here
eager leverage funds a $1,000 position with $500 of borrowed cash. A sample daily rate of 0.1%
charges $0.50 for each day after entry:

```python title="Charge interest on borrowed cash each day"
>>> close = pd.Series([100.0, 100.0, 100.0], index=pd.date_range("2026-01-01", periods=3))

>>> @njit
... def financing_nb(ctx, cash_earnings, daily_rate):
...     cash_earnings[ctx.i, ctx.col] = -ctx.last_debt[ctx.col] * daily_rate

>>> pf = vbt.PF.from_signals(
...     close,
...     entries=[True, False, False],
...     size=10,
...     init_cash=1000,
...     leverage=2,
...     leverage_mode="eager",
...     save_state=True,
...     cash_earnings=np.zeros(3),
...     adjust_func_nb=financing_nb,
...     adjust_args=(vbt.Rep("cash_earnings"), 0.001),
... )
>>> pd.DataFrame({"debt": pf.debt, "financing": pf.cash_earnings, "value": pf.value})
             debt  financing   value
2026-01-01  500.0        0.0  1000.0
2026-01-02  500.0       -0.5   999.5
2026-01-03  500.0       -0.5   999.0
```

The price stays flat, so the change in equity comes entirely from financing. The example keeps cash
available to pay it. Negative cash earnings are limited to free cash, so a fully invested account
cannot pay them.

Funding payments on perpetual swaps work the same way: a callback multiplies the position value by
the rate at each funding time. Set the payment's sign for the position's direction, and use your own
rate series and schedule. VBT includes these cash flows in portfolio value and returns.

## Futures and unusual instruments \[#futures-and-unusual-instruments]

A futures contract controls a multiple of its quoted price. `multiplier` applies that factor to
order values, cash flows, and profit, so one E-mini S\&P 500 contract moves $50 per point. Prices may
also be zero or negative, which happens with spreads and with crude oil futures in April 2020. The
[Contract multiplier](#contract-multiplier) and [Negative price](#negative-price) highlights below
show both on real data. Contract rolls are not handled for you: back-adjust a continuous series, or
close the expiring contract and open the next one with your own signals.

Profit and loss is realized when positions close, and equity marks open positions to market on every
bar. Daily variation margin, where exchanges move unrealized profit into cash each day, is not
modeled separately. For most strategy research the resulting equity curve is the same, but cash
balances during open trades differ from a futures account statement.

## Multiple currencies \[#multiple-currencies]

Cash and prices must be in the same currency. For a single currency pair, quote the price in your
account currency, inverting the pair if necessary, together with any order price arrays. For
holdings such as foreign stocks, convert each price series to the account currency before the
simulation. That includes currency exposure in each asset's value. Separate currency balances,
conversion costs, and conversion of leveraged FX profit and loss require custom accounting.

## 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 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, so profits are in dollars 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="Hold WTI crude oil through its negative close in 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 and exit on May 4, holding through the negative settlement.
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.

## 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)


## Related pages

*   [Orders and execution](/features/backtesting/orders-and-execution/): Simulate limit and stop-entry orders, fill prices, timing, rejections, and real fills
*   [Backtesting engine](/features/backtesting/backtesting-engine/): Simulate orders, signals, and callbacks across many assets and parameters at once
*   [Portfolio accounting](/features/backtesting/portfolio-accounting/): Track shared cash, deposits, dividends, positions, records, and exposure per bar
*   [Event-driven backtesting](/features/backtesting/event-driven-backtesting/): Write compiled callbacks for cooldowns, position limits, and custom simulators