Przejdź do treści
MQL5 · MetaTrader 5 · po polsku
kod odczarowany

MQL5 / Inżynieria

Architektura eksperta

Dlaczego ekspert napisany w jednym OnTick nie da się rozwijać ani sprawdzić, i jak go rozdzielić na trzy niezależne części.

3 fragmenty kodu z tej strony przeszły przez kompilator MetaEditor build 6090, 2026-08-05.

Typowy ekspert z internetu ma jedną funkcję OnTick na trzysta linii. W środku, poprzeplatane ze sobą: odczyt wskaźników, warunek wejścia, wyliczenie wolumenu, wysłanie zlecenia, zarządzanie stopem i obsługa błędów.

To działa, dopóki nie chcesz niczego zmienić.

Co konkretnie się psuje

Nie da się zmienić jednej rzeczy. Chcesz sprawdzić inny sposób liczenia wolumenu — musisz grzebać w funkcji, która przy okazji wysyła zlecenia. Każda zmiana niesie ryzyko zepsucia czegoś obok.

Nie da się niczego sprawdzić osobno. Cała logika istnieje tylko w kontekście działającego terminala, otwartego rachunku i napływających ticków.

Nie da się użyć ponownie. Trailing stop napisany wewnątrz OnTick jednego eksperta trzeba przy drugim przepisać ręcznie, razem z jego błędami.

Nie da się czytać. Po trzech miesiącach nie odróżnisz warunku wejścia od obsługi przypadku brzegowego u brokera, który nie podaje poziomu stopów.

Trzy odpowiedzialności

Ekspert, niezależnie od strategii, robi trzy różne rzeczy:

CzęśćOdpowiada na pytanieCzego potrzebuje
Sygnałczy i w którą stronędanych rynkowych
Ryzykojak dużą pozycją i gdzie stopstanu rachunku i parametrów instrumentu
Wykonaniejak to wysłać na serwerdostępu do funkcji handlowych

Te trzy części nie muszą o sobie wiedzieć. Sygnał nie ma pojęcia, że istnieją zlecenia. Ryzyko nie wie, skąd wziął się sygnał. Wykonanie nie wie, dlaczego ma wysłać kupno.

Interfejsy

MQL5 ma słowo kluczowe interface i to jest najprostszy sposób na rozdzielenie tych warstw.

Interfejs sygnału
interface ISygnal
  {
   //--- 1 kupno, -1 sprzedaż, 0 brak sygnału
   int  Kierunek(void);
   bool Przygotuj(void);
   void Zwolnij(void);
  };

Konkretna strategia implementuje interfejs i nie robi nic poza swoim zadaniem:

Implementacja: przecięcie dwóch średnich
class CPrzeciecieSrednich : public ISygnal
  {
private:
   int               m_szybka;
   int               m_wolna;
   int               m_okresSzybkiej;
   int               m_okresWolnej;

public:
                     CPrzeciecieSrednich(const int szybka, const int wolna)
     {
      m_okresSzybkiej = szybka;
      m_okresWolnej = wolna;
      m_szybka = INVALID_HANDLE;
      m_wolna = INVALID_HANDLE;
     }

   bool              Przygotuj(void)
     {
      m_szybka = iMA(_Symbol, _Period, m_okresSzybkiej, 0, MODE_EMA, PRICE_CLOSE);
      m_wolna  = iMA(_Symbol, _Period, m_okresWolnej, 0, MODE_EMA, PRICE_CLOSE);
      return (m_szybka != INVALID_HANDLE && m_wolna != INVALID_HANDLE);
     }

   void              Zwolnij(void)
     {
      if(m_szybka != INVALID_HANDLE)
         IndicatorRelease(m_szybka);
      if(m_wolna != INVALID_HANDLE)
         IndicatorRelease(m_wolna);
     }

   int               Kierunek(void)
     {
      double szybka[], wolna[];
      ArraySetAsSeries(szybka, true);
      ArraySetAsSeries(wolna, true);

      //--- świeca 1 i 2, nigdy 0
      if(CopyBuffer(m_szybka, 0, 1, 2, szybka) < 2)
         return 0;
      if(CopyBuffer(m_wolna, 0, 1, 2, wolna) < 2)
         return 0;

      if(szybka[1] <= wolna[1] && szybka[0] > wolna[0])
         return 1;
      if(szybka[1] >= wolna[1] && szybka[0] < wolna[0])
         return -1;

      return 0;
     }
  };

Co z tego wynika w OnTick

OnTick przestaje być miejscem, gdzie mieszka logika, i staje się tym, czym powinien być: dyspozytorem.

OnTick po rozdzieleniu
ISygnal *sygnal = NULL;

int OnInit()
  {
   sygnal = new CPrzeciecieSrednich(12, 26);
   if(!sygnal.Przygotuj())
      return INIT_FAILED;
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int powod)
  {
   if(sygnal != NULL)
     {
      sygnal.Zwolnij();
      delete sygnal;
      sygnal = NULL;
     }
  }

void OnTick()
  {
   //--- decyzja raz na świecę, patrz lekcja o nowej świecy
   static datetime ostatnia = 0;
   datetime biezaca = iTime(_Symbol, _Period, 0);
   if(biezaca == 0 || biezaca == ostatnia)
      return;
   ostatnia = biezaca;

   int kierunek = sygnal.Kierunek();
   if(kierunek == 0)
      return;

   //--- ryzyko i wykonanie mają własne funkcje
   Print("Sygnał: ", kierunek);
  }

Wymiana strategii to teraz jedna linia w OnInit. Dodanie drugiego sygnału i wymóg, żeby oba się zgadzały, to kilka linii, bez dotykania czegokolwiek innego.

Gdzie postawić granicę

Nie każdy ekspert potrzebuje interfejsów. Przy stu liniach to narzut bez zysku. Sygnałem, że pora rozdzielić, są zwykle trzy rzeczy:

  • OnTick przekracza ekran,
  • pojawia się druga strategia albo drugi instrument,
  • zaczynasz kopiować fragmenty między plikami.

Zanim sięgniesz po klasy, często wystarczy rozbicie na funkcje w osobnych plikach .mqh — sygnał, ryzyko, wykonanie jako trzy pliki nagłówkowe. Zysk w czytelności jest podobny, a próg wejścia zerowy.

Powiązane