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:
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:
\(P_t\) is the current price,
\(\alpha = \frac{2}{n+1}\) is the smoothing factor, and
\(n\) is the number of periods.
In our strategy, we use two EMAs:
Short-term EMA with a period of 50.
Long-term EMA with a period of 200.
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.
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.
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.
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.
A decision tree is composed of:
Root Node: Represents the entire dataset.
Internal Nodes: Each node represents a test on an attribute (feature).
Branches: The outcome of the test.
Leaf Nodes: Represent class labels or outcomes.
To split the data at each node, decision trees typically use measures of impurity such as Entropy or the Gini Index.
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 (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:
\(N\) is the total number of samples in the parent node,
\(N_i\) is the number of samples in child \(i\), and
\(H(\text{child}_i)\) is the entropy of child \(i\).
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.
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:
Feature Extraction:
The classifier uses features derived from technical indicators (e.g.,
EMA values, RSI, MACD, signal values) over a defined lookback
window.
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).
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.
Model Adaptation:
The model is retrained continuously using a rolling window, ensuring
that it adapts to new market conditions.
In this strategy, features are generated from a lookback window (default 30 periods) including:
Short-term EMA values (50 periods)
Long-term EMA values (200 periods)
RSI values (14 periods)
MACD values and its signal line
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.
Training Data:
The feature matrix \(X\) is constructed
from historical data (all rows except the last) and aligned with the
target variable \(y\) (shifted by one
period to maintain causality).
Prediction:
The most recent feature vector (last row of \(X\)) is fed into the decision tree to
predict whether the price will increase.
The strategy combines the machine learning prediction with the EMA crossover condition:
Entry Signal:
If the decision tree predicts a price increase (1) and the short-term
EMA is above the long-term EMA, a buy order is executed.
Position Sizing:
The size of the position is calculated based on available cash and the
asset price, with a minor adjustment factor (0.99) for risk
management.
Exit Signal:
If the short-term EMA falls below the long-term EMA, any open positions
are closed, signaling a potential trend reversal.
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):
= (("lookback_period", 30),)
params
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,
=12,
period_me1=26,
period_me2=9)
period_signalself.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
= np.array(self.emas.get(size=self.window))
emas_values = np.array(self.emal.get(size=self.window))
emal_values = np.array(self.rsi.get(size=self.window))
rsi_values = np.array(self.macd.macd.get(size=self.window))
macd_values = np.array(self.macd.signal.get(size=self.window))
signal_values
# Construct feature matrix X
= np.column_stack((emas_values, emal_values, rsi_values, macd_values, signal_values))
X
# Define target variable: 1 if price increased, 0 otherwise
= np.array(self.data_close.get(size=self.window + 1))
prices = np.where(np.diff(prices) > 0, 1, 0)
y
# Prepare training and testing data
= X[:-1]
X_train = y[1:] # Shift target by one period to align with features
y_train = X[-1]
X_test
# Train the decision tree on historical lookback data
self.model.fit(X_train, y_train)
# Predict the next move using the most recent features
= self.model.predict(X_test.reshape(1, -1))
prediction
# Trade execution: enter position if conditions are met
if not self.position:
= self.broker.get_cash()
cash = self.data_close[0]
asset_price = cash / asset_price * 0.99
position_size
# 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 or self.datas[0].datetime.date(0)
dt print(f"{dt.isoformat()}, {txt}")
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:
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.
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.