Closed CMobley7 closed 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
@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!
@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
@forLinDre you can access any parameter list using {param_name}_list
. Note that parameters are kept in raw (NumPy) format.
@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?
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
.
@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
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.
@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?
@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?
@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.
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.
@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))
@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:
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!
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 was20.5%
. So, it looks like thetake-profit stop
was triggered effectively, but I never saw a drop of20%
; so, thestop-loss stop
was not. In order to verify that thestop-loss stop
would function as expected, I setsl_stop
to3%
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?