Help with Stop-Loss
-
Hi there!
I've been reading documentation and articles about a Stop-loss in Backtrader since the start of the week, and to be honest, it's far away from my mind.
I've tried to follow by examples but something wrong happened and my strategy sells shares into the minus. Please help with understanding the Stop-loss realization in Backtrader.
Here is my code:
import pandas as pd import numpy as np import aiomoex import asyncio import plotly import matplotlib.pyplot as plt import matplotlib.pylab as pylab %matplotlib inline import backtrader as bt from datetime import datetime """ Download data from MOEX. """ async def load_candles(security_tiker, candle_interval, start_date, end_date): async with aiomoex.ISSClientSession(): data = await aiomoex.get_market_candles(security=security_tiker, interval=candle_interval, start=start_date, end="2020-06-23", market='shares', engine='stock') security_df = pd.DataFrame(data) return security_df # set variables for downloading equities start_date = "2019-07-01" end_date = "2020-07-01" candle_interval = 1 #1 - 1 minute #10 - 10 minutes #60 - 1 hour #24 - 1 day #7 - 1 week #31 - 1 month #4 - 1 quater benchmark = await load_candles("FXIT", candle_interval, start_date, end_date) test_security = await load_candles("FXIT", candle_interval, start_date, end_date) # do Backtrader-like data test_security_backtrader = test_security.filter(['end','open','high', 'low', 'close', 'value']) test_security_backtrader.columns = ['date','open','high', 'low', 'close', 'volume'] test_security_backtrader['openInterest'] = 0 test_security_backtrader['date'] = test_security_backtrader.astype('datetime64[ns]') ''' Test trading strategy with Backtrader ''' class MyStrategy(bt.Strategy): # list of parameters which are configurable for the strategy params = (('EMA_fast_period', 26), ('EMA_slow_period', 287), ('trail', 0.02) ) def __init__(self): # keep track of pending orders/buy price/buy commission self.order = None self.price = None self.comm = None # add an indicators self.EMA_fast = bt.indicators.EMA(self.data, period=self.p.EMA_fast_period) self.EMA_slow = bt.indicators.EMA(self.data, period=self.p.EMA_slow_period) self.Trend = bt.ind.CrossOver(self.EMA_fast, self.EMA_slow) # crossover signal def log(self, txt): #Logging function dt = self.datas[0].datetime.datetime(0) print('%s, %s' % (dt.strftime('%Y-%m-%d %H:%M:%S'), txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # order already submitted/accepted - no action required return # report executed order if order.status in [order.Completed]: if order.exectype in [bt.Order.StopTrail]: self.log(f'STOP-LOSS EXECUTED') else: if order.isbuy(): self.log(f'BUY EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}') self.price = order.executed.price self.comm = order.executed.comm else: self.log(f'SELL EXECUTED --- Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Commission: {order.executed.comm:.2f}') # report failed order elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Failed') # set no pending order self.order = None def next(self): if not self.position: # not in the market if self.Trend > 0: # if fast crosses slow to the upside it's UP trend if self.order: # do nothing if an order is pending return # calculate the max number of shares ('all-in') size = int(self.broker.getcash() / self.data.open) # buy order self.order = self.buy(size=size) # enter long #notificate about buy_order creation self.log(f'BUY CREATED --- Size: {size}, Cash: {self.broker.getcash():.2f}, Open: {self.datas[0].open[0]}, Close: {self.datas[0].close[0]}') ''' There is a STOP-LOSS ''' self.stop_loss = self.sell(exectype=bt.Order.StopTrail, trailamount=self.p.trail) if self.position: # in the market # Selling by signals if self.Trend < 0: # cross to the downside it's DOWN trend #sell order self.order = self.sell(size=self.position.size) #notificate about sell_order creation self.log(f'SELL CREATED --- Size: {self.position.size}') # download data data = bt.feeds.PandasData(dataname=test_security_backtrader, datetime='date', timeframe=bt.TimeFrame.Minutes, #set our timeframe of our data compression=60) #compress timeframe to hours # create a Cerebro entity cerebro = bt.Cerebro(stdstats = True) # set up the backtest #cerebro.adddata(data) cerebro.resampledata(data, timeframe=bt.TimeFrame.Minutes, compression=60) cerebro.broker.setcash(600000.0) cerebro.addstrategy(MyStrategy) cerebro.addobserver(bt.observers.Value) # run backtest print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}') cerebro.run() print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}') # plot results pylab.rcParams['figure.figsize'] = 12, 12 # that's image size for this interactive session pylab.rcParams['figure.facecolor'] = '#2D3642' cerebro.plot(style='bars', iplot=True, volume=False)
And as a result, we can see the strange thing with negative sell orders. What's wrong with Stop-loss?
Starting Portfolio Value: 600000.00 2019-08-13 17:00:00, BUY CREATED --- Size: 120, Cash: 600000.00, Open: 4994.0, Close: 5093.0 2019-08-13 18:00:00, Order Failed 2019-08-13 18:00:00, Order Failed 2019-08-16 17:00:00, BUY CREATED --- Size: 119, Cash: 600000.00, Open: 5041.0, Close: 5055.0 2019-08-16 18:00:00, Order Failed 2019-08-16 18:00:00, STOP-LOSS EXECUTED 2019-09-16 14:00:00, SELL CREATED --- Size: -1 2019-09-16 15:00:00, SELL EXECUTED --- Price: 5048.00, Cost: -5048.00, Commission: 0.00 2019-10-02 11:00:00, SELL CREATED --- Size: -2 2019-10-02 12:00:00, SELL EXECUTED --- Price: 5058.00, Cost: -10116.00, Commission: 0.00 2019-10-21 18:00:00, SELL CREATED --- Size: -4 2019-10-21 23:59:59, SELL EXECUTED --- Price: 5060.00, Cost: -20240.00, Commission: 0.00 2019-10-22 12:00:00, SELL CREATED --- Size: -8 2019-10-22 13:00:00, SELL EXECUTED --- Price: 5068.00, Cost: -40544.00, Commission: 0.00 2019-10-22 23:59:59, SELL CREATED --- Size: -16 2019-10-23 11:00:00, SELL EXECUTED --- Price: 5029.00, Cost: -80464.00, Commission: 0.00 2020-02-26 17:00:00, SELL CREATED --- Size: -32 2020-02-26 18:00:00, SELL EXECUTED --- Price: 5873.00, Cost: -187936.00, Commission: 0.00 Final Portfolio Value: 491674.98
-
From your log, the buy orders fail, the stop trail orders are being created. Without a size provided, it will use the set sizer. So it will set the size to 1. When size is 1, it will next time add the amount to the sell pos 1+1 = 2 + 2 = 4 + 4 = 8 ... and so on. To fix that you may consider using a buy_bracket or check if the buy order is created and then create a stop trail oder with the size of the buy order.
-
I think you might have a few issues at play:
First, you are setting the buy size of your trade to be the maximum possilbe.
size = int(self.broker.getcash() / self.data.open)
This is problematic because in an upward trending market, the total cash you need will be greater than what you have, resulting in a 'margin' failure of the order. To set up your algorithm, use a trade value much smaller than the market value, say 50%. You can adjust the buffer later. Some people will use the volatility or trading range to assist in setting the buffer. I typically will just you 2.5% or so and see if I get stopped out.
Second, you have two sell orders possible for every trade. So you need to have better verification if the trade should happen. When selling if there is a crossover down, you could use close instead of sell. This will only close an open position for the amount it is open. See here.
if self.position: # in the market if self.Trend < 0: # cross to the downside it's DOWN trend self.order = self.close()
Next, your sell stop trail has a very small value (0.02) compared to the size of unit price (5000). This means you are definitly going to get stopped out right away just with regular volatility.
The final issue is that you need to be able to kill the stop trail order if your sell due to crossdown occurs. As it stands, if you sell due to the cross down, your stop sell is still out there waiting to be triggered.
You can solve this by cancelling the order when you sell by crossdown.
if self.position: # in the market if self.Trend < 0: # cross to the downside it's DOWN trend self.order = self.close() self.cancel(self.stop_loss)
-
@run-out thanks!
I had heeded your advice. It taked me several days for studying documentation and at the end of the days, I've implemented sizer and bracket orders. Now it's better.
Thank you for the bracket feature that is really powerful.