Correct implementation of size, ATR based stop-loss and other things in first strategy
-
Hi! I want to ask a couple of things regarding one of my first attempts of coding a strategy using backtester. Also, I have less than a week using/learning to use backtrader, so any feedback is well recieved.
I implemented a strategy based on two indicators and I ccould "make it run successfuly". However, I have a strong feeling that I'm missing some parts to actually make it work.
The idea is pretty simple:
- If all indicators are telling to buy, then buy (same logic for selling).
- If there already a trade open and one indicator is telling to go to the other side, just close the open trade and wait to all indicators point to same direction again.
- Calculate the trade size (lots) based on account balance and the ATR.
- I also want to implement a
stop_loss
based on theATR
. So, if price touches that level, the trade is closed automatically.
So, reading the docs, other people's questions in the forum and other arcticles, I seem to have a confusion between trades, orders and positions. When I use the
self.buy()
function. I undertand that I am opening a new trade, but theStrategy
class returns anorder
object. Also, it seems that a trade can be composed by multiple orders (based on this link).My code looks like this:
class MoneyManagement(): def tradesize(self,trade_type): pip_value = 1 point = 0.00001 digits = 5 open_price = self.data.close[0]*(1-trade_type) + trade_type*self.data.close[0] stop_loss_price = np.round(open_price - (1-2*trade_type)*(self.p.atr_stoploss*self.atr[0]),digits) lots = self.p.risk*self.broker.getvalue()/(pip_value*(self.atr[0]*self.p.atr_stoploss/point)) lots = np.round(lots,2) return lots class strat(bt.Strategy,MoneyManagement): params = (('risk',0.01), ('atr_stoploss',1.5)) def __init__(self): self.kal = KalmanMovingAverage() self.aroon = AroonAverage() self.atr = bt.talib.ATR(self.data.high,self.data.low,self.data.close) self.myorder = False def next(self): cross_analyzer = self.cross_analyzer() self.exit_condition() if cross_analyzer>0: if self.position.size != 0: print("NOT BUYING, ORDER ALIVE") return else: print('BUYING, ORDER IS DEAD') size = self.tradesize(0) self.myorder = self.buy(size = size,price = self.data.close[0]) self.notify_order(self.myorder) elif cross_analyzer<0: if self.position.size != 0: print("NOT SELLING, ORDER ALIVE") return else: print('SELLING, ORDER IS DEAD') size = self.tradesize(1) self.myorder = self.sell(size = size,price = self.data.close[0]) self.notify_order(self.myorder) def notify_order(self,order): order_type = order.isbuy() size = order.size open_price = order.price date = self.data.datetime.datetime() msg = '{date} {order_type} @{open_price}/{size}'.format(date = date, order_type = 'BUY' if order_type else 'SELL', open_price = open_price, size = size) print(msg) def exit_condition(self): if isinstance(self.myorder,bool): return is_buy = self.myorder.isbuy() is_sell = self.myorder.issell() is_alive = self.position.size != 0 sma_cross = self.sma_cross() aroon_cross = self.aroon_cross() if is_alive and is_buy and (sma_cross <0 or aroon_cross < 0): print("CLOSING BUY") self.close() if is_alive and is_sell and (sma_cross>0 or aroon_cross > 0): print("CLOSING SELL") self.close() def sma_cross(self): if self.data.close[0]>self.kal[0] and self.data.close[-1]<self.kal[-1]: return 1 if self.data.close[0]<self.kal[0] and self.data.close[-1]>self.kal[-1]: return -1 if self.data.close[0]>self.kal[0]: return 2 if self.data.close[0]<self.kal[0]: return -2 return 0 def aroon_cross(self): if self.aroon.aroonup[0]>self.aroon.aroondown[0] and self.aroon.aroonup[-1]<self.aroon.aroondown[-1]: return 1 if self.aroon.aroonup[0]<self.aroon.aroondown[0] and self.aroon.aroonup[-1]>self.aroon.aroondown[-1]: return -1 if self.aroon.aroonup[0]>self.aroon.aroondown[0]: return 2 if self.aroon.aroonup[0]<self.aroon.aroondown[0]: return -2 return 0 def cross_analyzer(self): """ 1 y 1 -> buy -1 y -1 -> sell """ sma_cross = self.sma_cross() aroon_cross = self.aroon_cross() buy_condition = (sma_cross == aroon_cross == 1) or (sma_cross == 1 and aroon_cross == 2) or (aroon_cross == 1 and sma_cross == 2) sell_condition = (sma_cross == aroon_cross == -1) or (sma_cross == -1 and aroon_cross == -2) or (aroon_cross == -1 and sma_cross == -2) if buy_condition: return 1 elif sell_condition: return -1 else: return 0
The first thing to notice is that in the
MoneyManagement
class, theopen_price
is calculated based on the type of trade. When it is abuy
operation, the price usually is theask
price and when its a sell theopen_price
is the bid. Now,bid
price coresponds to the close price of the candle. How can I add theask
price to the datafeed? My data feed is like follows:dt = bt.feeds.PandasData(dataname = df.set_index('DateTime',drop=True)) df.columns Index(['DateTime', 'Open', 'High', 'Low', 'Close', 'Ask', 'Volume'], dtype='object')
Then, after adding data and the strategy, when I execute
cerebro.run()
I get an output like this:0 SELLING, ORDER IS DEAD 1 2020-05-18 14:58:17.433175 SELL @1.08567/-0.46 2 2020-05-18 15:13:48.853490 SELL @1.08567/-0.46 3 2020-05-18 15:13:48.853490 SELL @1.08567/-0.46 4 2020-05-18 15:13:48.853490 SELL @1.08567/-0.46 5 CLOSING SELL 6 2020-05-18 15:19:55.355002 BUY @None/0.46 7 2020-05-18 15:19:55.355002 BUY @None/0.46 8 2020-05-18 15:19:55.355002 BUY @None/0.46 9 BUYING, ORDER IS DEAD 10 2020-05-18 15:24:15.019919 BUY @1.08975/0.49 11 2020-05-18 15:27:29.651633 BUY @1.08975/0.49 12 2020-05-18 15:27:29.651633 BUY @1.08975/0.49 13 2020-05-18 15:27:29.651633 BUY @1.08975/0.49 ...
If you take a look at lines 1,2,3 and 4, 4 sell operations were executed and 3 of them were in the same candle. Also, when 'CLOSING SELL' is printed, the buy price is None. Why is this?
Also, how can I meet the requirement of "dont open more than one trade at time"?
And how does the trade size works? For example, if I open a trade in the EURUSD on a MT4 terminal, the minimum lot size is 0.01, but for what I have noticed sizing doesn't work like this in backtrader. -
UPDATE/EDIT:
After reading more of documentation and seeing the code in github, I realized that when using
self.close()
it opens a contrary operation with same size, so this explains why I was seeing "duplicated buy/sell operations". I also noticed that the closing operation is plotted whencerebro.plot()
is used. Is there a way to distinguish between the open of a trade and the close of it? And just for curiosity, why was this behaviour chosen instead of just setting the amount of trades to 0, or lowering the size of the trade by the desired amount in case of a partial close?Regarding the ATR
stoploss
, I have read the order creation execution docs and some posts here on the forum, but I can't understand how to implement correctly a price basedstoploss
, so if the price (high >= stoploss
for selling andlow <= stoploss
for buying) touches that line, the operation is closed and registered withclosing price
equal to thestoploss
price. How can I achieve this? -
Duplicate lines 1 thru 4 in your output is because the order goes thru the different states/statuses and each change in status calls
notify_order
. Which you call one more time innext
. -
Price based stoploss can be dilated by
Stop
order which will get active at broker side until the price goes thru it. -
@ab_trader Price based stoploss can be simulated by Stop order which will get active at broker side until the price goes thru it.
-
@ab_trader Thank you for the reply!
I tried to use
Stop orders
, but I get them rejected.This is my code where I put the Stop order:
size,self.stoploss = self.tradesize(1) self.order = self.sell(size = size,price = self.dataclose[0]) self.order = self.buy(exectype = bt.Order.Stop,size = size, price = self.stoploss,oco = self.order)
I am guessing that, if the strategy is telling to open a sell trade, then I should execute a buy
Stop Order
of the same size using the pricestoploss
that was calculated, right? Also (and maybe here is my mistake) I'm usingoco
because I want the trade to be closed.I think that I still have some confusion with
Trades
,Positions
andOrders
. In other attempts of coding my own backtesting engine, I just register theopen_time
,close_time
,open_price
andclose_price
of each trade, but for what I am learning, this logic doesn't apply here, right? -
@ppsev
bt
is event-driven backtester which simulates real trading. In real life you send order to broker and it opens for you position (or trade). Then you send another order to broker and broker can increase/decrease size of the position/trade or liquidate it. Also broker returns you prices which was used for order execution innotify_order
. -
@ab_trader Thanks for the explanation. I have a feeling that this is somehow different to what happens on a MT4 terminal, right?
-
@ppsev I didn't use mt4, so can say nothing on comparison.