Features
Portfolio accounting
Track shared cash, deposits, dividends, positions, records, and exposure per bar
Where did the cash go, and what does the portfolio hold at every bar? VBT keeps a full account for every backtest: cash shared across assets, deposits and withdrawals, dividends and interest, fees, positions, and their value. Everything can be inspected as a time series or as records, so a multi-asset backtest with shared cash adds up to the cent.
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()Sixty deposits of $500 and $1,756 of dividends went in, $16.96 of fees went out, and the dividends were reinvested at the next monthly rebalance. The final value equals the remaining cash plus the value of the shares.
Shared cash
By default, every column is its own account. Grouping columns with group_by and setting
cash_sharing=True turns them into one account: positions stay per asset, while cash, value, and
returns are tracked per group. init_cash is then the starting balance of each group, not of each
asset. Within a bar, the assets of a group trade one after another, and call_seq="auto" puts sells
first so their proceeds can fund the buys. Results can be read per group or per asset. For example,
pf.get_cash_earnings(group_by=False) splits the dividends above into $719.56 from SPY and
$1,036.51 from TLT.
Starting state
A simulation starts with init_cash, and optionally with positions from init_position and their
entry prices from init_price, for example to continue from a live account. When the amount of
capital should not limit the test, init_cash="auto" simulates with unlimited cash and then sets
the starting balance to the most the strategy ever needed:
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]With several columns, "autoalign" gives all of them the largest requirement, so their returns
share one capital base. Sizes relative to cash or value, such as "percent", "valuepercent", and
target weights, have nothing finite to refer to during such a run and raise an error. Simulate them
with real cash and, without leverage, call pf.replace(init_cash="auto") afterwards to learn how
much of it the strategy actually used.
Existing holdings
You can test what happens next to an account that already holds assets. Here the account starts with $1,000 in cash and ten shares, then sells the shares in two stages:
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.0The initial shares count toward the portfolio's value from the start. Selling four at $105 and the remaining six at $110 leaves $2,080 in cash. To keep an existing simulation running as new bars arrive, see Live simulation.
Cash flows
Money enters and leaves the account in four ways besides trading:
| Flow | Argument | Notes |
|---|---|---|
| Deposits and withdrawals | cash_deposits | Negative values withdraw, limited to the free cash |
| Dividends | cash_dividends | Per share, multiplied by the position |
| Interest and other income or costs | cash_earnings | Cash amounts, credited at the end of the bar |
| Trading costs | fees, fixed_fees | Charged per order |
Deposits are counted as invested capital, so they raise the input value without raising the return.
Flows that depend on the account itself, such as a management fee on the current value, a
performance fee at year end, or taxes on realized gains, are written into cash_earnings by a
callback while the simulation runs.
Valuation
The reported portfolio value is its cash plus the value of its positions at closing prices. During
simulation, val_price lets you choose the prices used to value positions for sizing decisions,
such as how many shares make up a target allocation. Missing closing prices are filled by default,
so a gap in the data does not zero out a position. The returns of a group come from its combined
value, so they are not the sum or the average of the asset returns, which
pf.get_returns(group_by=False) gives per asset. Profit splits into realized profit from closed
trades and unrealized profit from open ones, and every series is available per bar: pf.cash,
pf.assets, pf.asset_value, pf.value, and pf.returns.
Records
The account keeps four kinds of records, each with a readable table:
| Records | What one row is |
|---|---|
pf.orders | A filled order, with signal, creation, and fill time for signal strategies |
pf.logs | A requested order with the state before and after it, filled or not |
pf.trades | An entry matched with an exit, including partial exits |
pf.positions | A position from opening to flat, combining its trades |
Set log=True when simulating to keep order logs, including requests that did not fill.
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 OpenTo attach your own information to orders, such as the reason for an exit, record it in a side array from a callback and join it to the order records by row and column.
Allocations and exposure
pf.allocations gives the weight of each asset in its group on every bar. Exposure summarizes the
book: long and short exposure are the long and short asset values relative to portfolio value, gross
exposure is their sum, and net exposure their difference. Here a book starts 60% long SPY and 40%
short TLT and is left to drift:
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.422As SPY rose and TLT fell, the short shrank and the book became more net long. For time spent in the
market, pf.position_coverage gives the share of bars with an open position.
✅ Asset weighting lets you fine-tune the influence of individual assets or strategies within your portfolio, giving you enhanced control over your portfolio's overall performance. The key benefit is that these weights are not limited to returns. They are consistently applied to all time series and metrics, including orders, cash flows, and more. This comprehensive approach ensures that every aspect of your portfolio stays precisely aligned.
pf.apply_weights reweights completed results without rerunning the simulation. This is useful for
comparing allocations across assets or strategies you have already tested. To test how assets
compete for the same cash during trading, use shared cash. See
Multi-strategy portfolios for combining several
strategies.
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✅ Position views let you analyze your portfolio by focusing on either long or short positions, providing a clear and distinct perspective for each investment strategy.
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✅ Dynamic signal functions now have access to the current position information, such as (open) P&L.
@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✅ Cash can be deposited or withdrawn at any time.
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✅ Cash can be continuously earned or spent depending on the current position.
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()Related pages
- Backtesting engineSimulate orders, signals, and callbacks across many assets and parameters at once
- Strategies › Multi-strategy portfoliosStack strategies as one portfolio, weight them, and compare recurring investing plans
- Fees, slippage, and leverageModel commissions, slippage, leverage, futures multipliers, and negative prices
- Event-driven backtestingWrite compiled callbacks for cooldowns, position limits, and custom simulators
Copyright © 2021–2026 Oleg Polakow. All rights reserved.
Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.