Trend-following strategies are a cornerstone of quantitative trading, aiming to profit from sustained price movements. The Average Directional Index (ADX) is a popular indicator for assessing trend strength, often combined with its directional components, +DI and -DI, to determine trend direction. However, ADX-based strategies can still suffer from false signals or whipsaws in ranging markets.
To enhance the robustness of an ADX-based strategy, we can integrate Machine Learning (ML), specifically a Multi-Layer Perceptron (MLP) Neural Network. This article explores a Neural Network-Enhanced ADX Trend Strength Strategy, designed for short-term (3-month) data, implemented using the Backtrader framework. The neural network acts as an intelligent filter, using learned patterns to confirm the reliability of ADX signals.
We begin by importing all the necessary Python libraries for financial data, backtesting, and machine learning:
import backtrader as bt
import datetime
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import warnings
"ignore")
warnings.filterwarnings(
'figure.figsize'] = (12, 8) plt.rcParams[
backtrader
: The robust backtesting
framework, allowing us to simulate our strategy against historical
data.datetime
: For handling dates,
especially when specifying data ranges.yfinance
: A convenient library to
fetch historical stock and cryptocurrency data from Yahoo Finance.pandas
&
numpy
: Core libraries for data
manipulation and numerical operations, fundamental for handling
financial time series.matplotlib.pyplot
: For plotting the
backtesting results and visualizing strategy performance.sklearn.neural_network.MLPClassifier
:
The Multi-Layer Perceptron classifier from Scikit-learn, which forms the
neural network component of our strategy.sklearn.preprocessing.StandardScaler
,
MinMaxScaler
: Preprocessing tools to scale
features, crucial for neural networks to converge efficiently.
StandardScaler
is used here.sklearn.model_selection.train_test_split
:
For dividing our feature and label data into training and testing
sets.sklearn.metrics.accuracy_score
: To
evaluate the performance of our neural network.warnings
: Used to suppress minor
warnings during execution, keeping the output clean.NeuralNetworkEnhancedADXStrategy
ClassThis is the central component of our trading system, inheriting from
backtrader.Strategy
. It combines traditional ADX-based
trend analysis with a dynamically trained neural network.
class NeuralNetworkEnhancedADXStrategy(bt.Strategy):
"""
Neural Network-Enhanced ADX Trend Strength Strategy optimized for 3-month data
"""
= (
params # EXACT original parameters
'adx_period', 14),
('adx_threshold', 25),
('boll_period', 20),
('boll_devfactor', 2),
('confirmation_bars', 3),
('trail_percent', 0.02),
('printlog', True),
(# Neural Network parameters only
'nn_threshold', 0.8), # NN confidence threshold for 3-month data
('retrain_frequency', 30), # Retrain every 30 bars for 3-month data
('nn_lookback', 10), # Sequence length for neural network
( )
params
)The params
tuple allows for easy customization and
optimization of the strategy’s behavior without altering the code
structure:
adx_period
: The lookback period for
calculating the ADX, +DI, and -DI indicators.adx_threshold
: The minimum ADX value
to consider a trend strong enough for trading.boll_period
: The period for the
Bollinger Bands (middle band).boll_devfactor
: The number of standard
deviations for the Bollinger Bands’ upper and lower bands.confirmation_bars
: The number of
consecutive bars required to confirm a directional signal before
entering a trade.trail_percent
: The percentage for the
trailing stop-loss, used to protect profits.printlog
: A boolean flag to
enable/disable logging within the strategy.nn_threshold
: The minimum confidence
(probability) score from the neural network required to validate an ADX
signal.retrain_frequency
: The interval (in
bars) at which the neural network model will be re-trained. For 3-month
data, frequent retraining (e.g., every 30 bars, roughly a month of daily
data) helps the model adapt to recent market dynamics.nn_lookback
: The number of past bars
to consider when creating sequences of features for the neural network.
This allows the network to learn from short-term price and indicator
patterns.__init__
) and Logging (log
)The __init__
method sets up all necessary indicators and
internal variables. The log
method provides a consistent
way to print messages during the backtest.
def log(self, txt, dt=None, doprint=False):
"""EXACT original log function"""
if self.params.printlog or doprint:
= dt or self.datas[0].datetime.datetime(0)
dt print(f"{dt.isoformat()} - {txt}")
def __init__(self):
# EXACT original indicators
self.dataclose = self.datas[0].close
self.adx = bt.indicators.ADX(self.datas[0], period=self.params.adx_period)
self.plusdi = bt.indicators.PlusDI(self.datas[0], period=self.params.adx_period)
self.minusdi = bt.indicators.MinusDI(self.datas[0], period=self.params.adx_period)
self.boll = bt.indicators.BollingerBands(self.datas[0],
=self.params.boll_period,
period=self.params.boll_devfactor)
devfactor
# EXACT original tracking variables
self.reversal_counter = 0
self.order = None
self.trail_order = None
# Minimal additional indicators for NN features (optimized for 3-month data)
self.rsi = bt.indicators.RSI(period=7) # Fast RSI for 3-month data
self.sma = bt.indicators.SMA(period=10) # Fast SMA
self.volume_ma = bt.indicators.SMA(self.data.volume, period=10)
self.atr = bt.indicators.ATR(period=7) # Fast ATR
self.momentum = bt.indicators.Momentum(period=5)
# Neural Network components (optimized for 3-month data)
self.nn_model = None
self.scaler = StandardScaler()
self.feature_sequences = []
self.label_buffer = []
self.last_retrain = 0
self.nn_ready = False
# Performance tracking
self.total_signals = 0
self.nn_filtered_signals = 0
dataclose
: A convenient alias for the
closing price data feed.ADX
,
PlusDI
,
MinusDI
: These Backtrader indicators are
central to identifying trend strength and direction.BollingerBands
: Used to identify
periods of consolidation or expansion.reversal_counter
: Tracks consecutive bars confirming a
potential trend reversal.order
, trail_order
: To keep track of
pending and active orders.RSI
, SMA
, volume_ma
,
ATR
, and Momentum
. These provide a broader
view of market conditions.nn_model
: Will store the trained
MLPClassifier
.scaler
: An instance of StandardScaler
to
normalize the input features for the neural network.feature_sequences
, label_buffer
: Buffers
to collect historical data (sequences of features and their
corresponding labels) for NN training.last_retrain
: Records the bar index when the NN was
last trained.nn_ready
: A flag to indicate if the neural network is
trained and ready for predictions.total_signals
and nn_filtered_signals
track the impact of the neural
network filter.These methods are critical for preparing the data for the neural network, training the model, and obtaining its predictions.
calculate_features()
:
Bar-level Featuresdef calculate_features(self):
"""Calculate features optimized for ADX + neural networks + 3-month data"""
if (len(self.adx) < self.params.adx_period or
len(self.boll) < self.params.boll_period or
len(self.data) < self.params.nn_lookback):
return None
try:
= []
features
# Core ADX features
= self.adx[0] / 100 if not np.isnan(self.adx[0]) else 0.25
adx_strength = 1 if self.adx[0] > self.params.adx_threshold else 0
adx_above_threshold = (self.adx[0] - self.adx[-3]) / max(self.adx[-3], 1) if len(self.adx) > 3 and self.adx[-3] > 0 else 0
adx_trend
features.extend([adx_strength, adx_above_threshold, adx_trend])
# Directional Movement features
= self.plusdi[0] / 100 if not np.isnan(self.plusdi[0]) else 0.25
plusdi_norm = self.minusdi[0] / 100 if not np.isnan(self.minusdi[0]) else 0.25
minusdi_norm = (self.plusdi[0] - self.minusdi[0]) / 100
di_difference = self.plusdi[0] / max(self.minusdi[0], 1) if self.minusdi[0] > 0 else 1
di_ratio = 1 if self.plusdi[0] > self.minusdi[0] else 0
long_signal = 1 if self.minusdi[0] > self.plusdi[0] else 0
short_signal
features.extend([plusdi_norm, minusdi_norm, di_difference, di_ratio, long_signal, short_signal])
# Bollinger Band features
= (self.dataclose[0] - self.boll.mid[0]) / (self.boll.top[0] - self.boll.bot[0]) if (self.boll.top[0] - self.boll.bot[0]) > 0 else 0.5
bb_position = (self.boll.top[0] - self.boll.bot[0]) / self.dataclose[0] if self.dataclose[0] > 0 else 0
bb_width = 1 if bb_width < 0.01 else 0 # Bollinger band squeeze detection
bb_squeeze
features.extend([bb_position, bb_width, bb_squeeze])
# Price momentum features
= (self.dataclose[0] - self.dataclose[-1]) / self.dataclose[-1] if len(self.data) > 1 else 0
price_change_1 = (self.dataclose[0] - self.dataclose[-3]) / self.dataclose[-3] if len(self.data) > 3 else 0
price_change_3 = self.momentum[0] / self.dataclose[0] if self.dataclose[0] > 0 else 0
momentum_norm
features.extend([price_change_1, price_change_3, momentum_norm])
# Technical indicators (fast periods for 3-month data)
= self.rsi[0] / 100 if not np.isnan(self.rsi[0]) else 0.5
rsi_norm = (self.dataclose[0] - self.sma[0]) / self.sma[0] if self.sma[0] > 0 else 0
sma_distance = self.atr[0] / self.dataclose[0] if self.dataclose[0] > 0 else 0
atr_norm
features.extend([rsi_norm, sma_distance, atr_norm])
# Volume features
= self.data.volume[0] / self.volume_ma[0] if self.volume_ma[0] > 0 else 1.0
volume_ratio
features.append(volume_ratio)
# Trend confirmation features
= adx_strength * abs(di_difference)
trend_strength = self.reversal_counter / max(self.params.confirmation_bars, 1)
signal_persistence
features.extend([trend_strength, signal_persistence])
# Market condition features
= 1 if (self.adx[0] > self.params.adx_threshold and bb_width >= 0.01) else 0
market_trending
features.append(market_trending)
# Reversal context
= 1 if self.reversal_counter >= self.params.confirmation_bars else 0
potential_reversal
features.append(potential_reversal)
# Price action features
= (self.data.high[0] - self.data.low[0]) / self.dataclose[0] if self.dataclose[0] > 0 else 0
high_low_ratio
features.append(high_low_ratio)
# Clean features
= [0 if np.isnan(x) or np.isinf(x) else x for x in features]
features return np.array(features)
except:
return None
This comprehensive method generates a rich set of numerical features for the current bar. These features capture various aspects of price, volume, and indicator behavior relevant to trend strength and potential reversals. They are normalized where appropriate to assist the neural network’s learning. The features include:
reversal_counter
), and a binary flag for
trending market conditions or potential reversals.create_sequence_features()
:
Time-Series Context for the NNdef create_sequence_features(self):
"""Create sequence of features for neural network (time series approach)"""
if len(self.data) < self.params.nn_lookback:
return None
try:
= []
sequence for i in range(self.params.nn_lookback):
= -(self.params.nn_lookback - 1 - i)
lookback_idx
# Core time series features
if len(self.data) > abs(lookback_idx):
= self.dataclose[lookback_idx]
price = self.adx[lookback_idx] if len(self.adx) > abs(lookback_idx) else 25
adx_val = self.plusdi[lookback_idx] if len(self.plusdi) > abs(lookback_idx) else 25
plusdi_val = self.minusdi[lookback_idx] if len(self.minusdi) > abs(lookback_idx) else 25
minusdi_val
# Normalize values
= [
features_at_t / self.dataclose[0] if self.dataclose[0] > 0 else 1, # Price ratio
price / 100, # ADX normalized
adx_val / 100, # +DI normalized
plusdi_val / 100, # -DI normalized
minusdi_val - minusdi_val) / 100, # DI difference
(plusdi_val
]
# Add volume if available
if len(self.data.volume) > abs(lookback_idx):
= self.data.volume[lookback_idx] / max(self.data.volume[0], 1)
volume_norm
features_at_t.append(volume_norm)else:
1.0)
features_at_t.append(
sequence.extend(features_at_t)else:
# Pad with current values if not enough history
1.0, 0.25, 0.25, 0.25, 0.0, 1.0])
sequence.extend([
# Clean sequence
= [0 if np.isnan(x) or np.isinf(x) else x for x in sequence]
sequence return np.array(sequence)
except:
return None
Instead of just current bar features, neural networks (especially
MLPs used for time series) often benefit from a sequence of past
features. This method creates a flattened array of features
from the past nn_lookback
bars, giving the network context
on recent market behavior. It normalizes values relative to the current
bar’s close for consistency across time.
calculate_target_label()
:
Defining What to Predictdef calculate_target_label(self):
"""Calculate target for NN training - next 3 bars return for 3-month data"""
if len(self.data) < 5:
return 0
try:
# Use next 3 bars return for 3-month data
= (self.dataclose[-3] - self.dataclose[0]) / self.dataclose[0]
future_return return future_return
except:
return 0
The target label for the neural network is the percentage return of the closing price three bars into the future. This means the network learns to identify patterns that lead to significant moves within the next few bars, regardless of direction.
train_neural_network()
:
Training the MLP Classifierdef train_neural_network(self):
"""Train neural network model for 3-month data"""
if len(self.feature_sequences) < 25: # Minimum samples for 3-month data
return False
try:
= np.array(self.feature_sequences)
X = np.array(self.label_buffer)
y
# Remove invalid data
= np.isfinite(X).all(axis=1) & np.isfinite(y)
valid_mask = X[valid_mask], y[valid_mask]
X, y
if len(X) < 20:
return False
# Binary classification: top 50% returns are good for 3-month data
= np.percentile(np.abs(y), 50)
threshold = (np.abs(y) > threshold).astype(int)
y_binary
if len(np.unique(y_binary)) < 2:
return False
# Simple split for small datasets
if len(X) < 30:
= X, X, y_binary, y_binary
X_train, X_test, y_train, y_test else:
= train_test_split(
X_train, X_test, y_train, y_test =0.3, random_state=42
X, y_binary, test_size
)
# Scale features
= self.scaler.fit_transform(X_train)
X_train_scaled if len(X_test) > 0:
= self.scaler.transform(X_test)
X_test_scaled
# Neural Network optimized for small datasets and 3-month data
self.nn_model = MLPClassifier(
=(20, 10), # Small network for limited data
hidden_layer_sizes='relu',
activation='adam',
solver=0.01, # Regularization for small data
alpha='auto',
batch_size='adaptive',
learning_rate=0.001,
learning_rate_init=300, # More iterations for convergence
max_iter=True,
shuffle=42,
random_state=True,
early_stopping=0.2 if len(X_train) > 10 else 0.1,
validation_fraction=20
n_iter_no_change
)
self.nn_model.fit(X_train_scaled, y_train)
# Evaluate if we have test data
if len(X_test) > 0:
= accuracy_score(y_test, self.nn_model.predict(X_test_scaled))
accuracy else:
= accuracy_score(y_train, self.nn_model.predict(X_train_scaled))
accuracy
self.nn_ready = True
print(f"Neural Network trained - Accuracy: {accuracy:.3f}, Samples: {len(X)}")
return True
except Exception as e:
print(f"Neural Network training failed: {e}")
return False
This method is responsible for training the neural network:
feature_sequences
(X) and
label_buffer
(y).future_return
into a binary target. Here, returns with an
absolute value above the 50th percentile are labeled
1
(indicating a significant price move), and 0
otherwise. This simplifies the prediction task for the classifier.StandardScaler
,
crucial for neural network performance.MLPClassifier
is initialized with a small, two-layer
architecture ((20, 10)
) suitable for limited data. Key
parameters like alpha
(L2 regularization) and
early_stopping
are used to prevent overfitting, which is
common with small datasets. max_iter
and
n_iter_no_change
control convergence.self.nn_ready
is set to
True
, and the model’s accuracy on the (training/test) data
is printed.get_nn_confidence()
:
Getting Predictions from the NNdef get_nn_confidence(self, features):
"""Get neural network prediction confidence"""
if not self.nn_ready or self.nn_model is None or features is None:
return 0.5 # Neutral when NN not ready
try:
= self.scaler.transform(features.reshape(1, -1))
features_scaled = self.nn_model.predict_proba(features_scaled)[0]
proba return proba[1] if len(proba) > 1 else 0.5
except:
return 0.5
This method provides the neural network’s confidence
score for a given set of features. It takes the current bar’s
sequence_features
, scales them using the already
fitted scaler
, and then calls
predict_proba
on the nn_model
. This returns
the probability that the current market conditions belong to class
1
(a “significant move”). This probability is used as the
nn_confidence
score to filter trade signals.
These methods handle how the strategy interacts with the broker and tracks trade performance.
def notify_order(self, order):
"""EXACT original notify_order function"""
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.log(f"BUY EXECUTED at {order.executed.price:.2f}")
elif order.issell():
self.log(f"SELL EXECUTED at {order.executed.price:.2f}")
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log(f"Order Canceled/Margin/Rejected: {order.getstatusname()}")
# EXACT original order tracking reset
if order == self.order:
self.order = None
if order == self.trail_order:
self.trail_order = None
def notify_trade(self, trade):
"""EXACT original notify_trade function"""
if not trade.isclosed:
return
self.log(f"Trade Profit: GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}")
def cancel_trail(self):
"""EXACT original cancel_trail function"""
if self.trail_order:
self.log("Canceling active trailing stop order.")
self.cancel(self.trail_order)
self.trail_order = None
notify_order()
: A
backtrader
callback that updates the strategy on the status
of any placed order. It logs executions and handles the resetting of
self.order
and self.trail_order
once they are
completed, canceled, or rejected.notify_trade()
: Another
backtrader
callback that is triggered when a trade is
closed. It logs the gross and net profit/loss for that specific
trade.cancel_trail()
: A utility function to
cancel any active trailing stop order, typically called before placing a
new entry or reversal order.next()
Method: The Strategy’s Core LogicThe next()
method is the heart of the strategy, executed
for each new bar of data received by the system. This is where all the
indicators are evaluated, signals are generated, and trading decisions
are made, including the crucial ML filtering step.
def next(self):
# Collect features for NN training (minimal overhead)
= self.create_sequence_features()
sequence_features if sequence_features is not None and len(self.data) > 30:
= self.calculate_target_label()
target self.feature_sequences.append(sequence_features)
self.label_buffer.append(target)
# Keep buffer small for 3-month data
if len(self.feature_sequences) > 80:
self.feature_sequences = self.feature_sequences[-60:]
self.label_buffer = self.label_buffer[-60:]
# Retrain NN frequently for 3-month data
if len(self.data) - self.last_retrain >= self.params.retrain_frequency:
if self.train_neural_network():
self.last_retrain = len(self.data)
# EXACT original logic with NN enhancement
# EXACT original order check
if self.order:
return
# EXACT original position and trailing stop management
if self.position:
if not self.trail_order:
if self.position.size > 0:
self.log(f"Placing trailing stop order for long position at {self.dataclose[0]:.2f}")
self.trail_order = self.sell(
=bt.Order.StopTrail,
exectype=self.params.trail_percent)
trailpercentelif self.position.size < 0:
self.log(f"Placing trailing stop order for short position at {self.dataclose[0]:.2f}")
self.trail_order = self.buy(
=bt.Order.StopTrail,
exectype=self.params.trail_percent)
trailpercentreturn
# EXACT original data check
if len(self) < max(self.params.adx_period, self.params.boll_period):
return
# EXACT original ADX strength check
if self.adx[0] < self.params.adx_threshold:
self.log(f"Low ADX ({self.adx[0]:.2f}). Market trending weakly. Skipping trade.")
self.reversal_counter = 0
return
# EXACT original Bollinger Band width check
= self.boll.top[0] - self.boll.bot[0]
boll_width if boll_width < 0.01 * self.dataclose[0]:
self.log(f"Bollinger Bands narrow ({boll_width:.2f}). Market is range-bound. Skipping trade.")
self.reversal_counter = 0
return
# EXACT original directional signal
= self.plusdi[0] > self.minusdi[0]
long_signal = self.minusdi[0] > self.plusdi[0]
short_signal
# EXACT original confirmation logic
if (long_signal and self.position.size <= 0) or (short_signal and self.position.size >= 0):
self.reversal_counter += 1
else:
self.reversal_counter = 0
# EXACT original confirmation check
if self.reversal_counter < self.params.confirmation_bars:
return
# Track signals
self.total_signals += 1
# Get NN confidence
= self.get_nn_confidence(sequence_features)
nn_confidence
# EXACT original cancellation and execution with NN enhancement
self.cancel_trail()
# EXACT original execution with NN filter
if long_signal:
# NN ENHANCEMENT: Add NN filter
if not self.nn_ready or nn_confidence > self.params.nn_threshold:
if self.position and self.position.size < 0:
self.log(f"Reversing to long at {self.dataclose[0]:.2f} (NN: {nn_confidence:.3f})")
self.order = self.buy()
elif not self.position:
self.log(f"Going long at {self.dataclose[0]:.2f} (NN: {nn_confidence:.3f})")
self.order = self.buy()
else:
self.nn_filtered_signals += 1
self.log(f"LONG signal filtered - NN confidence {nn_confidence:.3f} < {self.params.nn_threshold}")
elif short_signal:
# NN ENHANCEMENT: Add NN filter
if not self.nn_ready or nn_confidence > self.params.nn_threshold:
if self.position and self.position.size > 0:
self.log(f"Reversing to short at {self.dataclose[0]:.2f} (NN: {nn_confidence:.3f})")
self.order = self.sell()
elif not self.position:
self.log(f"Going short at {self.dataclose[0]:.2f} (NN: {nn_confidence:.3f})")
self.order = self.sell()
else:
self.nn_filtered_signals += 1
self.log(f"SHORT signal filtered - NN confidence {nn_confidence:.3f} < {self.params.nn_threshold}")
The next()
method orchestrates the strategy’s real-time
decision-making process:
create_sequence_features()
and
calculate_target_label()
to build the training buffers. It
then checks if it’s time to retrain the neural network
based on retrain_frequency
and calls
train_neural_network()
. The buffers are kept compact to
manage memory and ensure the model adapts to recent data.adx_threshold
, confirming a strong trend.reversal_counter
ensures that a directional signal persists
for confirmation_bars
before a trade is considered,
reducing whipsaws.get_nn_confidence()
to obtain the
neural network’s probability that the current market state will lead to
a significant future move.total_signals
counter is incremented.nn_confidence
is
below the nn_threshold
, the signal is
filtered out, and nn_filtered_signals
is
incremented. This means the strategy abstains from trades the neural
network deems unlikely to succeed.nn_confidence
is sufficient, the strategy
cancels any existing trailing stop and places a buy()
or
sell()
order, potentially reversing an existing position or
opening a new one.stop()
Method: Post-Backtest Summarydef stop(self):
"""Enhanced stop function with NN statistics"""
= (self.nn_filtered_signals / self.total_signals * 100) if self.total_signals > 0 else 0
filter_rate
self.log(f"Ending Portfolio Value: {self.broker.getvalue():.2f}", doprint=True)
print(f'\n=== NEURAL NETWORK ENHANCED ADX RESULTS ===')
print(f'Total ADX Signals: {self.total_signals}')
print(f'NN Filtered Signals: {self.nn_filtered_signals} ({filter_rate:.1f}%)')
The stop()
method is automatically called at the very
end of the backtest. It prints a final summary, including the total
number of ADX signals generated and, crucially, how many of them were
filtered out by the neural network, along with the filtering rate. This
provides a clear metric of the neural network’s impact on trade
selection.
run_backtest()
FunctionThis standalone function sets up the backtrader.Cerebro
engine, loads data, configures the broker, runs the backtest, and
displays the results.
def run_backtest():
= bt.Cerebro()
cerebro =False)
cerebro.addstrategy(NeuralNetworkEnhancedADXStrategy, printlog
# Test with 3-month data
= 'BTC-USD'
ticker = datetime.datetime(2024, 1, 1) # 3 months
start_date = datetime.datetime(2024, 4, 1)
end_date
print(f"Fetching data for {ticker} from {start_date.date()} to {end_date.date()}")
try:
= yf.download(ticker, start=start_date, end=end_date, progress=False)
data_df if data_df.empty:
raise ValueError("No data fetched. Check ticker symbol or date range.")
# Standardize column names and ensure the index is datetime
= data_df.columns.droplevel(1).str.lower()
data_df.columns = pd.to_datetime(data_df.index)
data_df.index = bt.feeds.PandasData(dataname=data_df)
data
cerebro.adddata(data)except Exception as e:
print(f"Error fetching or processing data: {e}")
return
# Broker and position sizing setup
= 100000.0
initial_cash
cerebro.broker.setcash(initial_cash)=0.001)
cerebro.broker.setcommission(commission=95)
cerebro.addsizer(bt.sizers.PercentSizer, percents
# Add performance analyzers
='sharpe_ratio', timeframe=bt.TimeFrame.Days, riskfreerate=0.0)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='returns')
cerebro.addanalyzer(bt.analyzers.Returns, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='trades')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name
print(f"Starting Portfolio Value: {initial_cash:.2f}")
= cerebro.run()
results = cerebro.broker.getvalue()
final_value = ((final_value / initial_cash) - 1) * 100
total_return print(f"Final Portfolio Value: {final_value:.2f}")
print(f"Total Return: {total_return:.2f}%")
# Retrieve and print analysis results
= results[0]
strat = strat.analyzers
analyzers print("-" * 60)
print("Strategy Analysis (NN-Enhanced ADX Trend Strength - 3 Month Data):")
print("-" * 60)
try:
= analyzers.sharpe_ratio.get_analysis()
sharpe = sharpe.get("sharperatio") if sharpe else None
sharpe_value print(f"Sharpe Ratio: {sharpe_value:.3f}" if sharpe_value is not None else "Sharpe Ratio: None")
= analyzers.returns.get_analysis() or {}
returns if "rtot" in returns:
print(f"Total Return: {returns['rtot'] * 100:.2f}%")
if "rnorm100" in returns:
print(f"Annualized Return: {returns['rnorm100']:.2f}%")
= analyzers.drawdown.get_analysis() or {}
drawdown = drawdown.get("max", {})
dd_info = dd_info.get("drawdown", None)
max_drawdown_val print(f"Max Drawdown: {max_drawdown_val:.2f}%" if max_drawdown_val is not None else "Max Drawdown: None")
= analyzers.trades.get_analysis() or {}
trades = trades.get("total", {}).get("total", 0)
total_trades print(f"Total Trades: {total_trades}")
if total_trades > 0:
= trades.get("won", {}).get("total", 0)
winning_trades = trades.get("lost", {}).get("total", 0)
losing_trades = winning_trades / total_trades * 100
win_rate = trades.get("won", {}).get("pnl", {}).get("average", 0.0)
avg_win = trades.get("lost", {}).get("pnl", {}).get("average", 0.0)
avg_loss = (
profit_factor abs(trades.get("won", {}).get("pnl", {}).get("total", 0) /
"lost", {}).get("pnl", {}).get("total", 1))
trades.get(if trades.get("lost", {}).get("pnl", {}).get("total", 0) != 0
else "Inf"
)print(f"Winning Trades: {winning_trades}")
print(f"Losing Trades: {losing_trades}")
print(f"Win Rate: {win_rate:.2f}%")
print(f"Avg Winning Trade: {avg_win:.2f}")
print(f"Avg Losing Trade: {avg_loss:.2f}")
print(f"Profit Factor: {profit_factor}")
except Exception as e:
print(f"Analysis printing error: {e}")
print("-" * 60)
Cerebro
Setup: Initializes the
backtrader
engine and adds our
NeuralNetworkEnhancedADXStrategy
.
printlog=False
is passed to the strategy to control its
internal logging.BTC-USD
using yfinance
. It includes robust
error handling for data fetching and ensures correct column formatting
for backtrader
.backtrader.analyzers
to provide detailed performance
metrics:
SharpeRatio
: Measures risk-adjusted return.Returns
: Provides total and annualized returns.DrawDown
: Calculates maximum drawdown.TradeAnalyzer
: Offers detailed statistics on individual
trades (win rate, average win/loss, profit factor).cerebro.run()
executes the backtest. The script then prints a summary of the initial
and final portfolio values, total return, and the detailed analysis from
the added analyzers.cerebro.plot()
to
visualize price action, trades, and equity curve.A few rolling backtests generated quite promising results over the basic strategy. For example for a rolling backtest of 3-month windows from 2020 to 2025 for LINK-USD I got the following results:
Basic Strategy:
Augmented with Neural Networks:
This Neural Network-Enhanced ADX Trend Strength Strategy demonstrates a sophisticated approach to algorithmic trading by combining classic technical analysis with the adaptive power of machine learning. The neural network acts as an intelligent filter, aiming to improve trade quality by only taking signals that it has learned are likely to be profitable based on a sequence of market features.
The strategy’s design, including frequent retraining and the use of relevant features, is specifically tailored for short-term (3-month) datasets, allowing it to adapt quickly to evolving market conditions. While this implementation provides a solid foundation, further exploration could involve:
MLPClassifier
’s architecture and learning parameters, as
well as the strategy’s ADX and Bollinger Band parameters.By blending the interpretability of traditional indicators with the predictive strength of neural networks, such hybrid strategies offer a compelling path for developing more robust and intelligent automated trading systems.