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

For more information on the various attributes that can be used to set stops in PyBroker, you can refer to the ExecContext reference documentation.