# 仓位管理 (/zh/features/backtesting/position-sizing)

每笔交易应该买多少？答案对回测的影响不亚于入场规则。VBT 可在 Python 中测试固定单位、现金金额、净值比例、目标权重，或根据距止损距离计算的风险仓位，并并列比较这些规则。

```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.  每根 K 线开始时运行。空仓时确定下次入场数量，使触及止损时损失当前净值的 1%。
2.  为 `size` 提供完整数据形状，使函数可逐根写入一个值。

2015 至 2024 年 SPY 投资组合净值，比较固定 100 股仓位与每笔承担净值 1% 风险. [Figure data (JSON)](/assets/figures/features/backtesting/position-sizing-risk-vs-fixed.fb37233ca5fe.json)

两个版本使用相同 37 笔交易、2 ATR 止损和 4 ATR 目标。固定 100 股时，一笔亏损交易可能损失 253 至 1,773 美元，取决于市场波动。1% 风险仓位的损失接近净值
1%，但并不完全相等，因为价格可能跳空越过止损。

## 数量类型 \[#size-types]

`size` 是数值，`size_type` 决定其含义：

| 数量类型              | `size` 含义       | 示例                      |
| ----------------- | --------------- | ----------------------- |
| `"amount"`（默认）    | 资产单位数           | `10` 买入 10 股            |
| `"value"`         | 花费现金            | `1000` 买入价值 1,000 美元的资产 |
| `"percent"`       | 可用现金比例，卖出时为持仓比例 | `0.5` 使用一半现金            |
| `"valuepercent"`  | 投资组合总净值比例       | `0.5` 买入净值的一半           |
| `"targetamount"`  | 目标持仓单位数         | `10` 买卖至持有 10 单位        |
| `"targetvalue"`   | 目标持仓金额          | `1000` 持有价值 1,000 美元的资产 |
| `"targetpercent"` | 目标持仓权重          | `0.25` 持有净值的 25%        |

每种百分比类型都有 `100` 变体，将 `50` 读为 50%。同一订单重复时，可看出普通类型和目标类型的差别：

```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
```

普通类型逐根再次买入，目标类型只交易差额，因此第二个订单不操作。目标权重因此很适合再平衡。`NaN` 权重跳过该根并保持持仓，零权重则平仓：

```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.  先卖出超配资产，再买入另一资产，以便释放现金。

月内权重漂移，下次再平衡时恢复 60/40。

权重是组净值的比例，因此无杠杆时总和应不超过 1。模拟器按顺序处理本根各列，现金用尽后停止买入：三个资产各设 0.5 权重，会成交前两个，第三个无现金可用。

!!! warning "目标数量与信号"
    使用入场和离场信号时，目标数量可能与信号冲突。入场目标低于当前持仓会导致卖出。应在 `from_orders` 中使用目标数量，或用 `order_mode=True` 让
    `from_signals` 从目标推导信号，详见[信号回测](/zh/features/backtesting/signal-backtesting/#targets-as-signals)。

## 可用现金还是投资组合净值？ \[#available-cash-or-portfolio-equity]

一半可用现金与一半投资组合净值可能产生截然不同的配置。共享现金时，每次买入都会减少下一资产可用现金。下例两项资产均为 10 美元，A 先于 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"` 用 500 美元买 A，再用剩余 500 美元的一半买 B；`"valuepercent"` 各用 500 美元，因为净值仍为 1,000
美元。若每笔新交易应使用剩余现金的一部分，选前者；若数量应跟随账户总净值，选后者。资产如何共享账户，详见[投资组合记账](/zh/features/backtesting/portfolio-accounting/#shared-cash)。

## 基于风险确定仓位 \[#risk-based-sizing]

风险仓位用风险预算除以触及止损时每单位的损失。预算为固定现金金额时，只需数组运算：`size = 1000 / stop_dist`，配合
`size_type="amount"`。预算如上例依赖净值时，必须在模拟期间计算数量，因为净值取决于此前成交和未平仓头寸价值。

调整函数是最简单的实现位置。它在每根信号前运行，读取当前净值、现金和持仓，并写入下一订单使用的数量。回调内，`vbt.pf_nb.get_order_size_nb`
将百分比或目标数量转换为单位数，便于将仓位规则与信号结合。转换不考虑手续费、滑点和手数，因此现金紧张时应留余量。

相同模式还可实现波动率目标、根据历史交易估计的凯利比例，以及独立模型产生的仓位。

数量和目标权重也可以来自数组，因此外部模型可为每个资产和再平衡日提供不同配置。规则还需要模拟账户状态时，例如亏损后缩减下一订单，应使用回调。这样可用相同入场和离场，将自定义仓位规则与固定数量比较。根据组合目标计算权重，参见[投资组合优化](/zh/features/optimization/portfolio-optimization/)。

## 手数与数量步长 \[#lots-and-increments]

真实市场按整股、合约或最小手数步长交易。`size_granularity` 将每笔订单向下取整为步长的整数倍：

```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
```

结合 `min_size` 和 `size_type="value"`，可按交易所要求跳过名义金额不足的订单。数量步长也用于平仓，因此此前留下的零碎持仓可能无法完全平掉。

## 复利 \[#compounding]

是否将利润再投入取决于仓位规则。默认 `size=np.inf`
花费全部可用现金，因此每笔交易随账户增长。净值百分比和与净值挂钩的风险仓位也随账户变化。固定数量保持单位数不变，固定金额保持请求现金额不变，都可比较交易而不在盈利后自动增加请求数量。固定仓位仍需现金支付：账户不足时部分成交，设置
`allow_partial=False` 则拒绝。

## 反转仓位的数量 \[#sizing-for-reversals]

反转在一个订单中关闭当前持仓并开立相反持仓，因此订单数量大于新持仓：

```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
```

使用 `max_size` 时需注意：上限一单位会将一单位反转变为单纯平仓。若要减仓而非反转，应启用加减仓。
