Architektura wielosymbolowa: jeden ekspert zamiast dziesięciu
Dziesięć kopii eksperta na dziesięciu wykresach to antywzorzec. Jak zbudować silnik portfela sterowany z jednego wykresu i czym zastąpić OnTick.
3 fragmenty kodu z tej strony przeszły przez kompilator MetaEditor build 6090, 2026-08-05.
Początkujący programiści MQL5 rzadko zastanawiają się nad architekturą, gdy tworzą swojego pierwszego EA. Logika jest prosta: przypinasz Eksperta do wykresu EURUSD, używasz zmiennej _Symbol, a funkcja OnTick() wykonuje pracę. Kiedy przychodzi potrzeba uruchomienia tej samej strategii na 10 parach walutowych, trader po prostu otwiera 10 wykresów i wrzuca na nie 10 kopii robota. Wydaje się to naturalne. W rzeczywistości jest to antywzorzec architektoniczny, który prowadzi do wycieków pamięci, opóźnień w wykonaniu i katastrofalnych błędów na kontach Netting.
Przejście z modelu "Single-Symbol" na "Multi-Symbol Engine" to moment, w którym programista MQL5 przestaje pisać skrypty, a zaczyna tworzyć systemy algorytmiczne. W tym artykule przeprowadzimy dogłębną analizę tego, dlaczego podejście wielowykresowe zawodzi i jak napisać potężny silnik zarządzający portfelem z poziomu jednego wykresu.
1. Iluzja Podejścia Wielowykresowego (Multi-Chart)
Uruchomienie tego samego EA na 10 wykresach wydaje się łatwe, ale pod maską terminala MT5 dzieje się koszmar. Każdy wykres w MetaTrader 5 uruchamia osobny wątek dla EA. Oznacza to, że:
- Twój EA jest wczytywany do pamięci RAM 10 razy.
- Każda instancja tworzy własne uchwyty wskaźników (
iMA,iRSI), mimo że są to te same parametry dla tego samego instrumentu (choć MT5 posiada wewnętrzną optymalizację współdzielenia wskaźników, narzut strukturalny pozostaje).
- Każda instancja musi samodzielnie odpytywać środowisko o pozycje, zlecenia i historię, co drastycznie obciąża silnik bazy danych terminala.
2. Pułapka OnTick(): Dlaczego nie zadziała na innych instrumentach?
Głównym problemem w budowie silnika Multi-Symbol jest fakt, że funkcja OnTick() wywoływana jest tylko i wyłącznie dla instrumentu, na którym zawieszony jest wykres. Jeśli uruchomisz EA na wykresie EURUSD, funkcja OnTick odpali się przy zmianie ceny EURUSD. Jeśli w tym czasie na GBPUSD nastąpi potężny ruch i sygnał wejścia, Twoje EA o tym nie będzie wiedzieć, dopóki nie przyjdzie nowy tick EURUSD.
Na płynnych parach (Forex Major) to opóźnienie wynosi milisekundy. Na mało płynnych parach (np. egzotyczne krzyże lub metale) EA może "ślepić" przez minuty, gubiąc okazje rynkowe.
Rozwiązanie: OnTimer i pobieranie ticków asynchronicznie
Aby stworzyć niezależny od wykresu silnik, musisz zrezygnować z polegania na OnTick() jako głównym wyzwalaczu logiki. Zamiast tego używamy OnTimer() ustawionego na krótki interwał (np. 100-250 milisekund) i ręcznie odpytujemy serwer o najnowsze ceny dla wszystkich śledzonych instrumentów.
// Tablica instrumentów do śledzenia
string symbols[] = {"EURUSD", "GBPUSD", "USDJPY", "XAUUSD"};
int OnInit() {
// 1. Upewniamy się, że instrumenty są w Market Watch (wymagane do pobierania ticków)
for(int i = 0; i < ArraySize(symbols); i++) {
SymbolSelect(symbols[i], true);
}
// 2. Ustawiamy timer na 100 ms (10 razy na sekundę)
EventSetMillisecondTimer(100);
return(INIT_SUCCEEDED);
}
void OnTimer() {
// Pętla przechodząca przez wszystkie instrumenty
for(int i = 0; i < ArraySize(symbols); i++) {
string currentSymbol = symbols[i];
// Pobieramy ostatni tick dla tego instrumentu
MqlTick lastTick;
if(SymbolInfoTick(currentSymbol, lastTick)) {
// Przekazujemy tick do funkcji logicznej
ProcessSymbol(currentSymbol, lastTick);
}
}
}
void ProcessSymbol(string sym, MqlTick &tick) {
// Tutaj znajduje się logika, która kiedyś była w OnTick()
// Np. sprawdzanie wskaźników, warunków wejścia itp.
// if(tick.bid < someMovingAverage) { ... }
}3. Wyzwanie: Wskaźniki w architekturze Multi-Symbol
W tradycyjnym EA używałeś iMA(_Symbol, ...) w OnInit i zwracało to uchwyt (liczbę). W architekturze Multi-Symbol nie możesz mieć jednego uchwytu, bo każde iMA musi być wywołane osobno dla każdego instrumentu. Najlepszym wzorcem architektonicznym jest użycie struktur lub klas przechowujących "stan" każdego instrumentu.
// Struktura przechowująca kontekst pojedynczego instrumentu
struct SSymbolContext {
string symbol;
int maHandleFast;
int maHandleSlow;
double maBufferFast[];
double maBufferSlow[];
bool isPositionOpen;
};
SSymbolContext contexts[];
int OnInit() {
ArrayResize(contexts, ArraySize(symbols));
for(int i = 0; i < ArraySize(symbols); i++) {
contexts[i].symbol = symbols[i];
// Tworzymy osobne uchwyty dla każdego instrumentu
contexts[i].maHandleFast = iMA(symbols[i], PERIOD_M15, 10, 0, MODE_EMA, PRICE_CLOSE);
contexts[i].maHandleSlow = iMA(symbols[i], PERIOD_M15, 50, 0, MODE_EMA, PRICE_CLOSE);
ArraySetAsSeries(contexts[i].maBufferFast, true);
ArraySetAsSeries(contexts[i].maBufferSlow, true);
}
// ... ustawienie timera ...
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason) {
// KRYTYCZNE: Zwalnianie wielu uchwytów!
for(int i = 0; i < ArraySize(contexts); i++) {
IndicatorRelease(contexts[i].maHandleFast);
IndicatorRelease(contexts[i].maHandleSlow);
}
EventKillTimer();
}
void OnTimer() {
for(int i = 0; i < ArraySize(contexts); i++) {
// Kopiujemy dane wskaźników dla danego instrumentu
if(CopyBuffer(contexts[i].maHandleFast, 0, 0, 2, contexts[i].maBufferFast) > 0 &&
CopyBuffer(contexts[i].maHandleSlow, 0, 0, 2, contexts[i].maBufferSlow) > 0) {
// Sprawdzamy sygnał przecięcia (Cross)
if(contexts[i].maBufferFast[0] > contexts[i].maBufferSlow[0] &&
contexts[i].maBufferFast[1] <= contexts[i].maBufferSlow[1]) {
Print("Sygnał BUY na: ", contexts[i].symbol);
// Wykonanie zlecenia...
}
}
}
}4. Zarządzanie Pozycjami (Position Select)
Wielu programistów zapomina, że funkcja PositionSelect(_Symbol) wybiera pozycję dla bieżącego symbolu. W architekturze Multi-Symbol musisz używać funkcji PositionSelect(symbol) z jawnym przekazaniem nazwy instrumentu lub iterować po całej liście pozycji za pomocą PositionGetTicket(i).
bool IsPositionOpenForSymbol(string sym) {
// Iterujemy przez wszystkie otwarte pozycje w terminalu
int total = PositionsTotal();
for(int i = 0; i < total; i++) {
ulong ticket = PositionGetTicket(i);
if(ticket > 0) {
// Zamiast PositionSelect(_Symbol), sprawdzamy nazwę pozycji
if(PositionGetString(POSITION_SYMBOL) == sym) {
// Opcjonalnie: sprawdzamy Magic Number
if(PositionGetInteger(POSITION_MAGIC) == MyMagicNumber) {
return true;
}
}
}
}
return false;
}Podsumowanie
Przejście na architekturę Multi-Symbol to radykalna zmiana paradygmatu. Zamiast "reaktywnego" czekania na OnTick z jednego wykresu, budujesz "aktywny" silnik portfela z wykorzystaniem OnTimer i struktur danych przechowujących stany rynków. Chociaż wymaga to napisania znacznie więcej kodu inicjalizacyjnego (zarządzanie uchwytami, buforami i wyciszanie śmieci w OnDeinit), zyskujesz pełną kontrolę nad portfelem. Eliminujesz ryzyko kolizji na kontach Netting, drastycznie zmniejszasz zużycie RAM przez terminal i zyskujesz możliwość implementacji zaawansowanego zarządzania kapitałem (np. korelacji między instrumentami, limitów narazonych maksymalnie zysków/strat całego portfela), co jest niemożliwe w modelu wielowykresowym.