功能
投资组合记账
逐根跟踪共享现金、入金、股息、持仓、记录和敞口
现金去了哪里,每根 K 线上投资组合持有什么?VBT 为每次回测保留完整账目:资产间共享现金、入金与出金、股息与利息、手续费、持仓及其价值。全部可作为时间序列或记录检查,使共享现金的多资产回测精确核对到分。
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: float64pd.DataFrame({
"Deposited": pf.cash_deposits.cumsum(),
"Portfolio value": pf.value,
}).vbt.plot().show()共存入六十笔 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" 用无限现金模拟,再将起始余额设为策略曾需要的最大金额:
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.0pf.cash.tolist()[200.0, 140.0, 220.0, 0.0]多列时,"autoalign" 为全部列使用最大的资金需求,使收益共享同一资金基数。"percent"、"valuepercent"
和目标权重等相对现金或净值的数量,在这种运行中没有有限参照,因此会报错。应使用真实现金模拟;无杠杆时,之后调用 pf.replace(init_cash="auto"),了解策略实际用了多少资金。
初始持仓
可以测试已持有资产的账户接下来会怎样。这里账户初始有 1,000 美元现金和十股股票,随后分两次卖出:
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 保留订单日志,包括未成交请求。
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%,之后任其漂移:
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.422SPY 上涨、TLT 下跌时,空头缩小,组合净多头增加。若要了解在场时间,pf.position_coverage 给出持有未平仓头寸的 K 线比例。
✅ 资产加权可精细调整组合内各资产或策略的影响,增强对整体表现的控制。关键好处是权重不限于收益率,而是一致应用于全部时间序列和度量,包括订单、现金流等,确保投资组合各方面精确对应。
pf.apply_weights
在不重新模拟的情况下,为已完成结果重新加权,适合比较已测试资产或策略的配置。若要测试交易期间资产如何竞争同一笔现金,使用共享现金。组合多个策略参见多策略投资组合。
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: float64pf.sharpe_ratio 1.2132857343006869prices = 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.weightssymbol
AAPL 2.55696
MSFT 0.00000
GOOG 0.44304
dtype: float64weighted_pf.get_sharpe_ratio(group_by=True) 1.426112580298898✅ 持仓视图可聚焦多头或空头分析投资组合,为各投资策略提供清晰独立的视角。
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()long_pf.sharpe_ratio0.9185961894435091short_pf.sharpe_ratio0.2760864152147919✅ 动态信号函数现在可以访问当前持仓信息,例如未平仓损益。
@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✅ 可随时存入或提取现金。
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.0pf.final_value20674.328828315127✅ 现金可以根据当前持仓持续流入或流出。
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.9182408043298pf_kept.assets.iloc[-1] 15.37212731743495pf_reinvested = vbt.PF.from_orders(
data.close,
cash_dividends=data.get("Dividends")
)
pf_reinvested.cash.iloc[-1]0.0pf_reinvested.assets.iloc[-1]18.203284859405468fig = pf_kept.value.rename("Value (kept)").vbt.plot()
pf_reinvested.value.rename("Value (reinvested)").vbt.plot(fig=fig)
fig.show()版权所有 © 2021–2026 Oleg Polakow。保留所有权利。
本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。