preview
Integrating MQL5 with Data Processing Packages (Part 9): Entropy-Based Adaptive Volatility

Integrating MQL5 with Data Processing Packages (Part 9): Entropy-Based Adaptive Volatility

MetaTrader 5Examples |
226 0
Hlomohang John Borotho
Hlomohang John Borotho

Table of Contents

  1. Introduction
  2. Model and System Overview
  3. Getting Started
  4. Putting it all Together on MQL5
  5. Live Inference
  6. Conclusion



Introduction

Traders face a persistent challenge in unpredictable financial markets. Volatility can shift dramatically within a single session, turning a stable trend into chaotic, whipsawing price action. Traditional technical indicators often lag in rapid regime changes, leaving traders exposed to sudden reversals, excessive drawdowns, or missed opportunities. Fixed stop-loss and take-profit levels that work well in calm conditions become dangerously inadequate during volatility spikes, while rigid position sizing fails to account for the ever-changing risk landscape. The result is a frustrating cycle of premature stopouts, oversized losses, and inconsistent performance that erodes both capital and confidence.

This project addresses these challenges by calculating market entropy in real time. This quantitative approach continuously measures disorder and uncertainty in tick-level price data. By applying Shannon entropy to rolling windows of recent prices, the system instantly detects transitions between low, normal, high, and extreme volatility regimes. A neural network trained on entropy-derived features predicts directional probability, while an adaptive risk engine dynamically adjusts position size, stop-loss width, and take-profit targets based on the current market state. The pipeline collects MetaTrader 5 ticks, runs Flask-based inference, and executes trades automatically. It reacts in milliseconds instead of waiting for candle closes. This empowers traders with volatility-aware decision-making that protects capital during chaos and capitalizes on opportunity during calm.



Model and System Overview

The system consists of two core components working in tandem: an MQL5 Expert Advisor running inside MetaTrader 5 and a Python Flask server handling model inference. On every tick, the EA collects the most recent 50 bid prices and calculates RSI. This data is packaged as a JSON payload and sent via an HTTP POST request to the Flask server running locally. The server receives the price window, computes Shannon entropy alongside several volatility and trend metrics, then passes these features through a pre-trained neural network. The model returns a directional probability between zero and one, which the server combines with the current volatility regime to generate a final signal.

Raw tick data flows continuously into a rolling window where Shannon entropy measures market disorder. The resulting entropy score classifies the current volatility regime, which directly controls position sizing and stop distances.

When the Flask server returns a BUY or SELL signal with sufficient confidence, the EA evaluates current market exposure before acting. If no position exists and cooldown periods have elapsed, a market order is placed using MetaTrader's CTrade class. Position size, stop-loss, and take-profit levels are all scaled by the volatility multiplier received from the server. If an opposite position already exists and reversal conditions are met, the EA closes the existing trade before opening a new one. All of this occurs continuously on tick data, allowing the system to detect and adapt to volatility shifts as they happen rather than waiting for candles to close.

The EA sends tick data to the Flask server, where entropy features are extracted and passed through a neural network. The server returns a signal with adaptive risk parameters, and the EA executes the trade after validating position limits and cooldown constraints.


Getting Started

Gather Historical Data

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import pytz

# Display MetaTrader5 package information
print("MetaTrader5 package author:", mt5.__author__)
print("MetaTrader5 package version:", mt5.__version__)

# Pandas display settings
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1500)

# Initialize MT5 connection
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

# Define symbol
symbol = "XAUUSD.m"

# Ensure the symbol is available
if not mt5.symbol_select(symbol, True):
    print("Failed to select symbol:", symbol)
    mt5.shutdown()
    quit()

# Set timezone to UTC
timezone = pytz.timezone("Etc/UTC")

# Define date range
utc_from = datetime(2026, 1, 1, tzinfo=timezone)
utc_to   = datetime(2026, 4, 1, tzinfo=timezone)

# Get historical rates
rates = mt5.copy_rates_range(symbol, mt5.TIMEFRAME_H1, utc_from, utc_to)

# Shutdown MT5 connection
mt5.shutdown()

# Validate data
if rates is None or len(rates) == 0:
    print("No data retrieved. Check symbol or date range.")
else:
    
    print("First 10 bars:")
    for rate in rates[:10]:
        print(rate)

    # Convert to DataFrame
    rates_frame = pd.DataFrame(rates)

    # Convert time
    rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

    # Save to CSV
    filename = "XAUUSD_H1.csv"
    rates_frame.to_csv(filename, index=False)

    print("\nData saved to:", filename)

To get started, we will connect to MetaTrader 5 using its Python API and print basic package information. We then configure pandas display settings to make the output easier to read. After that, we initialize the connection and select the XAUUSD.m symbol to ensure it is available. We define a UTC time range using pytz so we can request accurate historical data. Next, we retrieve H1 price data, close the connection, and check if any data was returned. If data exists, we print a preview, convert it into a DataFrame, format the timestamps, and save the result to a CSV file for later use.

Feature Engineering

import numpy as np
from collections import deque

def compute_returns(prices):
    """Compute log returns from price series"""
    prices = np.array(prices, dtype=np.float32)
    return np.diff(np.log(prices + 1e-10))

def compute_entropy(returns, n_bins=10):
    """
    Compute normalized Shannon entropy of returns distribution.
    Higher entropy = more uncertainty/volatility.
    """
    if len(returns) < 2:
        return 0.0, 0.0
    
    # Use percentile-based bins for better distribution
    percentiles = np.linspace(0, 100, n_bins + 1)
    bins = np.percentile(returns, percentiles)
    bins = np.unique(bins)  # Remove duplicates
    
    if len(bins) < 2:
        return 0.0, 0.0
    
    states = np.digitize(returns, bins[:-1])
    values, counts = np.unique(states, return_counts=True)
    probs = counts / counts.sum()
    
    entropy = -np.sum(probs * np.log(probs + 1e-10))
    max_entropy = np.log(len(values)) if len(values) > 1 else 1
    
    # Also compute entropy of squared returns (volatility entropy)
    squared_returns = returns ** 2
    vol_bins = np.percentile(squared_returns, percentiles)
    vol_bins = np.unique(vol_bins)
    
    if len(vol_bins) >= 2:
        vol_states = np.digitize(squared_returns, vol_bins[:-1])
        vol_values, vol_counts = np.unique(vol_states, return_counts=True)
        vol_probs = vol_counts / vol_counts.sum()
        vol_entropy = -np.sum(vol_probs * np.log(vol_probs + 1e-10))
        vol_max = np.log(len(vol_values)) if len(vol_values) > 1 else 1
        vol_entropy_norm = vol_entropy / vol_max
    else:
        vol_entropy_norm = 0.0
    
    return entropy / max_entropy, vol_entropy_norm

def compute_volatility_metrics(returns):
    """Compute multiple volatility metrics"""
    if len(returns) < 2:
        return {
            'std': 0.0,
            'mad': 0.0,
            'range': 0.0,
            'skewness': 0.0,
            'kurtosis': 0.0
        }
    
    std = np.std(returns)
    mad = np.mean(np.abs(returns - np.mean(returns)))
    range_vol = np.max(returns) - np.min(returns)
    
    # Higher moments
    skewness = 0.0
    kurtosis = 0.0
    if std > 1e-10:
        skewness = np.mean((returns - np.mean(returns)) ** 3) / (std ** 3)
        kurtosis = np.mean((returns - np.mean(returns)) ** 4) / (std ** 4)
    
    return {
        'std': std,
        'mad': mad,
        'range': range_vol,
        'skewness': skewness,
        'kurtosis': kurtosis
    }

def compute_trend_strength(prices):
    """Compute trend strength using linear regression R²"""
    prices = np.array(prices, dtype=np.float32)
    if len(prices) < 2:
        return 0.0, 0.0
    
    x = np.arange(len(prices))
    y = prices
    
    # Linear regression
    n = len(x)
    sum_x = np.sum(x)
    sum_y = np.sum(y)
    sum_xy = np.sum(x * y)
    sum_xx = np.sum(x * x)
    sum_yy = np.sum(y * y)
    
    # Avoid division by zero
    denominator = n * sum_xx - sum_x * sum_x
    if denominator == 0:
        return 0.0, 0.0
    
    slope = (n * sum_xy - sum_x * sum_y) / denominator
    
    # R-squared
    y_mean = np.mean(y)
    ss_tot = np.sum((y - y_mean) ** 2)
    if ss_tot == 0:
        r_squared = 1.0
    else:
        y_pred = slope * x + (sum_y - slope * sum_x) / n
        ss_res = np.sum((y - y_pred) ** 2)
        r_squared = 1 - (ss_res / ss_tot)
    
    return slope, max(0.0, min(1.0, r_squared))

def build_features(prices, rsi=50.0, high_prices=None, low_prices=None):
    """
    Build comprehensive feature vector for model input.
    
    Parameters:
    - prices: array of close prices
    - rsi: RSI value (default 50.0)
    - high_prices: optional array of high prices
    - low_prices: optional array of low prices
    
    Returns:
    - features: numpy array of 8 features
    - metrics: dictionary with all calculated metrics
    """
    # Ensure inputs are numpy arrays
    prices = np.array(prices, dtype=np.float32).flatten()
    
    returns = compute_returns(prices)
    
    # Entropy metrics
    entropy, vol_entropy = compute_entropy(returns)
    
    # Volatility metrics
    vol_metrics = compute_volatility_metrics(returns)
    
    # Trend metrics
    slope, r_squared = compute_trend_strength(prices)
    
    # Mean and std of returns
    mean_ret = np.mean(returns) if len(returns) > 0 else 0.0
    std_ret = vol_metrics['std']
    
    # Normalize slope to [-1, 1] range
    slope_norm = np.tanh(slope * 100) if not np.isnan(slope) and not np.isinf(slope) else 0.0
    
    # Build feature vector (8 features)
    features = np.array([
        float(entropy),                    # 0: Market uncertainty
        float(vol_entropy),                # 1: Volatility uncertainty  
        float(mean_ret),                   # 2: Directional bias
        float(std_ret),                    # 3: Volatility level
        float(r_squared),                  # 4: Trend strength
        float(slope_norm),                 # 5: Normalized trend direction
        float(vol_metrics['skewness']),    # 6: Return asymmetry
        float(rsi / 100.0)                 # 7: Normalized RSI
    ], dtype=np.float32)
    
    # Replace any NaN or inf with 0
    features = np.nan_to_num(features, nan=0.0, posinf=1.0, neginf=-1.0)
    
    metrics = {
        'entropy': float(entropy),
        'vol_entropy': float(vol_entropy),
        'mean_ret': float(mean_ret),
        'std_ret': float(std_ret),
        'r_squared': float(r_squared),
        'slope': float(slope) if not np.isnan(slope) else 0.0,
        'skewness': float(vol_metrics['skewness']),
        'kurtosis': float(vol_metrics['kurtosis']),
        'rsi': float(rsi)
    }
    
    return features, metrics

In this code section, we start by building core mathematical tools using NumPy to transform raw price data into meaningful signals. We compute log returns to capture price changes in a stable way, and then measure market uncertainty using Shannon Entropy by grouping returns into percentile-based bins and calculating their probability distribution. We also extend this idea by computing entropy on squared returns, which helps us understand volatility behavior rather than just direction. Alongside entropy, we calculate additional volatility metrics such as standard deviation, mean absolute deviation, range, skewness, and kurtosis, which together describe how returns are distributed and whether the market is balanced or biased.

We then analyze price structure by estimating trend strength using a linear regression approach, where the slope gives direction and the R² value shows how clean or noisy the trend is. After that, we combine everything into a single feature vector using the build_features function, where we include entropy, volatility entropy, return statistics, trend strength, normalized slope, skewness, and a scaled RSI value. We also clean the data by removing invalid values and return both the feature array for model input and a metrics dictionary for monitoring. In this way, we move from raw prices to a structured representation of market behavior that an ML model can understand and use for decision-making.

class VolatilityRegimeDetector:
    """Adaptive volatility regime detection using entropy history"""
    
    def __init__(self, window_size=50, history_size=100):
        self.window_size = window_size
        self.history_size = history_size
        self.entropy_history = deque(maxlen=history_size)
        self.vol_entropy_history = deque(maxlen=history_size)
        self.std_history = deque(maxlen=history_size)
        self.regime_history = deque(maxlen=20)
        
    def update(self, metrics):
        """Update history and detect current regime"""
        self.entropy_history.append(metrics['entropy'])
        self.vol_entropy_history.append(metrics['vol_entropy'])
        self.std_history.append(metrics['std_ret'])
        return self.detect_regime(metrics)
    
    def detect_regime(self, metrics):
        """Detect current volatility regime with adaptive thresholds"""
        entropy = metrics['entropy']
        vol_entropy = metrics['vol_entropy']
        
        if len(self.entropy_history) < 20:
            # Not enough history - use static thresholds
            if entropy > 0.7:
                regime = "HIGH_VOLATILITY"
                multiplier = 1.5
                confidence_adj = 1.3
            elif entropy < 0.3:
                regime = "LOW_VOLATILITY"
                multiplier = 0.7
                confidence_adj = 0.8
            else:
                regime = "NORMAL"
                multiplier = 1.0
                confidence_adj = 1.0
        else:
            # Adaptive thresholds based on historical distribution
            entropy_array = np.array(list(self.entropy_history))
            mean_entropy = np.mean(entropy_array)
            std_entropy = np.std(entropy_array)
            
            # Dynamic thresholds
            high_threshold = min(0.85, mean_entropy + 1.5 * std_entropy)
            low_threshold = max(0.15, mean_entropy - 1.5 * std_entropy)
            extreme_threshold = min(0.95, mean_entropy + 2.5 * std_entropy)
            
            # Regime detection
            if entropy > extreme_threshold or vol_entropy > 0.9:
                regime = "EXTREME_VOLATILITY"
                multiplier = 2.5
                confidence_adj = 2.0
            elif entropy > high_threshold:
                regime = "HIGH_VOLATILITY"
                multiplier = 1.5
                confidence_adj = 1.3
            elif entropy < low_threshold:
                regime = "LOW_VOLATILITY"
                multiplier = 0.7
                confidence_adj = 0.8
            else:
                regime = "NORMAL"
                multiplier = 1.0
                confidence_adj = 1.0
        
        self.regime_history.append(regime)
        regime_change = self._detect_regime_change()
        
        return {
            'regime': regime,
            'volatility_multiplier': multiplier,
            'confidence_multiplier': confidence_adj,
            'regime_change': regime_change,
            'entropy_percentile': self._get_percentile(entropy),
            'vol_entropy_percentile': self._get_percentile(vol_entropy, is_vol=True)
        }
    
    def _get_percentile(self, value, is_vol=False):
        """Calculate percentile of current value in history"""
        history = self.vol_entropy_history if is_vol else self.entropy_history
        if len(history) < 10:
            return 50.0
        history_array = np.array(list(history))
        return (np.sum(history_array < value) / len(history_array)) * 100
    
    def _detect_regime_change(self):
        """Detect if regime has changed from previous state"""
        if len(self.regime_history) < 2:
            return False
        return self.regime_history[-1] != self.regime_history[-2]
    
    def get_adaptive_parameters(self, base_sl, base_tp, base_lot):
        """Calculate adaptive trading parameters"""
        if len(self.regime_history) == 0:
            return base_sl, base_tp, base_lot, "NORMAL"
        
        current_regime = self.regime_history[-1]
        
        if current_regime == "EXTREME_VOLATILITY":
            sl_mult = 3.0
            tp_mult = 2.0
            lot_mult = 0.3
        elif current_regime == "HIGH_VOLATILITY":
            sl_mult = 1.8
            tp_mult = 1.5
            lot_mult = 0.6
        elif current_regime == "LOW_VOLATILITY":
            sl_mult = 0.7
            tp_mult = 0.8
            lot_mult = 1.3
        else:
            sl_mult = 1.0
            tp_mult = 1.0
            lot_mult = 1.0
        
        adaptive_sl = int(base_sl * sl_mult)
        adaptive_tp = int(base_tp * tp_mult)
        adaptive_lot = base_lot * lot_mult
        
        return adaptive_sl, adaptive_tp, adaptive_lot, current_regime

Here we build a class that tracks entropy and volatility over time using rolling history stored in deques, and we use Shannon Entropy as the core signal to detect market regimes. On each update, we store the latest metrics and classify the market into LOW, NORMAL, HIGH, or EXTREME volatility. We first use fixed thresholds when there is little history. We then switch to adaptive thresholds based on the mean and standard deviation of past entropy values. Furthermore, we also compute percentiles to understand how extreme the current state is, detect regime changes, and return multipliers that adjust risk and confidence. Finally, we convert the detected regime into practical trading parameters by scaling stop loss, take profit, and lot size, so the system becomes adaptive to changing market conditions rather than using fixed settings.

Model Definition

import torch.nn as nn
import torch

class EntropyModel(nn.Module):
    """Enhanced model with 8 input features"""
    def __init__(self, dropout_rate=0.2):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(8, 32),
            nn.BatchNorm1d(32),
            nn.ReLU(),
            nn.Dropout(dropout_rate),
            
            nn.Linear(32, 16),
            nn.BatchNorm1d(16),
            nn.ReLU(),
            nn.Dropout(dropout_rate),
            
            nn.Linear(16, 8),
            nn.BatchNorm1d(8),
            nn.ReLU(),
            
            nn.Linear(8, 1),
            nn.Sigmoid()
        )
    
    def forward(self, x):
        return self.net(x)
    
    def predict_with_uncertainty(self, x, n_samples=10):
        """Monte Carlo dropout for prediction uncertainty"""
        self.train()  # Enable dropout
        predictions = []
        with torch.no_grad():
            for _ in range(n_samples):
                pred = self.net(x)
                predictions.append(pred.cpu().numpy())
        
        predictions = np.array(predictions)
        mean_pred = np.mean(predictions, axis=0)
        std_pred = np.std(predictions, axis=0)
        
        self.eval()  # Back to eval mode
        return mean_pred, std_pred

We define a neural network model using PyTorch that takes 8 input features and processes them through multiple fully connected layers to learn market patterns. We use batch normalization to stabilize training, ReLU activation to introduce non-linearity, and dropout to reduce overfitting by randomly disabling neurons during training. The model gradually reduces dimensionality from 32 to 1 output, where a sigmoid function produces a probability for a buy or sell decision. We also include a special method for uncertainty estimation, where we keep dropout active during inference and run multiple forward passes, then compute the mean prediction and standard deviation to measure confidence in the model’s output.

Training Pipeline

import numpy as np
import pandas as pd
import torch
from sklearn.preprocessing import StandardScaler
import joblib

from Model import EntropyModel
from Features import build_features

# ------------------------------
# 1. Load CSV data
# ------------------------------
CSV_FILE = "XAUUSD_H1.csv"

try:
    df = pd.read_csv(CSV_FILE)
    print(f"Loaded {len(df)} rows from {CSV_FILE}")
except FileNotFoundError:
    print(f"Error: {CSV_FILE} not found. Run 'Getting Hist Data.py' first.")
    exit()

# Extract prices
if 'close' not in df.columns:
    print("Error: CSV must contain a 'close' column.")
    exit()

prices = df['close'].dropna().values.astype(np.float32)
high_prices = df['high'].values.astype(np.float32) if 'high' in df.columns else None
low_prices = df['low'].values.astype(np.float32) if 'low' in df.columns else None

print(f"Price data shape: {prices.shape}")
if high_prices is not None:
    print(f"High/Low data available")

# ------------------------------
# 2. Feature Engineering
# ------------------------------
WINDOW = 50
HORIZON = 5

X, y = [], []

# Calculate simple RSI for each point
def calculate_rsi(prices, period=14):
    if len(prices) < period + 1:
        return 50.0
    deltas = np.diff(prices)
    seed = deltas[:period+1]
    up = seed[seed >= 0].sum() / period
    down = -seed[seed < 0].sum() / period
    if down == 0:
        return 100.0
    rs = up / down
    return 100.0 - (100.0 / (1.0 + rs))

for i in range(WINDOW, len(prices) - HORIZON):
    window = prices[i - WINDOW:i]
    
    # Get high/low for window if available
    window_high = high_prices[i - WINDOW:i] if high_prices is not None else None
    window_low = low_prices[i - WINDOW:i] if low_prices is not None else None
    
    # Calculate RSI for the window
    rsi = calculate_rsi(window, 14)
    
    # Call build_features with all 4 arguments
    features, metrics = build_features(
        prices=window,           # positional or keyword
        rsi=rsi,                 # positional or keyword
        high_prices=window_high, # keyword
        low_prices=window_low    # keyword
    )
    
    # Target: future return direction
    future_return = np.log(prices[i + HORIZON] / prices[i])
    label = 1 if future_return > 0 else 0
    
    X.append(features)
    y.append(label)

X = np.array(X, dtype=np.float32)
y = np.array(y, dtype=np.float32)

print(f"Dataset shape: X={X.shape}, y={y.shape}")
print(f"Feature vector length: {X.shape[1]}")
print(f"Positive labels: {np.sum(y)} / {len(y)} ({np.sum(y)/len(y)*100:.1f}%)")

# ------------------------------
# 3. Scaling
# ------------------------------
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

joblib.dump(scaler, "scaler.pkl")
print("Scaler saved to scaler.pkl")

In this section, we load historical market data from a CSV file using pandas, validate that the required price columns exist, and extract close, high, and low prices for processing. We then build a dataset by sliding a fixed window over the price series, where for each window we compute RSI, generate advanced features using the feature engine, and assign a label based on future price movement. After collecting all samples, we convert the data into NumPy arrays and inspect the dataset shape and label distribution. Finally, we normalize the features using StandardScaler to ensure stable model training, and we save the scaler with joblib so it can be reused later during live predictions.

# ------------------------------
# 4. Train/Val Split
# ------------------------------
split_idx = int(len(X) * 0.8)
X_train, X_val = X_scaled[:split_idx], X_scaled[split_idx:]
y_train, y_val = y[:split_idx], y[split_idx:]

print(f"Training samples: {len(X_train)}, Validation samples: {len(X_val)}")

X_train_tensor = torch.tensor(X_train, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)
X_val_tensor = torch.tensor(X_val, dtype=torch.float32)
y_val_tensor = torch.tensor(y_val, dtype=torch.float32).view(-1, 1)

# ------------------------------
# 5. Model Training
# ------------------------------
model = EntropyModel()

criterion = torch.nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-5)

EPOCHS = 50
best_val_loss = float('inf')

for epoch in range(EPOCHS):
    # Training
    model.train()
    optimizer.zero_grad()
    output = model(X_train_tensor)
    loss = criterion(output, y_train_tensor)
    loss.backward()
    optimizer.step()
    
    # Validation
    model.eval()
    with torch.no_grad():
        val_output = model(X_val_tensor)
        val_loss = criterion(val_output, y_val_tensor)
    
    # Save best model
    if val_loss < best_val_loss:
        best_val_loss = val_loss
        torch.save(model.state_dict(), "entropy_model.pth")
    
    if (epoch + 1) % 5 == 0 or epoch == 0:
        print(f"Epoch {epoch+1}/{EPOCHS} | Train Loss: {loss.item():.6f} | Val Loss: {val_loss.item():.6f}")

print(f"\nTraining complete. Best validation loss: {best_val_loss:.6f}")
print("Model saved to entropy_model.pth")

# ------------------------------
# 6. Quick evaluation
# ------------------------------
model.eval()
with torch.no_grad():
    train_pred = model(X_train_tensor)
    train_acc = ((train_pred > 0.5) == y_train_tensor).float().mean().item()
    
    val_pred = model(X_val_tensor)
    val_acc = ((val_pred > 0.5) == y_val_tensor).float().mean().item()

print(f"\nFinal Metrics:")
print(f"Train Accuracy: {train_acc:.3f}")
print(f"Val Accuracy: {val_acc:.3f}")

Here, we split the dataset into training and validation sets using an 80/20 ratio, and then convert both inputs and labels into tensors so they can be used with PyTorch. We initialize the model, define binary cross-entropy loss for classification, and use the Adam optimizer with weight decay to improve generalization. During training, we loop through epochs where we first train the model on the training data, then evaluate it on validation data without updating weights. We track the best validation loss and save the model whenever improvement occurs to avoid overfitting. Finally, we perform a quick evaluation by calculating training and validation accuracy based on a 0.5 threshold, which gives us a clear view of how well the model generalizes to unseen data.

Flask Server

from flask import Flask, request, jsonify
import numpy as np
import torch
import joblib
from collections import deque
import time
import json

from Model import EntropyModel
from Features import build_features, VolatilityRegimeDetector
import logging
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)  # Only show errors, not every request

app = Flask(__name__)

# Load model and scaler
try:
    model = EntropyModel()
    model.load_state_dict(torch.load("entropy_model.pth", map_location='cpu'))
    model.eval()
    print("Model loaded")
except:
    model = None
    print("Model not loaded")

try:
    scaler = joblib.load("scaler.pkl")
    print("Scaler loaded")
except:
    scaler = None
    print("Scaler not loaded")

# Fast regime detector with shorter history for tick-level responsiveness
regime_detector = VolatilityRegimeDetector(window_size=50, history_size=50)

# Track entropy for delta calculation
entropy_history = deque(maxlen=10)
last_entropy = None

@app.route('/predict', methods=['POST'])
def predict():
    global last_entropy
    
    try:
        data = request.json
        prices = np.array(data["prices"], dtype=np.float32)
        rsi = float(data.get("rsi", 50.0))
        
        # Fast feature calculation
        features, metrics = build_features(prices, rsi, None, None)
        
        # Track entropy for delta
        current_entropy = metrics['entropy']
        delta_entropy = 0.0 if last_entropy is None else current_entropy - last_entropy
        last_entropy = current_entropy
        entropy_history.append(current_entropy)
        
        # Detect regime
        regime_info = regime_detector.update(metrics)
        
        # Scale and predict
        if scaler is not None and model is not None:
            scaled = scaler.transform([features])
            x = torch.tensor(scaled, dtype=torch.float32)
            with torch.no_grad():
                prob = model(x).item()
        else:
            prob = 0.5
        
        # Adaptive signal with entropy momentum
        signal = generate_adaptive_signal(prob, metrics, regime_info, delta_entropy)
        confidence = calculate_confidence(prob, metrics, regime_info, delta_entropy)
        
        return jsonify({
            "probability": float(prob),
            "entropy": float(metrics['entropy']),
            "vol_entropy": float(metrics['vol_entropy']),
            "delta_entropy": float(delta_entropy),
            "signal": signal,
            "regime": regime_info['regime'],
            "volatility_multiplier": float(regime_info['volatility_multiplier']),
            "confidence": float(confidence),
            "entropy_momentum": float(calculate_entropy_momentum())
        })
        
    except Exception as e:
        return jsonify({"signal": "HOLD", "error": str(e)}), 200


def generate_adaptive_signal(prob, metrics, regime_info, delta_entropy):
    """Fast adaptive signal generation for tick-level trading."""
    entropy = metrics['entropy']
    regime = regime_info['regime']
    
    # Base thresholds
    buy_threshold = 0.60
    sell_threshold = 0.40
    
    # Adjust for regime
    if regime == "HIGH_VOLATILITY":
        buy_threshold = 0.65
        sell_threshold = 0.35
    elif regime == "LOW_VOLATILITY":
        buy_threshold = 0.55
        sell_threshold = 0.45
    
    # Entropy momentum adjustment
    if delta_entropy > 0.02:  # Increasing entropy = increasing uncertainty
        buy_threshold += 0.05
        sell_threshold -= 0.05
    
    if prob > buy_threshold:
        return "BUY"
    elif prob < sell_threshold:
        return "SELL"
    else:
        return "HOLD"


def calculate_confidence(prob, metrics, regime_info, delta_entropy):
    """Confidence score with entropy penalty."""
    base_conf = abs(prob - 0.5) * 2
    
    # Penalize high entropy
    entropy_penalty = metrics['entropy'] * 0.3
    
    # Penalize increasing entropy
    if delta_entropy > 0:
        entropy_penalty += delta_entropy * 2
    
    confidence = base_conf - entropy_penalty
    return max(0.0, min(1.0, confidence))


def calculate_entropy_momentum():
    """Calculate entropy momentum from history."""
    if len(entropy_history) < 3:
        return 0.0
    hist = list(entropy_history)
    return hist[-1] - hist[0]


@app.route('/health', methods=['GET'])
def health():
    return jsonify({"status": "ready"})


if __name__ == "__main__":
    print("\n" + "="*50)
    print("TICK-LEVEL ENTROPY SERVER")
    print("="*50)
    app.run(host="127.0.0.1", port=5000, debug=False, threaded=True)

This section sets up a REST API using Flask and loads the trained model and scaler for real-time predictions. We initialize a fast volatility regime detector and track entropy history to measure changes over time. When a request is received, we extract price data and RSI, compute features using the feature engine, and calculate entropy along with its change. We then detect the current market regime and use the scaler and model built with PyTorch to generate a probability. The API responds with key information such as entropy, volatility regime, prediction probability, and additional metrics that describe the current market state.

We then generate a trading signal using adaptive logic that adjusts thresholds based on volatility regime and entropy movement. When entropy rises, we increase thresholds to reduce risk, and when conditions are stable, we allow easier trade entry. We also calculate a confidence score by combining model certainty with penalties for high or increasing entropy, which helps filter out unreliable trades. We compute entropy momentum to track how uncertainty evolves. Likewise, we also expose a health endpoint to confirm the server is running and ready to communicate with MetaTrader 5.

Simple Execution WorkFlow


Simple execution workflow shows you how you can run each file separately inside JupyterLab cells.

Gather Historical Data:

%run GettingHistData.py

Feature Engineering:

%run Features.py

Model Definition:

%run Model.py

Training Pipeline:

%run Train.py

Flask Server:

%run Server.py


Putting it all Together on MQL5

//+------------------------------------------------------------------+
//|                                            Real-Time Entropy.mq5 |
//|                             Git, Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "Git, Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.00"


#include <Trade/Trade.mqh>
#include <Trade/PositionInfo.mqh>

//--- Input parameters
input string   ServerURL          = "http://127.0.0.1:5000/predict";  // Flask endpoint
input int      WindowSize         = 50;               // Number of prices in window
input int      TickCollection     = 50;               // Number of ticks to collect
input double   LotSize            = 0.03;             // Base lot size
input int      BaseStopLossPoints = 600;              // Base SL in points
input int      BaseTakeProfitPoints = 2555;           // Base TP in points
input int      MagicNumber        = 20240417;         // Magic number
input int      RequestTimeout     = 3000;             // WebRequest timeout
input int      MinMillisecondsBetweenRequests = 500;  // Throttle requests
input bool     AdaptiveRiskEnabled = true;            // Adaptive position sizing
input double   MinConfidenceThreshold = 0.4;          // Minimum confidence to trade
input bool     AllowReversal      = true;             // Allow position reversal
input int      MinMinutesBetweenTrades = 5;           // Minimum minutes between trades
input int      MaxPositions       = 1;                // Maximum concurrent positions
input int      SignalDebounceTicks = 10;              // Ticks to wait before repeating same signal

//--- Global objects
CTrade          TradeManager;
CPositionInfo   PositionInfo;
MqlTick         currentTick;
MqlTick         tickArray[];           // Rolling tick history
double          priceWindow[];         // Rolling price window
datetime        lastRequestTime = 0;
datetime        lastTradeTime = 0;
string          currentSignal = "HOLD";
string          previousSignal = "HOLD";
string          currentRegime = "NORMAL";
double          currentConfidence = 0.0;
double          currentVolMultiplier = 1.0;
double          currentEntropy = 0.0;
int             tickCount = 0;
int             signalRepeatCount = 0;    // Track same signal repetitions
int             positionCount = 0;         // Current number of positions

ulong openPositionTicket = 0;
bool silentMode = false;
datetime silentModeStart = 0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   TradeManager.SetExpertMagicNumber(MagicNumber);
   TradeManager.SetAsyncMode(false);
   TradeManager.SetDeviationInPoints(10);

   //--- Initialize arrays
   ArrayResize(tickArray, TickCollection);
   ArrayResize(priceWindow, WindowSize);

   //--- Fill initial data
   InitializeHistory();
   CheckSymbolProperties();
   CountCurrentPositions();

   Print("╔══════════════════════════════════════════════════════════════╗");
   Print("║              REAL-TIME ENTROPY INITIALIZED                   ║");
   Print("╠══════════════════════════════════════════════════════════════╣");
   Print("║ Symbol: ", _Symbol);
   Print("║ Window Size: ", WindowSize, " ticks");
   Print("║ Min Minutes Between Trades: ", MinMinutesBetweenTrades);
   Print("║ Max Positions: ", MaxPositions);
   Print("║ Current Positions: ", positionCount);
   Print("║ Adaptive Risk: ", AdaptiveRiskEnabled ? "ENABLED" : "DISABLED");
   Print("╚══════════════════════════════════════════════════════════════╝");

   return(INIT_SUCCEEDED);
  }

In MQL5, we begin by importing the required trade and position-management libraries. The input parameters provide full control over the EA's behavior without requiring recompilation. We define the Flask server endpoint, the rolling window size for price collection, and base risk parameters including lot size and stop distances in points. Additional inputs govern request throttling, adaptive risk toggles, confidence thresholds, reversal permissions, and cooldown periods between trades. Global variables track the current market state, such as the latest tick, price history arrays, current signal and regime, confidence scores, and position counts. We also maintain flags for silent mode operation to prevent log spam when a position is already open and market conditions remain unchanged.

During initialization, we configure the trade manager with our unique magic number and set it to synchronous mode for reliable execution. The tick and price arrays are sized according to the input parameters, and we immediately populate them with recent market data to avoid cold-start delays. A call to check symbol properties verifies that our lot size and stop levels are compatible with the broker's requirements. We count any existing positions managed by this EA to maintain accurate state awareness. Finally, a formatted panel prints all key configuration details to the expert's log.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Clear comment from chart
   Comment("");

   //--- Remove all objects created by this EA
   CleanupChartObjects();

  }
//+------------------------------------------------------------------+
//| Count current positions for this symbol/magic                    |
//+------------------------------------------------------------------+
void CountCurrentPositions()
  {
   positionCount = 0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(PositionInfo.SelectByIndex(i))
        {
         if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == MagicNumber)
            positionCount++;
        }
     }
  }

//+------------------------------------------------------------------+
//| Check if we can open a new position                              |
//+------------------------------------------------------------------+
bool CanOpenNewPosition()
  {
   CountCurrentPositions();

   //--- Check max positions limit
   if(positionCount >= MaxPositions)
     {
      //--- Only log once per minute to avoid spam
      static datetime lastLog = 0;
      if(TimeCurrent() - lastLog > 60)
        {
         Print("Max positions (", MaxPositions, ") reached. Skipping trade.");
         lastLog = TimeCurrent();
        }
      return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Check if position exists in same direction                       |
//+------------------------------------------------------------------+
bool HasPositionInDirection(ENUM_ORDER_TYPE orderType)
  {
   ENUM_POSITION_TYPE posType = (orderType == ORDER_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(PositionInfo.SelectByIndex(i))
        {
         if(PositionInfo.Symbol() == _Symbol &&
            PositionInfo.Magic() == MagicNumber &&
            PositionInfo.PositionType() == posType)
           {
            return true;
           }
        }
     }
   return false;
  }

//+------------------------------------------------------------------+
//| Check if position exists in opposite direction                   |
//+------------------------------------------------------------------+
bool HasPositionInOppositeDirection(ENUM_ORDER_TYPE orderType)
  {
   ENUM_POSITION_TYPE oppositeType = (orderType == ORDER_TYPE_BUY) ? POSITION_TYPE_SELL : POSITION_TYPE_BUY;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(PositionInfo.SelectByIndex(i))
        {
         if(PositionInfo.Symbol() == _Symbol &&
            PositionInfo.Magic() == MagicNumber &&
            PositionInfo.PositionType() == oppositeType)
           {
            return true;
           }
        }
     }
   return false;
  }

//+------------------------------------------------------------------+
//| Close all positions in opposite direction                        |
//+------------------------------------------------------------------+
void CloseOppositePositions(ENUM_ORDER_TYPE newOrderType)
  {
   ENUM_POSITION_TYPE oppositeType = (newOrderType == ORDER_TYPE_BUY) ? POSITION_TYPE_SELL : POSITION_TYPE_BUY;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      if(PositionInfo.SelectByIndex(i))
        {
         if(PositionInfo.Symbol() == _Symbol &&
            PositionInfo.Magic() == MagicNumber &&
            PositionInfo.PositionType() == oppositeType)
           {
            ulong ticket = PositionInfo.Ticket();
            if(TradeManager.PositionClose(ticket))
              {
               Print("Closed opposite position. Ticket: ", ticket);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Check symbol properties                                          |
//+------------------------------------------------------------------+
void CheckSymbolProperties()
  {
   double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   int stopLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);

   Print("╔══════════════════════════════════════════════════════════════╗");
   Print("║                    SYMBOL PROPERTIES                         ║");
   Print("╠══════════════════════════════════════════════════════════════╣");
   Print("║ Min Volume: ", volMin);
   Print("║ Max Volume: ", volMax);
   Print("║ Volume Step: ", volStep);
   Print("║ Stop Level: ", stopLevel, " points");
   Print("║ Base Lot Size: ", LotSize);
   Print("║ Validated Lot: ", NormalizeDouble(MathFloor(LotSize / volStep) * volStep, 2));
   Print("╚══════════════════════════════════════════════════════════════╝");
  }

//+------------------------------------------------------------------+
//| Initialize historical data                                       |
//+------------------------------------------------------------------+
void InitializeHistory()
  {
   for(int i = 0; i < TickCollection; i++)
     {
      SymbolInfoTick(_Symbol, tickArray[i]);
      Sleep(10);
     }
   UpdatePriceWindow();
  }

//+------------------------------------------------------------------+
//| Update price window from tick array                              |
//+------------------------------------------------------------------+
void UpdatePriceWindow()
  {
   int validTicks = 0;
   for(int i = 0; i < TickCollection; i++)
     {
      if(tickArray[i].time > 0 && tickArray[i].bid > 0)
         validTicks++;
     }

   if(validTicks < WindowSize)
     {
      for(int i = 0; i < WindowSize; i++)
        {
         if(i < validTicks)
            priceWindow[i] = tickArray[TickCollection - 1 - i].bid;
         else
            priceWindow[i] = iClose(_Symbol, PERIOD_H1, i - validTicks);
        }
     }
   else
     {
      for(int i = 0; i < WindowSize; i++)
        {
         priceWindow[i] = tickArray[TickCollection - 1 - i].bid;
        }
     }
  }

When the EA is removed from the chart, we perform a clean shutdown by clearing any on-screen comments and removing all chart objects that were created during operation. This prevents visual clutter from persisting after the EA is no longer running. We also maintain several utility functions to manage our position awareness throughout the trading session. The position-counting function loops backward through all open positions and tallies only those matching our symbol and magic number. From this foundation we can check whether we are allowed to open a new trade by comparing the current count against the maximum positions input. To avoid log spam, we throttle the warning message to appear no more than once per minute when the limit is reached.

We also provide directional awareness through functions that detect whether we already hold a position in the same or opposite direction of a potential new signal. These checks prevent duplicate entries and enable intelligent reversal logic when conditions warrant. If a reversal is triggered, the close opposite positions function systematically closes any conflicting trades before opening the new one. Before trading begins, we inspect the symbol's volume constraints and stop-level requirements. This validation ensures our lot size is properly rounded to the broker's allowed step and falls within minimum and maximum boundaries.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Get current tick
   if(!SymbolInfoTick(_Symbol, currentTick))
      return;

   //--- Update tick array (shift left, add new at end)
   for(int i = 0; i < TickCollection - 1; i++)
      tickArray[i] = tickArray[i + 1];
   tickArray[TickCollection - 1] = currentTick;
   tickCount++;

   //--- Update display
   UpdateDisplay();

   //--- Throttle requests
   if(GetTickCount() - lastRequestTime < MinMillisecondsBetweenRequests)
      return;

   //--- Update price window
   UpdatePriceWindow();

   //--- Calculate RSI
   double rsi = CalculateTickRSI();

   //--- Build and send request
   string json = BuildJsonPayload(priceWindow, rsi);
   string response = SendRequest(json);
   if(response == "")
      return;

   //--- Parse response
   string oldSignal = currentSignal;
   if(!ParseServerResponse(response))
      return;

   //--- Count positions
   CountCurrentPositions();

   // --- SMART LOGGING (Silent Mode) ---
   if(positionCount > 0 && currentSignal == oldSignal && currentSignal != "HOLD")
     {
      //--- Same signal while position exists - SILENT MODE
      if(!silentMode)
        {
         silentMode = true;
         silentModeStart = TimeCurrent();
         Print(" Silent mode activated - position open, signal unchanged");
        }
     }
   else
     {
      if(silentMode)
        {
         silentMode = false;
         Print(" Silent mode deactivated after ", TimeCurrent() - silentModeStart, " seconds");
        }

      //--- Normal logging
      PrintFormat("[TICK #%d] Signal: %s | Regime: %s | Conf: %.3f | Entropy: %.3f | Positions: %d",
                  tickCount, currentSignal, currentRegime, currentConfidence, currentEntropy, positionCount);
     }

   //--- Check confidence threshold
   if(currentConfidence < MinConfidenceThreshold)
     {
      Comment("Signal: ", currentSignal, " | Confidence LOW: ", DoubleToString(currentConfidence, 3));
      lastRequestTime = GetTickCount();
      return;
     }

   //--- Check cooldown period
   if(lastTradeTime > 0 && TimeCurrent() - lastTradeTime < MinMinutesBetweenTrades * 60)
     {
      int secondsLeft = MinMinutesBetweenTrades * 60 - (int)(TimeCurrent() - lastTradeTime);
      Comment("Signal: ", currentSignal, " | COOLDOWN: ", secondsLeft, "s remaining");
      lastRequestTime = GetTickCount();
      return;
     }

   //--- Process signal
   ProcessSignal();

   lastRequestTime = GetTickCount();
   previousSignal = currentSignal;
  }

//+------------------------------------------------------------------+
//| Calculate RSI from tick data                                     |
//+------------------------------------------------------------------+
double CalculateTickRSI()
  {
   int period = 14;
   int size = ArraySize(priceWindow);

   if(size < period + 1)
      return 50.0;

   double avgGain = 0, avgLoss = 0;

   for(int i = 1; i <= period; i++)
     {
      double change = priceWindow[size - i] - priceWindow[size - i - 1];
      if(change > 0)
         avgGain += change;
      else
         avgLoss -= change;
     }

   avgGain /= period;
   avgLoss /= period;

   if(avgLoss == 0)
      return 100.0;

   double rs = avgGain / avgLoss;
   return 100.0 - (100.0 / (1.0 + rs));
  }

The OnTick function serves as the heartbeat of our EA, executing on every incoming price update to maintain a continuous flow of market awareness and decision-making. We begin by capturing the current tick and shifting our rolling tick array to accommodate the newest data point while discarding the oldest. After updating the on-chart display, we enforce a request throttle to prevent overwhelming the Flask server with excessive calls. When the throttle permits, we refresh the price window from our tick history and compute a simple RSI value directly from that data. We then package the price array and RSI into a JSON payload and send it to the server via HTTP POST.

Upon receiving a response, we parse the signal, regime, confidence, and entropy values. A smart logging system activates silent mode when we already hold a position and the signal remains unchanged, which keeps the expert's journal clean during quiet periods. Before processing any trade, we validate that the confidence score meets our minimum threshold and that sufficient cooldown time has elapsed since the last execution. Only after passing these safeguards do we hand the signal off to our position management logic. The accompanying RSI function calculates the classic fourteen-period relative strength index using the most recent prices from our rolling window, providing a momentum context that complements the entropy features computed on the server side.

//+------------------------------------------------------------------+
//| Build JSON payload                                               |
//+------------------------------------------------------------------+
string BuildJsonPayload(const double &prices[], double rsi)
  {
   string json = "{\"prices\":[";
   int size = ArraySize(prices);

   for(int i = 0; i < size; i++)
     {
      json += DoubleToString(prices[i], _Digits);
      if(i < size - 1)
         json += ",";
     }

   json += "],\"rsi\":" + DoubleToString(rsi, 2) + "}";
   return json;
  }

//+------------------------------------------------------------------+
//| Send HTTP POST request                                           |
//+------------------------------------------------------------------+
string SendRequest(string json)
  {
   char postData[], resultData[];

   int len = StringLen(json);
   ArrayResize(postData, len);
   for(int i = 0; i < len; i++)
      postData[i] = (char)StringGetCharacter(json, i);

   string headers = "Content-Type: application/json\r\n";
   string responseHeaders;

   int res = WebRequest("POST", ServerURL, headers, RequestTimeout,
                        postData, resultData, responseHeaders);

   if(res == -1 || res != 200)
      return "";

   return CharArrayToString(resultData, 0, WHOLE_ARRAY, CP_UTF8);
  }

//+------------------------------------------------------------------+
//| Parse JSON response                                              |
//+------------------------------------------------------------------+
bool ParseServerResponse(string json)
  {
   currentSignal = ExtractStringValue(json, "signal");
   currentRegime = ExtractStringValue(json, "regime");
   currentConfidence = ExtractDoubleValue(json, "confidence");
   currentVolMultiplier = ExtractDoubleValue(json, "volatility_multiplier");
   currentEntropy = ExtractDoubleValue(json, "entropy");

   return (currentSignal != "");
  }

//+------------------------------------------------------------------+
//| Extract string value from JSON                                   |
//+------------------------------------------------------------------+
string ExtractStringValue(string json, string key)
  {
   string searchKey = "\"" + key + "\":";
   int start = StringFind(json, searchKey);

   if(start >= 0)
     {
      start += StringLen(searchKey);

      while(start < StringLen(json) && (StringGetCharacter(json, start) == ' ' || StringGetCharacter(json, start) == '\t'))
         start++;

      if(StringGetCharacter(json, start) == '\"')
        {
         start++;
         int end = start;
         while(end < StringLen(json) && StringGetCharacter(json, end) != '\"')
            end++;

         if(end > start)
            return StringSubstr(json, start, end - start);
        }
     }
   return "";
  }

//+------------------------------------------------------------------+
//| Extract double value from JSON                                   |
//+------------------------------------------------------------------+
double ExtractDoubleValue(string json, string key)
  {
   string searchKey = "\"" + key + "\":";
   int start = StringFind(json, searchKey);

   if(start >= 0)
     {
      start += StringLen(searchKey);

      while(start < StringLen(json) && (StringGetCharacter(json, start) == ' ' || StringGetCharacter(json, start) == '\t'))
         start++;

      int end = start;
      while(end < StringLen(json))
        {
         ushort ch = StringGetCharacter(json, end);
         if(ch == ',' || ch == '}' || ch == ' ' || ch == '\n' || ch == '\r')
            break;
         end++;
        }

      if(end > start)
         return StringToDouble(StringSubstr(json, start, end - start));
     }
   return 0.0;
  }

Here, we construct the communication bridge between MetaTrader 5 and our Python server through a carefully formatted JSON payload. The build function opens with a curly brace and the prices array key, then iterates through each value in our rolling price window. We convert each price to a string with the symbol's proper digit precision and append commas between elements while avoiding a trailing comma after the final value. The array closes, and we attach the calculated RSI value before sealing the object with a closing brace. This cleanly structured payload matches exactly what the Flask endpoint expects to receive. The send request function converts our JSON string into a character array suitable for HTTP transmission and sets the appropriate content type header.

Once the response arrives, we parse it using a set of lightweight extraction utilities that avoid the overhead of a full JSON library. The main parse function delegates to specialized helpers that locate keys within the response text and extract their associated values. For string values like the trading signal and volatility regime, we search for the key name followed by a colon, skip any whitespace, and capture everything between the opening and closing quotation marks. Numeric extraction follows a similar pattern but reads characters until hitting a delimiter such as a comma or closing brace.

//+------------------------------------------------------------------+
//| Process trading signal with position checks                      |
//+------------------------------------------------------------------+
void ProcessSignal()
  {
   if(currentSignal == "HOLD")
     {
      //--- Manage open position (update SL/TP if regime changed)
      if(positionCount > 0)
         ManageOpenPosition();
      return;
     }

   ENUM_ORDER_TYPE desiredOrder = (currentSignal == "BUY") ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;

   // --- POSITION CHECKS ---

   //--- Check if we can open a new position
   if(!CanOpenNewPosition() && !HasPositionInOppositeDirection(desiredOrder))
     {
      if(!silentMode)
         Print("Cannot open new position - max positions reached");

      //--- Still manage existing position
      if(positionCount > 0)
         ManageOpenPosition();
      return;
     }

   //--- Check if we already have a position in the SAME direction
   if(HasPositionInDirection(desiredOrder))
     {
      //--- Manage existing position (update SL/TP)
      ManageOpenPosition();

      if(!silentMode)
        {
         static datetime lastSameLog = 0;
         if(TimeCurrent() - lastSameLog > 60)
           {
            Print("Already have ", currentSignal, " position. Managing...");
            lastSameLog = TimeCurrent();
           }
        }
      return;
     }

   //--- Check for OPPOSITE position (reversal)
   if(HasPositionInOppositeDirection(desiredOrder))
     {
      if(AllowReversal && currentConfidence > 0.6)
        {
         Print("REVERSAL SIGNAL - Closing opposite position(s)");
         CloseOppositePositions(desiredOrder);
         Sleep(500);

         ExecuteAdaptiveTrade(desiredOrder, "TickEntropy REVERSE " + currentSignal);
         lastTradeTime = TimeCurrent();
         openPositionTicket = TradeManager.ResultOrder();
        }
      else
        {
         //--- Manage existing position despite opposite signal
         ManageOpenPosition();

         if(!silentMode)
            Print("Reversal signal but confidence too low. Managing existing position.");
        }
      return;
     }

   //--- NO POSITION ---
   if(positionCount == 0)
     {
      if(lastTradeTime > 0 && TimeCurrent() - lastTradeTime < MinMinutesBetweenTrades * 60)
        {
         if(!silentMode)
           {
            int secondsLeft = MinMinutesBetweenTrades * 60 - (int)(TimeCurrent() - lastTradeTime);
            Print("Cooldown active: ", secondsLeft, "s remaining");
           }
         return;
        }

      if(!CanOpenNewPosition())
         return;

      Print("Opening NEW position: ", currentSignal);
      ExecuteAdaptiveTrade(desiredOrder, "TickEntropy " + currentSignal);
      lastTradeTime = TimeCurrent();
      openPositionTicket = TradeManager.ResultOrder();
      return;
     }
  }

//+------------------------------------------------------------------+
//| Execute adaptive trade with proper lot size rounding             |
//+------------------------------------------------------------------+
void ExecuteAdaptiveTrade(ENUM_ORDER_TYPE orderType, string comment)
  {
   if(!SymbolInfoTick(_Symbol, currentTick))
      return;

   //--- Get symbol volume properties
   double volumeMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double volumeMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double volumeStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   double adaptiveLot = LotSize;
   int adaptiveSL = BaseStopLossPoints;
   int adaptiveTP = BaseTakeProfitPoints;

   if(AdaptiveRiskEnabled)
     {
      if(currentRegime == "EXTREME_VOLATILITY")
         adaptiveLot = LotSize * 0.3;
      else
         if(currentRegime == "HIGH_VOLATILITY")
            adaptiveLot = LotSize * 0.6;
         else
            if(currentRegime == "LOW_VOLATILITY")
               adaptiveLot = LotSize * 1.3;

      adaptiveSL = (int)(BaseStopLossPoints * currentVolMultiplier);
      adaptiveTP = (int)(BaseTakeProfitPoints * currentVolMultiplier);
     }

   //--- Proper lot size rounding
   adaptiveLot = MathFloor(adaptiveLot / volumeStep) * volumeStep;
   adaptiveLot = MathMax(volumeMin, MathMin(volumeMax, adaptiveLot));
   adaptiveLot = NormalizeDouble(adaptiveLot, 2);

   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   double stopLoss = 0.0, takeProfit = 0.0;
   double priceOpen = 0.0;

   if(orderType == ORDER_TYPE_BUY)
     {
      priceOpen = currentTick.ask;
      stopLoss = NormalizeDouble(currentTick.bid - adaptiveSL * point, digits);
      takeProfit = NormalizeDouble(currentTick.ask + adaptiveTP * point, digits);
     }
   else
     {
      priceOpen = currentTick.bid;
      stopLoss = NormalizeDouble(currentTick.ask + adaptiveSL * point, digits);
      takeProfit = NormalizeDouble(currentTick.bid - adaptiveTP * point, digits);
     }

   //--- Validate stop levels
   int stopLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double stopDist = stopLevel * point;

   if(orderType == ORDER_TYPE_BUY)
     {
      if(stopLoss > 0 && (priceOpen - stopLoss) < stopDist)
        {
         Print("Stop Loss too close. Required: ", stopDist);
         return;
        }
     }
   else
     {
      if(stopLoss > 0 && (stopLoss - priceOpen) < stopDist)
        {
         Print("Stop Loss too close.");
         return;
        }
     }

   string tradeComment = comment + " | " + currentRegime;

   //--- Execute trade
   for(int attempt = 1; attempt <= 3; attempt++)
     {
      bool sent;
      if(orderType == ORDER_TYPE_BUY)
         sent = TradeManager.Buy(adaptiveLot, _Symbol, priceOpen, stopLoss, takeProfit, tradeComment);
      else
         sent = TradeManager.Sell(adaptiveLot, _Symbol, priceOpen, stopLoss, takeProfit, tradeComment);

      if(sent)
        {
         uint retcode = TradeManager.ResultRetcode();
         if(retcode == TRADE_RETCODE_DONE || retcode == TRADE_RETCODE_DONE_PARTIAL)
           {
            Print("Trade executed. Ticket: ", TradeManager.ResultOrder(),
                  " | Lot: ", DoubleToString(adaptiveLot, 2),
                  " | SL: ", adaptiveSL, " | TP: ", adaptiveTP);
            CountCurrentPositions();
            return;
           }
         else
            if(retcode == TRADE_RETCODE_INVALID_VOLUME)
              {
               Print("Invalid volume: ", adaptiveLot);
               return;
              }
            else
               if(retcode == TRADE_RETCODE_REQUOTE || retcode == TRADE_RETCODE_PRICE_CHANGED)
                 {
                  Print("Price changed. Retry ", attempt, "/3");
                  SymbolInfoTick(_Symbol, currentTick);
                  Sleep(100);
                  continue;
                 }
               else
                 {
                  Print("Trade failed: ", TradeManager.ResultRetcodeDescription());
                  return;
                 }
        }
     }
   Print("Max retries reached.");
  }

//+------------------------------------------------------------------+
//| Manage open position - Update SL/TP based on regime changes      |
//+------------------------------------------------------------------+
void ManageOpenPosition()
  {
   if(positionCount == 0)
      return;

   //--- Select the position
   if(!PositionInfo.SelectByTicket(openPositionTicket))
     {
      //--- Try to find any position for this symbol/magic
      bool found = false;
      for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
         if(PositionInfo.SelectByIndex(i))
           {
            if(PositionInfo.Symbol() == _Symbol && PositionInfo.Magic() == MagicNumber)
              {
               openPositionTicket = PositionInfo.Ticket();
               found = true;
               break;
              }
           }
        }
      if(!found)
        {
         openPositionTicket = 0;
         return;
        }
     }

   //--- Track last regime for this position
   static string lastRegimeForPosition = "";
   static double lastVolMultiplier = 1.0;

   //--- Check if regime has changed significantly
   if(lastRegimeForPosition != currentRegime || MathAbs(lastVolMultiplier - currentVolMultiplier) > 0.2)
     {
      //--- Regime changed - adjust SL/TP
      if(AdaptiveRiskEnabled)
        {
         AdjustPositionStops();
         lastRegimeForPosition = currentRegime;
         lastVolMultiplier = currentVolMultiplier;
        }
     }
  }

//+------------------------------------------------------------------+
//| Adjust SL/TP for open position based on current regime           |
//+------------------------------------------------------------------+
void AdjustPositionStops()
  {
   if(!PositionInfo.SelectByTicket(openPositionTicket))
      return;

   if(!SymbolInfoTick(_Symbol, currentTick))
      return;

   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   //--- Calculate new adaptive SL/TP distances
   int adaptiveSL = (int)(BaseStopLossPoints * currentVolMultiplier);
   int adaptiveTP = (int)(BaseTakeProfitPoints * currentVolMultiplier);

   double newSL = 0.0, newTP = 0.0;
   ENUM_POSITION_TYPE posType = PositionInfo.PositionType();

   if(posType == POSITION_TYPE_BUY)
     {
      newSL = NormalizeDouble(currentTick.bid - adaptiveSL * point, digits);
      newTP = NormalizeDouble(currentTick.ask + adaptiveTP * point, digits);
     }
   else // SELL
     {
      newSL = NormalizeDouble(currentTick.ask + adaptiveSL * point, digits);
      newTP = NormalizeDouble(currentTick.bid - adaptiveTP * point, digits);
     }

   //--- Get current SL/TP
   double currentSL = PositionInfo.StopLoss();
   double currentTP = PositionInfo.TakeProfit();

   //--- Check if adjustment is significant (more than 10 points difference)
   double slDiff = MathAbs(newSL - currentSL) / point;
   double tpDiff = MathAbs(newTP - currentTP) / point;

   if(slDiff > 10 || tpDiff > 10)
     {
      //--- Don't tighten SL beyond current profitable level for buys
      if(posType == POSITION_TYPE_BUY)
        {
         if(newSL > currentTick.bid - 50 * point)  // Don't set SL too close
            newSL = currentSL;
         if(newSL < currentSL)  // Only move SL up (tighten), never down
            newSL = currentSL;
        }
      else // SELL
        {
         if(newSL < currentTick.ask + 50 * point)
            newSL = currentSL;
         if(newSL > currentSL)  // Only move SL down (tighten), never up
            newSL = currentSL;
        }

      //--- Attempt to modify position
      if(TradeManager.PositionModify(openPositionTicket, newSL, newTP))
        {
         Print("Regime changed to ", currentRegime, " - Adjusted SL/TP");
         PrintFormat("   SL: %d → %d points | TP: %d → %d points",
                     (int)(MathAbs(currentSL - PositionInfo.PriceOpen()) / point),
                     adaptiveSL,
                     (int)(MathAbs(currentTP - PositionInfo.PriceOpen()) / point),
                     adaptiveTP);
        }
     }
  }

The ProcessSignal function serves as our central decision hub, evaluating every incoming trading recommendation against the current state of our portfolio. We first handle HOLD signals by simply delegating to our position management routine if a trade is already open. When a directional signal arrives, we convert it into the corresponding order type and proceed through a series of logical gates. The first checkpoint verifies whether we are permitted to open a new position by checking our maximum position limit and the presence of any opposing trades. If we are at capacity without an opportunity to reverse, we bail out early but still allow existing positions to receive updated stop management. The second checkpoint detects whether we already hold a trade in the identical direction.

The third and most consequential checkpoint handles reversal scenarios where our signal conflicts with an existing position. When reversal permission is granted and the confidence score exceeds our elevated threshold of sixty percent, we systematically close all opposing trades and pause briefly to allow the broker to process the closure. We then immediately open a new position in the fresh direction with an appropriate comment label. If confidence falls short of the reversal requirement, we simply maintain our current stance and continue adjusting stops according to the prevailing regime.

The trade execution and ongoing management functions translate our risk-aware logic into concrete broker actions. When opening a trade, we first retrieve the symbol's volume constraints and calculate an adaptive lot size by multiplying our base amount against regime-specific factors. Extreme volatility reduces exposure to thirty percent of normal, while low volatility allows us to scale up to thirty percent larger. Stop and take profit distances are similarly multiplied by the server-supplied volatility factor before being normalized to the symbol's digit precision. We then validate that our proposed stops sit outside the broker's freeze level before dispatching the order with up to three retry attempts to handle requotes or brief price disconnections. The management routine runs continuously for open positions, detecting when the volatility regime has shifted meaningfully from its previous state. Upon such a change, we recalculate appropriate stop distances and modify the position, with the crucial safeguard that we never widen a stop against a winning trade nor place stops dangerously close to the current market price.

//+------------------------------------------------------------------+
//| Update chart display                                             |
//+------------------------------------------------------------------+
void UpdateDisplay()
  {
   CountCurrentPositions();

   string display = "=== TICK ENTROPY TRADER ===\n";
   display += "Tick #: " + IntegerToString(tickCount) + "\n";
   display += "Signal: " + currentSignal;
   if(signalRepeatCount > 0)
      display += " (x" + IntegerToString(signalRepeatCount) + ")";
   display += "\n";
   display += "Regime: " + currentRegime + "\n";
   display += "Entropy: " + DoubleToString(currentEntropy, 4) + "\n";
   display += "Confidence: " + DoubleToString(currentConfidence, 3) + "\n";
   display += "Positions: " + IntegerToString(positionCount) + "/" + IntegerToString(MaxPositions) + "\n";
   display += "Bid: " + DoubleToString(currentTick.bid, _Digits) + "\n";

   if(PositionInfo.SelectByTicket(PositionInfo.Ticket()))
     {
      display += "\n--- ACTIVE POSITION ---\n";
      display += "Type: " + EnumToString(PositionInfo.PositionType()) + "\n";
      display += "Profit: " + DoubleToString(PositionInfo.Profit(), 2) + "\n";
     }

   //--- Cooldown display
   if(lastTradeTime > 0)
     {
      int secondsSince = (int)(TimeCurrent() - lastTradeTime);
      if(secondsSince < MinMinutesBetweenTrades * 60)
        {
         int remaining = MinMinutesBetweenTrades * 60 - secondsSince;
         display += "\n⏳ Cooldown: " + IntegerToString(remaining) + "s\n";
        }
     }

   Comment(display);
  }

//+------------------------------------------------------------------+

The UpdateDisplay function provides us with real-time visual feedback directly on the chart, eliminating the need to constantly monitor the expert's journal. We begin by refreshing our position count to ensure accuracy, then construct a multi-line string that presents all critical system metrics in an organized panel. The display shows the cumulative tick count since EA startup, the current trading signal with any repetition counter appended, and the detected volatility regime. We also expose the underlying entropy reading, model confidence score, and our current position utilization relative to the maximum allowed. When an active trade exists, we append a dedicated section revealing the position type and floating profit. If a cooldown period is in effect following a recent execution, we calculate and display the remaining seconds until the next trade becomes permissible.


Live Inference

Below, the Flask server has successfully initialized and is running on localhost on port 5000, and is getting signals from Jupyter Lab to MetaTrader5.



Conclusion

Throughout this project, we have constructed a complete adaptive trading system that bridges the gap between quantitative market analysis and automated execution. Beginning with the mathematical foundation of Shannon entropy applied to tick-level price data, we built a feature engineering pipeline that extracts meaningful signals from market disorder. A neural network trained on these entropy-derived features learns to predict directional probability, while a volatility regime detector continuously classifies market conditions into four distinct states. The Flask server serves as the intelligence layer, processing incoming price windows and returning not just buy or sell signals but confidence scores and adaptive risk multipliers. On the MetaTrader side, we developed a robust Expert Advisor that manages the entire lifecycle of a trade from tick collection through position management to final exit.

What you will walk away with after engaging with this material is a production-ready framework for volatility-aware algorithmic trading that you can immediately deploy and extend. You now understand how to calculate market entropy in real time rather than waiting for candles to close, giving you an edge in detecting regime shifts as they happen. You have learned to build adaptive risk management that scales position size and stop distances based on current market conditions rather than using fixed parameters that fail when volatility spikes. The complete communication pipeline between MQL5 and Python demonstrates how to leverage the strengths of both platforms, combining MetaTrader's robust execution with Python's machine learning ecosystem. Perhaps most importantly, you now possess a modular architecture that can be enhanced with additional features, alternative models, or different symbols without starting from scratch.

Below is the brief description of what is contained inside 'Arc Files zip':

File Name   File Description
GettingHistData.py Connects to MetaTrader 5, downloads historical XAUUSD hourly price data.
Features.py Implements Shannon entropy calculation, volatility metrics extraction, trend strength measurement, and the volatility regime detector class that classifies market conditions into four distinct volatility states.
Model.py Defines a PyTorch neural network with batch normalization and dropout layers that maps eight entropy-derived features to a directional probability prediction for the future price movement.
Train.py Loads historical CSV data, engineers features using a rolling window approach, trains the neural network with validation monitoring, and saves both the trained model and feature scaler for deployment.
Server.py Runs a Flask web server that receives tick-level price data from the EA, computes entropy features, runs model inference, detects the current volatility regime, and returns an adaptive signal with a confidence score.
Entropy.ipynb A Jupyter notebook for simple execution workflow.
Real-Time Entropy.mq5 Is the MetaTrader 5 Expert Advisor that collects tick data, and communicates with the Flask Server.
Attached files |
Arc_Files.zip (15.87 KB)
Cross Recurrence Quantification Analysis (CRQA) in MQL5: Building a Complete Analysis Library Cross Recurrence Quantification Analysis (CRQA) in MQL5: Building a Complete Analysis Library
This article extends the MQL5 RQA library to Cross-Recurrence Quantification Analysis (CRQA) for comparing two time series. We implement dual‑series embedding, cross‑recurrence matrix construction, adapted metrics (CRR, CDET, CLAM, CENTR, and others), and rolling‑window analysis, with optional GPU acceleration via OpenCL. A ready-to-use indicator compares two symbols in real time, supporting timestamp alignment and normalization for practical inter-market analysis.
MQL5 Wizard Techniques you should know (Part 89): Using Bitwise Vectorization with Perceptron Classifiers MQL5 Wizard Techniques you should know (Part 89): Using Bitwise Vectorization with Perceptron Classifiers
This article presents a custom MQL5 signal class, CSignalBitwisePerceptron, for ultra-lightweight entry logic. It packs 64 bars into a single uint64 via bitwise vectorization and evaluates them with a perceptron that sums weights only for active bits. A two-gate flow (algorithmic hash map plus neural threshold) minimizes array iteration and heavy math. Readers get a practical template to cut latency and refine entry validation.
Building an Object-Oriented ONNX Inference Engine in MQL5 Building an Object-Oriented ONNX Inference Engine in MQL5
This article shows how to run Python-trained models natively in MetaTrader 5 via the terminal's ONNX functions. We build an MQL5 class that encapsulates session creation, fixes input/output tensor shapes, applies min-max feature normalization to mirror training, and executes OnnxRun once per bar to protect the CPU, the result is a reliable, maintainable inference path for live charts and the Strategy Tester without sockets or DLLs.
Building AI-Powered Trading Systems in MQL5 (Part 9): Creating an AI Signal Dispatcher Building AI-Powered Trading Systems in MQL5 (Part 9): Creating an AI Signal Dispatcher
We turn the MQL5 AI trading assistant into a dispatch-driven system that routes seven trading actions through a single central dispatcher. A line-based key-value protocol constrains AI output, while each action maps to market or pending orders and instrument-aware stop levels. A canvas-based UI with a custom prompt editor and pixel-accurate text fitting makes signals consistent, auditable, and ready to render on the chart