功能

投资组合记账

逐根跟踪共享现金、入金、股息、持仓、记录和敞口

现金去了哪里,每根 K 线上投资组合持有什么?VBT 为每次回测保留完整账目:资产间共享现金、入金与出金、股息与利息、手续费、持仓及其价值。全部可作为时间序列或记录检查,使共享现金的多资产回测精确核对到分。

Invest $500 a month in a 60/40 portfolio and collect dividends
data = vbt.YFData.pull(
    ["SPY", "TLT"],
    start="2020-01-01",
    end="2025-01-01",
    auto_adjust=False,  
)
month_start = ~data.index.tz_localize(None).to_period("M").duplicated()
deposits = pd.Series(0.0, index=data.index)
deposits[month_start] = 500
weights = data.symbol_wrapper.fill(np.nan)
weights[month_start] = [0.6, 0.4]

pf = vbt.PF.from_orders(
    data,
    size=weights,
    size_type="targetpercent",
    group_by=True,
    cash_sharing=True,  
    call_seq="auto",
    init_cash=0,
    cash_deposits=deposits,
    cash_dividends=data.get("Dividends"),  
    fees=0.0005,
)
pd.Series({
    "deposits": pf.cash_deposits.sum(),
    "dividends": pf.cash_earnings.sum(),
    "fees": pf.orders.fees.sum(),
    "cash": pf.cash.iloc[-1],
    "assets": pf.asset_value.iloc[-1],
    "value": pf.final_value,
}).round(2)
deposits     30000.00
dividends     1756.07
fees            16.96
cash           174.82
assets       34520.04
value        34694.86
dtype: float64
pd.DataFrame({
    "Deposited": pf.cash_deposits.cumsum(),
    "Portfolio value": pf.value,
}).vbt.plot().show()
2020 至 2024 年按月投入的 SPY 与 TLT 60/40 储蓄计划,累计入金和投资组合净值 图表数据(JSON)

共存入六十笔 500 美元及 1,756 美元股息,支付 16.96 美元手续费,股息在下次月度再平衡时投入。最终净值等于剩余现金加股票价值。

共享现金

默认每列是独立账户。通过 group_by 分组并设置 cash_sharing=True,可将多列变成同一账户:持仓仍按资产跟踪,现金、净值和收益按组跟踪。此时 init_cash 是每组而非每个资产的起始余额。一根 K 线内,组中资产依次交易,call_seq="auto" 使卖出先执行,以便用所得资金买入。结果可按组或资产读取。例如,pf.get_cash_earnings(group_by=False) 将上例股息拆分为 SPY 的 719.56 美元和 TLT 的 1,036.51 美元。

起始状态

模拟以 init_cash 开始,也可通过 init_position 提供持仓、init_price 提供入场价,例如从实盘账户继续。若不希望资金限制测试,init_cash="auto" 用无限现金模拟,再将起始余额设为策略曾需要的最大金额:

Find the cash a sequence of orders requires
index = pd.date_range("2026-01-01", periods=4)
close = pd.Series([10.0, 12.0, 8.0, 11.0], index=index)
pf = vbt.PF.from_orders(close, size=[5, 5, -10, 20], init_cash="auto")
print(pf.init_cash)
250.0
pf.cash.tolist()
[200.0, 140.0, 220.0, 0.0]

多列时,"autoalign" 为全部列使用最大的资金需求,使收益共享同一资金基数。"percent"、"valuepercent" 和目标权重等相对现金或净值的数量,在这种运行中没有有限参照,因此会报错。应使用真实现金模拟;无杠杆时,之后调用 pf.replace(init_cash="auto"),了解策略实际用了多少资金。

初始持仓

可以测试已持有资产的账户接下来会怎样。这里账户初始有 1,000 美元现金和十股股票,随后分两次卖出:

Start with shares already in the account
held_close = pd.Series([100.0, 105.0, 110.0], index=pd.date_range("2026-01-01", periods=3))
held_pf = vbt.PF.from_orders(
    held_close,
    size=[np.nan, -4, -6],
    init_cash=1000,
    init_position=10,
    init_price=100,
)
pd.DataFrame({"cash": held_pf.cash, "shares": held_pf.assets, "value": held_pf.value})
              cash  shares   value
2026-01-01  1000.0    10.0  2000.0
2026-01-02  1420.0     6.0  2050.0
2026-01-03  2080.0     0.0  2080.0

初始股票从一开始计入组合净值。105 美元卖出四股,110 美元卖出余下六股,最终现金为 2,080 美元。新 K 线到达时延续已有模拟,参见实时模拟。

现金流

除交易外,资金还通过四种方式进出账户:

现金流参数说明
入金与出金cash_deposits负数表示出金,不超过自由现金
股息cash_dividends每股金额,乘以持仓
利息及其他收入或成本cash_earnings现金金额,在本根结束时入账
交易成本fees、fixed_fees逐笔订单收取

入金计为投入资本,因此增加投入金额但不增加收益率。依赖账户本身的现金流,例如按当前净值收取管理费、年末业绩费或已实现收益税,可在模拟期间由回调写入 cash_earnings。

估值

报告的组合净值是现金加按收盘价计算的持仓价值。模拟期间,val_price 可选择仓位决策使用的估值价格,例如目标配置对应多少股。缺失收盘价默认填充,因此数据缺口不会使持仓价值归零。组收益来自合并净值,因此不是资产收益之和或均值;逐资产收益由 pf.get_returns(group_by=False) 提供。利润分为已平仓交易的已实现利润与未平仓交易的未实现利润,所有序列均可逐根获取:pf.cash、pf.assets、pf.asset_value、pf.value 和 pf.returns。

记录

账户保留四类记录,各有可读表格:

记录每行含义
pf.orders已成交订单;信号策略还含信号、创建和成交时间
pf.logs请求订单及前后状态,无论是否成交
pf.trades与离场匹配的入场,包括部分离场
pf.positions从开仓到空仓、合并各笔交易的持仓

模拟时设置 log=True 保留订单日志,包括未成交请求。

Read positions, including the one still open
pf = vbt.PF.from_orders(close, size=[5, 5, -10, 20], init_cash=1000)
cols = ["Entry Index", "Exit Index", "Size", "Avg Entry Price", "PnL", "Status"]
pf.positions.readable[cols]
  Entry Index Exit Index  Size  Avg Entry Price   PnL  Status
0  2026-01-01 2026-01-03  10.0             11.0 -30.0  Closed
1  2026-01-04 2026-01-04  20.0             11.0   0.0    Open

若要为订单附加自定义信息,例如离场原因,可在回调中写入旁侧数组,再按行列与订单记录连接。

配置与敞口

pf.allocations 给出每根 K 线上各资产在组中的权重。敞口概括组合:多头和空头敞口分别是多空资产价值相对组合净值的比例,总敞口是两者之和,净敞口是两者之差。这里初始做多 SPY 60%、做空 TLT 40%,之后任其漂移:

Track gross and net exposure of a long/short book
weights = data.symbol_wrapper.fill(np.nan)
weights.iloc[0] = [0.6, -0.4]
book_pf = vbt.PF.from_orders(
    data,
    size=weights,
    size_type="targetpercent",
    group_by=True,
    cash_sharing=True,
    call_seq="auto",
    init_cash=10_000,
)
pd.DataFrame({
    "long": book_pf.get_gross_exposure(direction="longonly"),
    "short": book_pf.get_gross_exposure(direction="shortonly"),
    "gross": book_pf.gross_exposure,
    "net": book_pf.net_exposure,
}).iloc[[0, 500, 1000]].round(3)
                            long  short  gross    net
Date
2020-01-02 00:00:00-05:00  0.600  0.400  1.000  0.200
2021-12-27 00:00:00-05:00  0.707  0.349  1.056  0.358
2023-12-21 00:00:00-05:00  0.631  0.209  0.839  0.422

SPY 上涨、TLT 下跌时,空头缩小,组合净多头增加。若要了解在场时间,pf.position_coverage 给出持有未平仓头寸的 K 线比例。

资产加权

✅ 资产加权可精细调整组合内各资产或策略的影响,增强对整体表现的控制。关键好处是权重不限于收益率,而是一致应用于全部时间序列和度量,包括订单、现金流等,确保投资组合各方面精确对应。

pf.apply_weights 在不重新模拟的情况下,为已完成结果重新加权,适合比较已测试资产或策略的配置。若要测试交易期间资产如何竞争同一笔现金,使用共享现金。组合多个策略参见多策略投资组合。

Maximize Sharpe of a random portfolio
data = vbt.YFData.pull(["AAPL", "MSFT", "GOOG"], start="2020")
pf = data.run("from_random_signals", n=vbt.Default(50), seed=42, group_by=True)
pf.get_sharpe_ratio(group_by=False)  
symbol
AAPL    1.401012
MSFT    0.456162
GOOG    0.852490
Name: sharpe_ratio, dtype: float64
pf.sharpe_ratio  
1.2132857343006869
prices = pf.get_value(group_by=False)
weights = vbt.pypfopt_optimize(prices=prices)  
weights
{'AAPL': 0.85232, 'MSFT': 0.0, 'GOOG': 0.14768}
weighted_pf = pf.apply_weights(weights, rescale=True)  
weighted_pf.weights
symbol
AAPL    2.55696
MSFT    0.00000
GOOG    0.44304
dtype: float64
weighted_pf.get_sharpe_ratio(group_by=True)  
1.426112580298898

持仓视图

✅ 持仓视图可聚焦多头或空头分析投资组合,为各投资策略提供清晰独立的视角。

Separate long and short positions of a basic SMA crossover portfolio
data = vbt.YFData.pull("BTC-USD")
fast_sma = data.run("talib_func:sma", timeperiod=20)
slow_sma = data.run("talib_func:sma", timeperiod=50)
long_entries = fast_sma.vbt.crossed_above(slow_sma)
short_entries = fast_sma.vbt.crossed_below(slow_sma)
pf = vbt.PF.from_signals(
    data,
    long_entries=long_entries,
    short_entries=short_entries,
    fees=0.01,
    fixed_fees=1.0
)

long_pf = pf.long_view
short_pf = pf.short_view

fig = vbt.make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.01)
fig = long_pf.assets.vbt.plot_against(
    0,
    trace_kwargs=dict(name="Long position", line_shape="hv", line_color="mediumseagreen"),
    other_trace_kwargs=dict(visible=False),
    add_trace_kwargs=dict(row=1, col=1),
    fig=fig
)
fig = short_pf.assets.vbt.plot_against(
    0,
    trace_kwargs=dict(name="Short position", line_shape="hv", line_color="coral"),
    other_trace_kwargs=dict(visible=False),
    add_trace_kwargs=dict(row=2, col=1),
    fig=fig
)
fig.show()
BTC-USD SMA 交叉投资组合,分别显示多头和空头资产 图表数据(JSON)
long_pf.sharpe_ratio
0.9185961894435091
short_pf.sharpe_ratio
0.2760864152147919

持仓信息

✅ 动态信号函数现在可以访问当前持仓信息,例如未平仓损益。

Enter randomly, exit randomly but only if in profit
@njit
def signal_func_nb(ctx, entries, exits):
    is_entry = vbt.pf_nb.select_nb(ctx, entries)
    is_exit = vbt.pf_nb.select_nb(ctx, exits)
    if is_entry:
        return True, False, False, False
    if is_exit:
        pos_info = ctx.last_pos_info[ctx.col]
        if pos_info["status"] == vbt.pf_enums.TradeStatus.Open:
            if pos_info["pnl"] >= 0:
                return False, True, False, False
    return False, False, False, False

data = vbt.YFData.pull("BTC-USD")
entries, exits = data.run("RANDNX", n=10, seed=42, unpack=True)
pf = vbt.Portfolio.from_signals(
    data,
    signal_func_nb=signal_func_nb,
    signal_args=(vbt.Rep("entries"), vbt.Rep("exits")),
    broadcast_named_args=dict(entries=entries, exits=exits),
    jitted=False  
)
pf.trades.readable[["Entry Index", "Exit Index", "PnL"]]
                Entry Index                Exit Index           PnL
0 2014-11-01 00:00:00+00:00 2016-01-08 00:00:00+00:00     39.134739
1 2016-03-27 00:00:00+00:00 2016-09-07 00:00:00+00:00     61.220063
2 2016-12-24 00:00:00+00:00 2016-12-31 00:00:00+00:00     14.471414
3 2017-03-16 00:00:00+00:00 2017-08-05 00:00:00+00:00    373.492028
4 2017-09-12 00:00:00+00:00 2018-05-05 00:00:00+00:00    815.699284
5 2019-02-15 00:00:00+00:00 2019-11-10 00:00:00+00:00   2107.383227
6 2019-12-04 00:00:00+00:00 2019-12-10 00:00:00+00:00     12.630214
7 2020-07-12 00:00:00+00:00 2021-11-14 00:00:00+00:00  21346.035444
8 2022-01-15 00:00:00+00:00 2023-03-06 00:00:00+00:00 -11925.133817

现金入金

✅ 可随时存入或提取现金。

DCA $10 into Bitcoin each month
data = vbt.YFData.pull("BTC-USD")
cash_deposits = data.symbol_wrapper.fill(0.0)
month_start_mask = ~data.index.tz_convert(None).to_period("M").duplicated()
cash_deposits[month_start_mask] = 10
pf = vbt.PF.from_orders(
    data.close,
    init_cash=0,
    cash_deposits=cash_deposits
)

pf.input_value  
1020.0
pf.final_value
20674.328828315127

现金收益

✅ 现金可以根据当前持仓持续流入或流出。

Backtest Apple without and with dividend reinvestment
data = vbt.YFData.pull("AAPL", start="2010")

pf_kept = vbt.PF.from_holding(  
    data.close,
    cash_dividends=data.get("Dividends")
)
pf_kept.cash.iloc[-1]  
93.9182408043298
pf_kept.assets.iloc[-1]  
15.37212731743495
pf_reinvested = vbt.PF.from_orders(  
    data.close,
    cash_dividends=data.get("Dividends")
)
pf_reinvested.cash.iloc[-1]
0.0
pf_reinvested.assets.iloc[-1]
18.203284859405468
fig = pf_kept.value.rename("Value (kept)").vbt.plot()
pf_reinvested.value.rename("Value (reinvested)").vbt.plot(fig=fig)
fig.show()
2010 年起 AAPL 投资组合净值,比较股息保留为现金与再投入 图表数据(JSON)

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

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