English
preview
Интеграция MQL5 с пакетами обработки данных (Часть 9): Адаптивная оценка волатильности на основе энтропии

Интеграция MQL5 с пакетами обработки данных (Часть 9): Адаптивная оценка волатильности на основе энтропии

MetaTrader 5Примеры |
82 1
Hlomohang John Borotho
Hlomohang John Borotho

Содержание

  1. Введение
  2. Обзор модели и системы
  3. Приступаем
  4. Объединяем все в MQL5
  5. Инференс в реальном времени
  6. Заключение



Введение

Трейдеры постоянно сталкиваются с проблемами на непредсказуемых финансовых рынках. Волатильность может резко измениться в течение одной сессии, превратив устойчивый тренд в хаотичное разнонаправленное ценовое движение. Традиционные технические индикаторы часто запаздывают при быстрых изменениях режима, подвергая трейдеров риску внезапных разворотов, чрезмерных просадок и упущенных возможностей. Фиксированные уровни стоп-лосса и тейк-профита, хорошо работающие в спокойных условиях, становятся опасно недостаточными при всплесках волатильности. Фиксированный размер позиции при этом не учитывает постоянно меняющийся уровень риска. В результате возникает порочный круг преждевременных срабатываний стоп-лосса, чрезмерных убытков и нестабильных результатов, подрывающий капитал и уверенность.

Этот проект решает указанные задачи с помощью расчета рыночной энтропии в реальном времени. Этот количественный подход непрерывно измеряет степень неупорядоченности и неопределенности в ценовых данных на уровне тиков. Применяя энтропию Шеннона к скользящим окнам последних цен, система мгновенно определяет переходы между режимами низкой, нормальной, высокой и экстремальной волатильности. Нейронная сеть, обученная на признаках, полученных из энтропии, прогнозирует вероятность направления движения. Модуль адаптивного управления рисками динамически регулирует размер позиции, ширину стоп-лосса и целевые уровни тейк-профита с учетом текущего состояния рынка. Пайплайн получает тики 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

Прикрепленные файлы |
Arc_Files.zip (15.87 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Alexey Volchanskiy
Alexey Volchanskiy | 29 авг. 2026 в 13:42

НУ И ГДЕ ПРИМЕР РЕАЛЬНОЙТОРГОВЛИ, ХОТЯ БЫ НА ДЕМО? 

Предлагаю такой мусор тут не публиковать. форум и так забит статейками от ИИ.

Моделирование рынка: Position View (XVI) Моделирование рынка: Position View (XVI)
В этой статье мы внесем необходимые изменения, чтобы индикатор позиции отображал финансовый результат. Таким образом, трейдер сможет получить представление о финансовом результате по открытой позиции. Кроме того, я расскажу вам о том, чего многие не знают, даже те, кто давно использует MQL5: как использовать статические переменные для совместного использования памяти и избежать объявления глобальной переменной в основном коде.
Возможности Мастера MQL5, которые вам нужно знать (Часть 93): Использование суффиксного автомата и автокодировщика в пользовательском классе управления капиталом Возможности Мастера MQL5, которые вам нужно знать (Часть 93): Использование суффиксного автомата и автокодировщика в пользовательском классе управления капиталом
В этой статье мы переходим к пользовательской реализации класса Мастера MQL5 для управления капиталом. Пользовательский класс CMoneySuffixAE создается путем объединения алгоритма суффиксного автомата и нейронной сети автокодировщика. Как и ранее, это решение можно тестировать с советниками, собранными Мастером MQL5 и настраиваемыми с различными сигналами входа и вариантами трейлинг-стопа.
Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Окончание) Нейросети в трейдинге: Двухуровневая адаптация торговой политики (Окончание)
Статья завершает адаптацию D2Skill к алгоритмической торговле и собирает полный учебный контур. Разобраны обучение Forecast, базового Актёра и Критиков, формирование Task и Step Skills и калибровка Utility на парных траекториях. Для каждого шага приведены настройки, кодовые точки интеграции и контрольные метрики. Итоговое OOS‑сравнение одной политики с памятью и без неё показывает влияние двухуровневой памяти на результат и риск.
От начального до среднего уровня: Классы (III) От начального до среднего уровня: Классы (III)
В этой статье мы рассмотрим, как лучше контролировать код при работе с объектно-ориентированным программированием. Хотя мы только начинаем изучать объектно-ориентированное программирование, то, что мы здесь рассмотрим, поможет вам понять различные аспекты. Это также поможет развеять сомнения, которые могут возникнуть позже.