← Back to Home
Decision Trees and EMA Crossover 50% Average Annual Returns

Decision Trees and EMA Crossover 50% Average Annual Returns

I am working on trading strategies that blend traditional technical indicators with machine learning to generate buy and sell signals. In this article I try mixing a simple EMA crossover strategy with Decision Trees. First I will explain a bit on the theory of the methods and then share the Python implementation of the strategy with some backtest results. You can read this article on my website as well: https://www.aliazary.com/. You will find more articles and resources as well. You can also subscribe with your email address so that you get my newsletter and don’t miss out on anything, especially my new backtesting app that I am working on. You can add your own strategies, modify the strategies and change the parameters and the asset and dates for backtesting the strategies to find the best strategies for trading:

1_2Wv9CPpkmEQFwszVpkcv2g 1.webp

1. Theoretical Foundations

1.1 Exponential Moving Average (EMA)

The Exponential Moving Average (EMA) is a weighted moving average that gives more importance to recent prices, making it more responsive to new information. The formula is:

\[\text{EMA}_t = \alpha \cdot P_t + (1 - \alpha) \cdot \text{EMA}_{t-1}\]

where:

In our strategy, we use two EMAs:

A bullish signal is generated when the short-term EMA crosses above the long-term EMA, while a bearish signal occurs when it crosses below.

1.2 Relative Strength Index (RSI)

The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and change of price movements. Its formula is:

\[\text{RSI} = 100 - \frac{100}{1 + RS}\]

with

\[RS = \frac{\text{Average Gain}}{\text{Average Loss}}\]

Typically, an RSI above 70 suggests that an asset may be overbought, while an RSI below 30 indicates oversold conditions.

1.3 Moving Average Convergence Divergence (MACD)

The MACD is a trend-following momentum indicator that shows the relationship between two EMAs of a security’s price. It is calculated as:

\[\text{MACD} = \text{EMA}_{\text{short}} - \text{EMA}_{\text{long}}\]

Usually, the short-term EMA is taken over 12 periods and the long-term EMA over 26 periods. A signal line, typically a 9-period EMA of the MACD, is also computed. In this strategy, the MACD histogram (the difference between the MACD line and its signal line) is used to capture momentum changes.


2. Decision Trees: Theory and Equations

2.1 Introduction to Decision Trees

A Decision Tree is a non-parametric supervised learning method used for both classification and regression. In classification, the goal is to assign a class label to a given input by learning decision rules inferred from the features.

2.2 Structure of a Decision Tree

A decision tree is composed of:

2.3 Splitting Criteria

To split the data at each node, decision trees typically use measures of impurity such as Entropy or the Gini Index.

Entropy

Entropy is a measure of the randomness or impurity in the data. For a binary classification, the entropy \(H\) is calculated as:

\[H(p) = -p \log_2(p) - (1-p) \log_2(1-p)\]

where \(p\) is the proportion of positive examples. A perfectly pure node (all examples of one class) has an entropy of 0.

Information Gain

Information Gain (IG) is used to measure the effectiveness of a split. It is defined as the difference between the entropy of the parent node and the weighted average of the entropies of the child nodes:

\[\text{IG} = H(\text{parent}) - \sum_{i=1}^{k} \frac{N_i}{N} H(\text{child}_i)\]

where:

Gini Index

The Gini Index is another measure of impurity:

\[\text{Gini}(p) = 1 - \sum_{i=1}^{C} p_i^2\]

where \(p_i\) is the probability of class \(i\) in the node. Lower values indicate higher purity.

2.4 Decision Trees in the Trading Strategy

In our strategy, the Decision Tree Classifier is used to predict whether the price will go up (represented by 1) or not (represented by 0). The steps include:

  1. Feature Extraction:
    The classifier uses features derived from technical indicators (e.g., EMA values, RSI, MACD, signal values) over a defined lookback window.

  2. Training:
    The decision tree is trained on historical data from the lookback window. The training involves splitting the data based on the feature values to minimize impurity (using either entropy or Gini Index).

  3. Prediction:
    The latest feature vector is passed to the trained decision tree, which predicts the class label (up or down). This prediction is then used as one of the signals for trade execution.

  4. Model Adaptation:
    The model is retrained continuously using a rolling window, ensuring that it adapts to new market conditions.


3. Strategy Implementation

3.1 Feature Engineering

In this strategy, features are generated from a lookback window (default 30 periods) including:

These features are stacked into a matrix \(X\) for the decision tree to process. The target variable \(y\) is defined based on whether the price increased in the lookback window.

3.2 Training and Prediction Process

3.3 Trade Execution Logic

The strategy combines the machine learning prediction with the EMA crossover condition:

3.4 Code Walkthrough

Below is a Python implementation of the the strategy for use with backtrader library (or the BACKTESTER app) that integrates these concepts:

class DecisionTree_EMA_Crossover_Strategy(bt.Strategy):
    params = (("lookback_period", 30),)

    def __init__(self):
        # Data series and lookback window
        self.data_close = self.datas[0].close
        self.window = self.params.lookback_period
        
        # Decision Tree Classifier initialization
        self.model = DecisionTreeClassifier(random_state=42)
        
        # Technical indicators initialization
        self.emas = bt.indicators.ExponentialMovingAverage(self.data_close, period=50)
        self.emal = bt.indicators.ExponentialMovingAverage(self.data_close, period=200)
        self.rsi = bt.indicators.RelativeStrengthIndex(self.data_close, period=14)
        self.macd = bt.indicators.MACDHisto(self.data_close,
                                             period_me1=12,
                                             period_me2=26,
                                             period_signal=9)
        self.order = None  # Track pending orders

    def next(self):
        # Ensure sufficient data is available for the lookback period
        if len(self) > self.window:
            # Extract indicator values over the lookback window
            emas_values = np.array(self.emas.get(size=self.window))
            emal_values = np.array(self.emal.get(size=self.window))
            rsi_values = np.array(self.rsi.get(size=self.window))
            macd_values = np.array(self.macd.macd.get(size=self.window))
            signal_values = np.array(self.macd.signal.get(size=self.window))
            
            # Construct feature matrix X
            X = np.column_stack((emas_values, emal_values, rsi_values, macd_values, signal_values))
            
            # Define target variable: 1 if price increased, 0 otherwise
            prices = np.array(self.data_close.get(size=self.window + 1))
            y = np.where(np.diff(prices) > 0, 1, 0)
            
            # Prepare training and testing data
            X_train = X[:-1]
            y_train = y[1:]  # Shift target by one period to align with features
            X_test = X[-1]
            
            # Train the decision tree on historical lookback data
            self.model.fit(X_train, y_train)
            
            # Predict the next move using the most recent features
            prediction = self.model.predict(X_test.reshape(1, -1))
            
            # Trade execution: enter position if conditions are met
            if not self.position:
                cash = self.broker.get_cash()
                asset_price = self.data_close[0]
                position_size = cash / asset_price * 0.99
                
                # Buy if prediction is 1 and the EMA crossover is bullish
                if prediction[0] == 1 and self.emas[0] > self.emal[0]:
                    self.buy(size=position_size)
                    self.log(f"Buy order placed at price: {asset_price:.2f}")
            else:
                # Close position if the EMA crossover indicates a bearish trend
                if self.emas[0] < self.emal[0]:
                    self.close()
                    self.log(f"Position closed at price: {self.data_close[0]:.2f}")

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return

        # Log order execution details
        if order.status == order.Completed:
            if order.isbuy():
                self.log(f"Buy executed: {order.executed.price:.2f}")
            elif order.issell():
                self.log(f"Sell executed: {order.executed.price:.2f}")
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log("Order canceled/margin/rejected")
        self.order = None

    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print(f"{dt.isoformat()}, {txt}")

5. Backtests

let’s see the backtest results for trading Bitcoin for 5 consecutive years from 2020 to 2025. Since the strategy only takes long positions, it won’t make money in bearish markets. However, we can easily add short positions using opposite conditions so that we can make money in any market regime. If you are trading using a margin or futures account you can take short positions as well:

1_2Wv9CPpkmEQFwszVpkcv2g.webp
1_ZhRVxa7aCOYNzouXc3pBCQ.webp
1_IrSuMTr6fSl9BAbzhcbJNQ.webp
1_NsD8a7tzNxEnXahla5Nakg.webp
1_kQHBhBLCMohnNUWJrBUXbQ.webp

Overall the results seem promising. In the ranging market of 2021 we lost about 20%, which can be easily avoided with a stop-loss. The best case was the bullish market of 2020 where we made more than 200%. For a long-only strategy its performance is not so bad even for ranging or bearish markets. If we implement short selling and also put in place stop-loss conditions or any other risk management strategy, it has great potential as a consistently profitable strategy. In the end, please make sure to backtest it thoroughly for different periods and different assets to make sure its performance is what you expect. Also please make sure to go over the code carefully so that there are no mistakes. Always be careful, and try with a small account for real trading, so you make sure the real-life performance is good enough and you don’t risk losing your money.


5. Conclusion

The DecisionTree_EMA_Crossover_Strategy represents a hybrid approach that integrates machine learning with traditional technical analysis. By employing technical indicators such as EMA, RSI, and MACD, the strategy gathers rich features that are fed into a decision tree classifier. The decision tree uses well-established splitting criteria—grounded in entropy, information gain, or the Gini Index—to predict future price movements. Coupled with the EMA crossover condition, this strategy aims to enhance trade execution by confirming machine-generated signals with trend-based indicators. As I mentioned before, you can make it even better adding short selling and implementing a simple risk management strategy like a stop-loss and end up with a very profitable trading bot that makes you money consistently.

This comprehensive overview provides both the theoretical background and the practical implementation details, offering a robust framework for adapting machine learning to dynamic trading environments. I hope you find it useful and I would also appreciate your ideas and comments if you have any.