Converting a Tradingview Strategy to a Backtrader Strategy - EMA Crossover
-
Hi Guys,
I need help with converting a tradingview strategy over into backtrader. I have been working on this for a couple of months and can't seem to get the numbers to matchup. We are getting the data from the Bybit API and loading into a pandas dataframe. We then use backtrader to load the dataframe into Cerebro.
Please see below for the BackTrader Strategy and Pandas Data.
class LongCloseLarpStrat(bt.Strategy): params = (('fastMA',22),('slowMA', 23)) def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.datetime(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): self.livetrades = {} self.counter = 0 self.fastMA = bt.indicators.ExponentialMovingAverage(period=self.p.fastMA) self.slowMA = bt.indicators.ExponentialMovingAverage(period=self.p.slowMA) self.crossover = bt.indicators.CrossOver(self.fastMA, self.slowMA) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted, order.Partial]:# Buy/Sell order submitted/accepted to/by broker/partially filled - Nothing to do return # Check if an order has been completed if order.status in [order.Completed]: if order.isbuy(): self.log('BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f, tradeID: %d' %(order.executed.price, order.executed.value, order.executed.comm, order.tradeid)) elif order.issell(): self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f, tradeID: %d' %(order.executed.price, order.executed.value, order.executed.comm, order.tradeid)) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') def notify_trade(self, trade): if trade.justopened: self.log('New trade just opened with id %d'%(trade.tradeid)) self.livetrades[trade.tradeid] = trade elif trade.isclosed: self.log('A trade just closed with id %d'%(trade.tradeid)) self.livetrades.pop(trade.tradeid) else: self.log('Trade id %d updated'%(trade.tradeid)) def next(self): if self.position.size: if self.crossover < 0: self.sell() elif self.crossover > 0: self.buy()
Pandas Data
class PandasData(bt.feeds.PandasData): lines = ('turnover','high_low2', 'ema_slow', 'ema_fast', 'macd_delta') params = ( ('datetime', None), ('open','Open'), ('high','High'), ('low','Low'), ('close','Close'), ('volume','Volume'), ('openinterest', None), ('turnover','Turnover'), ('high_low2','high_low2'), ('ema_slow', 'ema_slow'), ('ema_fast', 'ema_fast'), ('macd_delta', 'macd_delta') )
Please see below for the Pinescript Code.
//@version=3 strategy(title = "test", overlay=true, initial_capital = 10000, pyramiding = 0, commission_type = strategy.commission.percent, commission_value = 0.0, calc_on_order_fills = false, calc_on_every_tick = false, default_qty_type = strategy.percent_of_equity, default_qty_value = 100, currency = currency.USD) fastMA = ema(close, 22) slowMA = ema(close, 23) plot(fastMA, color = red, linewidth = 1, title = "Fast MA") plot(slowMA, color = blue, linewidth = 1, title = "Slow MA") buyCondition = crossover(fastMA, slowMA) closeCondition = crossunder(fastMA, slowMA) strategy.entry("MA Cross - Long", strategy.long) strategy.close("MA Cross - Long", closeCondition)
I apologize if I did not ask this question in the correct manner.
Please let me know if there is any additional questions or anything I can add.Here is a link to the tradingview ticker.
https://www.tradingview.com/symbols/BTCUSD/?exchange=BYBIT
We are working on the 3 hour timeframe.
Also, here is the code for getting the data from bybit.def get_historical_klines_pd(client, symbol, interval, start_str, end_str=None): """Get Historical Klines from Bybit See dateparse docs for valid start and end string formats http://dateparser.readthedocs.io/en/latest/ If using offset strings for dates add "UTC" to date string e.g. "now UTC", "11 hours ago UTC" :param symbol: Name of symbol pair -- BTCUSD, ETCUSD, EOSUSD, XRPUSD :type symbol: str :param interval: Bybit Kline interval -- 1 3 5 15 30 60 120 240 360 720 "D" "M" "W" "Y" :type interval: str :param start_str: Start date string in UTC format :type start_str: str :param end_str: optional - end date string in UTC format :type end_str: str :return: list of OHLCV values """ # set parameters for kline() interval = int(interval) timeframe = str(interval) limit = 200 start_ts = int(date_to_milliseconds(start_str)/1000) end_ts = None if end_str: end_ts = int(date_to_milliseconds(end_str)/1000) else: end_ts = int(date_to_milliseconds('now')/1000) # init our list output_data = [] # loop counter idx = 0 # it can be difficult to know when a symbol was listed on Binance so allow start time to be before list date symbol_existed = False while True: # fetch the klines from start_ts up to max 200 entries temp_tuple = client.Kline.Kline_get(symbol=symbol, interval=timeframe, **{'from':start_ts}, limit=limit).result() temp_dict = temp_tuple[0] # handle the case where our start date is before the symbol pair listed on Binance if not symbol_existed and len(temp_dict): symbol_existed = True if symbol_existed: # extract data and convert to list temp_data = [list(i.values()) for i in temp_dict['result']] # append this loops data to our output data output_data += temp_data # update our start timestamp using the last value in the array and add the interval timeframe # NOTE: current implementation does not support inteval of D/W/M/Y start_ts = temp_data[len(temp_data) - 1][2] + interval*60 else: # it wasn't listed yet, increment our start date start_ts += timeframe idx += 1 # check if we received less than the required limit and exit the loop if len(temp_data) < limit: # exit the while loop break # sleep after every 3rd call to be kind to the API if idx % 3 == 0: time.sleep(0.2) # convert to data frame df = pd.DataFrame(output_data, columns=['Symbol', 'Interval', 'TimeStamp', 'Open', 'High', 'Low', 'Close', 'Volume', 'Turnover']) df['Date'] = [datetime.fromtimestamp(i).strftime('%Y-%m-%d %H:%M:%S.%d')[:-3] for i in df['TimeStamp']] return df
Thanks,
Hernando -
@hfrog713 said in Converting a Tradingview Strategy to a Backtrader Strategy - EMA Crossover:
can't seem to get the numbers to matchup
Could you describe a bit more what the error is?
-
@run-out Hi Sorry, i am not getting an error......the problem is the number of trades, trade prices, and times of trades don't match at all with what i am seeing in the tradingview strategy. I have confirmed that the data looks the same in the dataframe as the data i am using in tradingview.
I can upload samples of the data if required.
The results i get from backtrader is the following.Trade Analysis Results:
Total Open Total Closed Total Won Total Lost
0 62 8 54
Strike Rate Win Streak Losing Streak PnL Net
12.9 2 11 -96628.15
Final Portfolio Value: $3371.849999999994The results i get from TradingView are attached as a picture.
Please let me know if you need anything else.
I believe I am just trying to get a simple EMA crossover to buy and sell on positive and negative crossover. You can see what i am trying to do directly in the PineScript.Thanks in advance for your help.
-
-
@hfrog713 From what I can see of your strategy (it would be better if you just included the whole code) you are running a basic crossover. This is plain vanilla stuff. I cannot see any errors in your
Strategy
. You have a pandas feed with extra lines. I have no idea why they are there.I don't feel I can answer your question very well about Tradingview. I think what you need to do, in my opinion, is to start hunting the anomolies between the two systems. This can be tedious.
Print out detailed logs of your backtest and make sure Backtrader is doing what you expect. Then try to compare the same data with your Tradingview charts and try to find out where the they diverge.
When you have something more specific about backtrader giving specific errors let us know. Good luck.
-
@hfrog713 said in Converting a Tradingview Strategy to a Backtrader Strategy - EMA Crossover:
def next(self): if self.position.size: if self.crossover < 0: self.sell() elif self.crossover > 0: self.buy()
Here is a indention error, the elif self.crossover > 0
-
I guess the crux of the question is.......
I have some logic in tradingview....
//@version=3
strategy(title = "test", overlay=true, initial_capital = 10000, pyramiding = 0, commission_type = strategy.commission.percent, commission_value = 0.0, calc_on_order_fills = false, calc_on_every_tick = false, default_qty_type = strategy.percent_of_equity, default_qty_value = 100, currency = currency.USD)fastMA = ema(close, 22) slowMA = ema(close, 23) buyCondition = crossover(fastMA, slowMA) closeCondition = crossunder(fastMA, slowMA) if buyCondition strategy.entry("MA Cross - Long", strategy.long) strategy.close("MA Cross - Long", closeCondition)
How do i convert that to logic in backtrader.......
I thought the following, but i end up with different results.
class LongCloseLarpStrat(bt.Strategy): params = (('fastMA',22),('slowMA', 23)) def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.datetime(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): self.livetrades = {} self.counter = 0 self.fastMA = bt.indicators.ExponentialMovingAverage(period=self.p.fastMA) self.slowMA = bt.indicators.ExponentialMovingAverage(period=self.p.slowMA) self.crossover = bt.indicators.CrossOver(self.fastMA, self.slowMA) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted, order.Partial]:# Buy/Sell order submitted/accepted to/by broker/partially filled - Nothing to do return # Check if an order has been completed if order.status in [order.Completed]: if order.isbuy(): self.log('BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f, tradeID: %d' %(order.executed.price, order.executed.value, order.executed.comm, order.tradeid)) elif order.issell(): self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f, tradeID: %d' %(order.executed.price, order.executed.value, order.executed.comm, order.tradeid)) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') def notify_trade(self, trade): if trade.justopened: self.log('New trade just opened with id %d'%(trade.tradeid)) self.livetrades[trade.tradeid] = trade elif trade.isclosed: self.log('A trade just closed with id %d'%(trade.tradeid)) self.livetrades.pop(trade.tradeid) else: self.log('Trade id %d updated'%(trade.tradeid)) def next(self): if self.position.size: if self.crossover == -1: self.sell() elif not self.position.size: if self.crossover == 1: self.buy()
Can someone help me out here?
Thanks in advance,
Hernando -
@hfrog713 It's hard to really provide any helpful information without knowing what "different results" means. Are you getting different EMA values? Are you getting different buy/sell signals from the crossover indicator? Are the signals the same but they result in different orders?
Additionally, without the full script and the data file, it's going to be hard to try it out locally. I ran your script against some AAPL prices and it just generates one sell signal during the period of test. Is that right or wrong? Is it consistent with what TradeView would show? It's hard for me to know.