Integrating MQL5 with Data Processing Packages (Part 9): Entropy-Based Adaptive Volatility
Table of Contents
- Introduction
- Model and System Overview
- Getting Started
- Getting Historical Data
- Feature Engineering
- Model Definition
- Training Pipeline
- Flask Server
- Simple Execution Workflow
- Putting it all Together on MQL5
- Live Inference
- 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. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Cross Recurrence Quantification Analysis (CRQA) in MQL5: Building a Complete Analysis Library
MQL5 Wizard Techniques you should know (Part 89): Using Bitwise Vectorization with Perceptron Classifiers
Building an Object-Oriented ONNX Inference Engine in MQL5
Building AI-Powered Trading Systems in MQL5 (Part 9): Creating an AI Signal Dispatcher
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use