Applying Stops
Stops are used to automatically buy or sell a security once it reaches a specified price level. They can be useful for limiting potential losses by allowing traders to exit bad trades automatically, as well as for taking profits by selling a security automatically when it reaches a certain price level.
PyBroker supports the simulation of stops, which is explained in detail in this notebook:
[1]:
import pybroker
from pybroker import Strategy, YFinance
pybroker.enable_data_source_cache('stops')
strategy = Strategy(YFinance(), '1/1/2018', '1/1/2023')
Stop Loss
A stop loss order is used to automatically exit a trade once the security’s price reaches or falls below a specified level. For example, the following code shows an example of a stop loss order set at 20%
below the entry price:
[2]:
def buy_with_stop_loss(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_loss_pct = 20
strategy.add_execution(buy_with_stop_loss, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 1 of 1 completed
Loaded bar data: 0:00:00
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:02
[2]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-03-28 | 21.36 | 17.09 | 4679 | -19988.69 | -20.00 | -19988.69 | 58 | -344.63 | loss |
2 | long | TSLA | 2018-03-29 | 2019-05-20 | 17.31 | 13.73 | 4622 | -16531.36 | -20.66 | -36520.04 | 286 | -57.80 | loss |
Take Profit
Similarly, a take profit order can be used to lock in profits on a trade. The following code adds a take profit order at 10%
above the entry price:
[3]:
def buy_with_stop_loss_and_profit(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_loss_pct = 20
ctx.stop_profit_pct = 10
strategy.clear_executions()
strategy.add_execution(buy_with_stop_loss_and_profit, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[3]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-01-22 | 21.36 | 23.50 | 4679 | 9994.34 | 10.0 | 9994.34 | 12 | 832.86 | profit |
2 | long | TSLA | 2018-01-23 | 2018-03-27 | 23.72 | 18.98 | 4637 | -21997.93 | -20.0 | -12003.58 | 44 | -499.95 | loss |
3 | long | TSLA | 2018-03-28 | 2018-04-04 | 17.36 | 19.10 | 4727 | 8206.07 | 10.0 | -3797.51 | 4 | 2051.52 | profit |
4 | long | TSLA | 2018-04-05 | 2018-06-07 | 19.82 | 21.80 | 4853 | 9618.65 | 10.0 | 5821.13 | 44 | 218.61 | profit |
5 | long | TSLA | 2018-06-08 | 2018-06-12 | 21.39 | 23.53 | 4947 | 10581.63 | 10.0 | 16402.77 | 2 | 5290.82 | profit |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
85 | long | TSLA | 2022-07-29 | 2022-10-07 | 288.71 | 230.97 | 1010 | -58319.42 | -20.0 | 133480.29 | 49 | -1190.19 | loss |
86 | long | TSLA | 2022-10-10 | 2022-11-09 | 222.68 | 178.14 | 1046 | -46584.66 | -20.0 | 86895.63 | 22 | -2117.48 | loss |
87 | long | TSLA | 2022-11-10 | 2022-12-19 | 185.51 | 148.41 | 1007 | -37361.71 | -20.0 | 49533.92 | 26 | -1436.99 | loss |
88 | long | TSLA | 2022-12-20 | 2022-12-27 | 143.07 | 114.46 | 997 | -28528.16 | -20.0 | 21005.76 | 4 | -7132.04 | loss |
89 | long | TSLA | 2022-12-28 | 2022-12-29 | 112.25 | 123.48 | 1078 | 12100.55 | 10.0 | 33106.31 | 1 | 12100.55 | profit |
89 rows × 13 columns
Trailing Stop
A trailing stop is an order that is used to exit a trade once the instrument’s price falls a certain percentage or cash amount below its highest market price. Here is an example of setting a trailing stop at 20%
below the highest market price:
[4]:
def buy_with_trailing_stop_loss_and_profit(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
ctx.stop_profit_pct = 10
strategy.clear_executions()
strategy.add_execution(buy_with_trailing_stop_loss_and_profit, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[4]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-01-22 | 21.36 | 23.50 | 4679 | 9994.34 | 10.00 | 9994.34 | 12 | 832.86 | profit |
2 | long | TSLA | 2018-01-23 | 2018-03-27 | 23.72 | 19.20 | 4637 | -20961.72 | -19.06 | -10967.37 | 44 | -476.40 | trailing |
3 | long | TSLA | 2018-03-28 | 2018-04-04 | 17.36 | 19.10 | 4783 | 8303.29 | 10.00 | -2664.08 | 4 | 2075.82 | profit |
4 | long | TSLA | 2018-04-05 | 2018-06-07 | 19.82 | 21.80 | 4910 | 9731.62 | 10.00 | 7067.54 | 44 | 221.17 | profit |
5 | long | TSLA | 2018-06-08 | 2018-06-12 | 21.39 | 23.53 | 5005 | 10705.70 | 10.00 | 17773.23 | 2 | 5352.85 | profit |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
102 | long | TSLA | 2022-08-02 | 2022-10-03 | 300.25 | 251.73 | 1095 | -53125.76 | -16.16 | 175768.16 | 43 | -1235.48 | trailing |
103 | long | TSLA | 2022-10-04 | 2022-10-24 | 249.75 | 199.80 | 1104 | -55144.80 | -20.00 | 120623.36 | 14 | -3938.91 | trailing |
104 | long | TSLA | 2022-10-25 | 2022-11-08 | 217.18 | 189.92 | 1015 | -27668.90 | -12.55 | 92954.46 | 10 | -2766.89 | trailing |
105 | long | TSLA | 2022-11-09 | 2022-12-13 | 186.50 | 160.66 | 1008 | -26050.75 | -13.86 | 66903.71 | 23 | -1132.64 | trailing |
106 | long | TSLA | 2022-12-14 | 2022-12-22 | 158.46 | 128.79 | 1036 | -30736.04 | -18.72 | 36167.67 | 6 | -5122.67 | trailing |
106 rows × 13 columns
Setting a Limit Price
A stop order can be combined with a limit price to ensure that the order is executed only at a specific price level. Below shows an example of placing a limit price on a stop order:
[5]:
def buy_with_trailing_stop_loss_and_profit(ctx):
if not ctx.long_pos():
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
ctx.stop_trailing_limit = ctx.close[-1] + 1
ctx.stop_profit_pct = 10
ctx.stop_profit_limit = ctx.close[-1] - 1
strategy.clear_executions()
strategy.add_execution(buy_with_trailing_stop_loss_and_profit, ['TSLA'])
result = strategy.backtest()
result.trades.head()
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[5]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-01-22 | 21.36 | 23.50 | 4679 | 9994.34 | 10.0 | 9994.34 | 12 | 832.86 | profit |
2 | long | TSLA | 2018-01-23 | 2019-12-18 | 23.72 | 26.09 | 4637 | 10998.96 | 10.0 | 20993.31 | 480 | 22.91 | profit |
3 | long | TSLA | 2019-12-19 | 2020-01-03 | 26.78 | 29.46 | 4518 | 12099.20 | 10.0 | 33092.51 | 9 | 1344.36 | profit |
4 | long | TSLA | 2020-01-06 | 2020-01-08 | 29.72 | 32.69 | 4478 | 13308.62 | 10.0 | 46401.13 | 2 | 6654.31 | profit |
5 | long | TSLA | 2020-01-09 | 2020-01-14 | 32.39 | 35.63 | 4462 | 14452.42 | 10.0 | 60853.55 | 3 | 4817.47 | profit |
Canceling a Stop
The following code shows an example of canceling a stop order:
[6]:
def buy_with_stop_trailing_and_cancel(ctx):
pos = ctx.long_pos()
if not pos:
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
elif pos.bars > 60:
ctx.cancel_stops(ctx.symbol)
strategy.clear_executions()
strategy.add_execution(buy_with_stop_trailing_and_cancel, ['TSLA'])
result = strategy.backtest()
result.trades
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[6]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-03-27 | 21.36 | 19.23 | 4679 | -9981.87 | -9.99 | -9981.87 | 57 | -175.12 | trailing |
Setting the Stop Exit Price
By default, stops are checked against the bar’s low and high prices, and they are exited at the stop’s threshold (e.g., -2%) on the same bar when the stop is triggered.
To set a custom exit price, the “exit_price” fields for each stop type can be used. When these fields are set, the stop will be checked against the exit_price
, and it will exit at the exit_price
when triggered. For example, the code below sets the stop_trailing_exit_price to the open price on the bar that triggers the stop:
[7]:
from pybroker import PriceType
def buy_with_stop_trailing_and_exit_price(ctx):
pos = ctx.long_pos()
if not pos:
ctx.buy_shares = ctx.calc_target_shares(1)
ctx.stop_trailing_pct = 20
ctx.stop_trailing_exit_price = PriceType.OPEN
strategy.clear_executions()
strategy.add_execution(buy_with_stop_trailing_and_exit_price, ['TSLA'])
result = strategy.backtest()
result.trades.head()
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:00
[7]:
type | symbol | entry_date | exit_date | entry | exit | shares | pnl | return_pct | agg_pnl | bars | pnl_per_bar | stop | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||||
1 | long | TSLA | 2018-01-03 | 2018-03-28 | 21.36 | 17.64 | 4679 | -17412.12 | -17.42 | -17412.12 | 58 | -300.21 | trailing |
2 | long | TSLA | 2018-03-29 | 2018-07-25 | 17.31 | 19.78 | 4771 | 11797.10 | 14.28 | -5615.03 | 81 | 145.64 | trailing |
3 | long | TSLA | 2018-07-26 | 2018-08-20 | 20.48 | 19.45 | 4585 | -4737.83 | -5.05 | -10352.86 | 17 | -278.70 | trailing |
4 | long | TSLA | 2018-08-21 | 2018-09-07 | 21.13 | 17.34 | 4242 | -16077.18 | -17.94 | -26430.04 | 12 | -1339.76 | trailing |
5 | long | TSLA | 2018-09-10 | 2018-12-26 | 18.57 | 20.00 | 3961 | 5664.23 | 7.70 | -20765.81 | 74 | 76.54 | trailing |