Система самообучения с подкреплением для алгоритмической торговли на MQL5
Введение в мир саморазвивающихся торговых систем
Представьте себе торгового робота, который не просто слепо следует заранее прописанным правилам, а действительно учится на своих ошибках и успехах, подобно тому, как это делает человек-трейдер. Именно такую систему представляет обучение с подкреплением в контексте алгоритмической торговли. Это не просто еще один технический индикатор или набор сигналов — это фундаментально иной подход к построению торговых стратегий, где машина сама открывает для себя закономерности рынка через непрерывное взаимодействие с ним.
#property copyright "BrainRL Expert v2.0" #property version "2.00" #property strict #include <Trade\Trade.mqh> #include <Arrays\ArrayObj.mqh> // Входные параметры системы input int BrainsCount = 5; // Количество мозгов в коллективе input double LearningRate = 0.10; // Скорость обучения input double ExplorationRate = 0.30; // Начальный уровень исследования input double Gamma = 0.90; // Фактор дисконтирования input double MemoryDecay = 0.99; // Затухание памяти input bool SaveBrains = true; // Сохранять обученные мозги input bool LoadBrains = false; // Загружать сохраненные мозги input int SaveInterval = 500; // Интервал автосохранения (в барах)
В классических торговых системах программист или трейдер должен явно описать каждое правило: когда покупать, когда продавать, где ставить стопы. Это требует не только глубокого понимания рынка, но и предполагает, что мы можем формализовать все нюансы рыночного поведения в виде четких инструкций. Но что если рынок слишком сложен для такой формализации? Что если закономерности постоянно меняются, адаптируются, эволюционируют? Именно здесь обучение с подкреплением демонстрирует свою истинную силу.
Система, которую мы рассматриваем в данной статье, реализована на языке MQL5 для платформы MetaTrader 5 и представляет собой полноценную многоагентную архитектуру машинного обучения. Она способна самостоятельно торговать на финансовых рынках, непрерывно совершенствуя свои стратегии на основе полученного опыта.
Философия архитектуры: от нейронов к коллективному разуму
Архитектура представленной системы напоминает устройство человеческого мозга больше, чем типичный торговый советник. В её основе лежит идея иерархической организации знаний и коллективного принятия решений. На самом нижнем уровне располагаются нейроны памяти — элементарные единицы опыта, каждая из которых хранит информацию об одном конкретном состоянии рынка и действии, которое было предпринято в этом состоянии.
struct MarketStateBinary { int binaryCode[32]; double volatility; double momentum; double trend; double price; datetime timestamp; double outcome; int activations; };
Эти нейроны не существуют изолированно. Они объединяются в структуры более высокого порядка, которые я для простоты назвал "мозгами" — индивидуальными агентами, способными принимать торговые решения. Каждый такой мозг представляет собой независимую торговую личность со своими воспоминаниями, предпочтениями и стилем торговли.
class CMachineBrain : public CObject { private: CArrayObj m_neurons; Episode m_episodes[]; int m_currentGeneration; double m_brainComplexity; double m_overallIntelligence; double m_learningRate; double m_explorationRate; double m_gamma; double m_memoryDecay; int m_lastActivatedNeuron; MarketStateBinary m_currentMarketState; double m_totalReward; int m_totalTrades; int m_successfulTrades; string m_name; // Статистика балансировки классов int m_buySignals; int m_sellSignals; int m_buyTrades; int m_sellTrades; double m_buyReward; double m_sellReward;
Но и это еще не вершина иерархии. Несколько индивидуальных агентов-мозгов объединяются в коллективный разум — своеобразный совет директоров, где каждый участник имеет право голоса, но вес этого голоса зависит от его прошлых достижений.
class CCollectiveMind { private: CMachineBrain* m_brains[]; int m_brainsCount; string m_groupName; public: CCollectiveMind(string name, int brainsCount, double lr, double eps, double gamma) { m_groupName = name; m_brainsCount = brainsCount; ArrayResize(m_brains, brainsCount); for(int i = 0; i < brainsCount; i++) { string brainName = name + "_Brain" + IntegerToString(i); double brainLR = lr * (0.8 + (MathRand() % 1000) / 10000.0 * 0.4); double brainEps = eps * (0.8 + (MathRand() % 1000) / 10000.0 * 0.4); m_brains[i] = new CMachineBrain(brainName, brainLR, brainEps, gamma); } Print("Коллективный разум '", name, "' создан | Мозгов: ", brainsCount); }
Такая трехуровневая архитектура обеспечивает систему сразу несколькими важными свойствами. Во-первых, она позволяет хранить и эффективно использовать огромное количество опыта. Каждый нейрон памяти — это микроскопический фрагмент знания о рынке, но вместе они формируют богатую картину рыночной динамики. Во-вторых, использование множественных агентов обеспечивает диверсификацию решений и снижает риск катастрофических ошибок. Даже если один мозг делает неверный вывод, другие могут скорректировать итоговое решение. В-третьих, система становится более робастной к изменениям рыночных условий, так как различные агенты могут специализироваться на разных типах рыночных ситуаций.
Анатомия нейрона памяти: квантование рыночного опыта
Давайте детально рассмотрим, что представляет собой базовый строительный блок системы — нейрон памяти. Это не просто запись в базе данных, а полноценная структура, инкапсулирующая целостный срез рыночного опыта.
class CMemoryNeuron { public: MarketStateBinary marketState; double action; double reward; double qValue; double confidence; datetime birthTime; int activations; double successRate; double importance; int generation; CMemoryNeuron() { action = 0.5; reward = 0; qValue = 0; confidence = 0.5; birthTime = TimeCurrent(); activations = 0; successRate = 0.5; importance = 1.0; generation = 1; ArrayInitialize(marketState.binaryCode, 0); marketState.volatility = 0; marketState.momentum = 0; marketState.trend = 0; marketState.price = 0; marketState.timestamp = 0; marketState.outcome = 0; marketState.activations = 0; } void UpdateMetrics(double newReward) { activations++; reward = newReward; // Обновление Q-значения qValue = qValue + LearningRate * (newReward - qValue); // Обновление процента успешности if(newReward > 0) successRate = successRate * 0.9 + 0.1; else if(newReward < 0) successRate = successRate * 0.9; // Обновление важности importance = successRate * MathLog(activations + 1); confidence = (successRate + MathAbs(qValue)) / 2.0; } };
Каждый нейрон хранит бинарное представление состояния рынка, закодированное в виде тридцатидвухбитного массива. Почему именно бинарное представление? Потому что оно обеспечивает компактность хранения и быстроту сравнения состояний. Кодирование состояния происходит через квантование непрерывных рыночных характеристик.
Помимо самого состояния рынка, нейрон хранит действие, которое было предпринято в этом состоянии. Действие представлено числом от нуля до единицы, где значения ближе к нулю соответствуют продаже, значения ближе к единице — покупке, а промежуточные значения могут интерпретироваться как различная степень уверенности в направлении, или даже как решение воздержаться от сделки. Это непрерывное представление действий дает системе гораздо большую гибкость, чем жесткий выбор между покупкой и продажей.
Нейрон также отслеживает свою статистику активаций — сколько раз это конкретное состояние встречалось и использовалось для принятия решений. Процент успешных сделок, совершенных на основе данного нейрона, формирует показатель успешности. Важность нейрона вычисляется как комбинация его успешности и частоты использования — таким образом система выделяет те фрагменты опыта, которые не только работают хорошо, но и встречаются достаточно часто, чтобы быть практически значимыми.
Стационарные признаки: новый взгляд на представление рынка
В отличие от ранних версий системы, которые работали с ограниченным набором базовых характеристик, версия 2.0 использует расширенное признаковое пространство на основе стационарных индикаторов. Стационарность — ключевое понятие в статистике временных рядов, означающее, что статистические свойства процесса не изменяются со временем. Для торговых систем это критически важно, поскольку нестационарность ценовых рядов — одна из главных причин деградации производительности алгоритмов.
Система извлекает одиннадцать типов стационарных признаков из рыночных данных. Индекс относительной силы RSI измеряет перекупленность или перепроданность актива, нормализуя информацию о скорости и масштабе ценовых изменений в диапазон от нуля до ста. Индекс товарного канала CCI оценивает отклонение текущей цены от её статистического среднего, что позволяет идентифицировать циклические паттерны независимо от абсолютного уровня цен.
Стохастический осциллятор определяет положение текущей цены закрытия относительно диапазона цен за определенный период, предоставляя безразмерную метрику импульса. Схождение-расхождение скользящих средних MACD фокусируется на взаимодействии между трендом и импульсом, выявляя потенциальные точки разворота через анализ разности экспоненциальных скользящих средних.
Средний истинный диапазон ATR квантифицирует волатильность рынка способом, который инвариантен к направлению движения цены. Полосы Боллинджера нормализуют положение цены относительно её статистической изменчивости, создавая адаптивный канал, который расширяется и сужается в зависимости от рыночных условий.
Отношения между скользящими средними различных периодов кодируют информацию о силе и устойчивости тренда. Фракталы Билла Вильямса идентифицируют локальные экстремумы, которые могут сигнализировать о потенциальных разворотах. Индекс среднего направленного движения ADX измеряет силу тренда, абстрагируясь от его направления.
Процентный диапазон Вильямса, подобно стохастику, оценивает положение цены в недавнем диапазоне, но с инвертированной шкалой и чувствительностью к коротким периодам. Наконец, нормализованные ценовые изменения за различные временные окна улавливают краткосрочную и среднесрочную динамику без привязки к конкретным ценовым уровням.
void CalculateStationaryFeatures(const MqlRates &rates[], MarketStateBinary &state) { int size = ArraySize(rates); if(size < 50) return; double prices[]; ArrayResize(prices, size); for(int i = 0; i < size; i++) prices[i] = rates[i].close; // RSI (14 периодов) double rsi = CalculateRSI(prices, 14); state.binaryCode[0] = rsi > 70 ? 1 : 0; state.binaryCode[1] = rsi < 30 ? 1 : 0; state.binaryCode[2] = rsi > 50 ? 1 : 0; // CCI (20 периодов) double cci = CalculateCCI(rates, 20); state.binaryCode[3] = cci > 100 ? 1 : 0; state.binaryCode[4] = cci < -100 ? 1 : 0; state.binaryCode[5] = cci > 0 ? 1 : 0; // Stochastic (14, 3, 3) double stoch = CalculateStochastic(rates, 14, 3, 3); state.binaryCode[6] = stoch > 80 ? 1 : 0; state.binaryCode[7] = stoch < 20 ? 1 : 0; state.binaryCode[8] = stoch > 50 ? 1 : 0; // MACD (12, 26, 9) double macd, signal; CalculateMACD(prices, 12, 26, 9, macd, signal); state.binaryCode[9] = macd > signal ? 1 : 0; state.binaryCode[10] = MathAbs(macd - signal) > 0.0001 ? 1 : 0; state.binaryCode[11] = macd > 0 ? 1 : 0; // ATR (14 периодов) double atr = CalculateATR(rates, 14); double atrNorm = atr / rates[size-1].close; state.binaryCode[12] = atrNorm > 0.01 ? 1 : 0; state.binaryCode[13] = atrNorm > 0.02 ? 1 : 0; state.binaryCode[14] = atrNorm > 0.005 ? 1 : 0; // Bollinger Bands (20, 2) double upper, middle, lower; CalculateBollingerBands(prices, 20, 2, upper, middle, lower); double bbPos = (prices[size-1] - lower) / (upper - lower); state.binaryCode[15] = bbPos > 0.8 ? 1 : 0; state.binaryCode[16] = bbPos < 0.2 ? 1 : 0; state.binaryCode[17] = bbPos > 0.5 ? 1 : 0; // MA Ratios (20 vs 50) double ma20 = CalculateMA(prices, 20); double ma50 = CalculateMA(prices, 50); state.binaryCode[18] = ma20 > ma50 ? 1 : 0; state.binaryCode[19] = (ma20 - ma50) / ma50 > 0.01 ? 1 : 0; state.binaryCode[20] = prices[size-1] > ma20 ? 1 : 0; // Fractals (5 периодов) int fractal = DetectFractal(rates, 5); state.binaryCode[21] = fractal == 1 ? 1 : 0; state.binaryCode[22] = fractal == -1 ? 1 : 0; state.binaryCode[23] = fractal == 0 ? 1 : 0; // ADX (14 периодов) double adx = CalculateADX(rates, 14); state.binaryCode[24] = adx > 25 ? 1 : 0; state.binaryCode[25] = adx > 40 ? 1 : 0; state.binaryCode[26] = adx > 15 ? 1 : 0; // Williams %R (14 периодов) double willR = CalculateWilliamsR(rates, 14); state.binaryCode[27] = willR > -20 ? 1 : 0; state.binaryCode[28] = willR < -80 ? 1 : 0; state.binaryCode[29] = willR > -50 ? 1 : 0; // Price Changes (10 баров) double priceChange = (prices[size-1] - prices[size-11]) / prices[size-11]; state.binaryCode[30] = priceChange > 0.01 ? 1 : 0; state.binaryCode[31] = priceChange < -0.01 ? 1 : 0; // Дополнительные метрики state.volatility = atrNorm; state.momentum = priceChange; state.trend = (ma20 - ma50) / ma50; state.price = prices[size-1]; state.timestamp = rates[size-1].time; }
Каждый из этих индикаторов вносит свой уникальный взгляд на рыночную динамику, и вместе они создают многомерное представление состояния рынка, которое значительно богаче простого анализа ценовых изменений. Критически важно, что все эти признаки являются стационарными или близкими к стационарным — их статистические свойства относительно стабильны во времени, что делает обучение более эффективным и результаты более надежными.
Механика обучения: от исследования к мастерству
Сердце любой системы обучения с подкреплением — это алгоритм обновления оценок действий на основе полученного опыта. В данной системе используется модифицированный алгоритм Q-learning, адаптированный к специфике финансовых рынков. Формула Q-learning элегантна в своей простоте: новая оценка Q-значения вычисляется, как старая оценка плюс произведение скорости обучения на разность между наблюдаемым вознаграждением и текущей оценкой.
Эта формула воплощает идею постепенного приближения к истинной оценке качества действий. Если полученное вознаграждение оказалось выше текущей оценки, Q-значение увеличивается, сигнализируя системе, что это действие в данном состоянии лучше, чем предполагалось. Если результат разочаровывает, оценка снижается. Скорость обучения контролирует, насколько агрессивно система обновляет свои представления — высокое значение приводит к быстрой адаптации, но может вызвать нестабильность, тогда как низкое значение обеспечивает плавное, консервативное обучение.
void Learn(double reward) { if(m_lastActivatedNeuron >= 0 && m_lastActivatedNeuron < m_neurons.Total()) { CMemoryNeuron *neuron = m_neurons.At(m_lastActivatedNeuron); neuron.UpdateMetrics(reward); // Отслеживание по классам if(neuron.action > 0.6) // BUY { m_buyTrades++; m_buyReward += reward; } else if(neuron.action < 0.4) // SELL { m_sellTrades++; m_sellReward += reward; } if(reward > 0) m_successfulTrades++; m_totalReward += reward; m_totalTrades++; } // Периодическая очистка и эволюция if(m_totalTrades % 100 == 0 && m_totalTrades > 0) { Prune(); Evolve(); } }
Однако просто обновлять Q-значения недостаточно. Система должна решить фундаментальную дилемму обучения с подкреплением: баланс между исследованием неизвестных стратегий и эксплуатацией уже известных хороших решений. Если агент всегда выбирает действие с наивысшим текущим Q-значением, он никогда не обнаружит потенциально лучшие альтернативы. Если же он постоянно экспериментирует, он не сможет эффективно использовать накопленное знание.
double Think(double &features[]) { EncodeToBinary(features, m_currentMarketState); int similarNeuron = FindSimilarState(m_currentMarketState); double action = 0.5; double randomValue = (double)(MathRand() % 10001) / 10000.0; if(m_totalTrades < 150) { // Фаза начального исследования int cycle = m_totalTrades % 6; if(cycle == 0 || cycle == 1) action = 0.75 + randomValue * 0.2; // BUY else if(cycle == 2 || cycle == 3) action = 0.1 + randomValue * 0.2; // SELL else action = 0.4 + randomValue * 0.2; // HOLD m_lastActivatedNeuron = -1; if(m_totalTrades % 10 == 0 && ShowDetailedLog) Print("Exploration: Сделка ", m_totalTrades, " | Action: ", DoubleToString(action, 3)); } else if(similarNeuron >= 0) { CMemoryNeuron *neuron = m_neurons.At(similarNeuron); if(randomValue > m_explorationRate) { // Эксплуатация action = neuron.action; if(neuron.successRate > 0.6) action = MathMin(1.0, action * 1.1); else if(neuron.successRate < 0.4) action = MathMax(0.0, action * 0.9); if(m_totalTrades % 20 == 0 && ShowDetailedLog) Print("Exploit: Action=", DoubleToString(action, 3), " | SR=", DoubleToString(neuron.successRate, 2)); } else { // Исследование action = randomValue; if(m_totalTrades % 20 == 0 && ShowDetailedLog) Print("Explore: Random=", DoubleToString(action, 3)); } m_lastActivatedNeuron = similarNeuron; neuron.activations++; } else { // Новое состояние action = randomValue; m_lastActivatedNeuron = -1; if(m_totalTrades % 20 == 0 && ShowDetailedLog) Print("New state: Random=", DoubleToString(action, 3)); } m_overallIntelligence = GetSuccessRate() * m_brainComplexity * 0.1; return action; }
Представленная система решает эту дилемму через многоступенчатую стратегию. На самой ранней стадии обучения, в течение первых ста пятидесяти сделок, система находится в режиме активного исследования. В этой фазе она не полагается на накопленный опыт, а циклически пробует различные типы действий: несколько сделок на покупку, несколько на продажу, несколько периодов бездействия. Это похоже на новичка-трейдера, который еще не имеет сформированного мнения о рынке и просто пробует разные подходы, чтобы почувствовать, что работает.
После завершения фазы начального исследования, система переходит к более сложному режиму. Теперь при каждом решении генерируется случайное число, и если оно превышает параметр эпсилон (уровень исследования), система выбирает лучшее известное действие — эксплуатирует свой опыт. Если же случайное число меньше эпсилон, совершается исследовательское действие — система пробует что-то новое, даже если это кажется неоптимальным на основе текущих знаний.
Балансировка классов: решение проблемы дисбаланса
Одно из ключевых нововведений версии два ноль — интегрированная система балансировки классов торговых решений. Проблема дисбаланса классов хорошо известна в машинном обучении: когда один класс значительно чаще встречается в обучающих данных, модель может научиться систематически предсказывать более частый класс, игнорируя менее частый. В контексте торговых систем это проявляется как склонность открывать только покупки или только продажи, что приводит к неоптимальному использованию рыночных возможностей и повышенному риску.
Система отслеживает количество сигналов на покупку и продажу, а также их результативность. Когда обнаруживается существенный дисбаланс — если более семидесяти процентов сигналов приходится на один класс — активируется механизм коррекции.
double ApplyClassBalance(double action) { int totalSignals = m_buySignals + m_sellSignals; if(totalSignals < 50) return action; double buyRatio = (double)m_buySignals / totalSignals; double sellRatio = (double)m_sellSignals / totalSignals; // Коррекция дисбаланса if(buyRatio > 0.70 && action > 0.6) { action = action * 0.7 - 0.15; action = MathMax(0.0, action); if(ShowDetailedLog && m_totalTrades % 50 == 0) Print("Баланс: BUY ratio=", DoubleToString(buyRatio, 2), " -> Коррекция на SELL"); } else if(sellRatio > 0.70 && action < 0.4) { action = action * 0.7 + 0.35; action = MathMin(1.0, action); if(ShowDetailedLog && m_totalTrades % 50 == 0) Print("Баланс: SELL ratio=", DoubleToString(sellRatio, 2), " -> Коррекция на BUY"); } // Усиление успешного класса if(m_buyTrades > 10 && m_sellTrades > 10) { double buyWinRate = m_buyTrades > 0 ? m_buyReward / m_buyTrades : 0; double sellWinRate = m_sellTrades > 0 ? m_sellReward / m_sellTrades : 0; if(buyWinRate > sellWinRate + 0.3 && action > 0.4 && action < 0.6) { action = action * 1.2; action = MathMin(1.0, action); } else if(sellWinRate > buyWinRate + 0.3 && action > 0.4 && action < 0.6) { action = action * 0.8; action = MathMax(0.0, action); } } // Отслеживание сигналов if(action > 0.6) m_buySignals++; else if(action < 0.4) m_sellSignals++; return action; }
Коррекция работает путем модификации предлагаемого действия. Если система склонна к чрезмерному количеству покупок и генерирует очередной сигнал на покупку, этот сигнал ослабляется математическим преобразованием, смещая его ближе к нейтральной зоне или даже к продаже. Аналогичная логика применяется при избытке сигналов на продажу.
Дополнительно, система анализирует результативность каждого класса действий. Если покупки систематически приносят значительно больше прибыли, чем продажи, система дает преимущество сигналам на покупку в нейтральной зоне неопределенности, где решение неоднозначно. Это позволяет эффективно использовать статистические преимущества без полного игнорирования менее успешного класса.
Такой подход обеспечивает диверсификацию торговых решений и предотвращает развитие систематического смещения, которое может быть катастрофическим при изменении рыночных условий. Система, которая научилась торговать только в одном направлении, окажется беспомощной, когда рынок развернется.
Управление сложностью: прунинг и эволюция
По мере обучения количество нейронов в системе неуклонно растет. Каждое новое рыночное состояние потенциально порождает новый нейрон, и если не контролировать этот процесс, система быстро станет неуправляемо большой. Но проблема не только в размере — избыток нейронов приводит к переобучению, когда система запоминает случайные флуктуации рынка вместо того, чтобы выделять истинные закономерности.
void Prune()
{
if(m_neurons.Total() < 1000) return;
int removed = 0;
for(int i = m_neurons.Total() - 1; i >= 0 && removed < 100; i--)
{
CMemoryNeuron *neuron = m_neurons.At(i);
if(neuron.activations > 10 && neuron.successRate < 0.3)
{
m_neurons.Delete(i);
removed++;
}
}
if(removed > 0 && ShowDetailedLog)
Print("Прунинг: удалено ", removed, " нейронов");
} Процесс прунинга — зачистки нейронной сети — активируется автоматически, когда количество нейронов превышает тысячу. Система начинает анализировать свою память в поисках бесполезных или вредных нейронов. Критерии отбора просты, но эффективны: нейрон должен быть достаточно опытным, чтобы иметь статистически значимую оценку его качества, но при этом демонстрировать низкую успешность.
Нейрон, который активировался более десяти раз, но показал процент успешности ниже тридцати процентов, явно представляет плохую стратегию. Это не случайная неудача — это устойчивый паттерн проигрышей. Такие нейроны безжалостно удаляются из системы. За одну операцию прунинга может быть удалено до ста нейронов, что предотвращает бесконтрольное разрастание сети и освобождает ресурсы для более перспективных областей пространства состояний.
После каждой сотой сделки, помимо прунинга, происходит эволюционный переход к новому поколению:
void Evolve() { m_currentGeneration++; m_explorationRate *= m_memoryDecay; m_explorationRate = MathMax(0.01, m_explorationRate); if(ShowDetailedLog) Print("Поколение ", m_currentGeneration, " | Exploration: ", DoubleToString(m_explorationRate, 3)); }
Это не просто инкремент счетчика — это символический момент взросления системы. С каждым поколением уровень исследования снижается, сигнализируя о том, что система накопила достаточно опыта и может больше полагаться на проверенные стратегии. Коэффициент затухания, применяемый к параметру исследования, установлен на уровне девяносто девять сотых. Это означает, что с каждым поколением уровень исследования сохраняется на девяносто девять процентов от предыдущего значения.
Мудрость толпы: коллективное принятие решений
Отдельный агент, каким бы совершенным он ни был, всегда подвержен риску систематических ошибок. Коллективный разум решает эту проблему через диверсификацию — вместо одного агента создается ансамбль независимых мозгов, каждый из которых обучается с немного разными параметрами и, следовательно, развивает свой уникальный взгляд на рынок.
double ConsensusThink(double &state[]) { double totalAction = 0; double totalWeight = 0; for(int i = 0; i < m_brainsCount; i++) { if(CheckPointer(m_brains[i]) != POINTER_DYNAMIC) continue; double action = m_brains[i].Think(state); double weight = m_brains[i].GetSuccessRate() + 0.1; totalAction += action * weight; totalWeight += weight; } return totalWeight > 0 ? totalAction / totalWeight : 0.5; }
Когда приходит время принимать торговое решение, каждый мозг в коллективе генерирует свою рекомендацию независимо. Эти рекомендации не просто усредняются — они взвешиваются по успешности каждого агента. Мозг, который последовательно показывает хорошие результаты, получает больший вес в финальном консенсусе. Мозг, который борется с текущими рыночными условиями, вносит меньший вклад.
Формула консенсуса элегантна в своей простоте. Каждое действие, предложенное агентом, умножается на вес, равный его проценту успешности плюс небольшая константа (обычно одна десятая). Эта константа гарантирует, что даже агент, показывающий нулевую успешность, имеет хоть какой-то голос — возможно, именно его нестандартное мнение окажется спасительным в необычной ситуации.
void CollectiveRemember(double action) { for(int i = 0; i < m_brainsCount; i++) if(CheckPointer(m_brains[i]) == POINTER_DYNAMIC) m_brains[i].Remember(action); } void CollectiveLearn(double reward) { for(int i = 0; i < m_brainsCount; i++) if(CheckPointer(m_brains[i]) == POINTER_DYNAMIC) m_brains[i].Learn(reward); }
Система также отслеживает коллективные метрики — общее количество нейронов во всех мозгах, средний интеллект коллектива. Эти метрики предоставляют высокоуровневое представление о зрелости и способностях системы.
Кодирование рыночной реальности: от цен к паттернам
Чтобы система могла эффективно учиться, она должна воспринимать рынок не как хаотический поток цен, а как структурированную последовательность состояний. Процесс кодирования рыночной информации в компактное бинарное представление — это искусство нахождения баланса между информативностью и обобщением.
int FindSimilarState(MarketStateBinary &state) { int bestMatch = -1; double bestSimilarity = 0; for(int i = 0; i < m_neurons.Total(); i++) { CMemoryNeuron *neuron = m_neurons.At(i); double similarity = CalculateSimilarity(state, neuron.marketState); if(similarity > bestSimilarity) { bestSimilarity = similarity; bestMatch = i; } } return bestSimilarity > 0.75 ? bestMatch : -1; } double CalculateSimilarity(MarketStateBinary &state1, MarketStateBinary &state2) { int matches = 0; for(int i = 0; i < 32; i++) if(state1.binaryCode[i] == state2.binaryCode[i]) matches++; return (double)matches / 32.0; }
Когда система сталкивается с новым рыночным состоянием, она вычисляет его бинарный код и ищет похожие состояния в своей памяти. Похожесть определяется простым подсчетом совпадающих битов. Если совпадает двадцать четыре бита из тридцати двух, это семьдесят пять процентов сходства. Порог в семьдесят пять процентов выбран как компромисс: достаточно высокий, чтобы состояния действительно были похожи, но достаточно низкий, чтобы система могла обобщать опыт между близкими ситуациями.
Функция вознаграждения: язык обратной связи
Любая обучающаяся система требует петлю обратной связи — способа оценить, насколько хорошо или плохо было принятое решение. Для торговой системы естественное вознаграждение — это прибыль или убыток от сделки.
double CalculateReward(double action, double entryPrice, double exitPrice, double spread) { double reward; if(action > 0.6) // BUY { reward = (exitPrice - entryPrice - spread) / Point; } else if(action < 0.4) // SELL { reward = (entryPrice - exitPrice - spread) / Point; } else // HOLD { reward = 0; } // Нормализация в диапазон [-1, +1] reward = MathMax(-1.0, MathMin(1.0, reward / 100.0)); return reward; }
Математически вознаграждение вычисляется как разница цен, нормализованная в диапазон от минус единицы до плюс единицы. Для сделки на продажу логика инвертируется — вознаграждение положительно, если цена упала. Важный нюанс — из разницы цен вычитается спред, представляющий транзакционные издержки.
Персистентность знаний: сохранение и восстановление
Обучение с подкреплением требует значительных вычислительных ресурсов и времени. Терять накопленный опыт при каждом перезапуске системы было бы расточительно. Поэтому критически важной является способность сохранять состояние обученных моделей и восстанавливать их при необходимости.
bool SaveToFile() { string filename = BrainFolder + "/" + m_name + ".brain"; int handle = FileOpen(filename, FILE_WRITE|FILE_BIN|FILE_COMMON); if(handle == INVALID_HANDLE) return false; // Метаданные FileWriteInteger(handle, m_neurons.Total()); FileWriteInteger(handle, m_currentGeneration); FileWriteDouble(handle, m_totalReward); FileWriteInteger(handle, m_totalTrades); FileWriteInteger(handle, m_successfulTrades); FileWriteDouble(handle, m_learningRate); FileWriteDouble(handle, m_explorationRate); FileWriteDouble(handle, m_gamma); // Статистика балансировки FileWriteInteger(handle, m_buySignals); FileWriteInteger(handle, m_sellSignals); FileWriteInteger(handle, m_buyTrades); FileWriteInteger(handle, m_sellTrades); FileWriteDouble(handle, m_buyReward); FileWriteDouble(handle, m_sellReward); // Нейроны int toSave = MathMin(m_neurons.Total(), 5000); for(int i = 0; i < toSave; i++) { CMemoryNeuron *n = m_neurons.At(i); for(int j = 0; j < 32; j++) FileWriteInteger(handle, n.marketState.binaryCode[j]); FileWriteDouble(handle, n.action); FileWriteDouble(handle, n.qValue); FileWriteInteger(handle, n.activations); FileWriteDouble(handle, n.successRate); FileWriteDouble(handle, n.importance); FileWriteInteger(handle, n.generation); } FileClose(handle); Print("Мозг '", m_name, "' сохранен: ", toSave, " нейронов"); return true; }
Каждый мозг в системе может сохранить себя в бинарный файл. Этот файл содержит всю информацию, необходимую для полного восстановления состояния мозга: количество нейронов, текущее поколение, накопленную награду, общее количество сделок, число успешных сделок, параметры обучения и статистику балансировки классов. Затем для каждого нейрона сохраняется его полное состояние.
Восстановление из файла происходит при инициализации мозга. Система пытается найти соответствующий файл, и если он существует, читает все данные и воссоздает массив нейронов. После загрузки система имеет точно такое же состояние, как в момент последнего сохранения, и может продолжить обучение или торговлю с того места, где остановилась.
Практические результаты и перспективы развития
Рассмотрим тест системы без какого-либо предварительного обучения (полностью онлайн самообучение на полностью новых данных изначально) за 2025 год на паре EURUSD на ценах открытия H1:

Вот статистика системы, и она неплохая, даже при условии совсем элементарной торговой логики с жестко фиксированным стопом и тейком:

Как видим, нейронная сеть адаптировалась к рынку, поняла закономерности и успешно заработала +41% за неполный торговый год.
А вот как данная система показывает себя при использовании сложной квантовой схемы в процессе Q-обучения и более эффективной системы управления ордерами и рисками на ценах открытия H1 EURUSD также на полном самообучении:

Тестирование на исторических данных показывает способность системы к адаптации и самообучению. Даже без предварительной подготовки она демонстрирует устойчивую положительную динамику, самостоятельно выявляя рыночные закономерности и генерируя прибыль.
Ключевое улучшение версии 2.0 — решение проблемы дисбаланса классов. Ранее система склонялась к покупкам или продажам, теперь встроенная балансировка обеспечивает равномерное распределение решений, повышая устойчивость и диверсификацию.
Расширенное признаковое пространство на основе 11 типов стационарных индикаторов даёт системе более глубокое понимание рыночной динамики, позволяя улавливать тонкие закономерности и адаптироваться к разным режимам рынка.
Модуль персистентности знаний позволяет сохранять и загружать обученные модели, избавляя от необходимости повторного обучения и обеспечивая непрерывное онлайн-обучение.
Основные ограничения — высокие требования к ресурсам и риск переобучения. С ростом числа нейронов замедляется процесс принятия решений, а прунинг лишь частично решает проблему. Кроме того, возможна потеря эффективности при смене рыночных условий, что требует постоянного мониторинга и адаптации модели.
Заключение
Система демонстрирует, что полноценная торговая стратегия на основе обучения с подкреплением возможна в MQL5 и может быть реализована компактно и эффективно. Архитектура коллективного разума обеспечивает устойчивость и адаптацию к изменяющимся рыночным условиям, а каждый модуль — от балансировки классов до прунинга — усиливает интеллектуальные возможности системы.
Обучение с подкреплением позволяет выявлять скрытые рыночные паттерны и усиливает аналитический потенциал человека, а не заменяет его. Дальнейшее развитие — интеграция глубоких архитектур, внимания и мета-обучения — откроет путь к более гибким и адаптивным системам.
В итоге ценность таких решений определяется их способностью стабильно приносить прибыль при контролируемом риске. Представленная система служит прочной основой для будущих поколений саморазвивающихся торговых интеллектов.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Возможности Мастера MQL5, которые вам нужно знать (Часть 54): Обучение с подкреплением с гибридным SAC и тензорами
Моделирование рынка (Часть 07): Сокеты (I)
Нейросети в трейдинге: Агрегация движения по времени (TMA)
Моделирование рынка (Часть 06): Перенос данных из MetaTrader 5 в Excel
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования