← Back to Home
Neural Network-Enhanced ADX Trend Strength Strategy A Backtrader Implementation

Neural Network-Enhanced ADX Trend Strength Strategy A Backtrader Implementation

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.

1. Essential Libraries and Setup

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
warnings.filterwarnings("ignore")

plt.rcParams['figure.figsize'] = (12, 8)

2. The NeuralNetworkEnhancedADXStrategy Class

This 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
    )

2.1. Strategy Parameters (params)

The params tuple allows for easy customization and optimization of the strategy’s behavior without altering the code structure:


2.2. Initialization (__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 = dt or self.datas[0].datetime.datetime(0)
            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], 
                                                 period=self.params.boll_period, 
                                                 devfactor=self.params.boll_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

2.3. Neural Network Data Preparation and Training

These methods are critical for preparing the data for the neural network, training the model, and obtaining its predictions.

calculate_features(): Bar-level Features

    def 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
            adx_strength = self.adx[0] / 100 if not np.isnan(self.adx[0]) else 0.25
            adx_above_threshold = 1 if self.adx[0] > self.params.adx_threshold else 0
            adx_trend = (self.adx[0] - self.adx[-3]) / max(self.adx[-3], 1) if len(self.adx) > 3 and self.adx[-3] > 0 else 0
            features.extend([adx_strength, adx_above_threshold, adx_trend])
            
            # Directional Movement features
            plusdi_norm = self.plusdi[0] / 100 if not np.isnan(self.plusdi[0]) else 0.25
            minusdi_norm = self.minusdi[0] / 100 if not np.isnan(self.minusdi[0]) else 0.25
            di_difference = (self.plusdi[0] - self.minusdi[0]) / 100
            di_ratio = self.plusdi[0] / max(self.minusdi[0], 1) if self.minusdi[0] > 0 else 1
            long_signal = 1 if self.plusdi[0] > self.minusdi[0] else 0
            short_signal = 1 if self.minusdi[0] > self.plusdi[0] else 0
            features.extend([plusdi_norm, minusdi_norm, di_difference, di_ratio, long_signal, short_signal])
            
            # Bollinger Band features
            bb_position = (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_width = (self.boll.top[0] - self.boll.bot[0]) / self.dataclose[0] if self.dataclose[0] > 0 else 0
            bb_squeeze = 1 if bb_width < 0.01 else 0  # Bollinger band squeeze detection
            features.extend([bb_position, bb_width, bb_squeeze])
            
            # Price momentum features
            price_change_1 = (self.dataclose[0] - self.dataclose[-1]) / self.dataclose[-1] if len(self.data) > 1 else 0
            price_change_3 = (self.dataclose[0] - self.dataclose[-3]) / self.dataclose[-3] if len(self.data) > 3 else 0
            momentum_norm = self.momentum[0] / self.dataclose[0] if self.dataclose[0] > 0 else 0
            features.extend([price_change_1, price_change_3, momentum_norm])
            
            # Technical indicators (fast periods for 3-month data)
            rsi_norm = self.rsi[0] / 100 if not np.isnan(self.rsi[0]) else 0.5
            sma_distance = (self.dataclose[0] - self.sma[0]) / self.sma[0] if self.sma[0] > 0 else 0
            atr_norm = self.atr[0] / self.dataclose[0] if self.dataclose[0] > 0 else 0
            features.extend([rsi_norm, sma_distance, atr_norm])
            
            # Volume features
            volume_ratio = self.data.volume[0] / self.volume_ma[0] if self.volume_ma[0] > 0 else 1.0
            features.append(volume_ratio)
            
            # Trend confirmation features
            trend_strength = adx_strength * abs(di_difference)
            signal_persistence = self.reversal_counter / max(self.params.confirmation_bars, 1)
            features.extend([trend_strength, signal_persistence])
            
            # Market condition features
            market_trending = 1 if (self.adx[0] > self.params.adx_threshold and bb_width >= 0.01) else 0
            features.append(market_trending)
            
            # Reversal context
            potential_reversal = 1 if self.reversal_counter >= self.params.confirmation_bars else 0
            features.append(potential_reversal)
            
            # Price action features
            high_low_ratio = (self.data.high[0] - self.data.low[0]) / self.dataclose[0] if self.dataclose[0] > 0 else 0
            features.append(high_low_ratio)
            
            # Clean features
            features = [0 if np.isnan(x) or np.isinf(x) else x for x in 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:

create_sequence_features(): Time-Series Context for the NN

    def 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):
                lookback_idx = -(self.params.nn_lookback - 1 - i)
                
                # Core time series features
                if len(self.data) > abs(lookback_idx):
                    price = self.dataclose[lookback_idx]
                    adx_val = self.adx[lookback_idx] if len(self.adx) > abs(lookback_idx) else 25
                    plusdi_val = self.plusdi[lookback_idx] if len(self.plusdi) > abs(lookback_idx) else 25
                    minusdi_val = self.minusdi[lookback_idx] if len(self.minusdi) > abs(lookback_idx) else 25
                    
                    # Normalize values
                    features_at_t = [
                        price / self.dataclose[0] if self.dataclose[0] > 0 else 1,  # Price ratio
                        adx_val / 100,  # ADX normalized
                        plusdi_val / 100,  # +DI normalized
                        minusdi_val / 100,  # -DI normalized
                        (plusdi_val - minusdi_val) / 100,  # DI difference
                    ]
                    
                    # Add volume if available
                    if len(self.data.volume) > abs(lookback_idx):
                        volume_norm = self.data.volume[lookback_idx] / max(self.data.volume[0], 1)
                        features_at_t.append(volume_norm)
                    else:
                        features_at_t.append(1.0)
                    
                    sequence.extend(features_at_t)
                else:
                    # Pad with current values if not enough history
                    sequence.extend([1.0, 0.25, 0.25, 0.25, 0.0, 1.0])
            
            # Clean sequence
            sequence = [0 if np.isnan(x) or np.isinf(x) else x for x in 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 Predict

    def 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
            future_return = (self.dataclose[-3] - self.dataclose[0]) / self.dataclose[0]
            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 Classifier

    def 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:
            X = np.array(self.feature_sequences)
            y = np.array(self.label_buffer)
            
            # Remove invalid data
            valid_mask = np.isfinite(X).all(axis=1) & np.isfinite(y)
            X, y = X[valid_mask], y[valid_mask]
            
            if len(X) < 20:
                return False
            
            # Binary classification: top 50% returns are good for 3-month data
            threshold = np.percentile(np.abs(y), 50)
            y_binary = (np.abs(y) > threshold).astype(int)
            
            if len(np.unique(y_binary)) < 2:
                return False
            
            # Simple split for small datasets
            if len(X) < 30:
                X_train, X_test, y_train, y_test = X, X, y_binary, y_binary
            else:
                X_train, X_test, y_train, y_test = train_test_split(
                    X, y_binary, test_size=0.3, random_state=42
                )
            
            # Scale features
            X_train_scaled = self.scaler.fit_transform(X_train)
            if len(X_test) > 0:
                X_test_scaled = self.scaler.transform(X_test)
            
            # Neural Network optimized for small datasets and 3-month data
            self.nn_model = MLPClassifier(
                hidden_layer_sizes=(20, 10),    # Small network for limited data
                activation='relu',
                solver='adam',
                alpha=0.01,                     # Regularization for small data
                batch_size='auto',
                learning_rate='adaptive',
                learning_rate_init=0.001,
                max_iter=300,                   # More iterations for convergence
                shuffle=True,
                random_state=42,
                early_stopping=True,
                validation_fraction=0.2 if len(X_train) > 10 else 0.1,
                n_iter_no_change=20
            )
            
            self.nn_model.fit(X_train_scaled, y_train)
            
            # Evaluate if we have test data
            if len(X_test) > 0:
                accuracy = accuracy_score(y_test, self.nn_model.predict(X_test_scaled))
            else:
                accuracy = accuracy_score(y_train, self.nn_model.predict(X_train_scaled))
            
            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:

get_nn_confidence(): Getting Predictions from the NN

    def 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:
            features_scaled = self.scaler.transform(features.reshape(1, -1))
            proba = self.nn_model.predict_proba(features_scaled)[0]
            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.


2.4. Order and Trade Management

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

2.5. The next() Method: The Strategy’s Core Logic

The 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)
        sequence_features = self.create_sequence_features()
        if sequence_features is not None and len(self.data) > 30:
            target = self.calculate_target_label()
            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(
                        exectype=bt.Order.StopTrail,
                        trailpercent=self.params.trail_percent)
                elif self.position.size < 0:
                    self.log(f"Placing trailing stop order for short position at {self.dataclose[0]:.2f}")
                    self.trail_order = self.buy(
                        exectype=bt.Order.StopTrail,
                        trailpercent=self.params.trail_percent)
            return

        # 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
        boll_width = self.boll.top[0] - self.boll.bot[0]
        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
        long_signal = self.plusdi[0] > self.minusdi[0]
        short_signal = self.minusdi[0] > self.plusdi[0]

        # 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
        nn_confidence = self.get_nn_confidence(sequence_features)
        
        # 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:

  1. ML Data Management: It continuously calls 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.
  2. Order Management: It first checks if any orders are pending. If there’s an open position and no trailing stop is active, it places one. This ensures that profit protection is always in place.
  3. Market Filtering: Before considering a trade, the strategy applies several traditional filters:
    • Data Availability: Ensures enough bars for indicator calculation.
    • ADX Strength: Checks if the ADX is above adx_threshold, confirming a strong trend.
    • Bollinger Band Width: Verifies that the Bollinger Bands are not too narrow, indicating the market isn’t in a tight range.
  4. Directional Signal: It determines the current trend direction based on the crossover of +DI and -DI.
  5. Confirmation Logic: The reversal_counter ensures that a directional signal persists for confirmation_bars before a trade is considered, reducing whipsaws.
  6. Neural Network Filtering (The Enhancement): This is the core of the ML integration. Once an ADX-based signal is generated and confirmed:
    • The strategy calls get_nn_confidence() to obtain the neural network’s probability that the current market state will lead to a significant future move.
    • The total_signals counter is incremented.
    • If the neural network is ready and its 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.
    • If the 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.

2.6. stop() Method: Post-Backtest Summary

    def stop(self):
        """Enhanced stop function with NN statistics"""
        filter_rate = (self.nn_filtered_signals / self.total_signals * 100) if self.total_signals > 0 else 0
        
        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.


3. Running the Backtest: run_backtest() Function

This standalone function sets up the backtrader.Cerebro engine, loads data, configures the broker, runs the backtest, and displays the results.

def run_backtest():
    cerebro = bt.Cerebro()
    cerebro.addstrategy(NeuralNetworkEnhancedADXStrategy, printlog=False)

    # Test with 3-month data
    ticker = 'BTC-USD'
    start_date = datetime.datetime(2024, 1, 1)  # 3 months
    end_date = datetime.datetime(2024, 4, 1)

    print(f"Fetching data for {ticker} from {start_date.date()} to {end_date.date()}")
    try:
        data_df = yf.download(ticker, start=start_date, end=end_date, progress=False)
        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 = data_df.columns.droplevel(1).str.lower()
        data_df.index = pd.to_datetime(data_df.index)
        data = bt.feeds.PandasData(dataname=data_df)
        cerebro.adddata(data)
    except Exception as e:
        print(f"Error fetching or processing data: {e}")
        return

    # Broker and position sizing setup
    initial_cash = 100000.0
    cerebro.broker.setcash(initial_cash)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95)

    # Add performance analyzers
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio', timeframe=bt.TimeFrame.Days, riskfreerate=0.0)
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')

    print(f"Starting Portfolio Value: {initial_cash:.2f}")
    results = cerebro.run()
    final_value = cerebro.broker.getvalue()
    total_return = ((final_value / initial_cash) - 1) * 100
    print(f"Final Portfolio Value: {final_value:.2f}")
    print(f"Total Return: {total_return:.2f}%")

    # Retrieve and print analysis results
    strat = results[0]
    analyzers = strat.analyzers
    print("-" * 60)
    print("Strategy Analysis (NN-Enhanced ADX Trend Strength - 3 Month Data):")
    print("-" * 60)
    try:
        sharpe = analyzers.sharpe_ratio.get_analysis()
        sharpe_value = sharpe.get("sharperatio") if sharpe else None
        print(f"Sharpe Ratio: {sharpe_value:.3f}" if sharpe_value is not None else "Sharpe Ratio: None")

        returns = analyzers.returns.get_analysis() or {}
        if "rtot" in returns:
            print(f"Total Return: {returns['rtot'] * 100:.2f}%")
        if "rnorm100" in returns:
            print(f"Annualized Return: {returns['rnorm100']:.2f}%")

        drawdown = analyzers.drawdown.get_analysis() or {}
        dd_info = drawdown.get("max", {})
        max_drawdown_val = dd_info.get("drawdown", None)
        print(f"Max Drawdown: {max_drawdown_val:.2f}%" if max_drawdown_val is not None else "Max Drawdown: None")

        trades = analyzers.trades.get_analysis() or {}
        total_trades = trades.get("total", {}).get("total", 0)
        print(f"Total Trades: {total_trades}")
        if total_trades > 0:
            winning_trades = trades.get("won", {}).get("total", 0)
            losing_trades = trades.get("lost", {}).get("total", 0)
            win_rate = winning_trades / total_trades * 100
            avg_win = trades.get("won", {}).get("pnl", {}).get("average", 0.0)
            avg_loss = trades.get("lost", {}).get("pnl", {}).get("average", 0.0)
            profit_factor = (
                abs(trades.get("won", {}).get("pnl", {}).get("total", 0) /
                    trades.get("lost", {}).get("pnl", {}).get("total", 1))
                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)

Results

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:

Pasted image 20250708153304.png

Augmented with Neural Networks:

Pasted image 20250708153339.png

Conclusion 📈

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:

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.