Архитектура и проектирование торговых советников в MQL5

Николай Севастьянов
20.11.2025
297 Просмотры

Вторая статья из цикла «Автоматизация Forex-трейдинга в MetaTrader4/5 - Category foreks avtomatizacziya ttrejdinga» посвящена проектированию архитектуры торговых советников(начало здесь - ). Рассматриваются принципы модульного проектирования, паттерны проектирования для алгоритмического трейдинга, система управления состоянием и обработки ошибок, а также лучшие практики разработки поддерживаемого кода.

1. Введение в архитектуру торговых систем

1.1. Эволюция подходов к разработке

Традиционный подход (монолитный):

// ПРИМЕР: Наивная реализация (антипаттерн)
void OnTick()
{
   double maFast = iMA(_Symbol, _Period, 14, 0, MODE_SMA, PRICE_CLOSE, 0);
   double maSlow = iMA(_Symbol, _Period, 28, 0, MODE_SMA, PRICE_CLOSE, 0);

   if(maFast > maSlow && !HasPosition())
   {
      OrderSend(_Symbol, OP_BUY, 0.1, Ask, 3, 0, 0, "MA Cross", 0, 0, clrGreen);
   }
}

Проблемы монолитного подхода:

  • Сложность тестирования
  • Низкая переиспользуемость кода
  • Трудности модификации
  • Высокая связанность компонентов

1.2. Принципы модульной архитектуры

SOLID-принципы в MQL5:

// S - Single Responsibility Principle
class CTradeManager
{
public:
   bool OpenPosition(ENUM_ORDER_TYPE type, double volume);
   bool ClosePosition(ulong ticket);
   double CalculatePositionSize();
};

class CRiskManager
{
public:
   bool ValidateRisk(double volume);
   double CalculateMaxVolume();
   double GetAccountRiskPercent();
};

class CSignalGenerator
{
public:
   bool GetBuySignal();
   bool GetSellSignal();
   void UpdateIndicators();
};

2. Проектирование архитектуры советника

2.1. Многослойная архитектура

┌─────────────────────────────────────────────────┐
│                 Presentation Layer              │
│               (Input Parameters, GUI)           │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│                 Business Logic                  │
│          (Trading Strategy, Signal Generation)  │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│                 Service Layer                   │
│       (Trade Execution, Risk Management)        │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│                 Data Access Layer               │
│        (Market Data, Indicators, History)       │
└─────────────────────────────────────────────────┘

2.2. Реализация слоеной архитектуры

// Основной класс советника
class CExpertAdvisor : public CObject
{
private:
   CMarketData       *m_marketData;
   CSignalGenerator  *m_signalGenerator;
   CRiskManager      *m_riskManager;
   CTradeExecutor    *m_tradeExecutor;
   CJournal          *m_journal;

public:
   bool              OnInit();
   void              OnTick();
   void              OnDeinit(const int reason);

   // Конструктор, деструктор
   CExpertAdvisor();
   ~CExpertAdvisor();
};

// Инициализация компонентов
bool CExpertAdvisor::OnInit()
{
   m_journal = new CJournal("EA_Log");
   m_marketData = new CMarketData(m_journal);
   m_riskManager = new CRiskManager(2.0, m_journal); // 2% risk
   m_signalGenerator = new CSignalGenerator(m_marketData, m_journal);
   m_tradeExecutor = new CTradeExecutor(m_riskManager, m_journal);

   return true;
}

3. Ключевые компоненты архитектуры

3.1. Менеджер рыночных данных

class CMarketData : public CObject
{
private:
   CJournal          *m_log;
   string            m_symbol;
   ENUM_TIMEFRAME    m_timeframe;

public:
   double GetPrice(ENUM_PRICE_TYPE priceType);
   double GetIndicatorValue(string indicatorName, int shift);
   bool   IsNewBar();
   bool   IsMarketOpen();
   int    GetSpread();

   CMarketData(CJournal *logger);
};

// Реализация методов
double CMarketData::GetPrice(ENUM_PRICE_TYPE priceType)
{
   MqlTick tick;
   if(SymbolInfoTick(m_symbol, tick))
   {
      switch(priceType)
      {
         case PRICE_ASK: return tick.ask;
         case PRICE_BID: return tick.bid;
         case PRICE_LAST: return tick.last;
      }
   }
   m_log.Error("Failed to get price for " + m_symbol);
   return 0;
}

3.2. Генератор торговых сигналов

class CSignalGenerator : public CObject
{
private:
   CMarketData       *m_marketData;
   CJournal          *m_log;

public:
   enum ENUM_SIGNAL
   {
      SIGNAL_NONE = 0,
      SIGNAL_BUY = 1,
      SIGNAL_SELL = 2,
      SIGNAL_CLOSE = 3
   };

   ENUM_SIGNAL GetSignal();
   double GetSignalStrength();

   CSignalGenerator(CMarketData *marketData, CJournal *logger);
};

// Реализация стратегии MA Cross
ENUM_SIGNAL CSignalGenerator::GetSignal()
{
   double maFast = m_marketData->GetIndicatorValue("MA", 0);
   double maSlow = m_marketData->GetIndicatorValue("MA", 1);
   double maPrevFast = m_marketData->GetIndicatorValue("MA", 1);
   double maPrevSlow = m_marketData->GetIndicatorValue("MA", 2);

   // Определение пересечения
   bool crossUp = (maFast > maSlow) && (maPrevFast <= maPrevSlow);
   bool crossDown = (maFast < maSlow) && (maPrevFast >= maPrevSlow);

   if(crossUp) return SIGNAL_BUY;
   if(crossDown) return SIGNAL_SELL;

   return SIGNAL_NONE;
}

3.3. Менеджер рисков

class CRiskManager : public CObject
{
private:
   double            m_riskPercent;
   CJournal          *m_log;

public:
   struct RiskParams
   {
      double volume;
      double stopLoss;
      double takeProfit;
      double riskRewardRatio;
   };

   RiskParams CalculatePositionParams(ENUM_ORDER_TYPE type);
   bool ValidatePositionSize(double volume);
   double CalculateOptimalVolume(double stopLossPips);
   double GetAccountFreeMarginPercent();

   CRiskManager(double riskPercent, CJournal *logger);
};

// Расчет объема позиции
double CRiskManager::CalculateOptimalVolume(double stopLossPips)
{
   double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskAmount = accountBalance * m_riskPercent / 100.0;
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOT);
   double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);

   if(stopLossPips == 0 || tickValue == 0 || tickSize == 0)
   {
      m_log.Error("Invalid parameters for volume calculation");
      return 0.1; // default volume
   }

   double volume = riskAmount / (stopLossPips * tickValue * (tickSize / _Point));
   return NormalizeDouble(volume, 2);
}

3.4. Исполнитель торговых операций

class CTradeExecutor : public CObject
{
private:
   CRiskManager      *m_riskManager;
   CJournal          *m_log;
   CTrade            m_trade;

public:
   bool OpenPosition(ENUM_ORDER_TYPE type, double volume, string comment = "");
   bool ClosePosition(ulong ticket);
   bool ModifyPosition(ulong ticket, double sl, double tp);
   int  GetOpenPositionsCount();

   CTradeExecutor(CRiskManager *riskManager, CJournal *logger);
};

// Открытие позиции с обработкой ошибок
bool CTradeExecutor::OpenPosition(ENUM_ORDER_TYPE type, double volume, string comment)
{
   double price = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) 
                                          : SymbolInfoDouble(_Symbol, SYMBOL_BID);

   CRiskManager::RiskParams params = m_riskManager->CalculatePositionParams(type);

   if(!m_riskManager->ValidatePositionSize(volume))
   {
      m_log.Warning("Position size validation failed: " + DoubleToString(volume));
      return false;
   }

   bool result = m_trade.Buy(volume, _Symbol, 0, params.stopLoss, params.takeProfit, comment);

   if(!result)
   {
      m_log.Error("Failed to open position: " + IntegerToString(m_trade.ResultRetcode()));
      return false;
   }

   m_log.Info("Position opened: " + comment + ", Volume: " + DoubleToString(volume));
   return true;
}
Что можно получить открывая счет в робофорекс -

4. Система управления состоянием

4.1. Конечный автомат состояний советника

enum ENUM_EA_STATE
{
   STATE_INIT = 0,
   STATE_READY = 1,
   STATE_WAITING_SIGNAL = 2,
   STATE_POSITION_OPEN = 3,
   STATE_POSITION_MANAGEMENT = 4,
   STATE_ERROR = 5,
   STATE_SHUTDOWN = 6
};

class CStateMachine : public CObject
{
private:
   ENUM_EA_STATE     m_currentState;
   CJournal          *m_log;

public:
   bool TransitionTo(ENUM_EA_STATE newState);
   ENUM_EA_STATE GetCurrentState() const { return m_currentState; }
   string StateToString(ENUM_EA_STATE state);

   CStateMachine(CJournal *logger);
};

// Управление переходами состояний
bool CStateMachine::TransitionTo(ENUM_EA_STATE newState)
{
   // Валидация перехода
   bool isValidTransition = false;

   switch(m_currentState)
   {
      case STATE_INIT:
         isValidTransition = (newState == STATE_READY || newState == STATE_ERROR);
         break;
      case STATE_READY:
         isValidTransition = (newState == STATE_WAITING_SIGNAL || newState == STATE_ERROR);
         break;
      // ... другие правила переходов
   }

   if(isValidTransition)
   {
      m_log.Info("State transition: " + StateToString(m_currentState) + 
                " -> " + StateToString(newState));
      m_currentState = newState;
      return true;
   }

   m_log.Error("Invalid state transition: " + StateToString(m_currentState) + 
              " -> " + StateToString(newState));
   return false;
}

5. Система логирования и мониторинга

5.1. Компонент журналирования

enum ENUM_LOG_LEVEL
{
   LOG_LEVEL_ERROR = 0,
   LOG_LEVEL_WARNING = 1,
   LOG_LEVEL_INFO = 2,
   LOG_LEVEL_DEBUG = 3
};

class CJournal : public CObject
{
private:
   string            m_name;
   ENUM_LOG_LEVEL    m_logLevel;
   string            m_logFile;

public:
   void Error(string message);
   void Warning(string message);
   void Info(string message);
   void Debug(string message);
   void Trade(string message, double price, double volume);

   CJournal(string name, ENUM_LOG_LEVEL level = LOG_LEVEL_INFO);
};

// Реализация логирования
void CJournal::Error(string message)
{
   if(m_logLevel >= LOG_LEVEL_ERROR)
   {
      string logMessage = TimeToString(TimeCurrent()) + " [ERROR] " + m_name + ": " + message;
      Print(logMessage);
      WriteToFile(logMessage);
      SendNotification(logMessage); // Отправка уведомления
   }
}

6. Паттерны проектирования для MQL5

6.1. Паттерн «Стратегия» для торговых алгоритмов

class ITradingStrategy : public CObject
{
public:
   virtual bool      GetSignal() = 0;
   virtual string    GetName() = 0;
   virtual void      Update() = 0;
};

class CMovingAverageStrategy : public ITradingStrategy
{
private:
   CMarketData       *m_marketData;

public:
   bool GetSignal() override
   {
      // Реализация стратегии MA
      return false;
   }

   string GetName() override { return "Moving Average Strategy"; }
   void Update() override {}
};

class CRsiStrategy : public ITradingStrategy
{
public:
   bool GetSignal() override
   {
      // Реализация стратегии RSI
      return false;
   }

   string GetName() override { return "RSI Strategy"; }
   void Update() override {}
};

6.2. Паттерн «Наблюдатель» для событий рынка

class IMarketObserver
{
public:
   virtual void OnNewTick() = 0;
   virtual void OnNewBar() = 0;
   virtual void OnPriceAlert(string symbol, double price) = 0;
};

class CMarketEventManager : public CObject
{
private:
   IMarketObserver   *m_observers[];

public:
   void AddObserver(IMarketObserver *observer);
   void RemoveObserver(IMarketObserver *observer);
   void NotifyNewTick();
   void NotifyNewBar();
};

7. Сборка финальной архитектуры

// Главный файл советника
CExpertAdvisor *ea;

int OnInit()
{
   ea = new CExpertAdvisor();
   return ea.OnInit() ? INIT_SUCCEEDED : INIT_FAILED;
}

void OnTick()
{
   ea.OnTick();
}

void OnDeinit(const int reason)
{
   delete ea;
}

// Реализация основного цикла
void CExpertAdvisor::OnTick()
{
   // Обновление данных
   m_marketData->Update();

   // Проверка состояния
   if(m_stateMachine.GetCurrentState() == STATE_WAITING_SIGNAL)
   {
      // Получение сигнала
      CSignalGenerator::ENUM_SIGNAL signal = m_signalGenerator->GetSignal();

      if(signal == CSignalGenerator::SIGNAL_BUY)
      {
         CRiskManager::RiskParams params = m_riskManager->CalculatePositionParams(ORDER_TYPE_BUY);
         m_tradeExecutor->OpenPosition(ORDER_TYPE_BUY, params.volume, "Buy Signal");
         m_stateMachine.TransitionTo(STATE_POSITION_OPEN);
      }
   }

   // Логирование состояния
   m_journal->Debug("EA State: " + m_stateMachine.StateToString(m_stateMachine.GetCurrentState()));
}

8. Заключение

Правильно спроектированная архитектура — основа успешного торгового советника. Модульный подход позволяет:

  • Легко тестировать отдельные компоненты
  • Быстро модифицировать стратегии без изменения всей системы
  • Повторно использовать код в различных проектах
  • Упрощать отладку и сопровождение

Ключевые принципы успешной архитектуры:

  1. Разделение ответственности — каждый класс решает одну задачу
  2. Слабая связанность — минимизация зависимостей между компонентами
  3. Инкапсуляция — сокрытие внутренней реализации
  4. Расширяемость — возможность легко добавлять новый функционал

📚 Что дальше?

В следующей статье цикла мы рассмотрим:
«Продвинутые техники управления рисками и капиталом»:

  • Многоуровневая система управления рисками
  • Динамическое позиционирование и пирамидинг
  • Корреляционный анализ и диверсификация
  • Stress-тестирование стратегий

💡 Практическое задание

Спроектируйте архитектуру для собственного торгового советника на основе изученных принципов. Создайте диаграмму классов и реализуйте базовые интерфейсы для основных компонентов системы.


Готовы к углублению в управление рисками? Продолжаем совершенствовать наши торговые системы! 💪

Автор Николай Севастьянов

Профессиональный любитель финансовых рынков с практическим опытом более 10 лет.