Интеграция MQL5 с пакетами обработки данных (Часть 9): Адаптивная оценка волатильности на основе энтропии
Содержание
- Введение
- Обзор модели и системы
- Приступаем
- Сбор исторических данных
- Создание признаков
- Определение модели
- Пайплайн обучения
- Сервер Flask
- Простой порядок выполнения
- Объединяем все в MQL5
- Инференс в реальном времени
- Заключение
Введение
Трейдеры постоянно сталкиваются с проблемами на непредсказуемых финансовых рынках. Волатильность может резко измениться в течение одной сессии, превратив устойчивый тренд в хаотичное разнонаправленное ценовое движение. Традиционные технические индикаторы часто запаздывают при быстрых изменениях режима, подвергая трейдеров риску внезапных разворотов, чрезмерных просадок и упущенных возможностей. Фиксированные уровни стоп-лосса и тейк-профита, хорошо работающие в спокойных условиях, становятся опасно недостаточными при всплесках волатильности. Фиксированный размер позиции при этом не учитывает постоянно меняющийся уровень риска. В результате возникает порочный круг преждевременных срабатываний стоп-лосса, чрезмерных убытков и нестабильных результатов, подрывающий капитал и уверенность.
Этот проект решает указанные задачи с помощью расчета рыночной энтропии в реальном времени. Этот количественный подход непрерывно измеряет степень неупорядоченности и неопределенности в ценовых данных на уровне тиков. Применяя энтропию Шеннона к скользящим окнам последних цен, система мгновенно определяет переходы между режимами низкой, нормальной, высокой и экстремальной волатильности. Нейронная сеть, обученная на признаках, полученных из энтропии, прогнозирует вероятность направления движения. Модуль адаптивного управления рисками динамически регулирует размер позиции, ширину стоп-лосса и целевые уровни тейк-профита с учетом текущего состояния рынка. Пайплайн получает тики MetaTrader 5, выполняет инференс через Flask и автоматически исполняет сделки. Он реагирует за миллисекунды, не дожидаясь закрытия свечи. Это позволяет трейдерам принимать решения с учетом волатильности, защищать капитал в периоды хаоса и использовать возможности в спокойные периоды.
Обзор модели и системы
Система состоит из двух совместно работающих основных компонентов: советника MQL5, работающего в MetaTrader 5, и сервера Flask на Python, выполняющего инференс модели. На каждом тике советник получает последние 50 цен Bid и рассчитывает RSI. Эти данные упаковываются в JSON-объект и отправляются HTTP-запросом POST на локально работающий сервер Flask. Сервер получает окно цен, вычисляет энтропию Шеннона и несколько показателей волатильности и тренда, затем передает эти признаки предварительно обученной нейронной сети. Модель возвращает вероятность направления от нуля до единицы. Сервер объединяет ее с текущим режимом волатильности, формируя итоговый сигнал.

Необработанные тиковые данные непрерывно поступают в скользящее окно, где энтропия Шеннона измеряет рыночную неупорядоченность. Полученный показатель энтропии классифицирует текущий режим волатильности, напрямую определяющий размер позиции и расстояние до стоп-лосса.
Когда сервер Flask возвращает сигнал Buy или Sell с достаточной уверенностью, советник перед выполнением действия оценивает текущий риск по открытым позициям. Если позиции нет и периоды ожидания истекли, рыночный ордер размещается с помощью класса CTrade, встроенного в MetaTrader. Размер позиции, уровни стоп-лосса и тейк-профита масштабируются с учетом множителя волатильности, полученного от сервера. Если уже существует позиция противоположного направления и выполнены условия разворота, советник закрывает текущую сделку перед открытием новой. Все это происходит непрерывно на тиковых данных, что позволяет системе обнаруживать изменения волатильности и адаптироваться к ним по мере возникновения, не дожидаясь закрытия свечей.

Советник отправляет тиковые данные на сервер Flask, где извлекаются признаки энтропии и передаются нейронной сети. Сервер возвращает сигнал с адаптивными параметрами риска, а советник исполняет сделку после проверки ограничений по позициям и периодов ожидания.
Приступаем
Сбор исторических данных
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)
Для начала подключимся к MetaTrader 5 с помощью его Python API и выведем основную информацию о пакете. Затем настраиваются параметры отображения pandas, чтобы сделать вывод более удобным для чтения. После этого инициализируется соединение и выбирается символ XAUUSD.m, чтобы убедиться в его доступности. С помощью pytz определяется диапазон времени UTC, позволяющий запрашивать точные исторические данные. Далее получаются ценовые данные на таймфрейме H1, соединение закрывается и проверяется, были ли возвращены данные. Если данные получены, выводится их предварительный просмотр, они преобразуются в DataFrame, форматируются временные метки, а результат сохраняется в CSV-файл для последующего использования.
Создание признаков
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
В этом разделе кода с помощью NumPy создаются основные математические инструменты для преобразования необработанных ценовых данных в значимые сигналы. Вычисляются логарифмические доходности, позволяющие стабильно фиксировать изменение цены. Затем с помощью энтропии Шеннона измеряется рыночная неопределенность: доходности группируются в интервалы на основе процентилей и рассчитывается их распределение вероятностей. Этот подход также расширяется расчетом энтропии квадратов доходностей, что позволяет анализировать поведение волатильности, а не только направление движения. Наряду с энтропией рассчитываются дополнительные показатели волатильности: стандартное отклонение, среднее абсолютное отклонение (MAD), размах, асимметрия и эксцесс. Вместе они описывают распределение доходностей и показывают, сбалансирован ли рынок или имеет выраженную направленность.
Затем анализируется ценовая структура: сила тренда оценивается с помощью линейной регрессии, где наклон задает направление, а значение R² показывает, насколько тренд чистый или зашумленный. После этого с помощью функции build_features все данные объединяются в единый вектор признаков: энтропия, энтропия волатильности, статистика доходностей, сила тренда, нормализованный наклон, асимметрия и масштабированное значение RSI. Данные также очищаются заменой некорректных значений. Затем возвращаются массив признаков для входа модели и словарь показателей для мониторинга. Таким образом, необработанные цены преобразуются в структурированное представление поведения рынка, которое ML-модель может использовать для принятия решений.
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
Здесь создается класс, отслеживающий энтропию и волатильность во времени с помощью скользящей истории, хранящейся в двусторонних очередях deque (деках). Энтропия Шеннона используется как основной сигнал для определения рыночных режимов. При каждом обновлении сохраняются последние показатели, а текущий режим волатильности классифицируется как LOW, NORMAL, HIGH или EXTREME. Сначала используются фиксированные пороги, когда исторических данных недостаточно. Затем применяются адаптивные пороги, основанные на среднем значении и стандартном отклонении предыдущих значений энтропии. Дополнительно вычисляются процентили, позволяющие оценить экстремальность текущего состояния, определить изменения режима и вернуть множители, корректирующие риск и уровень уверенности. Наконец, обнаруженный режим преобразуется в практические торговые параметры путем масштабирования стоп-лосса, тейк-профита и размера лота. Благодаря этому система адаптируется к изменяющимся рыночным условиям вместо использования фиксированных настроек.
Определение модели
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
С помощью PyTorch определяется модель нейронной сети, принимающая 8 входных признаков и обрабатывающая их несколькими полносвязными слоями для выявления рыночных паттернов. Используются пакетная нормализация для стабилизации обучения, активация ReLU для введения нелинейности и dropout для снижения переобучения путем случайного отключения нейронов во время обучения. Модель постепенно уменьшает размерность с 32 до одного выходного значения, где сигмоидная функция формирует вероятность сигнала BUY или SELL. Также используется специальный метод оценки неопределенности: dropout остается активным во время инференса, выполняется несколько прямых проходов, после чего вычисляются средний прогноз и стандартное отклонение для оценки уверенности в выходных данных модели.
Пайплайн обучения
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")
В этом разделе исторические рыночные данные загружаются из CSV-файла с помощью pandas, проверяется наличие необходимых ценовых столбцов и извлекаются цены закрытия, максимума и минимума для обработки. Затем формируется набор данных: фиксированное окно перемещается по ценовому ряду, для каждого окна вычисляется RSI, с помощью модуля признаков создаются расширенные признаки, а метка присваивается на основе будущего движения цены. После сбора всех образцов данные преобразуются в массивы NumPy, затем проверяются форма набора данных и распределение меток. Наконец, признаки нормализуются с помощью StandardScaler для обеспечения стабильного обучения модели, а масштабировщик сохраняется с помощью joblib для последующего использования при прогнозировании в реальном времени.
# ------------------------------ # 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}")
Здесь набор данных разделяется на обучающую и валидационную выборки в соотношении 80/20. Затем входные данные и метки преобразуются в тензоры для использования с PyTorch. Инициализируется модель, определяется функция бинарной кросс-энтропии для классификации и используется оптимизатор Adam с затуханием весов для улучшения обобщающей способности. Во время обучения выполняются эпохи: сначала модель обучается на обучающей выборке, затем оценивается на валидационной выборке без обновления весов. Отслеживается наилучшее значение функции потерь на валидации, и при каждом улучшении модель сохраняется, чтобы избежать переобучения. Наконец, проводится быстрая оценка: рассчитывается точность на обучающей и валидационной выборках на основе порога 0,5. Это показывает, насколько хорошо модель обобщает результаты на ранее не встречавшиеся данные.
Сервер Flask
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)
В этом разделе с помощью Flask настраивается REST API и загружаются обученная модель и масштабировщик для прогнозирования в реальном времени. Инициализируется быстрый детектор режима волатильности и отслеживается история энтропии для измерения изменений во времени. При получении запроса извлекаются ценовые данные и RSI, с помощью модуля признаков вычисляются признаки, а также рассчитываются энтропия и ее изменение. Затем определяется текущий режим рынка, а с помощью масштабировщика и модели, созданной в PyTorch, вычисляется вероятность. API возвращает ключевую информацию: энтропию, режим волатильности, вероятность прогноза и дополнительные показатели, описывающие текущее состояние рынка.
Затем формируется торговый сигнал с помощью адаптивной логики, корректирующей пороги в зависимости от режима волатильности и изменения энтропии. Когда энтропия растет, пороги повышаются для снижения риска, а при стабильных условиях критерии входа становятся менее жесткими. Также рассчитывается показатель уверенности: уверенность модели объединяется со штрафами за высокую или растущую энтропию. Это помогает отфильтровывать ненадежные сделки. Вычисляется моментум энтропии, позволяющий отслеживать изменение неопределенности. Также предоставляется эндпоинт проверки состояния, подтверждающий, что сервер работает и готов к взаимодействию с MetaTrader 5.
Простой порядок выполнения
Ниже показано, как запускать каждый файл отдельно в ячейках JupyterLab.
Сбор исторических данных:
%run GettingHistData.py

Создание признаков:
%run Features.py
Определение модели:
%run Model.py
Пайплайн обучения:
%run Train.py

Сервер Flask:
%run Server.py
Объединяем все в MQL5
//+------------------------------------------------------------------+ //| Real-Time Entropy.mq5 | //| Git, Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/ru/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Git, Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/ru/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); }
В MQL5 сначала импортируются необходимые библиотеки для торговли и управления позициями. Входные параметры позволяют полностью управлять поведением советника без необходимости перекомпиляции. Определяются эндпоинт сервера Flask, размер скользящего окна для сбора цен и базовые параметры риска, включая размер лота и расстояние до стоп-лосса в пунктах. Дополнительные входные параметры задают ограничение частоты запросов, переключатели адаптивного риска, пороги уверенности, разрешения на разворот и периоды ожидания между сделками. Глобальные переменные отслеживают текущее состояние рынка: последний тик, массивы истории цен, текущие сигнал и режим, показатели уверенности и количество позиций. Также поддерживаются флаги тихого режима, предотвращающие спам в логе, когда позиция уже открыта и рыночные условия не меняются.
При инициализации менеджеру торговли присваивается уникальное магическое число и включается синхронный режим для надежного исполнения сделок. Размеры массивов тиков и цен задаются согласно входным параметрам, после чего они сразу заполняются недавними рыночными данными, чтобы избежать задержек холодного старта. Проверка свойств символа подтверждает, что размер лота и уровни стоп-лосса соответствуют требованиям брокера. Подсчитываются все существующие позиции, управляемые этим советником, для точного отслеживания состояния. Наконец, в журнал советника выводится структурированный блок с ключевыми параметрами конфигурации.
//+------------------------------------------------------------------+ //| 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; } } }
При удалении советника с графика выполняется корректное завершение работы: очищаются комментарии на экране и удаляются все графические объекты, созданные во время работы. Это предотвращает сохранение лишних графических элементов после остановки советника. Также поддерживаются несколько вспомогательных функций для отслеживания позиций в течение торговой сессии. Функция подсчета позиций перебирает открытые позиции в обратном порядке и учитывает только те, которые соответствуют текущему символу и магическому числу. На этой основе можно проверить, разрешено ли открыть новую сделку, сравнив текущее количество с параметром максимального числа позиций. Чтобы избежать спама в логе, вывод предупреждающего сообщения ограничивается одним разом в минуту при достижении лимита.
Также используются функции, определяющие, имеется ли уже позиция в том же или противоположном направлении относительно потенциального нового сигнала. Эти проверки предотвращают повторные входы и обеспечивают логику разворота, когда это оправдано условиями рынка. Если срабатывает разворот, функция закрытия позиций противоположного направления последовательно закрывает все конфликтующие сделки перед открытием новой. Перед началом торговли проверяются ограничения объема по символу и требования к уровням стоп-лосса. Эта проверка обеспечивает округление размера лота до разрешенного брокером шага и его соответствие минимальному и максимальному допустимым значениям.
//+------------------------------------------------------------------+ //| 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)); }
Функция OnTick() является основным циклом советника и выполняется при каждом обновлении цены, обеспечивая непрерывный анализ рынка и принятие решений. Сначала получается текущий тик, а массив скользящих тиков сдвигается для добавления новой точки данных и удаления самой старой. После обновления отображения на графике применяется ограничение частоты запросов, предотвращающее перегрузку сервера Flask чрезмерным числом вызовов. Когда ограничение частоты запросов позволяет, окно цен обновляется по истории тиков и непосредственно на этих данных рассчитывается простое значение RSI. Затем массив цен и RSI упаковываются в JSON-объект и отправляются на сервер HTTP-запросом POST.
После получения ответа извлекаются значения сигнала, режима, уверенности и энтропии. Система логирования активирует тихий режим, когда уже имеется позиция и сигнал не меняется. Благодаря этому журнал советника не засоряется в спокойные периоды. Перед обработкой любой сделки проверяется, что уверенность не ниже минимального порога и что после последнего исполнения прошел достаточный период ожидания. Только после прохождения этих проверок сигнал передается логике управления позициями. Сопутствующая функция RSI вычисляет классический 14-периодный индекс относительной силы по последним ценам из скользящего окна. Этот индекс предоставляет контекст моментума, дополняющий признаки энтропии, вычисляемые на стороне сервера.
//+------------------------------------------------------------------+ //| 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; }
Здесь создается канал связи между MetaTrader 5 и Python-сервером через аккуратно сформированный JSON-объект. Функция build начинает формирование с открывающей фигурной скобки и ключа массива prices, затем перебирает каждое значение скользящего окна цен. Каждая цена преобразуется в строку с точностью, соответствующей символу, между элементами добавляются запятые, а после последнего значения завершающая запятая не ставится. Затем массив закрывается, добавляется вычисленное значение RSI, после чего объект завершается закрывающей фигурной скобкой. Этот корректно структурированный JSON-объект в точности соответствует формату, ожидаемому эндпоинтом Flask. Функция отправки запроса преобразует JSON-строку в массив символов, подходящий для HTTP-передачи, и устанавливает соответствующий заголовок Content-Type.
После поступления ответа он разбирается набором легковесных утилит извлечения данных, позволяющих избежать накладных расходов полноценной JSON-библиотеки. Основная функция разбора передает работу специализированным вспомогательным функциям, которые находят ключи в тексте ответа и извлекают соответствующие значения. Для строковых значений, таких как торговый сигнал и режим волатильности, выполняется поиск имени ключа с последующим двоеточием, пропускаются пробельные символы и извлекается все между открывающей и закрывающей кавычками. Извлечение числовых значений выполняется аналогично, но символы считываются до обнаружения разделителя, например запятой или закрывающей фигурной скобки.
//+------------------------------------------------------------------+ //| 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); } } }
Функция ProcessSignal служит центральным узлом принятия решений, оценивая каждую поступающую торговую рекомендацию с учетом текущего состояния портфеля. Сначала обрабатываются сигналы HOLD: если сделка уже открыта, управление передается процедуре сопровождения позиции. При поступлении направленного сигнала он преобразуется в соответствующий тип ордера, после чего выполняется последовательность логических проверок. Первая проверка определяет, разрешено ли открывать новую позицию: проверяются максимальный лимит позиций и наличие сделок противоположного направления. Если достигнут максимальный лимит позиций и разворот невозможен, функция завершает работу, но продолжает обновлять стоп-лоссы существующих позиций. Вторая контрольная проверка определяет, открыта ли уже сделка в том же направлении.
Третья и наиболее важная контрольная проверка обрабатывает сценарии разворота, при которых сигнал конфликтует с существующей позицией. Когда разрешен разворот и показатель уверенности превышает повышенный порог 60%, последовательно закрываются все сделки противоположного направления. Затем делается короткая пауза, чтобы брокер обработал закрытие. Затем сразу открывается новая позиция в новом направлении с соответствующей меткой комментария. Если показатель уверенности не достигает порога для разворота, сохраняется текущая позиция, а стоп-лоссы продолжают корректироваться в соответствии с текущим режимом.
Функции исполнения сделок и текущего сопровождения преобразуют логику с учетом рисков в конкретные действия у брокера. При открытии сделки сначала получаются ограничения объема по символу и рассчитывается адаптивный размер лота: базовый объем умножается на коэффициенты, зависящие от режима. При экстремальной волатильности рыночная экспозиция снижается до 30% от обычной, а при низкой волатильности ее можно увеличить на 30%. Расстояния до стоп-лосса и тейк-профита аналогично умножаются на полученный от сервера коэффициент волатильности, а затем нормируются до точности символа. Затем проверяется, что предложенные стоп-лоссы соответствуют минимально допустимому расстоянию, заданному брокером. После этого ордер отправляется с возможностью до трех повторных попыток на случай реквот или кратковременного отсутствия ценовых данных. Процедура сопровождения непрерывно работает с открытыми позициями, определяя существенное изменение режима волатильности по сравнению с предыдущим состоянием. При таком изменении пересчитываются подходящие расстояния до стоп-лосса и модифицируется позиция. Важное ограничение: стоп-лосс никогда не расширяется против прибыльной сделки и не размещается опасно близко к текущей рыночной цене.
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Функция UpdateDisplay обеспечивает визуальную обратную связь в реальном времени непосредственно на графике, устраняя необходимость постоянно следить за журналом советника. Сначала обновляется число позиций для обеспечения точности, затем формируется многострочная строка, структурно представляющая все важные показатели системы на панели. На панели отображаются общее количество тиков с момента запуска советника, текущий торговый сигнал с добавленным счетчиком повторений и определенный режим волатильности. Также отображаются текущее значение энтропии, показатель уверенности модели и текущее число позиций относительно максимально допустимого. При наличии активной сделки добавляется отдельный раздел с типом позиции и плавающей прибылью. Если после недавнего исполнения действует период ожидания, рассчитывается и отображается число секунд до момента, когда следующая сделка станет допустимой.
Инференс в реальном времени
Ниже показано, что сервер Flask успешно инициализирован, работает на localhost:5000 и обменивается сигналами с MetaTrader 5; запуск выполняется из JupyterLab. 

Заключение
В ходе проекта создана полноценная адаптивная торговая система, объединяющая количественный анализ рынка и автоматическое исполнение сделок. На основе математического аппарата энтропии Шеннона, примененного к тиковым ценовым данным, создан пайплайн создания признаков, извлекающий значимые сигналы из рыночной неупорядоченности. Нейронная сеть, обученная на признаках, полученных из энтропии, учится прогнозировать вероятность направления движения. Одновременно детектор режима волатильности непрерывно классифицирует рыночные условия по четырем различным состояниям. Сервер Flask служит аналитическим слоем: он обрабатывает поступающие окна цен и возвращает не только сигналы Buy или Sell, но и показатели уверенности и адаптивные множители риска. На стороне MetaTrader разработан надежный советник, управляющий всем жизненным циклом сделки: от сбора тиков и сопровождения позиции до окончательного выхода.
Изучив этот материал, вы получите в свое распоряжение готовый к развертыванию и расширению фреймворк для алгоритмической торговли с учетом волатильности. Теперь понятно, как рассчитывать рыночную энтропию в реальном времени, не дожидаясь закрытия свечей. Это дает преимущество при определении смены режимов по мере их возникновения. Создана адаптивная система управления рисками, корректирующая размер позиции и расстояние до стоп-лосса с учетом текущих рыночных условий вместо фиксированных параметров, не работающих при всплесках волатильности. Полный пайплайн обмена данными между MQL5 и Python демонстрирует использование сильных сторон обеих платформ, сочетая надежное исполнение MetaTrader с экосистемой машинного обучения Python. Особенно важно, что создана модульная архитектура, которую можно расширять дополнительными функциями, альтернативными моделями или другими символами без необходимости начинать работу с нуля.
Ниже приведено краткое описание содержимого zip-архива "Arc Files":
| Имя файла | Описание файла |
|---|---|
| GettingHistData.py | Подключается к MetaTrader 5 и загружает исторические ценовые данные XAUUSD на таймфрейме H1. |
| Features.py | Реализует расчет энтропии Шеннона, извлечение показателей волатильности, измерение силы тренда и класс детектора режима волатильности, классифицирующий рыночные условия по четырем отдельным состояниям волатильности. |
| Model.py | Определяет нейронную сеть PyTorch с пакетной нормализацией и слоями dropout, преобразующую восемь признаков, полученных из энтропии, в прогноз вероятности направления будущего движения цены. |
| Train.py | Загружает исторические данные из CSV, создает признаки с помощью скользящего окна, обучает нейронную сеть с мониторингом валидации и сохраняет обученную модель и масштабировщик признаков для развертывания. |
| Server.py | Запускает веб-сервер Flask, получающий тиковые ценовые данные от советника, вычисляющий признаки энтропии, выполняющий инференс модели, определяющий текущий режим волатильности и возвращающий адаптивный сигнал с показателем уверенности. |
| Entropy.ipynb | Блокнот Jupyter для простого сценария выполнения. |
| Real-Time Entropy.mq5 | Советник MetaTrader 5, который собирает тиковые данные и взаимодействует с сервером Flask. |
Проект поддерживается в Algo Forge.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22220
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Моделирование рынка: Position View (XVI)
Возможности Мастера MQL5, которые вам нужно знать (Часть 93): Использование суффиксного автомата и автокодировщика в пользовательском классе управления капиталом
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Окончание)
От начального до среднего уровня: Классы (III)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
НУ И ГДЕ ПРИМЕР РЕАЛЬНОЙТОРГОВЛИ, ХОТЯ БЫ НА ДЕМО?
Предлагаю такой мусор тут не публиковать. форум и так забит статейками от ИИ.