Problem with crossovers
-
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.