Tutorials
Stop signals
Test stop-loss, trailing-stop, and take-profit configurations
Our goal is to use large-scale backtesting to compare the performance of trading with and without stop loss (SL), trailing stop (TS), and take profit (TP) signals. To ensure this analysis is comprehensive, we will conduct a large number of experiments across three dimensions: instruments, time, and parameters.
First, we will select 10 cryptocurrencies by market capitalization (excluding stablecoins such as USDT) and gather 3 years of their daily pricing data. Specifically, we will backtest the period from 2018 to 2021, as this range includes periods of sharp price declines (such as corrections after the all-time high in December 2017 and during the coronavirus crisis in March 2020) as well as surges (the all-time high in December 2020). This provides a balanced perspective. For each instrument, we will split this period into 400 smaller, overlapping time windows, each lasting 6 months. We will run our tests on each of these windows to account for different market conditions. For each instrument and time window, we will generate an entry signal at the very first bar and determine an exit signal according to the stop configuration. We will test 100 stop values, increasing by 1% increments, and compare the performance of each one to trading randomly and holding during that specific time window. In total, we will conduct 2,000,000 tests.
Make sure you have at least 16 GB of free RAM available, or memory swapping enabled.
Parameters
The first step is to define the parameters of the analysis pipeline. As discussed above, we will backtest 3 years of pricing data, use 400 time windows, 10 cryptocurrencies, and 100 stop values. We will also set fees and slippage to 0.25% each, and the initial capital to $100. The absolute amount does not matter, but it must be consistent across all assets to allow for direct comparison. Feel free to change any parameter of interest.
from vectorbtpro import *
import ipywidgets
seed = 42
symbols = [
"BTC-USD", "ETH-USD", "XRP-USD", "BCH-USD", "LTC-USD",
"BNB-USD", "EOS-USD", "XLM-USD", "XMR-USD", "ADA-USD"
]
start_date = vbt.utc_timestamp("2018-01-01")
end_date = vbt.utc_timestamp("2021-01-01")
time_delta = end_date - start_date
window_len = vbt.timedelta("180d")
window_cnt = 400
exit_types = ["SL", "TS", "TP", "Random", "Holding"]
step = 0.01
stops = np.arange(step, 1 + step, step)
vbt.settings.wrapping["freq"] = "d"
vbt.settings.plotting["layout"]["template"] = "vbt_dark"
vbt.settings.portfolio["init_cash"] = 100.
vbt.settings.portfolio["fees"] = 0.0025
vbt.settings.portfolio["slippage"] = 0.0025
pd.Series({
"Start date": start_date,
"End date": end_date,
"Time period (days)": time_delta.days,
"Assets": len(symbols),
"Window length": window_len,
"Windows": window_cnt,
"Exit types": len(exit_types),
"Stop values": len(stops),
"Tests per asset": window_cnt * len(stops) * len(exit_types),
"Tests per window": len(symbols) * len(stops) * len(exit_types),
"Tests per exit type": len(symbols) * window_cnt * len(stops),
"Tests per stop type and value": len(symbols) * window_cnt,
"Tests total": len(symbols) * window_cnt * len(stops) * len(exit_types)
})Start date 2018-01-01 00:00:00+00:00
End date 2021-01-01 00:00:00+00:00
Time period (days) 1096
Assets 10
Window length 180 days 00:00:00
Windows 400
Exit types 5
Stop values 100
Tests per asset 200000
Tests per window 5000
Tests per exit type 400000
Tests per stop type and value 4000
Tests total 2000000
dtype: objectOur configuration produces sample sizes with enough statistical power to analyze four variables: assets (200k tests per asset), time (5k tests per time window), exit types (400k tests per exit type), and stop values (4k tests per stop type and value). Similar to Tableau's approach to dimensions and measures, we can group our performance metrics by each of these variables. However, we will mainly focus on 5 exit types: SL exits, TS exits, TP exits, random exits, and holding exits (executed at the last bar).
cols = ["Open", "Low", "High", "Close", "Volume"]
yfdata = vbt.YFData.pull(symbols, start=start_date, end=end_date)yfdata.data.keys()dict_keys(['BTC-USD', 'ETH-USD', 'XRP-USD', 'BCH-USD', 'LTC-USD',
'BNB-USD', 'EOS-USD', 'XLM-USD', 'XMR-USD', 'ADA-USD'])yfdata.data["BTC-USD"].shape(1096, 7)The data instance yfdata contains a dictionary of OHLCV data, keyed by cryptocurrency name. Each
DataFrame contains 1096 rows (days) and 5 columns (O, H, L, C, and V). You can plot each DataFrame as
follows:
yfdata.plot(symbol="BTC-USD").show() Since assets are one of the dimensions we want to analyze, VBT expects us to combine them as columns into a single DataFrame and label them clearly. To do this, we swap assets and features to create a dictionary of DataFrames, now with assets as columns and keyed by feature name, such as "Open".
ohlcv = yfdata.concat()
ohlcv.keys()dict_keys(['Open', 'High', 'Low', 'Close',
'Volume', 'Dividends', 'Stock Splits'])ohlcv['Open'].shape(1096, 10)Time windows
Stop orders are a vital part of an investor's toolkit. They help limit losses on a security position and secure profits. But what would be the optimal combination of stop types and values for a specific market regime?
✅ Learn how to run 2 million tests in just a few minutes to answer this question for top cryptocurrency markets. Unlike the open-source article, we will also include the stop price in our analysis to improve accuracy!
Copyright © 2021–2026 Oleg Polakow. All rights reserved.
Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.