Skip to content
New: Star-import, signal unraveling, Python 3.12 support, and more

#
Portfolio

Question

Learn more in Portfolio documentation.

## From data

To quickly simulate a portfolio from any OHLC data, either use Data.run or pass the data instance (or just a symbol or `class_name:symbol`) to the simulation method.

Various way to quickly simulate a portfolio from a Data instance

    pf = data.run("from_holding")  # (1)!
    pf = data.run("from_random_signals", n=10)  # (2)!

    pf = vbt.PF.from_holding(data)  # (3)!
    pf = vbt.PF.from_holding("BTC-USD")  # (4)!
    pf = vbt.PF.from_holding("BinanceData:BTCUSDT")  # (5)!

1. Simulate a buy-and-hold portfolio from data
2. Simulate a portfolio from data with 10 entries and 10 exits placed randomly
3. Same as the first line
4. Pull the symbol "BTC-USD" from Yahoo Finance (default) and simulate a buy-and-hold portfolio
5. Pull the symbol "BTCUSDT" from Binance and simulate a buy-and-hold portfolio

## From signals

This simulation method is easy to use but still very powerful as long as your strategy can be expressed as signals, such as buy, sell, short sell, and buy to cover.

Various signal configurations

    pf = vbt.PF.from_signals(data, ...)  # (1)!
    pf = vbt.PF.from_signals(open=open, high=high, low=low, close=close, ...)  # (2)!
    pf = vbt.PF.from_signals(close, ...)  # (3)!

    pf = vbt.PF.from_signals(data, entries, exits)  # (4)!
    pf = vbt.PF.from_signals(data, entries, exits, direction="shortonly")  # (5)!
    pf = vbt.PF.from_signals(data, entries, exits, direction="both")  # (6)!
    pf = vbt.PF.from_signals(  # (7)!
        data,
        long_entries=long_entries,
        long_exits=long_exits,
        short_entries=short_entries,
        short_exits=short_exits,
    )

1. When a data instance is passed (not a DataFrame!), VBT will extract OHLC automatically
2. Same as above but pass all price features manually
3. For tick data or when OHL isn't available, pass only close
4. Enter a long position when True in `entries` and exit it when True in `exits` (no short positions)
5. Enter a short position when True in `entries` and exit it when True in `exits` (no long positions)
6. Enter a long position when True in `entries` and enter a short position when True in `exits` through reversals
7. Enter a long position when True in `long_entries`, exit it when True in `long_exits`, enter a short position when True in `short_entries`, and exit it when True in `short_exits`

To specify a different price or other argument for long and short signals, create an empty array and use each signal type as a mask to set the corresponding value.

Manually apply a 1% slippage to the price

    price = data.symbol_wrapper.fill()
    price[entries] = data.close * (1 + 0.01)  # (1)!
    price[exits] = data.close * (1 - 0.01)

1. Entries and exits here act as masks: set values where there is a True value. If you want to replace `data.close` with a NumPy array, use `arr[entries]` on the right side as well.

Use the ask price for buying and the bid price for selling

    price = (bid_price + ask_price) / 2
    price[entries] = ask_price
    price[exits] = bid_price

To exit a trade after a specific amount of time or number of rows, use the `td_stop` argument. The measurement is done from the opening time of the entry row.

Various ways to close out a position after some time

    pf = vbt.PF.from_signals(..., td_stop="7 days")  # (1)!
    pf = vbt.PF.from_signals(..., td_stop=pd.Timedelta(days=7))
    pf = vbt.PF.from_signals(..., td_stop=td_arr)  # (2)!

    pf = vbt.PF.from_signals(..., td_stop=7, time_delta_format="rows")  # (3)!
    pf = vbt.PF.from_signals(..., td_stop=int_arr, time_delta_format="rows")  # (4)!

    pf = vbt.PF.from_signals(..., td_stop=vbt.Param(["1 day", "7 days"]))  # (5)!

1. Exit after 7 days
2. Specify a custom timedelta for each row using an array. Here, for example, you can use `pd.TimedeltaIndex`
3. Exit after 7 rows (i.e., bars)
4. For each potential entry row, specify after how many rows to exit
5. Any of the above can be tested as a parameter

To exit a trade at some specific point of time or number of rows, use the `dt_stop` argument. If you pass a timedelta (like above), the position will be exited at the last bar before the target date. Otherwise, if you pass an exact date or time, the position will be exited at or after it. This behavior can be overridden via the argument config.

Various ways to close out a position at some time

    pf = vbt.PF.from_signals(..., dt_stop="daily")  # (1)!
    pf = vbt.PF.from_signals(..., dt_stop=pd.Timedelta(days=1))
    pf = vbt.PF.from_signals(  # (2)!
        ...,
        dt_stop="daily",
        arg_config=dict(dt_stop=dict(last_before=False))
    )

    pf = vbt.PF.from_signals(..., dt_stop="16:00")  # (3)!
    pf = vbt.PF.from_signals(..., dt_stop=datetime.time(16, 0))
    pf = vbt.PF.from_signals(  # (4)!
        ...,
        dt_stop="16:00",
        arg_config=dict(dt_stop=dict(last_before=True))
    )

    pf = vbt.PF.from_signals(..., dt_stop="2024-01-01")  # (5)!
    pf = vbt.PF.from_signals(..., dt_stop=pd.Timestamp("2024-01-01"))
    pf = vbt.PF.from_signals(  # (6)!
        ...,
        dt_stop="2024-01-01",
        arg_config=dict(dt_stop=dict(last_before=True))
    )
    pf = vbt.PF.from_signals(..., dt_stop=dt_arr)  # (7)!

    pf = vbt.PF.from_signals(..., dt_stop=int_arr, time_delta_format="rows")  # (8)!

    pf = vbt.PF.from_signals(..., dt_stop=vbt.Param(["1 day", "7 days"]))  # (9)!

1. Exit at the last bar before the end of the day
2. Exit at or after the end of the day
3. Exit at or after 16:00
4. Exit at the last bar before 16:00
5. Exit on or after January 1st, 2024. Will be applied only to entries issued before that date.
6. Exit at the last bar before January 1st, 2024. Will be applied only to entries issued before that date.
7. Specify a custom datetime for each row using an array. Here, for example, you can use `pd.DatetimeIndex`
8. For each potential entry row, specify at which row to exit
9. Any of the above can be tested as a parameter

Note

Don't confuse `td_stop` with `dt_stop` - "td" is an abbreviation for a timedelta while "dt" is an abbreviation for a datetime.

### Callbacks

To save an information piece at one timestamp and re-use at a later timestamp in a callback, create a NumPy array and pass it to the callback. The array should be one-dimensional and have the same number of elements as there are columns. The element under the current column can then be read and written using the same mechanism as accessing the latest position via `c.last_position[c.col]`. More information pieces would require either more arrays or one structured array. Multiple arrays can be put into a named tuple for convenience.

Execute only the first signal

    from collections import namedtuple

    Memory = namedtuple("Memory", ["signal_executed"])

    @njit
    def signal_func_nb(c, entries, exits, memory):
        is_entry = vbt.pf_nb.select_nb(c, entries)
        is_exit = vbt.pf_nb.select_nb(c, exits)
        if is_entry and not memory.signal_executed[c.col]:  # (1)!
            memory.signal_executed[c.col] = True  # (2)!
            return True, False, False, False
        if is_exit:
            return False, True, False, False
        return False, False, False, False

    def init_memory(target_shape):
        return Memory(
            signal_executed=np.full(target_shape[1], False)  # (3)!
        )

    pf = vbt.PF.from_signals(
        ...,
        entries=entries,
        exits=exits,
        signal_func_nb=signal_func_nb,
        signal_args=(
            vbt.Rep("entries"),
            vbt.Rep("exits"),
            vbt.RepFunc(init_memory)
        )
    )

1. Read the element under the current column
2. Write the element under the current column
3. Create a boolean array filled with False that has one element per column

If signals are generated dynamically and only a subset of the signals are actually executed, you may want to keep track of all the generated signals for later analysis. For this, use function templates to create global custom arrays and fill those arrays during the simulation.

Place entries and exits randomly and access them outside the simulation

    custom_arrays = dict()

    def create_entries_out(wrapper):  # (1)!
        entries_out = np.full(wrapper.shape_2d, False)
        custom_arrays["entries"] = entries_out  # (2)!
        return entries_out

    def create_exits_out(wrapper):
        exits_out = np.full(wrapper.shape_2d, False)
        custom_arrays["exits"] = exits_out
        return exits_out

    @njit
    def signal_func_nb(c, entry_prob, exit_prob, entries_out, exits_out):
        entry_prob_now = vbt.pf_nb.select_nb(c, entry_prob)
        exit_prob_now = vbt.pf_nb.select_nb(c, exit_prob)
        if np.random.uniform(0, 1) < entry_prob_now:
            is_entry = True
            entries_out[c.i, c.col] = True  # (3)!
        else:
            is_entry = False
        if np.random.uniform(0, 1) < exit_prob_now:
            is_exit = True
            exits_out[c.i, c.col] = True
        else:
            is_exit = False
        return is_entry, is_exit, False, False

    pf = vbt.PF.from_signals(
        ...,
        signal_func_nb=signal_func_nb,
        signal_args=(
            vbt.Rep("entry_prob"),
            vbt.Rep("exit_prob"),
            vbt.RepFunc(create_entries_out),  # (4)!
            vbt.RepFunc(create_exits_out),
        ),
        broadcast_named_args=dict(
            entry_prob=0.1,
            exit_prob=0.1
        )
    )

    print(custom_arrays)

1. Function to create an empty NumPy array. Wrapper contains the shape of the simulation.
2. Store the reference to the array for later use
3. Write information to the array
4. Use a template to call the function once the shape of the simulation is known

To limit the number of active positions within a group, in a custom signal function, disable any entry signal whenever the number has been reached. The exit signal should be allowed to be executed at any time.

Allow at most one active position at a time

    @njit
    def signal_func_nb(c, entries, exits, max_active_positions):
        is_entry = vbt.pf_nb.select_nb(c, entries)
        is_exit = vbt.pf_nb.select_nb(c, exits)
        n_active_positions = vbt.pf_nb.get_n_active_positions_nb(c)
        if n_active_positions >= max_active_positions:
            return False, is_exit, False, False  # (1)!
        return is_entry, is_exit, False, False

    pf = vbt.PF.from_signals(
        ...,
        entries=entries,
        exits=exits,
        signal_func_nb=signal_func_nb,
        signal_args=(vbt.Rep("entries"), vbt.Rep("exits"), 1),
        group_by=True  # (2)!
    )

1. Disable any entry signal
2. Form one group out of all assets. This can be customized.

To activate SL or other stop order after a certain condition, set it initially to infinity and then change the stop value in a callback once the condition is met.

Activate an SL once a condition is met

    @njit
    def adjust_func_nb(c):
        ...
        if condition_met:
            sl_info = c.last_sl_info[c.col]
            if np.isinf(sl_info.stop):
                sl_info.stop = 0.1

    pf = vbt.PF.from_signals(
        ...,
        adjust_func_nb=adjust_func_nb,
        stop_entry_price="fillprice",
        sl_stop=np.inf
    )

To set a ladder dynamically, use `stop_ladder="dynamic"` and then in a callback use the current ladder step to pull information from a custom array and override the stop information with it.

Set a ladder based on ATR multipliers

    @njit
    def adjust_func_nb(c, atr, multipliers, exit_sizes):
        tp_info = c.last_tp_info[c.col]
        if vbt.pf_nb.is_stop_info_ladder_active_nb(tp_info):
            if np.isnan(tp_info["stop"]):
                step = tp_info["step"]
                init_atr = vbt.pf_nb.select_nb(c, atr, i=tp_info["init_idx"])
                tp_info["stop"] = init_atr * multipliers[step]
                tp_info["delta_format"] = vbt.pf_enums.DeltaFormat.Absolute
                tp_info["exit_size"] = exit_sizes[step]
                tp_info["exit_size_type"] = vbt.pf_enums.SizeType.Percent

    pf = vbt.PF.from_signals(
        ...,
        adjust_func_nb=adjust_func_nb,
        adjust_args=(
            vbt.Rep("atr"),
            np.array([1, 2]),
            np.array([0.5, 1.0])
        ),
        stop_ladder="dynamic",
        broadcast_named_args=dict(atr=atr)
    )

Position metrics such as the current open P&L and return are available via the `last_pos_info` context field, which is an array with one record per column and the data type trade_dt.

By hitting an unrealized profit of 100%, lock in 50% of it with SL

    @njit
    def adjust_func_nb(c, x, y):  # (1)!
        pos_info = c.last_pos_info[c.col]
        if pos_info["status"] == vbt.pf_enums.TradeStatus.Open:
            if pos_info["return"] >= x:
                sl_info = c.last_sl_info[c.col]
                if not vbt.pf_nb.is_stop_info_active_nb(sl_info):
                    entry_price = pos_info["entry_price"]
                    if vbt.pf_nb.in_long_position_nb(c):
                        x_price = entry_price * (1 + x)  # (2)!
                        y_price = entry_price * (1 + y)  # (3)!
                    else:
                        x_price = entry_price * (1 - x)
                        y_price = entry_price * (1 - y)
                    vbt.pf_nb.set_sl_info_nb(
                        sl_info,
                        init_idx=c.i,
                        init_price=x_price,
                        stop=y_price,
                        delta_format=vbt.pf_enums.DeltaFormat.Target
                    )

    pf = vbt.PF.from_signals(
        ...,
        adjust_func_nb=adjust_func_nb,
        adjust_args=(1.0, 0.5)
    )

1. Both arguments (x = return as %, y = SL as %) are expected as single values
2. SL is activated at the price where `x` was hit
3. The target price of SL is calculated relatively to the entry price to make `x` and `y` share the same scale (10% and 10% -> 0% final return)

To dynamically determine and apply an optimal position size, create an empty size array full of NaN, and in a callback, compute the target size and write it to the size array.

Risk only 1% of the cash balance with each trade

    @njit
    def adjust_func_nb(c, size, sl_stop, delta_format, risk_amount):
        close_now = vbt.pf_nb.select_nb(c, c.close)
        sl_stop_now = vbt.pf_nb.select_nb(c, sl_stop)
        delta_format_now = vbt.pf_nb.select_nb(c, delta_format)
        risk_amount_now = vbt.pf_nb.select_nb(c, risk_amount)
        free_cash_now = vbt.pf_nb.get_free_cash_nb(c)

        stop_price = vbt.pf_nb.resolve_stop_price_nb(
            init_price=close_now,
            stop=sl_stop_now,
            delta_format=delta_format_now,
            hit_below=True
        )
        price_diff = abs(close_now - stop_price)
        size[c.i, c.col] = risk_amount_now * free_cash_now / price_diff

    pf = vbt.PF.from_signals(
        ...,
        adjust_func_nb=adjust_func_nb,
        adjust_args=(
            vbt.Rep("size"),
            vbt.Rep("sl_stop"),
            vbt.Rep("delta_format"),
            vbt.Rep("risk_amount")
        ),
        size=vbt.RepFunc(lambda wrapper: np.full(wrapper.shape_2d, np.nan)),
        sl_stop=0.1,
        delta_format="percent",
        broadcast_named_args=dict(risk_amount=0.01)
    )

To check at the end of the bar whether a signal has been executed, use `post_signal_func_nb` or `post_segment_func_nb`. The former is called right after an order was executed and can access information on the result of the executed order (`c.order_result`). The latter is called after all the columns in the current group were processed (just one column if there's no grouping), cash deposits and earnings were applied, and the portfolio value and returns were updated.

Apply a 20% tax on any positive P&L generated from closing out a position

    @njit
    def post_signal_func_nb(c, cash_earnings, tax):
        if vbt.pf_nb.order_closed_position_nb(c):
            pos_info = c.last_pos_info[c.col]
            pnl = pos_info["pnl"]
            if pnl > 0:
                cash_earnings[c.i, c.col] = -tax * pnl

    pf = vbt.PF.from_signals(
        ...,
        post_signal_func_nb=post_signal_func_nb,
        post_signal_args=(vbt.Rep("cash_earnings"), 0.2),
        cash_earnings=vbt.RepEval("np.full(wrapper.shape_2d, 0.0)")
    )

To be able to access the running total return of the simulation, create an empty array for cumulative returns and populate it inside the `post_segment_func_nb` callback. The same array accessed by other callbacks can be used to get the total return at any time step.

Access the running total return from within a simulation

    @njit
    def adjust_func_nb(c, cum_return):
        if c.cash_sharing:
            total_return = cum_return[c.group] - 1
        else:
            total_return = cum_return[c.col] - 1
        ...  # (1)!

    @njit
    def post_segment_func_nb(c, cum_return):
        if c.cash_sharing:
            cum_return[c.group] *= 1 + c.last_return[c.group]
        else:
            for col in range(c.from_col, c.to_col):
                cum_return[col] *= 1 + c.last_return[col]

    cum_return = None
    def init_cum_return(wrapper):
        global cum_return
        if cum_return is None:
            cum_return = np.full(wrapper.shape_2d[1], 1.0)
        return cum_return

    pf = vbt.PF.from_signals(
        ...,
        adjust_func_nb=adjust_func_nb,
        adjust_args=(vbt.RepFunc(init_cum_return),),
        post_segment_func_nb=post_segment_func_nb,
        post_segment_args=(vbt.RepFunc(init_cum_return),),
    )

1. Your logic here

The same procedure can be applied to access the running trade records of the simulation.

Access the running trade records from within a simulation

    from collections import namedtuple

    TradeMemory = namedtuple("TradeMemory", ["trade_records", "trade_counts"])

    @njit
    def adjust_func_nb(c, trade_memory):
        trade_count = trade_memory.trade_counts[c.col]
        trade_records = trade_memory.trade_records[:trade_count, c.col]
        ...  # (1)!

    @njit
    def post_signal_func_nb(c, trade_memory):
        if vbt.pf_nb.order_filled_nb(c):
            exit_trade_records = vbt.pf_nb.get_exit_trade_records_nb(c)
            trade_memory.trade_records[:len(exit_trade_records), c.col] = exit_trade_records
            trade_memory.trade_counts[c.col] = len(exit_trade_records)

    trade_memory = None
    def init_trade_memory(target_shape):
        global trade_memory
        if trade_memory is None:
            trade_memory = TradeMemory(
                trade_records=np.empty(target_shape, dtype=vbt.pf_enums.trade_dt),  # (2)!
                trade_counts=np.full(target_shape[1], 0)
            )
        return trade_memory

    pf = vbt.PF.from_random_signals(
        ...,
        post_signal_func_nb=post_signal_func_nb,
        post_signal_args=(vbt.RepFunc(init_trade_memory),),
    )

1. Your logic here
2. Create an array of the full shape to accommodate the worst case of one trade per bar. If you expect fewer trades, you can limit the number of rows in that shape.

## Records

There are various ways to examine the orders, trades, and positions generated by a simulation. They all represent different concepts in vectorbtpro, make sure to learn their differences by reading the examples listed at the top of the trades module.

Print out information on various records

    print(pf.orders.records_readable)  # (1)!
    print(pf.entry_trades.records_readable)  # (2)!
    print(pf.exit_trades.records_readable)  # (3)!
    print(pf.trades.records_readable)  # (4)!
    print(pf.positions.records_readable)  # (5)!

    print(pf.trade_history)  # (6)!

1. Information on orders
2. Information on trades that open or increase a position
3. Information on trades that close or decrease a position
4. Same as `exit_trades` by default
5. Information on positions
6. Information on orders and trades merged together

## Metrics

The default year frequency is 365 days, which also assumes that a trading day spans over 24 hours, but when trading stocks or other securities it must be changed to 252 days or less. Also, you must account for trading hours when dealing with a sub-daily data frequency.

Change the year frequency

    vbt.settings.returns.year_freq = "auto"  # (1)!

    vbt.settings.returns.year_freq = "252 days"  # (2)!
    vbt.settings.returns.year_freq = pd.Timedelta(days=252)  # (3)!
    vbt.settings.returns.year_freq = pd.offsets.BDay() * 252  # (4)!
    vbt.settings.returns.year_freq = pd.Timedelta(hours=6.5) * 252  # (5)!

    returns_df.vbt.returns(year_freq="252 days").stats()  # (6)!
    pf = vbt.PF.from_signals(..., year_freq="252 days")  # (7)!

1. Determine the year frequency automatically
2. Change the year frequency to 252 days. Assumes daily frequency.
3. Same as above as a timedelta
4. Same as above as a date offset (business days)
5. Account for 6.5 trading hours per day. Assumes intra-daily frequency.
6. Year frequency can also be set locally per DataFrame
7. Year frequency can also be passed directly to the portfolio

Info

The year frequency will be divided by the frequency of your data to get the annualization factor. For example, `pd.Timedelta(hours=6.5) * 252` divided by `15 minutes` will yield a factor of 6552.

To instruct VBT to put zero instead of infinity and NaN in any generated returns, create a configuration file (such as `vbt.config`) with the following content:

    [returns]
    inf_to_nan = True
    nan_to_zero = True

Note

If there is no change, run `vbt.clear_pycache()` and restart the kernel.

To compute a metric based on the returns or other time series of each trade rather than the entire equity, use projections to extract the time series range that corresponds to the trade.

Calculate the average total log return of winning and losing trades

    winning_trade_returns = pf.trades.winning.get_projections(pf.log_returns, rebase=False)
    losing_trade_returns = pf.trades.losing.get_projections(pf.log_returns, rebase=False)
    avg_winning_trade_return = vbt.pd_acc.returns(winning_trade_returns, log_returns=True).total().mean()
    avg_losing_trade_return = vbt.pd_acc.returns(losing_trade_returns, log_returns=True).total().mean()

To compute a trade metric in pure Numba: convert order records into trade records, calculate the column map for the trade records, and then reduce each column into a single number.

Calculate trade win rate in Numba

    order_records = sim_out.order_records  # (1)!

    order_col_map = vbt.rec_nb.col_map_nb(
        order_records["col"],
        close.shape[1]  # (2)!
    )
    trade_records = vbt.pf_nb.get_exit_trades_nb(
        order_records,
        close,
        order_col_map
    )
    trade_col_map = vbt.rec_nb.col_map_nb(
        trade_records["col"],
        close.shape[1]
    )
    win_rate = vbt.rec_nb.reduce_mapped_nb(
        trade_records["pnl"],
        trade_col_map,
        np.nan,
        vbt.pf_nb.win_rate_reduce_nb
    )

1. Simulation output is an output of a simulation function such as `from_signals_nb`
2. Close must be a two-dimensional NumPy array

Same goes for drawdown records, which are based on cumulative returns.

Calculate maximum drawdown duration in Numba

    returns = sim_out.in_outputs.returns

    cum_returns = vbt.ret_nb.cum_returns_nb(returns)  # (1)!
    drawdown_records = vbt.nb.get_drawdowns_nb(None, None, None, cum_returns)
    dd_duration = vbt.nb.range_duration_nb(  # (2)!
        drawdown_records["start_idx"],
        drawdown_records["end_idx"],
        drawdown_records["status"]
    )
    dd_col_map = vbt.rec_nb.col_map_nb(
        drawdown_records["col"],
        returns.shape[1]
    )
    max_dd_duration = vbt.rec_nb.reduce_mapped_nb(  # (3)!
        dd_duration,
        dd_col_map,
        np.nan,
        vbt.nb.max_reduce_nb
    )

1. Expects a two-dimensional NumPy array
2. Returns one value per record
3. Returns one value per column

Return metrics aren't based on records but can be calculated directly from returns.

Calculate various return metrics in Numba

    returns = sim_out.in_outputs.returns

    total_return = vbt.ret_nb.cum_returns_final_nb(returns, start_value=0)  # (1)!
    max_dd = vbt.ret_nb.max_drawdown_nb(returns)  # (2)!
    sharpe_ratio = vbt.ret_nb.sharpe_ratio_nb(returns, ann_factor=ann_factor)  # (3)!

1. Expects a two-dimensional NumPy array
2. Returns one value per column
3. Annualization factor is the average number of bars in a year, such as 365

## Metadata

The columns and groups of the portfolio can be accessed via its wrapper and grouper respectively.

Various ways to get the columns and groups of a Portfolio instance

    print(pf.wrapper.columns)  # (1)!
    print(pf.wrapper.grouper.is_grouped())  # (2)!
    print(pf.wrapper.grouper.grouped_index)  # (3)!
    print(pf.wrapper.get_columns())  # (4)!

    columns_or_groups = pf.wrapper.get_columns()
    first_pf = pf[columns_or_groups[0]]  # (5)!

1. Get the columns regardless of any grouping
2. Check whether the object is grouped
3. If the above is True, get the groups
4. Get the columns or groups (if grouped). Recommended.
5. Select the first column or group (if grouped) from the object

## Stacking

Multiple compatible array-based strategies can be put into the same portfolio by stacking their respective arrays along columns.

Simulate and analyze multiple strategies jointly

    strategy_keys = pd.Index(["strategy1", "strategy2"], name="strategy")
    entries = pd.concat((entries1, entries2), axis=1, keys=strategy_keys)
    exits = pd.concat((exits1, exits2), axis=1, keys=strategy_keys)
    pf = vbt.PF.from_signals(data, entries, exits)

Multiple incompatible strategies such as those that require different simulation methods or argument combinations can be simulated independently and then stacked for joint analysis. This will combine their data, order records, initial states, in-output arrays, and more, as if they were stacked prior to the simulation with grouping disabled.

Simulate multiple strategies separately but analyze them jointly

    strategy_keys = pd.Index(["strategy1", "strategy2"], name="strategy")
    pf1 = vbt.PF.from_signals(data, entries, exits)
    pf2 = vbt.PF.from_orders(data, size, price)
    pf = vbt.PF.column_stack((pf1, pf2), wrapper_kwargs=dict(keys=strategy_keys))

    # ______________________________________________________________

    pf = vbt.PF.column_stack(
        (pf1, pf2),
        wrapper_kwargs=dict(keys=strategy_keys),
        group_by=strategy_keys.name  # (1)!
    )

1. Represent each portfolio as a group in the new portfolio

#### Cookie consent

We use cookies to recognize your repeated visits and preferences, as well as to measure the effectiveness of our documentation and whether users find what they're searching for. With your consent, you're helping us to make our documentation better.

- Google Analytics
- GitHub

Manage settings