Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5
Введение
Тестер стратегий MetaTrader 5 — мощный инструмент для исторического тестирования стратегий. Однако его штатный механизм формирования отчётов создаёт узкое место при строгом сравнительном анализе. При переборе параметров нескольких трендовых фильтров, таких как SMA, EMA, WMA, KAMA, DEMA, TEMA, ZLEMA, VIDYA или SAMA, Тестер формирует отдельные строки результатов, которые не сохраняются после завершения сеанса. Они доступны только на графике оптимизации и в таблице результатов. При этом Тестер не умеет встроенными средствами объединять результаты по различным типам фильтров, таймфреймам и этапам тестирования в один сводный CSV-файл.
На практике это ограничение проявляется в трёх основных проблемах:
- Нет единого файла результатов для разных фильтров: каждый проход оптимизации формирует собственный набор данных. Для сравнения SMA, EMA и KAMA в одинаковых рыночных условиях приходится вручную извлекать и объединять результаты.
- Пользовательские метрики не сохраняются автоматически: показатели, которые Тестер не рассчитывает средствами самого Тестера, теряются после завершения сеанса тестирования. К ним относятся коэффициент Сортино, средняя продолжительность сделки, а также показатели качества сигналов — запаздывание и частота ложных переворотов (whipsaws).
- Нет удобного программного интерфейса для последующей обработки: CSV-файл можно напрямую загрузить в Python, R или любую электронную таблицу, тогда как штатный XML-отчёт Тестера не загружается в них напрямую.
В статье предлагается архитектурное решение: подключаемый файл, пригодный для повторного использования MQL5 CSVExportEngine.mqh, который извлекает пользовательские метрики, формирует строку CSV и безопасно добавляет её в сводный сводный файл при параллельной многоядерной работе. Сопутствующий демонстрационный советник MultiFilter_Pullback_Demo_EA.mq5 показывает применение системы на примере реалистичной мультифильтровой стратегии трендового отката.
Раздел 1. Недостающие пользовательские метрики: коэффициент Сортино, продолжительность сделки и качество сигналов
Функция TesterStatistics() предоставляет стандартные показатели эффективности: чистую прибыль, профит-фактор, максимальную просадку, коэффициент Шарпа и количество сделок. Этого достаточно для оценки одной стратегии в отдельности, но недостаточно для строгого сравнения различных архитектур фильтров. Особое значение имеют три категории пользовательских метрик.
Коэффициент Сортино
В знаменателе коэффициента Шарпа положительная и отрицательная волатильность учитываются одинаково. Для трендовых стратегий, рассчитанных на получение крупных асимметричных прибылей, это теоретически не вполне корректно. Коэффициент Сортино учитывает только отрицательную доходность — убыточные сделки, — поэтому лучше подходит для сравнения трендовых систем.
В данной системе используется следующая формула:
Коэффициент Сортино = Средняя прибыль на сделку / Downside Deviation (нисходящее отклонение)
где:
Downside Deviation (нисходящее отклонение) = sqrt(сумма(результат_убыточной_сделки²) / количество_убыточных_сделок)
Для расчёта код перебирает историю сделок с помощью HistoryDealsTotal() и HistoryDealGetTicket(), выделяет убыточные сделки, суммирует квадраты убытков и вычисляет среднеквадратическое (RMS) значение. Поскольку расчёт выполняется по исходной истории сделок, получить его через TesterStatistics() нельзя — метрику необходимо вычислять самостоятельно.
Средняя длительность удержания позиции
Средняя длительность удержания позиции показывает среднее время нахождения позиции в рынке. Это важный структурный показатель для сравнения фильтров: быстро реагирующий фильтр, например EMA с коротким периодом, может формировать более частые сигналы с меньшей средней продолжительностью, тогда как медленный адаптивный фильтр способен удерживать позиции значительно дольше. Для расчёта фреймворк сопоставляет каждую закрывающую сделку DEAL_ENTRY_OUT с соответствующей открывающей сделкой DEAL_ENTRY_IN по идентификатору DEAL_POSITION_ID, вычисляет разницу во времени в секундах и накапливает общую сумму. Для удобства результат выводится в часах.
Метрики качества сигналов
Метрики качества сигналов характеризуют поведение самой линии фильтра независимо от прибыльности. В цикле OnTick() советника отслеживаются четыре таких показателя:
- Среднее запаздывание на разворотах (в барах): при изменении направления наклона фильтра система определяет, сколько баров прошло с последнего ценового экстремума. Чем выше значение, тем медленнее фильтр подтверждает смену направления.
- Ложные перевороты (whipsaws): если изменение наклона отменяется в течение трёх баров, оно считается ложным переворотом (whipsaw) и увеличивает соответствующий счётчик. Этот показатель отражает чувствительность фильтра к рыночному шуму.
- Среднее расстояние от цены до линии (в пунктах): среднее абсолютное расстояние между ценой закрытия и значением фильтра по всем барам. Чем больше среднее расстояние, тем менее отзывчив фильтр к движению цены.
- Частота пересечений (%): доля баров, на которых цена закрытия пересекает линию фильтра. Очень высокая частота указывает на чрезмерную чувствительность, а слишком низкая — на избыточное запаздывание.
Эти метрики отслеживаются функцией TrackSignalQualityMetrics(), которая вызывается один раз на бар внутри OnTick(), а по завершении тестового прогона передаются механизму экспорта.
Раздел 2. Проблема параллельного доступа: конфликт из-за блокировки файла
Это центральная техническая проблема системы, и её необходимо понять до рассмотрения решения.
Когда Тестер стратегий MetaTrader 5 выполняет оптимизацию с включённым параметром «Использовать многоядерную обработку», отдельные комбинации параметров распределяются между несколькими параллельно работающими агентами. Каждый агент запускает независимый экземпляр советника с определённым набором параметров. По завершении каждого прогона для этого экземпляра вызывается OnDeinit().
Если оптимизация охватывает несколько типов фильтров или диапазонов параметров, несколько агентов могут почти одновременно дойти до вызова OnDeinit(). Когда они пытаются открыть один и тот же CSV-файл для записи, происходит следующее:
- Агент A вызывает FileOpen() для файла MultiFilter_InSample_EURUSD_H1.csv. Операционная система предоставляет ему блокировку на запись, и дескриптор файла становится действительным.
- Через несколько микросекунд агент B вызывает FileOpen() для того же файла. Поскольку агент A удерживает блокировку на запись, агент B получает INVALID_HANDLE. Функция GetLastError() обычно возвращает код ERR_CANNOT_OPEN_FILE (5004).
- Вызов экспорта у агента B завершается без явного сообщения об ошибке. Вся строка результатов, соответствующая полному проходу оптимизации, безвозвратно теряется.

Рис. 1. Конфликт параллельного доступа при многоядерной оптимизации. Агент A блокирует общий CSV-файл, поэтому одновременная попытка записи агента B завершается неудачей и приводит к потере данных.
Масштаб потери данных напрямую зависит от степени параллелизма. На 16-ядерном компьютере могут одновременно работать 16 агентов, перебирающих большую сетку параметров. Поэтому необходим механизм разрешения конфликтов доступа, который не позволит системе незаметно отбрасывать результаты.
Стандартный способ разрешения конфликтов доступа к заблокированному ресурсу в системном программировании — функция Sleep() — в среде Тестера стратегий MetaTrader 5 отключена. Во время тестирования и оптимизации Sleep() фактически ничего не делает. Поэтому код, который использует Sleep(ms) для паузы перед повторной попыткой открытия файла, работать должным образом не будет. Требуется другой подход.
Раздел 3. Решение со спин-блокировкой: итерационные повторные попытки без потери данных
Для решения используется итерационная спин-блокировка. В коротком цикле for вызов FileOpen() непрерывно повторяется без пауз. Цикл продолжается до получения действительного дескриптора либо до достижения максимального количества итераций.
Основной механизм:
int file_handle = INVALID_HANDLE; int max_spin_attempts = 50000; for(int attempt = 0; attempt < max_spin_attempts; attempt++) { file_handle = FileOpen(file_name, FILE_CSV | FILE_WRITE | FILE_READ | FILE_ANSI | FILE_COMMON, ','); if(file_handle != INVALID_HANDLE) break; // Lock acquired. Exit the spin loop. }
Почему это работает в среде Тестера:Каждая итерация цикла выполняется чрезвычайно быстро — за несколько тактов процессора, — поэтому повторные попытки следуют практически непрерывно. Агент A удерживает блокировку и записывает строку данных всего за несколько миллисекунд. В это время агент B быстро повторяет попытки в коротком цикле. Как только агент A закрывает дескриптор файла, агент B получает освободившуюся блокировку уже на следующей итерации.
Ограничение в 50 000 итераций: Это консервативный верхний предел. Обычная файловая операция занимает лишь несколько миллисекунд. Поскольку процессор выполняет миллионы итераций в секунду, 50 000 повторов дают окно повторных попыток в несколько секунд. Это значительно больше, чем требуется при обычном конфликте доступа, и не создаёт заметных накладных расходов в процессе, который и без того длится несколько минут.
Если все попытки завершились неудачей: Если все 50 000 попыток завершились неудачей, система записывает критическую ошибку в журнал Experts. Сообщение содержит код GetLastError(). Такая ситуация указывает уже не на обычную конкуренцию, а на серьёзную системную проблему. Благодаря записи в журнал ошибка не остаётся незамеченной.

Рис. 2. Блок-схема решения с циклом повторных попыток. Агент B выполняет до 50 000 быстрых проверок, ожидая освобождения общего файла агентом A. Благодаря этому данные оптимизации безопасно записываются, а не отбрасываются.
Раздел 4. Реализация подключаемого файла CSVExportEngine.mqh
Определение этапа тестирования, формирование имени файла, спин-блокировка и запись CSV реализованы в одном подключаемом файле. Такая архитектура полностью отделяет механизм экспорта от торговой стратегии советника. Чтобы добавить эту возможность в любой новый советник, достаточно директивы #include, заполненной структуры и одного вызова функции.
Структура SExportMetrics
Вместо передачи функции экспорта более двадцати скалярных параметров специальная структура объединяет все метрики в один именованный аргумент. Благодаря этому сигнатура функции остаётся компактной, а вызывающий код становится самодокументируемым.
//+------------------------------------------------------------------+ //| Data structure bundling all metrics for a single export row. | //| The EA populates this struct, then passes it to ExportToCSV(). | //+------------------------------------------------------------------+ struct SExportMetrics { //--- Identification Fields string filterName; // e.g., "EMA", "SAMA" int filterPeriod; // Primary lookback period double paramLearningRate; // SAMA-specific: learning rate (0.0 for non-SAMA) double paramSlMult; // SL ATR multiplier used in the test double paramTpMult; // TP ATR multiplier used in the test //--- Standard Performance Metrics (sourced from TesterStatistics) double netProfit; double profitFactor; double expectedPayoff; double winRate; // Expressed as a percentage (0-100) double maxDrawdown; // Max relative equity drawdown (%) double recoveryFactor; double sharpeRatio; double sortinoRatio; // Custom-calculated; not from TesterStatistics int totalTrades; double avgProfitPerTrade; //--- Time and Stability Metrics double avgTradeDurationHrs; // Average holding time in hours double marketShareTime; // % of test duration spent in a position int positiveYears; // Positive-years count //--- Signal Quality Metrics (sourced from TrackSignalQualityMetrics) double avgLagBars; // Average filter lag on direction changes int falseFlipWhipsaws; // Count of slope flips that reversed within 3 bars double avgDistPoints; // Average price-to-filter distance in points double intersectFreqPct; // % of bars where price crossed the filter line };
Автоматическое определение этапа тестирования
Запросы MQLInfoInteger() позволяют различать три отдельных режима тестирования. Логика записана явно, чтобы исключить неоднозначность:
- Если MQL_OPTIMIZATION возвращает false, советник выполняет одиночный бэктест, обозначаемый как "Baseline".
- Если MQL_OPTIMIZATION возвращает true и MQL_FORWARD также возвращает true, советник выполняет проход форвардной проверки с меткой "OutSample".
- Если MQL_OPTIMIZATION возвращает true, а MQL_FORWARD — false, советник выполняет стандартный внутривыборочный проход оптимизации с меткой "InSample".
Этот префикс добавляется как к имени файла, так и к значению столбца Test_Phase в строке данных. В результате один сводный файл может содержать чётко помеченные строки для всех трёх режимов и использоваться для прямого сравнения.
//+------------------------------------------------------------------+ //| Determines the active test phase and returns the row prefix. | //| | //| Returns: | //| "Baseline" - Single backtest (MQL_OPTIMIZATION is false) | //| "InSample" - Optimization pass (MQL_FORWARD is false) | //| "OutSample" - Forward validation pass (MQL_FORWARD is true) | //+------------------------------------------------------------------+ string GetTestPhasePrefix() { if(!MQLInfoInteger(MQL_OPTIMIZATION)) return("Baseline"); // Standard single-pass backtest if(MQLInfoInteger(MQL_FORWARD)) return("OutSample"); // Optimization forward-validation segment return("InSample"); // Standard optimization in-sample segment }
Переключение между сводным файлом и отдельными файлами фильтров
Логический входной параметр InpConsolidatedMode управляет формированием имён файлов. При значении true все типы фильтров записывают данные в один сводный файл. При false для каждого фильтра создаётся отдельный файл. В обоих режимах используется спин-блокировка, поскольку несколько агентов могут одновременно обращаться даже к файлу одного фильтра, если проверяют разные значения периода.
Таблица 1. Структура файла в сводном режиме. При InpConsolidatedMode = true все типы фильтров записывают данные в один сводный файл.
| // Сводный режим: один сводный файл для всех фильтров |
|---|
| // "MultiFilter_InSample_EURUSD_H1.csv" |
Таблица 2. Структура файлов в режиме отдельных фильтров. При InpConsolidatedMode = false каждый активный тип фильтра создаёт собственный файл.
| // Отдельные фильтры: собственный файл для каждого активного фильтра |
|---|
| // "MultiFilter_InSample_EURUSD_H1_EMA.csv" |
Открытие файла со спин-блокировкой
Вспомогательная функция SpinLockFileOpen() изолирует механизм повторных попыток, благодаря чему основная функция экспорта остаётся читаемой, а механизм спин-блокировки можно тестировать отдельно.
//+------------------------------------------------------------------+ //| Attempts to open a file using an iteration-based spin-lock. | //| This is the core concurrency-safety mechanism. Sleep() is a | //| no-op in the Strategy Tester, so a CPU-spin retry loop is used | //| instead to wait out a competing agent's write lock. | //| | //| file_name : Full filename to open | //| max_attempts : Upper bound on retry iterations (default: 50000) | //| | //| Returns a valid file handle, or INVALID_HANDLE if all retries | //| are exhausted (indicating a severe system-level lock failure). | //+------------------------------------------------------------------+ int SpinLockFileOpen(const string file_name, const int max_attempts = 50000) { int file_handle = INVALID_HANDLE; for(int attempt = 0; attempt < max_attempts; attempt++) { //--- Attempt to acquire the write lock on each iteration. file_handle = FileOpen(file_name, FILE_CSV | FILE_WRITE | FILE_READ | FILE_ANSI | FILE_COMMON, ','); if(file_handle != INVALID_HANDLE) break; // Lock successfully acquired; exit the spin loop. } return(file_handle); }
Основная функция экспорта
ExportToCSV() — единственная публичная функция, вызываемая советником. Она управляет определением этапа тестирования, формированием имени файла, захватом блокировки файла, условной записью заголовка только для нового файла и добавлением строки данных.
//+------------------------------------------------------------------+ //| Primary public function. Call from OnDeinit() inside MQL_TESTER. | //| | //| metrics : Struct populated by the EA with all computed metrics | //| tf_str : Timeframe string derived from EnumToString(_Period) | //+------------------------------------------------------------------+ void ExportToCSV(const SExportMetrics &metrics, const string tf_str) { string phase_prefix = GetTestPhasePrefix(); string file_name = BuildFileName(phase_prefix, metrics.filterName, tf_str, InpConsolidatedMode); int file_handle = SpinLockFileOpen(file_name); if(file_handle != INVALID_HANDLE) { //--- Move to end of file for appending. FileSeek(file_handle, 0, SEEK_END); //--- Write the header only if the file was just created (size is zero). if(FileSize(file_handle) == 0) { FileWrite(file_handle, BuildCSVHeader()); } FileWrite(file_handle, BuildCSVDataRow(metrics, phase_prefix, tf_str)); FileClose(file_handle); } else { //--- Log a critical error if all spin-lock retries were exhausted. PrintFormat( "CRITICAL ERROR [CSVExportEngine]: Failed to acquire file lock for '%s' " "after %d spin-lock retries. Data row was NOT written. Last error: %d", file_name, 50000, GetLastError()); } }
Раздел 5. Демонстрационный советник MultiFilter_Pullback_Demo_EA.mq5
Демонстрационный советник реализует стратегию отката по тренду, которая структурно не зависит от конкретного фильтра. Основная логика проверяет всего два условия: подтверждает направление тренда по наклону фильтра, а затем ожидает отката цены к его линии. Процесс одинаков для SMA, EMA, KAMA и любых других поддерживаемых фильтров. Поскольку сама стратегия не меняется, она служит удобной основой для контролируемого сравнительного исследования.
Сбор метрик в OnDeinit()
Функция OnDeinit() запускает сбор и экспорт метрик только при работе в Тестере, когда MQLInfoInteger(MQL_TESTER) возвращает true. Она вызывает локальную вспомогательную функцию CollectAndExportMetrics(), которая выполняет следующие действия:
- Запрашивает все стандартные метрики через TesterStatistics().
- Перебирает историю сделок с помощью HistorySelect() и HistoryDealsTotal(), чтобы вычислить коэффициент Сортино, среднюю продолжительность сделки, долю времени в рынке и количество прибыльных лет.
- Считывает накопленные показатели качества сигналов, заполненные функцией TrackSignalQualityMetrics() во время прогона.
- Заполняет структуру SExportMetrics всеми рассчитанными значениями.
- Вызывает ExportToCSV() из подключаемого файла и передаёт ей заполненную структуру.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Only attempt CSV export when running inside the Strategy Tester if(MQLInfoInteger(MQL_TESTER)) { CollectAndExportMetrics(); } //--- Release indicator handles if(g_filter_handle != INVALID_HANDLE) IndicatorRelease(g_filter_handle); if(g_atr_handle != INVALID_HANDLE) IndicatorRelease(g_atr_handle); }
Вызов механизма экспорта
Вся процедура экспорта сводится к одному вызову ExportToCSV(). Эта функция полностью инкапсулирует экспорт, самостоятельно управляя работой с файлами, правилами именования и параллельным доступом.
Важное требование к настройке:
Перед запуском советника загрузите файл SAMA_NLMS.mq5, приложенный в конце статьи, и скопируйте его в каталог индикаторов терминала:
C:\Users\USERNAME\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Indicators
Советник использует этот пользовательский индикатор, поэтому без него компиляция завершится ошибкой.
Раздел 6. Практический порядок работы: сравнительный анализ в электронной таблице
После завершения оптимизации результирующие файлы находятся в каталоге MQL5\Files\Common, который задаётся флагом FILE_COMMON в FileOpen(). Типичный путь в Windows:
C:\Users\<UserName>\AppData\Roaming\MetaQuotes\Terminal\Common\Files\ Сводный файл, например MultiFilter_InSample_AUDJPY_H1.csv, содержит по одной строке для каждого завершённого прохода агента оптимизации. Каждая строка содержит поля Test_Phase, Indicator_Name, Filter_Period и полным набором соответствующих метрик.

Рис. 3. Пример данных из MultiFilter_InSample_AUDJPY_H1.csv, открытых в MS Excel.
Практический анализ полученных данных можно организовать следующим образом:
- Шаг 1 — фильтрация по этапу: используйте столбец Test_Phase, чтобы отделить внутривыборочные строки от вневыборочных. Метрики, которые выглядят сильными во внутривыборочном сегменте (InSample), но резко ухудшаются во вневыборочном сегменте (OutSample), следует проверить на переоптимизацию.
- Шаг 2 — ранжирование по Сортино: отсортируйте данные по убыванию Sortino_Ratio. Такое ранжирование с учётом риска лучше подходит для трендовых стратегий, чем использование одного коэффициента Шарпа.
- Шаг 3 — сопоставление с качеством сигналов: фильтры с высоким коэффициентом Сортино, но большим количеством False_Flips_Whipsaws или высоким Avg_Lag_On_Turn_Bars могут показывать хорошие внутривыборочные результаты из-за особенностей конкретных данных, а не благодаря устойчивой структуре.
- Шаг 4 — оценка стабильности: столбец Stability_Over_Years показывает, сколько календарных лет тестового периода завершились с положительным финансовым результатом. Если за пятилетний период стратегия была прибыльной лишь один-два года, её эффективность может быть ограничена отдельным временным участком.
- Шаг 5 — автоматизация последующего анализа: поскольку результат представляет собой чистый CSV-файл с единообразными заголовками столбцов, его можно напрямую загрузить в DataFrame библиотеки pandas или в data.frame языка R для визуализации, корреляционного анализа и дополнительной фильтрации без ручной обработки данных.
Заключение
Эта система устраняет существенный пробел в процессе оптимизации MQL5. В настоящее время MQL5 не предоставляет встроенного способа автоматически объединять пользовательские показатели эффективности при многоядерной оптимизации. Предложенное решение собирает данные в один переносимый сводный файл с единообразной структурой.
Функциональность CSVExportEngine.mqh основана на трёх ключевых возможностях:
- Во-первых, запросы MQLInfoInteger() определяют активный этап тестирования во время выполнения. Это полностью устраняет необходимость вручную управлять файлами для внутривыборочных, вневыборочных и режима Baseline.
- Во-вторых, конфликт параллельного доступа к файлу разрешается с помощью итерационной спин-блокировки. Это особенно важно в среде оптимизации, где штатная функция Sleep() отключена.
- Наконец, простая структура и один вызов функции полностью отделяют логику экспорта от торговой стратегии. Благодаря этому подключаемый файл можно без изменений использовать в любом советнике.
Система намеренно не предписывает, как интерпретировать экспортированные данные. Выбор относительной значимости коэффициентов Сортино и Шарпа требует аналитического решения. Допустимая частота ложных разворотов и минимальные требования к стабильности полностью зависят от конкретного инструмента, таймфрейма и риск-профиля. Задача системы — обеспечить принятие таких решений на основе полного и правильно структурированного набора данных, а не неполных результатов Тестера или вручную собранных метрик.
Программы, используемые в статье:
| # | Название | Тип | Описание |
|---|---|---|---|
| 1 | CSVExportEngine.mqh | Подключаемый файл | Основной механизм экспорта. Выполняет определение этапа тестирования, формирование имени файла, захват блокировки файла с помощью спин-блокировки и запись строк CSV. Любой советник может подключить его с помощью одной директивы #include и одного вызова функции. |
| 2 | MultiFilter_Pullback_Demo_EA.mq5 | Демонстрационный советник | Стратегия отката по тренду с поддержкой девяти вариантов фильтров: SMA, EMA, WMA, KAMA, DEMA, TEMA, ZLEMA, VIDYA и SAMA. Она отслеживает метрики качества сигналов на каждом баре, рассчитывает коэффициент Сортино и продолжительность сделок по истории и вызывает ExportToCSV() в конце каждого тестового прогона. |
| 3 | SAMA_NLMS.mq5 | Пользовательский индикатор | Самоадаптивная скользящая средняя на основе алгоритма NLMS, вызываемая через iCustom() в режиме FILTER_SAMA. Индикатор включён для демонстрации интеграции внешних ресурсов в советник при полной совместимости с механизмом параллельного экспорта данных. Примечание: файл SAMA_NLMS.mq5 необходимо поместить в каталог MQL5\Indicators терминала; без него компиляция завершится ошибкой. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22874
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Кодирование свечных моделей (Часть 2): Моделирование движения цены в виде упорядоченной последовательности
Возможности Мастера MQL5, которые вам нужно знать (Часть 79): Использование осцилляторов Gator и A/D в обучении с учителем
Можно ли запустить DOOM в MetaTrader 5: DLL, рендеринг и ввод через MQL5?
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования