# Backtesting engine (/features/backtesting/backtesting-engine)

## Portfolio continuation \[#portfolio-continuation]

New in v2026.9.5

(Recently added)

✅ Pick up right where your simulation left off as new data arrives. VBT carries positions, order IDs,
and stop state into each update, including entry timestamps and trailing highs. Your trailing stops
remember their peaks, your time stops keep counting, and each update returns a new portfolio with the
combined history.

=== "Trailing stop"
    ```python title="Preserve a trailing stop across two updates"
    >>> close = pd.Series(
    ...     [100.0, 110.0, 108.0, 98.0, 103.0, 115.0],
    ...     index=pd.date_range("2026-09-01", periods=6, freq="D")
    ... )
    >>> entries = pd.Series([True, False, False, False, True, False], index=close.index)
    >>> pf_kwargs = dict(size=1, init_cash=1000, tsl_stop=0.05, freq="1D")

    >>> pf = vbt.PF.from_signals(
    ...     close.iloc[:2],
    ...     entries=entries.iloc[:2],
    ...     attach_preparer=True,  # (1)
    ...     **pf_kwargs,
    ... )
    >>> for start in [2, 4]:
    ...     pf = pf.update(  # (2)
    ...         close.iloc[start:start + 2],
    ...         entries=entries.iloc[start:start + 2],
    ...     )

    >>> print(pf.orders.readable[["Order Id", "Fill Index", "Side", "Price", "Stop Type"]])
       Order Id Fill Index  Side  Price Stop Type
    0         0 2026-09-01   Buy  100.0      None
    1         1 2026-09-04  Sell   98.0       TSL
    2         2 2026-09-05   Buy  103.0      None

    >>> full_pf = vbt.PF.from_signals(close, entries=entries, **pf_kwargs)  # (3)
    >>> pf.orders.records.equals(full_pf.orders.records) and pf.value.equals(full_pf.value)
    True
    ```

    1.  Keep the simulation arguments available for subsequent updates.
    2.  Preserve the $110 trailing high across updates. The 5% stop exits at $98, the next available close below the threshold. Updates join the history by default. Use `stack=False` to return only the new segment.
    3.  Compare the combined order records and portfolio value with a single run over the full dataset.

=== "Time stop"
    ```python title="Preserve a three-day time stop across an update"
    >>> close = pd.Series(
    ...     [100.0, 110.0, 108.0, 98.0, 103.0, 115.0],
    ...     index=pd.date_range("2026-09-01", periods=6, freq="D")
    ... )
    >>> entries = pd.Series([True, False, False, False, True, False], index=close.index)

    >>> pf = vbt.PF.from_signals(
    ...     close.iloc[:2],
    ...     entries=entries.iloc[:2],
    ...     td_stop="3 days",
    ...     size=1,
    ...     init_cash=1000,
    ...     attach_preparer=True,
    ...     freq="1D",
    ... )
    >>> pf = pf.update(close.iloc[2:], entries=entries.iloc[2:])  # (1)

    >>> print(pf.orders.readable[["Fill Index", "Side", "Stop Type"]])
      Fill Index  Side Stop Type
    0 2026-09-01   Buy      None
    1 2026-09-04  Sell        TD
    2 2026-09-05   Buy      None
    ```

    1.  The entry timestamp remains September 1, so the three-day stop exits on September 4. The next entry on September 5 starts a new holding period.

!!! info "Tutorial"
    Learn more in the
    [Live simulation](https://members.vectorbt.pro/tutorials/from-python-to-rust/live/) tutorial.

## Chaining simulations \[#chaining-simulations]

New in v2026.3.1

✅ Every built-in simulation function now returns the final simulation state after processing all data,
including the last cash, position, pending order(s), and other relevant information. This state can be passed
to the next simulation run, allowing you to continue from where the previous run ended. This enables seamless
chaining of simulations across different time periods or datasets.

=== "Example 1: Monthly"
    ```python title="Simulate a SMA crossover strategy on a monthly basis"
    >>> data = vbt.BinanceData.pull(  # (1)
    ...     "BTCUSDT",
    ...     start="one year ago",
    ...     timeframe="5 minutes",
    ...     cache=True
    ... )

    >>> fast_sma = data.run("talib_func:sma", timeperiod=20)  # (2)
    >>> 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)

    >>> single_pf = vbt.PF.from_signals(  # (3)
    ...     data,
    ...     long_entries=long_entries,
    ...     short_entries=short_entries,
    ... )

    >>> data_splits = data.split(by="month")  # (4)
    >>> long_entries_splits = long_entries.vbt.split(by="month", into=None)
    >>> short_entries_splits = short_entries.vbt.split(by="month", into=None)

    >>> pf_list = []
    >>> last_state = None
    >>> for i in range(len(data_splits)):  # (5)
    ...     pf = vbt.PF.from_signals(
    ...         data_splits.iloc[i],
    ...         long_entries=long_entries_splits.iloc[i],
    ...         short_entries=short_entries_splits.iloc[i],
    ...         last_state=last_state,
    ...     )
    ...     pf_list.append(pf)
    ...     last_state = pf.last_state

    >>> stacked_pf = vbt.PF.row_stack(*pf_list, chained=True)  # (6)
    >>> print(stacked_pf.returns.equals(single_pf.returns))
    True
    ```

    1.  Pull 5-minute BTC data for the past year from Binance and cache it for faster access.
    2.  Generate long and short entry signals for full dataset based on a simple SMA crossover strategy.
    3.  Split the data and signals by month to run simulations on a monthly basis.
    4.  Iterate through each month, running a simulation with the corresponding data and signals,
        and passing the last state from the previous simulation to the next one.
    5.  Row-stack the resulting portfolio objects together, enabling seamless continuation across months.
    6.  The resulting portfolio is identical to running a single simulation on the full dataset.

=== "Example 2: Live data stream"
    ```python title="Simulate a SMA crossover strategy on a live data stream"
    >>> def generate_signals(data):
    ...     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)
    ...     return long_entries, short_entries

    >>> data = vbt.BinanceData.pull(  # (1)
    ...     "BTCUSDT",
    ...     start="1 day ago",
    ...     end="1 minute ago",
    ...     timeframe="1 minute"
    ... )
    >>> long_entries, short_entries = generate_signals(data)
    >>> pf = vbt.PF.from_signals(  # (2)
    ...     data,
    ...     long_entries=long_entries,
    ...     short_entries=short_entries,
    ...     attach_preparer=True
    ... )

    >>> with vbt.ProgressBar() as pbar:
    ...     while True:
    ...         try:
    ...             vbt.wait("1 minute", floor=True)  # (3)
    ...         except KeyboardInterrupt:
    ...             break
    ...
    ...         n_bars = len(data.index)
    ...         data = data.update()  # (4)
    ...         if len(data.index) == n_bars:
    ...             pbar.set_postfix(str(data.index[-1]))
    ...             pbar.update()
    ...             continue
    ...
    ...         long_entries, short_entries = generate_signals(data)
    ...         new_pf = pf.update(  # (5)
    ...             data.iloc[n_bars:],
    ...             long_entries=long_entries.iloc[n_bars:],
    ...             short_entries=short_entries.iloc[n_bars:],
    ...             stack=False
    ...         )
    ...         new_orders = new_pf.orders.readable
    ...         if len(new_orders) > 0:  # (6)
    ...             for i in range(len(new_orders)):
    ...                 print("NEW ORDER:")
    ...                 print(new_orders.iloc[i])
    ...                 print()
    ...
    ...         pf = vbt.PF.row_stack(  # (7)
    ...             (pf, new_pf),
    ...             chained=True,
    ...             preparer=new_pf.preparer
    ...         )
    ...         pbar.set_postfix(str(data.index[-1]))
    ...         pbar.update()
    ```

    1.  Pull 1-minute BTC data for the past day from Binance.
        Don't include the most recent minute to avoid incomplete data.
    2.  Run an initial simulation on the full dataset and attach the preparer
        to access the original simulation arguments later.
    3.  Wait for the next minute to start, ensuring that we have new data to work with.
        Flooring the wait time ensures that we align with the start of the minute.
    4.  Update the data to get the latest bars. If no new bars are available, skip the rest of the loop.
    5.  Update the portfolio with the new data and signals,
        without stacking it with the previous portfolio to keep it separate.
    6.  Check for new orders generated by the update and print them out.
    7.  Row-stack the new portfolio with the previous one, enabling seamless continuation
        while keeping the history of the simulation intact.

## Full callback support \[#full-callback-support]

New in v2025.12.31

✅ Portfolio simulation method based on signals now fully supports callbacks at every step of the process.
This includes pre-processing and post-processing callbacks for the simulation as a whole, as well as
per-group and per-segment callbacks, and even an order modification callback. This allows you to customize
the simulation behavior to a great extent.

```python title="DCA in $100 every month until 2x in profit, take out initial investment, and DCA out"
>>> DCAMode = namedtuple("DCAMode", ["In", "Out"])(0, 1)  # (1)

>>> @njit
... def pre_group_func_nb(ctx):  # (2)
...     total_deposited = np.full(1, 0.0)
...     dca_mode = np.full(1, DCAMode.In)
...     return (total_deposited, dca_mode)

>>> @njit
... def pre_segment_func_nb(ctx, total_deposited, dca_mode, cash_deposits, dca_amount):  # (3)
...     if ctx.i == 0 or vbt.dt_nb.month_nb(ctx.index[ctx.i - 1]) != vbt.dt_nb.month_nb(ctx.index[ctx.i]):
...         dca_amount_now = vbt.pf_nb.select_from_group_nb(ctx, ctx.group, dca_amount)
...         if dca_mode[0] == DCAMode.In and ctx.track_cash_deposits:
...             cash_deposits[ctx.i, ctx.group] = dca_amount_now
...             total_deposited[0] += dca_amount_now
...     else:
...         dca_amount_now = 0.0
...     return (total_deposited, dca_mode, dca_amount_now)

>>> @njit
... def signal_func_nb(ctx, total_deposited, dca_mode, dca_amount_now, size):  # (4)
...     if dca_amount_now > 0:
...         size[ctx.i, ctx.col] = dca_amount_now
...         if dca_mode[0] == DCAMode.In:
...             return True, False, False, False
...         return False, True, False, False
...     return False, False, False, False

>>> @njit
... def post_order_func_nb(ctx, total_deposited, dca_mode, dca_amount_now):  # (5)
...     if dca_mode[0] == DCAMode.In:
...         if vbt.pf_nb.order_increased_position_nb(ctx):
...             tp_info = ctx.last_tp_info[ctx.col]
...             tp_info["stop"] = 1.0
...             tp_info["init_price"] = ctx.last_pos_info[ctx.col]["entry_price"]
...             tp_info["exit_size"] = total_deposited[0]
...             tp_info["exit_size_type"] = vbt.pf_enums.SizeType.Value
...         elif vbt.pf_nb.get_last_order_nb(ctx)["stop_type"] == vbt.pf_enums.StopType.TP:
...             dca_mode[0] = DCAMode.Out

>>> pf = vbt.PF.from_signals(
...     vbt.YFData.pull("AAPL", start="2018"),
...     pre_group_func_nb=pre_group_func_nb,
...     pre_segment_func_nb=pre_segment_func_nb,
...     pre_segment_args=(
...         vbt.Rep("cash_deposits"),
...         vbt.Rep("dca_amount")
...     ),
...     signal_func_nb=signal_func_nb,
...     signal_args=(
...         vbt.Rep("size"),
...     ),
...     post_order_func_nb=post_order_func_nb,
...     broadcast_named_args=dict(dca_amount=100),
...     arg_config=dict(
...         cash_deposits=dict(full_shape=True),  # (6)
...         size=dict(full_shape=True)
...     ),
...     accumulate=True,
...     size_type="value",
...     cash_sharing=True
... )
>>> pf.plot_orders().show()
```

1.  Define DCA modes as an enum for better readability.
2.  Pre-group callback to initialize group-level variables: total cash deposited and DCA mode.
    This callback is called before processing each group (asset in this case because grouping is disabled).
    The variables are returned as a tuple to be passed to the pre-segment callback.
3.  Pre-segment callback to handle DCA deposits at the start of each month. This callback is called
    for each group at the beginning of each bar, before generating signals for each asset in the group.
    The variables that are required by either the signal or post-order callbacks are returned as a tuple.
4.  Signal callback to generate buy or sell signals based on the DCA amount for the current segment.
    If in DCA-in mode, a buy signal is generated; otherwise, a sell signal is generated.
    The order size is set to the DCA amount.
5.  Post-order callback to handle take-profit orders and switch DCA modes. If a buy order was executed,
    our take-profit order is updated to exit the initial investment when the position doubles in price.
    If the take-profit order was executed, the DCA mode is switched to DCA-out.
6.  Specify that cash deposits and size should have the full shape of the portfolio
    (number of bars x number of assets) such that they can be modified in the callbacks.

AAPL OHLC with monthly DCA orders that recover the initial investment and then DCA out. [Figure data (JSON)](/assets/figures/features/portfolio/dca-callbacks.fd5b757bbe5a.json)

!!! info "Documentation"
    Learn more in the [Documentation](/documentation/) → Portfolio → From signals → Callbacks.

## Portfolio preparers \[#portfolio-preparers]

New in 1.12.0

✅ When you pass an argument to a simulation method such as `Portfolio.from_signals`,
it goes through a complex preparation pipeline to convert it into a format suitable for Numba.
This pipeline usually involves enum mapping, broadcasting, data type checks, template
substitution, and many other steps. To make VBT more transparent, this pipeline has been
moved to a separate class, giving you full control over the arguments that reach the Numba
functions! You can even extend the preparers to automatically prepare arguments for your
own simulators.

```python title="Get a prepared argument, modify it, and use in a new simulation"
>>> data = vbt.YFData.pull("BTC-USD", end="2017-01")
>>> prep_result = vbt.PF.from_holding(
...     data,
...     stop_ladder="uniform",
...     tp_stop=vbt.Param([
...         [0.1, 0.2, 0.3, 0.4, 0.5],
...         [0.4, 0.5, 0.6],
...     ], keys=["tp_ladder_1", "tp_ladder_2"]),
...     return_prep_result=True  # (1)
... )
>>> prep_result.target_args["tp_stop"]  # (2)
array([[0.1, 0.4],
       [0.2, 0.5],
       [0.3, 0.6],
       [0.4, nan],
       [0.5, nan]])

>>> new_tp_stop = prep_result.target_args["tp_stop"] + 0.1
>>> new_prep_result = prep_result.replace(target_args=dict(tp_stop=new_tp_stop), nested_=True)  # (3)
>>> new_prep_result.target_args["tp_stop"]
array([[0.2, 0.5],
       [0.3, 0.6],
       [0.4, 0.7],
       [0.5, nan],
       [0.6, nan]])

>>> pf = vbt.PF.from_signals(new_prep_result)  # (4)
>>> pf.total_return
tp_stop
tp_ladder_1    0.4
tp_ladder_2    0.6
Name: total_return, dtype: float64

>>> sim_out = new_prep_result.target_func(**new_prep_result.target_args)  # (5)
>>> pf = vbt.PF(sim_out=sim_out, **new_prep_result.pf_args)
>>> pf.total_return
tp_stop
tp_ladder_1    0.4
tp_ladder_2    0.6
Name: total_return, dtype: float64
```

1.  Returns the result of the preparation.
2.  Contains two attributes: target arguments as `target_args` and portfolio arguments as `pf_args`.
3.  Replace the argument. Since it is inside another dictionary (`target_args`), you need to enable
    `nested_`. The result is a new instance of `PFPrepResult`.
4.  Pass the new preparation result as the first argument to the base simulation method.
5.  Or simulate manually!

## Staticization \[#staticization]

New in 1.11.0

✅ One major limitation of Numba is that functions passed as arguments (that is, callbacks)
make the main function uncacheable, forcing it to be recompiled in every new runtime,
again and again. This especially affects the performance of simulator functions, as they can
take up to a minute to compile. Thankfully, there is a new trick available: "staticization".
Here is how it works. First, the source code of a function is annotated with a special syntax.
The annotated code is then extracted (also called "cutting" ✂️), modified into a
cacheable version by removing any callbacks from the arguments, and saved to a Python file.
Once the function is called again, the cacheable version is executed. Sound complicated?
Take a look below!

```python title="Define the signal function in a Python file, here signal_func_nb.py"
from vectorbtpro import *

@njit
def signal_func_nb(ctx, fast_sma, slow_sma):  # (1)
    long = vbt.pf_nb.iter_crossed_above_nb(ctx, fast_sma, slow_sma)
    short = vbt.pf_nb.iter_crossed_below_nb(ctx, fast_sma, slow_sma)
    return long, False, short, False
```

1.  Make sure the function is named exactly the same as the callback argument.

```python title="Run a staticized simulation. Running the script again won't re-compile."
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_signals(
...     data,
...     signal_func_nb="signal_func_nb.py",  # (1)
...     signal_args=(vbt.Rep("fast_sma"), vbt.Rep("slow_sma")),
...     broadcast_named_args=dict(
...         fast_sma=data.run("sma", 20, hide_params=True, unpack=True),
...         slow_sma=data.run("sma", 50, hide_params=True, unpack=True)
...     ),
...     staticized=True  # (2)
... )
```

1.  Path to the module where the function is located.
2.  Handles all the magic ✨

## Pre-computation \[#pre-computation]

New in 1.0.10

✅ There is a tradeoff between memory usage and execution speed: a dataset with 1000 columns is
usually processed much faster than processing a 1-column dataset 1000 times. However, the first
dataset also requires 1000 times more memory than the second. That's why, during the simulation phase,
VBT primarily generates orders, while other portfolio attributes such as balances, equity, and returns
are reconstructed later during the analysis phase if the user needs them. For cases where performance
is the main concern, arguments are now available that let you pre-compute these attributes during
simulation! ⏩

```python title="Benchmark a random portfolio with 1000 columns without and with pre-computation"
>>> data = vbt.YFData.pull("BTC-USD")
```

```python
>>> %%timeit  # (1)
>>> for n in range(1000):
...     pf = vbt.PF.from_random_signals(data, n=n, seed=42, save_returns=False)
...     pf.sharpe_ratio
15 s ± 829 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

1.  Good for RAM, bad for performance.

```python
>>> %%timeit
>>> pf = vbt.PF.from_random_signals(data, n=np.arange(1000).tolist(), seed=42, save_returns=False)
>>> pf.sharpe_ratio
855 ms ± 6.26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

```python
>>> %%timeit
>>> pf = vbt.PF.from_random_signals(data, n=np.arange(1000).tolist(), seed=42, save_returns=True)
>>> pf.sharpe_ratio
593 ms ± 7.07 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

## In-place outputs \[#in-place-outputs]

New in 1.0.0

✅ The portfolio can now accept and return any user-defined arrays filled during simulation,
such as signals. In-place output arrays can broadcast together with regular arrays using templates and
broadcastable named arguments. Additionally, VBT will (semi-)automatically determine how to correctly
wrap and index each array, for example, whenever you select a column from the entire portfolio.

```python title="Track the debt of a random portfolio during the simulation"
>>> data = vbt.YFData.pull(["BTC-USD", "ETH-USD"], missing_index="drop")
>>> size = data.symbol_wrapper.fill(np.nan)
>>> np.random.seed(42)
>>> rand_indices = np.random.choice(np.arange(len(size)), 10)
>>> size.iloc[rand_indices[0::2]] = -np.inf
>>> size.iloc[rand_indices[1::2]] = np.inf

>>> @njit
... def post_segment_func_nb(ctx):
...     for col in range(ctx.from_col, ctx.to_col):
...         col_debt = ctx.last_debt[col]
...         ctx.in_outputs.debt[ctx.i, col] = col_debt
...         if col_debt > ctx.in_outputs.max_debt[col]:
...             ctx.in_outputs.max_debt[col] = col_debt

>>> pf = vbt.PF.from_def_order_func(
...     data.close,
...     size=size,
...     post_segment_func_nb=post_segment_func_nb,
...     in_outputs=dict(
...         debt=vbt.RepEval("np.empty_like(close)"),
...         max_debt=vbt.RepEval("np.full(close.shape[1], 0.)")
...     )  # (1)
... )
>>> pf.get_in_output("debt")  # (2)
symbol                       BTC-USD    ETH-USD
Date
2017-11-09 00:00:00+00:00   0.000000   0.000000
2017-11-10 00:00:00+00:00   0.000000   0.000000
2017-11-11 00:00:00+00:00   0.000000   0.000000
2017-11-12 00:00:00+00:00   0.000000   0.000000
2017-11-13 00:00:00+00:00   0.000000   0.000000
...                              ...        ...
2023-02-08 00:00:00+00:00  43.746892  25.054571
2023-02-09 00:00:00+00:00  43.746892  25.054571
2023-02-10 00:00:00+00:00  43.746892  25.054571
2023-02-11 00:00:00+00:00  43.746892  25.054571
2023-02-12 00:00:00+00:00  43.746892  25.054571

[1922 rows x 2 columns]

>>> pf.get_in_output("max_debt")  # (3)
symbol
BTC-USD    75.890464
ETH-USD    25.926328
Name: max_debt, dtype: float64
```

1.  Instruct the portfolio class to wait until all arrays are broadcast and create a new floating array of the final shape.
2.  The portfolio instance knows how to properly wrap a custom NumPy array as a pandas object.
3.  The same applies to reduced NumPy arrays.

## Flexible attributes \[#flexible-attributes]

New in 1.0.0

✅ Portfolio attributes can now be partially or even fully computed from user-defined arrays.
This gives you greater control over post-simulation analysis, such as overriding some simulation data,
testing hyperparameters without re-simulating the entire portfolio, or avoiding repeated reconstruction
when caching is disabled.

```python title="Compute the net exposure by caching its components"
>>> data = vbt.YFData.pull("BTC-USD")
>>> pf = vbt.PF.from_random_signals(data.close, n=100, seed=42)
>>> value = pf.get_value()  # (1)
>>> long_exposure = vbt.PF.get_gross_exposure(  # (2)
...     asset_value=pf.get_asset_value(direction="longonly"),
...     value=value,
...     wrapper=pf.wrapper
... )
>>> short_exposure = vbt.PF.get_gross_exposure(
...     asset_value=pf.get_asset_value(direction="shortonly"),
...     value=value,
...     wrapper=pf.wrapper
... )
>>> del value  # (3)
>>> net_exposure = vbt.PF.get_net_exposure(
...     long_exposure=long_exposure,
...     short_exposure=short_exposure,
...     wrapper=pf.wrapper
... )
>>> del long_exposure
>>> del short_exposure
>>> net_exposure
Date
2014-09-17 00:00:00+00:00    1.0
2014-09-18 00:00:00+00:00    1.0
2014-09-19 00:00:00+00:00    1.0
2014-09-20 00:00:00+00:00    1.0
2014-09-21 00:00:00+00:00    1.0
                             ...
2023-02-08 00:00:00+00:00    0.0
2023-02-09 00:00:00+00:00    0.0
2023-02-10 00:00:00+00:00    0.0
2023-02-11 00:00:00+00:00    0.0
2023-02-12 00:00:00+00:00    0.0
Freq: D, Length: 3071, dtype: float64
```

1.  Call the instance method to use the data stored in the portfolio.
2.  Call the class method to provide all the data explicitly.
3.  Delete the object as soon as it is no longer needed to free memory.

## Shortcut properties \[#shortcut-properties]

New in 1.0.0

✅ [In-place output arrays](#in-place-outputs) can be used to override regular portfolio attributes. The portfolio
will automatically pick the pre-computed array and perform all future calculations using this array,
avoiding redundant reconstruction.

```python title="Modify the returns from within the simulation"
>>> data = vbt.YFData.pull("BTC-USD")
>>> size = data.symbol_wrapper.fill(np.nan)
>>> np.random.seed(42)
>>> rand_indices = np.random.choice(np.arange(len(size)), 10)
>>> size.iloc[rand_indices[0::2]] = -np.inf
>>> size.iloc[rand_indices[1::2]] = np.inf

>>> @njit
... def post_segment_func_nb(ctx):
...     for col in range(ctx.from_col, ctx.to_col):
...         return_now = ctx.last_return[col]
...         return_now = 0.5 * return_now if return_now > 0 else return_now
...         ctx.in_outputs.returns[ctx.i, col] = return_now

>>> pf = vbt.PF.from_def_order_func(
...     data.close,
...     size=size,
...     size_type="targetpercent",
...     post_segment_func_nb=post_segment_func_nb,
...     in_outputs=dict(
...         returns=vbt.RepEval("np.empty_like(close)")
...     )
... )

>>> pf.returns  # (1)
Date
2014-09-17 00:00:00+00:00    0.000000
2014-09-18 00:00:00+00:00    0.000000
2014-09-19 00:00:00+00:00    0.000000
2014-09-20 00:00:00+00:00    0.000000
2014-09-21 00:00:00+00:00    0.000000
                                  ...
2023-02-08 00:00:00+00:00   -0.015227
2023-02-09 00:00:00+00:00   -0.053320
2023-02-10 00:00:00+00:00   -0.008439
2023-02-11 00:00:00+00:00    0.005569  << modified
2023-02-12 00:00:00+00:00    0.001849  << modified
Freq: D, Length: 3071, dtype: float64

>>> pf.get_returns()  # (2)
Date
2014-09-17 00:00:00+00:00    0.000000
2014-09-18 00:00:00+00:00    0.000000
2014-09-19 00:00:00+00:00    0.000000
2014-09-20 00:00:00+00:00    0.000000
2014-09-21 00:00:00+00:00    0.000000
                                  ...
2023-02-08 00:00:00+00:00   -0.015227
2023-02-09 00:00:00+00:00   -0.053320
2023-02-10 00:00:00+00:00   -0.008439
2023-02-11 00:00:00+00:00    0.011138
2023-02-12 00:00:00+00:00    0.003697
Freq: D, Length: 3071, dtype: float64
```

2.  Pre-computed returns.
3.  Actual returns.
