polakowo / vectorbt

Find your trading edge, using the fastest engine for backtesting, algorithmic trading, and research.
https://vectorbt.dev
Other
4.22k stars 609 forks source link

Stop-Loss Question #181

Closed CMobley7 closed 3 years ago

CMobley7 commented 3 years ago

First, I want to thank you for such a great backtesting library. It's really easy to work with and fast. However, I'm struggling to determine what I'm doing wrong when setting stop losses. I used the following code to generate the portfolio stats below. The worst trade during the period was -4.46%, while the best trade was 20.5%. So, it looks like the take-profit stop was triggered effectively, but I never saw a drop of 20%; so, the stop-loss stop was not. In order to verify that the stop-loss stop would function as expected, I set sl_stop to 3% given the worst trade was previously -4.46%. I expected the worst trade to drop to approximately -3% however, this resulted in a -7.74% worst trade. What am I doing wrong here, or how am I thinking about this problem incorrectly?

buy_triggers = ohlcv["normalized_indicator"].ge(buy_trigger_value)
sell_triggers = ohlcv["normalized_indicator"].le(sell_trigger_value)
entries, exits = buy_triggers.vbt.signals.clean(sell_triggers)
stop_exits = entries.vbt.signals.generate_ohlc_stop_exits(ohlcv['Open'], ohlcv['High'], ohlcv['Low'], ohlcv['Close'], sl_stop=0.2, tp_stop=0.2)
exits = (exits.vbt | stop_exits).vbt.signals.first(reset_by=entries, allow_gaps=True)
portfolio = vbt.Portfolio.from_signals(ohlcv['Open'], entries, exits, size=0.5, size_type="percent", direction="longonly")
print(portfolio.stats())
Start                                            0
End                                          18689
Duration                        1320 days 19:12:00
Max Drawdown [%]                          32.31549
Avg Drawdown [%]                          0.241831
Max Drawdown Duration            134 days 19:07:00
Avg Drawdown Duration    0 days 01:26:02.978389768
Trade Count                                   9370
Best Trade [%]                           20.504278
Worst Trade [%]                          -4.469504
Max Trade Duration                 1 days 18:22:00
Avg Trade Duration       0 days 01:51:21.477054429
buy_triggers = ohlcv["normalized_indicator"].ge(buy_trigger_value)
sell_triggers = ohlcv["normalized_indicator"].le(sell_trigger_value)
entries, exits = buy_triggers.vbt.signals.clean(sell_triggers)
stop_exits = entries.vbt.signals.generate_ohlc_stop_exits(ohlcv['Open'], ohlcv['High'], ohlcv['Low'], ohlcv['Close'], sl_stop=0.03, tp_stop=0.2)
exits = (exits.vbt | stop_exits).vbt.signals.first(reset_by=entries, allow_gaps=True)
portfolio = vbt.Portfolio.from_signals(ohlcv['Open'], entries, exits, size=0.5, size_type="percent", direction="longonly")
print(portfolio.stats())
Start                                            0
End                                          18689
Duration                        1320 days 19:12:00
Max Drawdown [%]                          32.31549
Avg Drawdown [%]                          0.240221
Max Drawdown Duration            135 days 09:09:00
Avg Drawdown Duration    0 days 01:27:08.438429506
Trade Count                                   9370
Best Trade [%]                           20.504278
Worst Trade [%]                           -7.77313
Max Trade Duration                 1 days 18:22:00
Avg Trade Duration       0 days 01:49:22.661686232
polakowo commented 3 years ago

Hi @CMobley7, your code looks legit, but there is a catch. Each of your stop prices is being hit within a bar, but you close each of your trades using the closing price. To close the trade immediately once the stop price has been hit, you need to query the stop price (see out_dict here) and use it as order price. The other issue is that each of your stop prices is based on the opening price, but you open your trades in Portfolio.from_signals using the closing price. In generate_ohlc_stop_exits, open is not necessarily the opening price, but an entry price. Here's a correct approach using the closing price as entry price and the stop/closing price as exit price:

import vectorbt as vbt

data = vbt.YFData.download(
    "BTC-USD",
    start='2017-01-01 UTC',
    end='2020-01-01 UTC'
)
order_price = data.get('Close')
randenex = vbt.RAND.run(data.wrapper.shape[0], n=10)
pf = vbt.Portfolio.from_signals(data.get('Close'), randenex.entries, randenex.exits, price=order_price)  # without SL
pf.stats()
Worst Trade [%]                           -36.2034

out_dict = {}
stop_exits = randenex.entries.vbt.signals.generate_ohlc_stop_exits(
    order_price, 
    data.get('High'),
    data.get('Low'), 
    data.get('Close'), 
    sl_stop=0.1, 
    tp_stop=0.1,
    out_dict=out_dict
)
exits = randenex.exits.vbt | stop_exits
order_price = order_price.copy()
order_price[stop_exits] = out_dict['hit_price'][stop_exits]
pf2 = vbt.Portfolio.from_signals(data.get('Close'), randenex.entries, exits, price=order_price)  # with SL
pf2.stats()
Worst Trade [%]                                -10

Note that since v0.19.0 stop loss is integrated directly into Portfolio.from_signals and it's now the preferred way:

pf3 = vbt.Portfolio.from_signals(
    data.get('Close'), randenex.entries, randenex.exits, 
    open=data.get('Open'), high=data.get('High'), low=data.get('Low'),
    sl_stop=0.1, tp_stop=0.1, stop_entry_price='price')
pf3.stats()
Worst Trade [%]                                -10
CMobley7 commented 3 years ago

@polakowo wow, thank you for your thorough and prompt response! I really appreciate it. I updated my code to Portfolio.from_signals and was seeing approximately the stop losses I set. Thanks again. God bless!

forLinDre commented 2 years ago

@polakowo I'm a little lost on how to access tp_stop and sl_stop prices from the OHLCSTCX signal generator. I have my signal generator set up as shown below. But I'm trying to plot both the tp_stop and sl_stop values on top of my price, indicator, and entry/exit signals. I tried to access these values by using RMA_strat.stop_price.vbt.plot but this does not properly display these values. Is there a way that's built into the signal generator itself?

perf_metrics = ['total_return', 'positions.win_rate', 'positions.expectancy', 'max_drawdown']
perf_metric_names = ['Total return', 'Win rate', 'Expectancy', 'Max drawdown']

RMA_slider = widgets.IntRangeSlider(
    value=[EMA3_span, EMA4_span],
    min=EMA_span_min,
    max=EMA_span_max,
    step=1,
    layout=dict(width='500px'),
    continuous_update=True
)

pct_change_slider = widgets.IntSlider(
    value=pct_change_span,
    min=pct_change_window_min,
    max=pct_change_window_max,
    step=1,
    orientation='horizontal',
    layout=dict(width='500px'),
    continuous_update=True
)

pct_change_thres_slider = widgets.FloatSlider(
    value=pct_change_thres_span,
    min=pct_change_thres_min,
    max=pct_change_thres_max,
    step=0.1,
    orientation='horizontal',
    layout=dict(width='500px'),
    continuous_update=True
)

RMA1_exp_checkbox = widgets.Checkbox(
    value=EMA3_exp,
    description='RMA 1 is exponential',
    continuous_update=True
)

RMA2_exp_checkbox = widgets.Checkbox(
    value=EMA4_exp,
    description='RMA 2 is exponential',
    continuous_update=True
)

sl_stop_pct_slider = widgets.FloatSlider(
    value=sl_stop_span,
    min=sl_stop_min,
    max=sl_stop_max,
    step=0.001,
    orientation='horizontal',
    layout=dict(width='500px'),
    continuous_update=True,
    readout_format='.3f'
)

tp_stop_pct_slider = widgets.FloatSlider(
    value=tp_stop_span,
    min=tp_stop_min,
    max=tp_stop_max,
    step=0.001,
    orientation='horizontal',
    layout=dict(width='500px'),
    continuous_update=True,
    readout_format='.3f'
)

sl_trail_checkbox = widgets.Checkbox(
    value=sl_trail,
    description='Stop Loss is Trailing',
    continuous_update=True
)

metrics_html = widgets.HTML()

chart = None
#xaxis_kwarg = dict(rangebreaks=[dict(pattern='day of week', bounds=['sat', 'mon']), dict(pattern='hour', bounds=[16, 9.5]), dict(values=hols)])
xaxis_kwarg = dict(rangebreaks=[dict(pattern='day of week', bounds=['sat', 'mon']), dict(pattern='hour', bounds=[16, 9.5])])

def RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span):
    global chart

    # Process custom indicator
    RMA_strat_indicator = RMA_Strat_Indicator.run(
        EWM_window1=EMA3_span,
        EWM_window2=EMA4_span,
        ewm1=EMA3_exp,
        ewm2=EMA4_exp,
        change_window=pct_change_span,
        change_thres=pct_change_thres_span,
        param_product=False,
        run_unique=False
    )

    RMA_strat_indicator = RMA_strat_indicator[indicator_mask]

    # Define RMA entry statement
    RMA_strat_entries =\
    (RMA_strat_indicator.pct_change_below(RMA_strat_indicator.change_thres) & RMA_strat_indicator.RMA1_above(RMA_strat_indicator.low) & (pd.Series(RMA_strat_indicator.entry_st.index.tz_localize(None), index=RMA_strat_indicator.entry_st.index).vbt >= RMA_strat_indicator.entry_st) & (pd.Series(RMA_strat_indicator.entry_st.index.tz_localize(None), index=RMA_strat_indicator.entry_st.index).vbt <= RMA_strat_indicator.entry_et))\
    |\
    (RMA_strat_indicator.pct_change_below(RMA_strat_indicator.change_thres) & RMA_strat_indicator.RMA2_above(RMA_strat_indicator.low) & (pd.Series(RMA_strat_indicator.entry_st.index.tz_localize(None), index=RMA_strat_indicator.entry_st.index).vbt >= RMA_strat_indicator.entry_st) & (pd.Series(RMA_strat_indicator.entry_st.index.tz_localize(None), index=RMA_strat_indicator.entry_st.index).vbt <= RMA_strat_indicator.entry_et))

    # Generate exits
    RMA_strat = vbt.OHLCSTCX.run(
    entries=RMA_strat_entries,
    open=Open_analysis, high=High_analysis, low=Low_analysis, close=Close_analysis,
    sl_stop=sl_stop_span, #Percentage value for stop loss.
    sl_trail=sl_trail, #Whether sl_stop is trailing.
    tp_stop=tp_stop_span, #Percentage value for take profit.
    param_product=False,
    run_unique=False,
    )

    RMA_strat_pf = vbt.Portfolio.from_signals(Low_analysis, RMA_strat.new_entries, RMA_strat.exits)

    # Update figure
    if chart is None:
        chart = vbt.make_subplots(
            rows=2,
            cols=1,
            start_cell='top-left',
            shared_xaxes=True,
            row_width=[0.2, 0.8],
            specs=[[{"secondary_y": True}], [{"secondary_y": False}]]
        )

        stock_data_analysis.vbt.ohlcv.plot(
            plot_type='OHLC',
            fig=chart,
            xaxis_rangeslider_visible=False,
            showlegend=True,
            title=interface.stock,
            ohlc_kwargs=dict(
                name=str(interface.stock),
                yaxis='y1',
                showlegend=True
            ),
            ohlc_add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=False
            ),
            volume_kwargs=dict(
                name='Volume',
                yaxis='y2',
                showlegend=True,
                opacity=0.2
            ),
            volume_add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=True
            ),
            xaxis1=xaxis_kwarg,
            xaxis2=dict(
                rangebreaks=[
                    dict(pattern='day of week', bounds=['sat', 'mon']),
                    dict(pattern='hour', bounds=[16, 9.5])
                ],
                title='Date'
            ),
            yaxis1=dict(
                title='Price',
                domain=[0.2, 1.0]
            ),
            yaxis2=dict(
                title='Volume',
                anchor='x1',
                overlaying='y1',
                side='right',
                domain=[0.2, 1.0]
            ),
            #autosize=True
            width=1200,
            height=600,
        )

        RMA_strat_indicator.RMA1.vbt.plot(
            trace_kwargs=dict(
                name='RMA1',
                yaxis='y1',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=False
            ),
            fig=chart,
        )

        RMA_strat_indicator.RMA2.vbt.plot(
            trace_kwargs=dict(
                name='RMA2',
                yaxis='y1',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=False
            ),
            fig=chart
        )

        RMA_strat.new_entries.vbt.signals.plot_as_entry_markers(
            Low_analysis,
            trace_kwargs=dict(
                name='Entry',
                yaxis='y1',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=False
            ),
            fig=chart)

        RMA_strat.exits.vbt.signals.plot_as_exit_markers(
            Low_analysis,
            trace_kwargs=dict(
                name='Exit',
                yaxis='y1',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=False
            ),
            fig=chart)

        RMA_strat_indicator.pct_change.vbt.plot(
            trace_kwargs=dict(
                name='Percent Price Change',
                yaxis='y3',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=2,
                col=1,
                secondary_y=False
            ),
            yaxis3=dict(
                title='Percent',
                domain=[0.0, 0.2],
                range=[pct_change_thres_min - 1, pct_change_thres_max + 1]
            ),
            fig=chart,
        )

        RMA_strat_indicator.change_thres.vbt.plot(
            trace_kwargs=dict(
                name='Percent Change Threshold',
                yaxis='y3',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=2,
                col=1,
                secondary_y=False
            ),
            fig=chart
        )

        RMA_strat.stop_price.vbt.plot(
            trace_kwargs=dict(
                name='Exit Stop Price',
                yaxis='y1',
                showlegend=True
            ),
            add_trace_kwargs=dict(
                row=1,
                col=1,
                secondary_y=False
            ),
            fig=chart
        )

    else:
        with chart.batch_update():
            chart.data[2].y = RMA_strat_indicator.RMA1
            chart.data[3].y = RMA_strat_indicator.RMA2
            chart.data[4].x = Low_analysis.index[RMA_strat.new_entries]
            chart.data[4].y = Low_analysis[RMA_strat.new_entries]
            chart.data[5].x = Low_analysis.index[RMA_strat.exits]
            chart.data[5].y = Low_analysis[RMA_strat.exits]
            chart.data[6].y = RMA_strat_indicator.pct_change
            chart.data[7].y = RMA_strat_indicator.change_thres
            chart.data[8].x = RMA_strat.stop_price.index[~np.isnan(RMA_strat.stop_price)]
            chart.data[8].y = RMA_strat.stop_price[~np.isnan(RMA_strat.stop_price)]

    # Update metrics table
    sr = pd.Series([RMA_strat_pf.deep_getattr(m) for m in perf_metrics], 
                   index=perf_metric_names, name='Performance')
    metrics_html.value = sr.to_frame().style.set_properties(**{'text-align': 'right'}).render()

def RMA_value_change(RMA):
    EMA3_span, EMA4_span = RMA['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def pct_value_change(pct_change_window):
    pct_change_span = pct_change_window['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def pct_thres_value_change(pct_change_thres):
    pct_change_thres_span = pct_change_thres['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def RMA1_exp_value_change(RMA1_exp):
    EMA3_exp = RMA1_exp['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def RMA2_exp_value_change(RMA2_exp):
    EMA4_exp = RMA2_exp['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def pct_sl_stop_change(pct_sl_stop):
    sl_stop_span = pct_sl_stop['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def pct_tp_stop_change(pct_tp_stop):
    tp_stop_span = pct_tp_stop['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

def sl_trail_value_change(trail_sl):
    sl_trail = trail_sl['new']
    RMA_widget_func(EMA3_span, EMA4_span, EMA3_exp, EMA4_exp, pct_change_span, pct_change_thres_span, sl_stop_span, sl_trail, tp_stop_span)

RMA_slider.observe(RMA_value_change, names='value')
pct_change_slider.observe(pct_value_change, names='value')
pct_change_thres_slider.observe(pct_thres_value_change, names='value')
RMA1_exp_checkbox.observe(RMA1_exp_value_change, names='value')
RMA2_exp_checkbox.observe(RMA2_exp_value_change, names='value')
sl_stop_pct_slider.observe(pct_sl_stop_change, names='value')
tp_stop_pct_slider.observe(pct_tp_stop_change, names='value')
sl_trail_checkbox.observe(sl_trail_value_change, names='value')

RMA_value_change({'new': RMA_slider.value})
pct_value_change({'new': pct_change_slider.value})
pct_thres_value_change({'new': pct_change_thres_slider.value})
RMA1_exp_value_change({'new': RMA1_exp_checkbox.value})
RMA2_exp_value_change({'new': RMA2_exp_checkbox.value})
pct_sl_stop_change({'new': sl_stop_pct_slider.value})
pct_tp_stop_change({'new': tp_stop_pct_slider.value})
sl_trail_value_change({'new': sl_trail_checkbox.value})

dashboard = widgets.VBox([
    widgets.HBox([widgets.Label('RMA1 & RMA2 window:'), RMA_slider]),
    widgets.HBox([widgets.Label('Percent change window:'), pct_change_slider]),
    widgets.HBox([widgets.Label('Percent change threshold:'), pct_change_thres_slider]),
    RMA1_exp_checkbox,
    RMA2_exp_checkbox,
    widgets.HBox([widgets.Label('Stop Loss Percentage:'), sl_stop_pct_slider]),
    widgets.HBox([widgets.Label('Take Profit Percentage:'), tp_stop_pct_slider]),
    sl_trail_checkbox,
    chart,
    metrics_html
])

dashboard

image

polakowo commented 2 years ago

@forLinDre you can access any parameter list using {param_name}_list. Note that parameters are kept in raw (NumPy) format.

forLinDre commented 2 years ago

@polakowo, I'm not exactly trying to just access my sl_stop and to_stop percentage list. I'm trying to verify and visually observe that my exits are properly exiting when the tp or sl value is hit. How do I go about acquiring indexed data for these price values once I have an entry? The signal generator must somehow now when a high or low value crosses the sl or tp limits. How do I go about pulling these limits from the generator so I can plot them?

polakowo commented 2 years ago

There is no option to do so, parameters are not regular arrays even though they can behave as such. What you are asking is quite easy to accomplish manually though: just stack the list along column axis and use RMA_strat.wrapper.wrap.

forLinDre commented 2 years ago

@polakowo would you mind explaining with greater detail? When you say "stack the list" are you referring to the sl_stop_list and tp_stop_list? stack them together? Then use them to find the stop prices based on entry price? What about if the sl is trailing? If you could provide a quick example, that would be really nice. Thank you

polakowo commented 2 years ago

No, I mean stacking elements of sl_stop_list using np.column_stack and then use the wrapper if you want to get a DataFrame that has the same index and columns as all of your outputs.

forLinDre commented 2 years ago

@polakowo, would you have some time to create a quick example? I am completely confused on how I am supposed to use the RMA_strat.wrapper.wrap to wrap my stacked sl_stop_list such as array([[0.01, 0.02]]) to match my outputs. I am even more confused on how this would help me acquire my sl and tp price data between entries and exits especially if the stop loss is trailing. Were you thinking I would than calculate these values after I had my sl/tp_stop_lists broadcast across my dataframe?

polakowo commented 2 years ago

@forLinDre I don't quite understand what you're trying to achieve. You used OHLCSTX to generate signals, right? You passed your sl_stop and sl_trail as parameters to the indicator. Now you want to receive the parameters you passed but in a fully broadcast form? And what do you mean by "sl and tp price data"? Are you looking for stop price?

forLinDre commented 2 years ago

@polakowo let me try to explain myself more clearly with a drawing. Sorry if I wasn't clear. I used OHLCSTCX to generate my stop limits and also new entries based on initial entry algo. After an entry signal, I would like to see what my OHLCSTCX signal generator is considering as its SL price, TP price, and in the case of a trailing SL, the trailing SL price. I have drawn these for one set of entries/exits below. image

I initially tried to acquire these price values using stop_price but it did not work as expected and generated the orange line shown below. image

polakowo commented 2 years ago

@forLinDre the family of OHLC indicators only saves the hit price, not the stop price targets at each time step, otherwise you would need at least two more arrays (one for SL and one for TP) to save this info, thus they are tracked as constants to save memory. I guess the most flexible you can do is to override the choice function here and use this config with your function to create a generator like it's done here. Just define a couple more in-output arrays and save information that you need.

If you still want to go the path of working with fully broadcasted param arrays, here's how to broadcast them to the output shape:

bc_param_list = []
for p in ohlcstx.sl_stop_list:
    input_rows = ohlcstx.wrapper.shape_2d[0]
    input_cols = ohlcstx.wrapper.shape_2d[1] // len(ohlcstx.sl_stop_list)
    bc_param_list.append(np.broadcast_to(p, (input_rows, input_cols)))
sl_stop = ohlcstx.wrapper.wrap(np.column_stack(bc_param_list))
forLinDre commented 2 years ago

@polakowo, thanks for the help earlier, I was away for a few days. I have taken a modified approach to the param arrays method you suggested.

After running my indicator to create regular moving averages and then creating separate entry signals for each moving average, I use the OHLCSTCX signal generator to create exit signals:

# Process custom indicator
    RMA_strat_indicator = RMA_Strat_Indicator.run(
        EWM_window3=EMA3_span,
        EWM_window4=EMA4_span,
        ewm3=EMA3_exp,
        ewm4=EMA4_exp,
        change_window=pct_change_span,
        change_thres=pct_change_thres_span,
        param_product=False,
        run_unique=False
    )

    RMA_strat_indicator = RMA_strat_indicator[indicator_mask]

    # Define RMA entry statement
    RMA1_strat_entries =\
    (RMA_strat_indicator.pct_change_below(RMA_strat_indicator.change_thres) & RMA_strat_indicator.RMA1_above(RMA_strat_indicator.low, crossover=True) & (pd.Series(RMA_strat_indicator.entry_st.index.tz_localize(None), index=RMA_strat_indicator.entry_st.index).vbt >= RMA_strat_indicator.entry_st))

    RMA2_strat_entries =\
    (RMA_strat_indicator.pct_change_below(RMA_strat_indicator.change_thres) & RMA_strat_indicator.RMA2_above(RMA_strat_indicator.low, crossover=True) & (pd.Series(RMA_strat_indicator.entry_st.index.tz_localize(None), index=RMA_strat_indicator.entry_st.index).vbt >= RMA_strat_indicator.entry_st))

    # Generate exits
    RMA1_strat = vbt.OHLCSTCX.run(
    entries=RMA1_strat_entries,
    open=Open_analysis, high=High_analysis, low=Low_analysis, close=Close_analysis,
    sl_stop=sl_stop_span, #Percentage value for stop loss.
    sl_trail=sl_trail, #Whether sl_stop is trailing.
    tp_stop=tp_stop_span, #Percentage value for take profit.
    param_product=False,
    run_unique=False,
    )

    RMA2_strat = vbt.OHLCSTCX.run(
    entries=RMA2_strat_entries,
    open=Open_analysis, high=High_analysis, low=Low_analysis, close=Close_analysis,
    sl_stop=sl_stop_span, #Percentage value for stop loss.
    sl_trail=sl_trail, #Whether sl_stop is trailing.
    tp_stop=tp_stop_span, #Percentage value for take profit.
    param_product=False,
    run_unique=False,
    )

I then manually calculate the stop loss and take profit targets between every entry and exit signal. For now, I have not bothered with trailing-stop losses.

# Calculate sl & tp prices
    # Identify index of exits
    exit_index1 = np.where(RMA1_strat._exits == True)
    exit_index2 = np.where(RMA2_strat._exits == True)

    # Identify index of entries and clip entry index to match exit index (in the case that there is no exit at the end of the analysis period)
    entry_index_cut1 = []
    for arr in np.where(RMA1_strat._new_entries == True):
        entry_index_cut1.append(arr[:exit_index1[0].shape[0]])
    entry_index_cut2 = []
    for arr in np.where(RMA2_strat._new_entries == True):
        entry_index_cut2.append(arr[:exit_index2[0].shape[0]])

    # stack and Convert to arrays
    entry_ind1 = np.array(np.vstack(entry_index_cut1))
    exit_ind1 = np.array(exit_index1)
    entry_ind2 = np.array(np.vstack(entry_index_cut2))
    exit_ind2 = np.array(exit_index2)

    # Reshape arrays to [entry row, entry col] and [exit row, exit col] shape
    entry1 = entry_ind1.ravel(order='F').reshape(entry_ind1.shape[1],entry_ind1.shape[0])
    exit1 = exit_ind1.ravel(order='F').reshape(exit_ind1.shape[1],exit_ind1.shape[0])
    entry2 = entry_ind2.ravel(order='F').reshape(entry_ind2.shape[1],entry_ind2.shape[0])
    exit2 = exit_ind2.ravel(order='F').reshape(exit_ind2.shape[1],exit_ind2.shape[0])

    # sort by column position to ensure entry and exit indices line up.
    en1 = entry1[entry1[:, 1].argsort()]
    ex1 = exit1[exit1[:, 1].argsort()]
    en2 = entry2[entry2[:, 1].argsort()]
    ex2 = exit2[exit2[:, 1].argsort()]

    # Create and populate sl/tp prices
    sl_price1 = np.full_like(RMA1_strat._exits, np.nan, float)
    tp_price1 = np.full_like(RMA1_strat._exits, np.nan, float)

    for n1, x1 in zip(en1, ex1):
        sl_price1[n1[0]:x1[0] + 1, n1[1]] = RMA_strat_indicator.RMA1.iloc[n1[0]] - (RMA1_strat._sl_stop_list[n1[1]].item() * RMA_strat_indicator.RMA1.iloc[n1[0]])
        tp_price1[n1[0]:x1[0] + 1, n1[1]] = RMA_strat_indicator.RMA1.iloc[n1[0]] + (RMA1_strat._tp_stop_list[n1[1]].item() * RMA_strat_indicator.RMA1.iloc[n1[0]])

    sl_price2 = np.full_like(RMA2_strat._exits, np.nan, float)
    tp_price2 = np.full_like(RMA2_strat._exits, np.nan, float)

    for n2, x2 in zip(en2, ex2):
        sl_price2[n2[0]:x2[0] + 1, n2[1]] = RMA_strat_indicator.RMA2.iloc[n2[0]] - (RMA2_strat._sl_stop_list[n2[1]].item() * RMA_strat_indicator.RMA2.iloc[n2[0]])
        tp_price2[n2[0]:x2[0] + 1, n2[1]] = RMA_strat_indicator.RMA2.iloc[n2[0]] + (RMA2_strat._tp_stop_list[n2[1]].item() * RMA_strat_indicator.RMA2.iloc[n2[0]])

    # wrap sl/tp prices into strat dataframe
    sl1 = RMA1_strat.wrapper.wrap(sl_price1)
    sl2 = RMA1_strat.wrapper.wrap(sl_price2)
    tp1 = RMA1_strat.wrapper.wrap(tp_price1)
    tp2 = RMA1_strat.wrapper.wrap(tp_price2)

plotting everything I get the following plot: image

You can see that the entry functions just fine. As intended, the entry signal comes in when the regular moving average crosses above the low price. However, the exit does not come in when expected. The exit should occur after 1% profit is reached (shown by the green straight line). However, you can see it happens a few candles later. I notice the same for stop-loss exits. Do you know what my issue might be? I did hand calculate the take-profit value to check my sanity and it seems it is correct, 1% above the entry price. Thank you!