功能

仓位管理

按单位数、现金、净值比例、目标权重或每笔风险确定仓位

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

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):  
    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)),  
    **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()
2015 至 2024 年 SPY 投资组合净值,比较固定 100 股仓位与每笔承担净值 1% 风险 图表数据(JSON)

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

数量类型

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%。同一订单重复时,可看出普通类型和目标类型的差别:

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 权重跳过该根并保持持仓,零权重则平仓:

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",  
    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

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

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

目标数量与信号

使用入场和离场信号时,目标数量可能与信号冲突。入场目标低于当前持仓会导致卖出。应在 from_orders 中使用目标数量,或用 order_mode=True 让 from_signals 从目标推导信号,详见信号回测。

可用现金还是投资组合净值?

一半可用现金与一半投资组合净值可能产生截然不同的配置。共享现金时,每次买入都会减少下一资产可用现金。下例两项资产均为 10 美元,A 先于 B 交易:

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 美元。若每笔新交易应使用剩余现金的一部分,选前者;若数量应跟随账户总净值,选后者。资产如何共享账户,详见投资组合记账。

基于风险确定仓位

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

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

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

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

手数与数量步长

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

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",可按交易所要求跳过名义金额不足的订单。数量步长也用于平仓,因此此前留下的零碎持仓可能无法完全平掉。

复利

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

反转仓位的数量

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

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 时需注意:上限一单位会将一单位反转变为单纯平仓。若要减仓而非反转,应启用加减仓。

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。