# Position sizing (/features/backtesting/position-sizing)

How much should each trade buy? The answer changes a backtest as much as the entry rule does. VBT
lets you test position sizing in Python with fixed units, cash amounts, percent of equity, target
weights, or risk-based sizes computed from the distance to your stop, and compare the rules side by
side.

```python title="Risk 1% of equity per trade instead of buying 100 shares"
>>> data = vbt.YFData.pull("SPY", start="2015-01-01", end="2025-01-01")
>>> entries = data.close.vbt.crossed_above(data.close.rolling(50).mean())
>>> stop_dist = 2 * data.run("atr", window=14).atr

>>> @njit
... def risk_size_nb(c, size, stop_dist, risk):  # (1)
...     if c.last_position[c.col] == 0:
...         equity = c.last_value[c.col]
...         size[c.i, c.col] = risk * equity / vbt.pf_nb.select_nb(c, stop_dist)

>>> kwargs = dict(
...     sl_stop=stop_dist,
...     tp_stop=2 * stop_dist,
...     delta_format="absolute",
...     init_cash=100_000,
... )
>>> fixed_pf = vbt.PF.from_signals(data, entries, size=100, **kwargs)
>>> risk_pf = vbt.PF.from_signals(
...     data,
...     entries,
...     adjust_func_nb=risk_size_nb,
...     adjust_args=(vbt.Rep("size"), vbt.Rep("stop_dist"), 0.01),
...     broadcast_named_args=dict(stop_dist=stop_dist),
...     size=np.nan,
...     arg_config=dict(size=dict(full_shape=True)),  # (2)
...     **kwargs,
... )

>>> pd.DataFrame({
...     "100 shares": fixed_pf.trades.losing.pnl.to_pd(ignore_index=True).describe(),
...     "1% risk": risk_pf.trades.losing.pnl.to_pd(ignore_index=True).describe(),
... }).loc[["count", "mean", "std", "min", "max"]].round(0)
       100 shares  1% risk
count        17.0     17.0
mean       -882.0  -1100.0
std         437.0    196.0
min       -1773.0  -1459.0
max        -253.0   -513.0

>>> values = pd.DataFrame({"100 shares": fixed_pf.value, "1% risk": risk_pf.value})
>>> values.vbt.plot().show()
```

1.  Runs at the start of every bar. When flat, it sizes the next entry so that hitting the stop loses
    1% of the current equity.
2.  Give `size` the full shape of the data so the function can write one value per bar.

SPY portfolio value from 2015 to 2024 for a fixed 100-share position size and for 1% equity risk per trade. [Figure data (JSON)](/assets/figures/features/backtesting/position-sizing-risk-vs-fixed.fb37233ca5fe.json)

Both versions take the same 37 trades with a 2 ATR stop and a 4 ATR target. With 100 shares, a
losing trade cost anywhere from $253 to $1,773, depending on how volatile the market was. With 1%
risk, losses stay close to 1% of equity. They are not exactly equal because prices can gap through
the stop.

## Size types \[#size-types]

`size` is a number, and `size_type` says what it means:

| Size type            | `size` means                                             | Example                        |
| -------------------- | -------------------------------------------------------- | ------------------------------ |
| `"amount"` (default) | Units of the asset                                       | `10` buys 10 shares            |
| `"value"`            | Cash to spend                                            | `1000` buys $1,000 worth       |
| `"percent"`          | Share of available cash, or of the position when selling | `0.5` uses half the cash       |
| `"valuepercent"`     | Share of total portfolio value                           | `0.5` buys half the equity     |
| `"targetamount"`     | The position to hold, in units                           | `10` buys or sells to reach 10 |
| `"targetvalue"`      | The position to hold, in cash                            | `1000` holds $1,000 worth      |
| `"targetpercent"`    | The position to hold, as a weight                        | `0.25` holds 25% of equity     |

Each percentage type also has a `100` variant that reads `50` as 50%. The difference between
ordinary and target types shows when the same order repeats:

```python title="Send the same order on two bars"
>>> close = pd.Series([50.0, 50.0], index=pd.date_range("2026-01-01", periods=2))
>>> pf = vbt.PF.from_orders(
...     close,
...     size=vbt.Param([10, 1000, 0.5, 0.25], level=0),
...     size_type=vbt.Param(
...         ["amount", "value", "valuepercent", "targetpercent"],
...         level=0
...     ),
...     init_cash=10_000,
... )
>>> pf.assets.iloc[-1]
size     size_type
10.00    amount            20.0
1000.00  value             40.0
0.50     valuepercent     200.0
0.25     targetpercent     50.0
Name: 2026-01-02 00:00:00, dtype: float64
```

Ordinary types buy again on every bar. A target type only trades the difference, so the second order
does nothing. That makes target weights the natural way to rebalance. A `NaN` weight skips the bar
and keeps the position, while a weight of zero closes it:

```python title="Rebalance to 60/40 on the first trading day of each month"
>>> data = vbt.YFData.pull(["SPY", "TLT"], start="2020-01-01", end="2025-01-01")
>>> rebalance = ~data.index.tz_localize(None).to_period("M").duplicated()
>>> weights = data.symbol_wrapper.fill(np.nan)
>>> weights[rebalance] = [0.6, 0.4]

>>> pf = vbt.PF.from_orders(
...     data,
...     size=weights,
...     size_type="targetpercent",
...     group_by=True,
...     cash_sharing=True,
...     call_seq="auto",  # (1)
...     fees=0.0005,
... )
>>> pf.allocations.iloc[[0, 20, 21]].round(3)
symbol                       SPY    TLT
Date
2020-01-02 00:00:00-05:00  0.600  0.400
2020-01-31 00:00:00-05:00  0.582  0.418
2020-02-03 00:00:00-05:00  0.600  0.400
```

1.  Sell the overweight asset before buying the other, so the cash is available.

The weights drift during the month and return to 60/40 on the next rebalance.

Weights are fractions of the group's value, so without leverage they should add up to at most 1. The
simulator processes the columns of a bar in order and stops buying when the cash runs out: weights
of 0.5 on three assets fill the first two and leave nothing for the third.

!!! warning "Target sizes and signals"
    With entry and exit signals, a target size can contradict the signal. An entry with a target below
    the current position would sell. Use target sizes with `from_orders`, or let `from_signals` derive
    the signals from the targets with `order_mode=True`, as shown on the
    [Signal backtesting](/features/backtesting/signal-backtesting/#targets-as-signals) page.

## Available cash or portfolio equity? \[#available-cash-or-portfolio-equity]

Half the available cash and half the portfolio value can produce very different allocations. With
shared cash, each buy leaves less cash for the next asset. In this example both assets cost $10, and
A trades before B:

```python title="Compare half the available cash with half the equity"
>>> close = pd.DataFrame(
...     {"A": [10.0], "B": [10.0]},
...     index=pd.date_range("2026-01-01", periods=1),
... )
>>> positions = {}
>>> for size_type in ["percent", "valuepercent"]:
...     pf = vbt.PF.from_orders(
...         close,
...         size=0.5,
...         size_type=size_type,
...         group_by=True,
...         cash_sharing=True,
...         init_cash=1000,
...     )
...     positions[size_type] = pf.assets.iloc[0]
>>> pd.DataFrame(positions)
   percent  valuepercent
A     50.0          50.0
B     25.0          50.0
```

`"percent"` spends $500 on A, then half the remaining $500 on B. `"valuepercent"` spends $500 on
each because the equity is still $1,000. Choose the first when each new trade should use a share of
remaining cash, and the second when its size should follow total account value. The
[Portfolio accounting](/features/backtesting/portfolio-accounting/#shared-cash) page covers how
assets share that account.

## Risk-based sizing \[#risk-based-sizing]

Risk-based sizing divides a risk budget by the loss per unit if the stop is hit. When the budget is
a fixed amount of cash, it is a plain array computation: `size = 1000 / stop_dist` with
`size_type="amount"`. When the budget depends on equity, as in the example above, the size must be
computed during the simulation, because equity depends on earlier fills and the value of open
positions.

An adjustment function is the simplest place for that. It runs before each bar's signals, reads the
current equity, cash, and position, and writes the size the next order will use. Inside callbacks,
`vbt.pf_nb.get_order_size_nb` converts a percentage or target size into units, which helps when you
combine a sizing rule with signals. The conversion ignores fees, slippage, and lot sizes, so leave a
margin when cash is tight.

The same pattern covers volatility targeting, Kelly-style fractions estimated from past trades, and
sizes produced by a separate model.

Sizes and target weights can also come from arrays, so an external model can supply a different
allocation for every asset and rebalance date. Use callbacks when the rule also needs the simulated
account state, such as reducing the next order after losses. This lets you compare your own sizing
rules with fixed sizes using the same entries and exits. For weights calculated from a portfolio
objective, see [Portfolio optimization](/features/optimization/portfolio-optimization/).

## Lots and increments \[#lots-and-increments]

Real markets trade in whole shares, contracts, or minimum lot steps. `size_granularity` rounds every
order down to a multiple of the step:

```python title="Round a $1,000 order to whole shares"
>>> close = pd.Series([33.0], index=pd.date_range("2026-01-01", periods=1))
>>> pf = vbt.PF.from_orders(
...     close,
...     size=1000,
...     size_type="value",
...     size_granularity=vbt.Param([np.nan, 1]),
...     init_cash=10_000,
... )
>>> pf.orders.size.to_pd().iloc[0]
size_granularity
NaN    30.30303
1.0    30.00000
Name: 2026-01-01 00:00:00, dtype: float64
```

Combine it with `min_size` and `size_type="value"` to skip orders below a minimum notional, as
exchanges require. A granularity setting also applies when closing, so a fractional position left
over from earlier orders may not close completely.

## Compounding \[#compounding]

Whether profits are reinvested depends on the sizing rule. The default, `size=np.inf`, spends all
available cash, so each trade grows with the account. Percent of equity and risk-based sizes tied to
equity also scale with the account. A fixed amount keeps the unit count constant, while a fixed
value keeps the requested cash amount constant. Both let you compare trades without automatically
increasing the requested size after a profit. A fixed size still needs the cash to pay for it: when
the account is short, the order fills partially, and `allow_partial=False` rejects it instead.

## Sizing for reversals \[#sizing-for-reversals]

A reversal closes the current position and opens the opposite one in a single order, so the order is
larger than the new position:

```python title="Reverse a one-unit long into a one-unit short"
>>> close = pd.Series([10.0, 11.0, 12.0], index=pd.date_range("2026-01-01", periods=3))
>>> pf = vbt.PF.from_signals(
...     close,
...     entries=pd.Series([True, False, False], index=close.index),
...     exits=pd.Series([False, True, False], index=close.index),
...     direction="both",
...     size=1,
... )
>>> pf.orders.readable[["Fill Index", "Size", "Side"]]
  Fill Index  Size  Side
0 2026-01-01   1.0   Buy
1 2026-01-02   2.0  Sell
```

Keep this in mind with `max_size`: a cap of one unit turns a one-unit reversal into a plain exit. To
reduce a position instead of reversing it, enable accumulation.


## Related pages

*   [Orders and execution](/features/backtesting/orders-and-execution/): Simulate limit and stop-entry orders, fill prices, timing, rejections, and real fills
*   [Signal backtesting](/features/backtesting/signal-backtesting/): Turn entry and exit signals into long, short, reversing, and pyramided positions
*   [Portfolio accounting](/features/backtesting/portfolio-accounting/): Track shared cash, deposits, dividends, positions, records, and exposure per bar
*   [Stop loss and take profit](/features/backtesting/stop-loss-and-take-profit/): Backtest stop losses, trailing stops, take profits, time stops, and exit ladders