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



  • @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')
    
    


  • @ab_trader is this enough??



  • 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.



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



  • @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.



  • @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.



  • 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.



  • @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.


Log in to reply
 

});