Backtrader Community

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

    Problem with crossovers

    Indicators/Strategies/Analyzers
    4
    28
    1993
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Sajil Thamban
      Sajil Thamban @ab_trader last edited by

      @ab_trader

      import backtrader as bt
      import pandas as pd
      import backtrader.analyzers as btanalyzers
      import numpy as np
      import datetime
      # import CustomEMAIndicator as EMA
      startcash = 10000
      contract_size = 100000
      maintanance_margin = 2150
      pfast = 8
      pslow = 21
      pips_profit_loss = 200
      entry_condition = 'Close'
      csv_file = "EURUSDMTONE.csv"
      exit_at_crossover = False
      
      
      class MAcrossoveer(bt.Strategy):
          # Moving average parameters
          params = (('pfast', pfast), ('pslow', pslow),)
      
          def __init__(self):
              # Keeps track of bracket orders.
              self.o_li = list()
              self.crossovers = []
              self.ema_fast = bt.indicators.ExponentialMovingAverage(self.data, period=self.p.pfast)
              self.ema_slow = bt.indicators.ExponentialMovingAverage(self.data, period=self.p.pslow)
              # self.ema_fast = EMA(self.data.close.array, n=self.p.pfast)
              # self.ema_slow = EMA(self.data.close.array, n=self.p.pslow)
              self.trades_occured = False
              self.crossover = bt.ind.CrossOver(self.ema_fast, self.ema_slow)
              self.contract_size = contract_size
              self.maintanance_margin = maintanance_margin
              self.LookBack = 14
              self.lows = []
              self.highs = []
              self.count = 0
              self.altering_params = dict(pips_profit_loss=pips_profit_loss, entry_condition=entry_condition)
              self.profits = []
              self.losses = []
              self.pending_orders = []
              self.buy_sell_request_made = False
              self.equity = []
              self.index = []
              self.trades_occured_list = []
      
          def log(self, txt, dt=None):
              """ Logging function fot this strategy"""
              dt = dt or self.data.datetime[0]
              if isinstance(dt, float):
                  dt = bt.num2date(dt)
              print("%s, %s" % (dt.date(), txt))
      
          def print_signal(self):
              """ Prints OHLCV """
              self.log(
                  "o {}\th {}\tl {}\tc {}\tv {}".format(
                      self.datas[0].open[0],
                      self.datas[0].high[0],
                      self.datas[0].low[0],
                      self.datas[0].close[0],
                      self.datas[0].volume[0],
                  )
              )
              self.count += 1
      
          def notify_order(self, order):
              """Triggered upon changes to orders. Notifications on order changes are here."""
      
              # Suppress notification if it is just a submitted order.
              if order.status == order.Submitted:
                  return
      
              # Print out the date, security name, order number and status.
              dt, dn = self.datetime.date(), order.data._name
      
              self.log(
                  "{} Order {} Status {}".format(dn, order.ref, order.getstatusname())
              )
      
              # Check if an order has been completed
              # Attention: broker could reject order if not enough cash
              if order.status in [order.Completed, order.Margin]:
                  if order.isbuy():
                      self.log(
                          "BUY EXECUTED for {}, Price: {}, Cost: {}, Comm {}, Order Ref {}".format(
                              dn,
                              order.executed.price,
                              order.executed.value,
                              order.executed.comm,
                              order.ref
                          )
                      )
                  else:  # Sell
                      self.log(
                          "SELL EXECUTED for {}, Price: {}, Cost: {}, Comm {}, Order Ref {}".format(
                              dn,
                              order.executed.price,
                              order.executed.value,
                              order.executed.comm,
                              order.ref
                          )
                      )
                  if order in self.pending_orders:
                      self.pending_orders.remove(order)
      
          def notify_trade(self, trade):
              if not trade.isclosed:
                  return
              print("*******************************************************************************************************************************")
              self.log("OPERATION PROFIT, GROSS %s, NET %s, CURRENT CASH VALUE %s" %
                       (trade.pnl, trade.pnlcomm,cerebro.broker.getvalue()))
              print("********************************************************************************************************************************")
              self.trades_occured_list.append(trade)
              for order in self.pending_orders:
                  self.cancel(order)
              self.pending_orders = []
              if trade.pnl < 0:
                  self.losses.append(trade.pnl)
              elif trade.pnl > 0:
                  self.profits.append(trade.pnl)
      
          def stop(self):
              def _data_period(index):
                  """Return data index period as pd.Timedelta"""
                  values = pd.Series(index[-100:])
                  return values.diff().median()
      
              def _round_timedelta(value, _period=_data_period(self.index)):
                  if not isinstance(value, pd.Timedelta):
                      return value
                  resolution = getattr(_period, 'resolution_string', None) or _period.resolution
                  return value.ceil(resolution)
              start = self.index[0]
              end = self.index[-1]
              dd = 1 - self.equity / np.maximum.accumulate(self.equity)
              dd = pd.Series(dd, index=self.index)
              iloc = np.unique(np.r_[(dd == 0).values.nonzero()[0], len(dd) - 1])
              iloc = pd.Series(iloc, index=dd.index[iloc])
              df = iloc.to_frame('iloc').assign(prev=iloc.shift())
              df = df[df['iloc'] > df['prev'] + 1].astype(int)
              # If no drawdown since no trade, avoid below for pandas sake and return nan series
              if not len(df):
                  return (dd.replace(0, np.nan),) * 2
              dd_dur = df['duration'] = df['iloc'].map(dd.index.__getitem__) - df['prev'].map(dd.index.__getitem__)
              dd_peaks = df['peak_dd'] = df.apply(lambda row: dd.iloc[row['prev']:row['iloc'] + 1].max(), axis=1)
              df = df.reindex(dd.index)
              print('Start -> ', start)
              print('End -> ', end)
              print('Duration -> ', end - start)
              print('Equity Final [$] -> ', self.equity[-1])
              print('Equity Peak [$] -> ', max(self.equity))
              print('Return [%] -> ', (self.equity[-1] - self.equity[0]) / self.equity[0] * 100)
              c = self.data.close.array
              print('Buy & Hold Return [%] -> ', abs(c[-1] - c[0]) / c[0] * 100)  # long OR short
              max_dd = -np.nan_to_num(dd.max()) * 100
              print('Max. Drawdown [%] -> ', max_dd)
              print('Avg. Drawdown [%] -> ', -dd_peaks.mean() * 100)
              print('Max. Drawdown Duration - >', _round_timedelta(dd_dur.max()))
              print('Avg. Drawdown Duration -> ', _round_timedelta(dd_dur.mean()))
              n_trades = len(self.trades_occured_list)
              print('# Trades -> ', n_trades)
              pl = pd.Series([t.pnl for t in self.trades_occured_list])
              win_rate = np.nan if not n_trades else (pl > 0).sum() / n_trades * 100  # noqa: E501
              print('Win Rate [%] -> ', win_rate)
              returns = pd.Series([(t.pnl/self.contract_size)/t.price for t in self.trades_occured_list])
              print('Best Trade [%] -> ' , returns.max() * 100)
              print('Worst Trade [%] -> ', returns.min() * 100)
              mean_return = np.exp(np.log(1 + returns).sum() / (len(returns) or np.nan)) - 1
              print('Avg. Trade [%] -> ', mean_return * 100)
              print('Profit Factor -> ', returns[returns > 0].sum() / (abs(returns[returns < 0].sum()) or np.nan))  # noqa: E501
              print('Expectancy [%] -> ', ((returns[returns > 0].mean() * win_rate -
                                          returns[returns < 0].mean() * (100 - win_rate))))
              print('SQN -> ', np.sqrt(n_trades) * pl.mean() / (pl.std() or np.nan))
              print('Sharpe Ratio -> ', mean_return / (returns.std() or np.nan))
              print('Sortino Ratio -> ', mean_return / (returns[returns < 0].std() or np.nan))
              print('Calmar Ratio -> ', mean_return / ((-max_dd / 100) or np.nan))
      
      
          def next(self):
              self.index.append(bt.num2date(self.data.datetime[0]).date())
              self.equity.append(cerebro.broker.getvalue())
              self.print_signal()
              if self.altering_params['entry_condition'] == 'Open':
                  # price_array = self.data.Open
                  price_array = self.datas[0].open
              elif self.altering_params['entry_condition'] == 'High':
                  # price_array = self.data.High
                  price_array = self.datas[0].high
              elif self.altering_params['entry_condition'] == 'Low':
                  # price_array = self.data.Low
                  price_array = self.datas[0].low
              elif self.altering_params['entry_condition'] == 'Close':
                  # price_array = self.data.Close
                  price_array = self.datas[0].close
      
              try:
                  self.recentLow = min(self.data.low.get(size=self.LookBack+1)[:self.LookBack])
                  self.lows.append(self.recentLow)
                  self.recentHigh = max(self.data.high.get(size=self.LookBack+1)[:self.LookBack])
                  self.highs.append(self.recentHigh)
      
              except Exception as e:
                  print(e)
      
              if cerebro.broker.getvalue() < self.maintanance_margin:
                  pass
              # pending opening orders do nothing
              # if len(self.o_li) > 0:
              #     return
              if self.crossover == 1:
                  if self.buy_sell_request_made and exit_at_crossover:
                      self.close()
                  price = price_array[0]
                  for low in self.lows[::-1]:
                      if price > low:
                          stop_loss = low
                          break
                  print("-----------------------------------------------------------")
                  print(" FAST MA CROSSES OVER SLOW MA ON DATE -> ", bt.num2date(self.data.datetime[0]).date())
                  print("-----------------------------------------------------------")
                  self.crossovers.append(self.datetime.date())
                  take_profit = price + self.altering_params['pips_profit_loss']/10000
                  # for order in self.pending_orders:
                  #     self.cancel(order)
                  # self.pending_orders = []
                  if not exit_at_crossover:
                      self.long_buy_order = self.buy_bracket(
                          exectype=bt.Order.Stop,
                          price=price,
                          stopprice=stop_loss,
                          stopexec=bt.Order.Stop,
                          limitprice=take_profit,
                          limitexec=bt.Order.Limit)
                  else:
                      self.long_buy_order = self.buy(
                          exectype=bt.Order.Stop,
                          price=price)
                  self.buy_sell_request_made = True
                  if isinstance(self.long_buy_order,list):
                      self.pending_orders = self.pending_orders + self.long_buy_order
                      self.log(
                          "LONG BUY at market {}: Oref {} / Buy at {} / Stop loss at {} / Take Profit at {}".format(
                              self.datetime.date(),
                              self.long_buy_order[0].ref,
                              price,
                              stop_loss,
                              take_profit
                          ))
                  else:
                      self.pending_orders.append(self.long_buy_order)
                      self.log(
                      "LONG BUY at market {}: Oref {} / Buy at {} / Stop loss at {} / Take Profit at {}".format(
                          self.datetime.date(),
                          self.long_buy_order.ref,
                          price,
                          stop_loss,
                          take_profit
                      ))
                  # Store orders in a list
      
              else:
                  pass
      
              if self.crossover == -1:
                  if self.buy_sell_request_made and exit_at_crossover:
                      self.close()
                  price = price_array[0]
                  # price = self.data.Close[-1]
                  for high in self.highs[::-1]:
                      if price < high:
                          stop_loss = high
                          break
                  print("-----------------------------------------------------------")
                  print(" SLOW MA CROSSES OVER FAST MA ON DATE -> ", bt.num2date(self.data.datetime[0]).date())
                  print("-----------------------------------------------------------")
                  self.crossovers.append(self.datetime.date())
                  take_profit = price - self.altering_params['pips_profit_loss']/10000
                  # for order in self.pending_orders:
                  #     self.cancel(order)
                  # self.pending_orders = []
                  if not exit_at_crossover:
                      self.short_sell_order = self.sell_bracket(
                          exectype=bt.Order.Stop,
                          price=price,
                          stopprice=stop_loss,
                          stopexec=bt.Order.Stop,
                          limitprice=take_profit,
                          limitexec=bt.Order.Limit
                      )
                  else:
                      self.short_sell_order = self.sell(
                          exectype=bt.Order.Stop,
                          price=price
                      )
                  if isinstance(self.short_sell_order,list):
                      self.pending_orders = self.pending_orders + self.short_sell_order
                      self.log(
                          "SHORT SELL at market {}: Oref {} / Sell at {} / Stop Loss at {} / Take Profit at {}".format(
                              self.datetime.date(),
                              self.short_sell_order[0].ref,
                              price,
                              stop_loss,
                              take_profit
                          )
                      )
                  else:
                      self.pending_orders.append(self.short_sell_order)
                      self.log(
                          "SHORT SELL at market {}: Oref {} / Sell at {} / Stop Loss at {} / Take Profit at {}".format(
                              self.datetime.date(),
                              self.short_sell_order.ref,
                              price,
                              stop_loss,
                              take_profit
                          )
                      )
                  self.trades_occured = True
      
                  # Store orders in a list
      
              else:
                  pass
      
      
      if __name__ == "__main__":
          cerebro = bt.Cerebro()
          # Set commission
          cerebro.broker.setcommission(margin=maintanance_margin, mult=contract_size)
          date_format = '%Y-%m-%d'
          for _ in range(5):
              try:
                  dateparse = lambda x: pd.datetime.strptime(x, date_format)
                  df = pd.read_csv(csv_file, parse_dates=[0], date_parser=dateparse)
              except:
                  date_format = '%d-%m-%Y'
          if date_format != "%Y-%m-%d":
              csv_file = csv_file + '_date_reformatted.csv'
              adj_close_in_dataframe = "Adj Close" in df or "adj close" in df
              if not adj_close_in_dataframe:
                  df.insert(5, 'Adj Close', df['Close'])
              df.to_csv(csv_file, index=False, date_format="%Y-%m-%d")
      
          else:
              adj_close_in_dataframe = "Adj Close" in df or "adj close" in df
              if not adj_close_in_dataframe:
                  df.insert(5, 'Adj Close', df['Close'])
          data = bt.feeds.YahooFinanceCSVData(
              dataname=csv_file,
              dtformat=("%Y-%m-%d"),
              timeframe=bt.TimeFrame.Days,
              round=False,decimals=6
          )
          cerebro.adddata(data)
          cerebro.broker.setcash(startcash)
          cerebro.addstrategy(MAcrossoveer)
          # Execute
          # cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='mysharpe')
          # cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name="ta")
          # cerebro.addanalyzer(btanalyzers.SQN, _name="sqn")
          # cerebro.addanalyzer(btanalyzers.DrawDown, _name="drawdown")
          # cerebro.addanalyzer(btanalyzers.Calmar, _name= "calmar")
          # cerebro.addanalyzer(bt.analyzers.SharpeRatio_A, _name="sharpe_a")
          q = cerebro.run()
          thestrat = q[0]
          # print('Sharpe Ratio:', thestrat.analyzers.mysharpe.get_analysis())
          # print('SQN:', thestrat.analyzers.sqn.get_analysis())
          # print('Drawdown:', thestrat.analyzers.drawdown.get_analysis())
          # print('Calmar:', thestrat.analyzers.calmar.get_analysis())
          # print('Sharpe_A:', thestrat.analyzers.sharpe_a.get_analysis())
          crossovers = q[0].crossovers
          print('Cross Overs --> ', crossovers)
          profits = sum(q[0].profits)
          losses = sum(q[0].losses)
          # portvalue = cerebro.broker.getvalue()
          # pnl = portvalue - startcash
          # print('Final Portfolio Value: ${}'.format(portvalue))
          # print('P/L: ${}'.format(pnl))
          cerebro.plot(style='candlestick', barup='green')
      
      
      1 Reply Last reply Reply Quote 0
      • Sajil Thamban
        Sajil Thamban @ab_trader last edited by

        @ab_trader is this enough??

        A 1 Reply Last reply Reply Quote 1
        • run-out
          run-out last edited by

          Try this just to see if your assumptions are correct. At the first line of next, print the [datetime, ema1, ema2 and crossover] You can manually check to see if your crossover is on the right date by looking at the ema values when the crossover occurs.

          RunBacktest.com

          1 Reply Last reply Reply Quote 2
          • run-out
            run-out last edited by

            Oh, and yes, that was the perfect amount of code. Thanks.

            RunBacktest.com

            1 Reply Last reply Reply Quote 1
            • A
              ab_trader @Sajil Thamban last edited by

              @Sajil-Thamban said in Problem with crossovers:

              is this enough??

              More than enough.

              First, actual line crossover can happen only between two bars by crossover nature. Second, it can be registered only on the second of two bars. With this in mind, the recorded crossovers on the first picture (where the deviation between recorded and "actual" crossovers is shown) are the correct crossovers. For me it is clear that in the first instance the bar marked as recorded crossover have red EMA higher than blue EMA, and previous bar (marked as first crossover) have blue EMA higher than red EMA. Same approach is valid for second crossover instance on the picture, considering that the colors will be opposite.

              Or you can follow @run-out's advice and print EMA values to verify the crossover signal.

              • If my answer helped, hit reputation up arrow at lower right corner of the post.
              • Python Debugging With Pdb
              • New to python and bt - check this out
              Sajil Thamban 1 Reply Last reply Reply Quote 2
              • Sajil Thamban
                Sajil Thamban @ab_trader last edited by

                @ab_trader Is there a way to fix this or perhaps a way to make the crossover be recorded where the crossover has actually occurred.

                1 Reply Last reply Reply Quote 0
                • A
                  ab_trader last edited by

                  There is no mistake, so no need to fix anything. Crossover is registered by bt at the time when it can be registered in real life.

                  • If my answer helped, hit reputation up arrow at lower right corner of the post.
                  • Python Debugging With Pdb
                  • New to python and bt - check this out
                  1 Reply Last reply Reply Quote 0
                  • kian hong Tan
                    kian hong Tan last edited by

                    @Sajil-Thamban I'm guessing when the crossover happens closer to the end of a bar, it will be treated as happening in the next bar. Because I observed similar behavior for my testing. I have the exact same strategy tested in backtrader and Tradingview. For some trades, the enter and exit (crossovers) occur at the same bar but for some trades, the crossovers will occur one bar earlier or later, even visually I saw the crossover as happening at the same bar on both platforms. The situation will become even worse when both MA lines overlapping each other during a tight range. Just my 2 cents worth of opinion.

                    1 Reply Last reply Reply Quote 0
                    • 1
                    • 2
                    • 2 / 2
                    • First post
                      Last post
                    Copyright © 2016, 2017, 2018, 2019, 2020, 2021 NodeBB Forums | Contributors