4. MQL4 dla początkujących. Część IV.

4.3. Przykład kombinacji operatorów while, if oraz return

W celu demonstracji działania operatora pętli while w połączeniu z operatorami if oraz return przygotujemy skrypt, zadaniem którego będzie znalezienie na wykresie notowań (począwszy od prawej strony) pierwszej świecy, długość ciała której będzie większa lub równa 20 punktom, a następnie wyświetlenie w logach terminala daty i czasu utworzenia tej świecy. Zanim przystąpimy do kodowania, wyjaśnijmy pojęcia "punkt" i "długość ciała świecy".

W handlu walutami pod pojęciem punkt najczęściej rozumie się zmiana ceny notowania o 1 w 4-tej cyfrze po przecinku w 4-cyfrowym notowaniu ceny (np. EURUSD) lub zmiana o 1 w 2-giej cyfrze po przecinku w 2-cyfrowym notowaniu ceny (np. USDJPY). Na przykład, jeśli cena EURUSD była 1.1234 a po chwili stała 1.1235 to mówi się, że notowanie EURUSD wzrosło o 1 punkt. Jeśli cena USDJPY była np. 100.12 a po chwili stała 100.11 to mówi się, że notowanie USDJPY spadło o 1 punkt.

Jeśli już próbowałeś handlować na Forex-ie za pośrednictwem terminala MetaTrader 4 to pewnie zauważyłeś, że wiele brokerów dostarcza notowania z dokładnością 5 (np. EURUSD) lub 3 (np. USDJPY) cyfr po przecinku. Przy takiej dokładności zmiana ceny o 1 w 4-tej cyfrze po przecinku w 5-cyfrowym notowaniu lub o 1 w 2-giej cyfrze po przecinku w 3-cyfrowym notowaniu będzie oznaczać zmianę o 10 punktów. Na przykład, jeśli cena EURUSD była 1.12345 a po chwili stała 1.12355 to rozumie się, że cena EURUSD wzrosła o 10 punktów dla 5-cyfrowego notowania. Jeśli cena USDJPY była np. 100.123 a po chwili stała 100.113 to rozumie się, że cena USDJPY spadła o 10 punktów dla 3-cyfrowego notowania.

Rys. 1. Przykłady 4- i 5-cyfrowych notowań w MetaTrader 4.


Ponieważ historycznie się ułożyło tak, że na początku brokerzy udostępniali 4-cyfrowe notowania, to wiele traderów słysząc o zmianie notowania o 1 punkt nadal rozumieją to jako zmianę w 4-tej cyfrze nawet pracując na 5-cyfrowym notowaniu. Dlatego w MetaTrader 4 wysyłając zlecenie otwarcia pozycji ostatnia 5-ta cyfra wizualnie jest wyróżniana od poprzednich.

Rys. 2. Okno zlecenia otwarcia pozycji w MetaTrader 4.


Z kolei my jako programiści MQL4 powinniśmy pamiętać, że dla programu 1 punkt w 5-cyfrowym notowaniu to zmiana o 1 w 5-tej cyfrze po kropce, a w 3-cyfrowym to zmiana w 3 cyfrze. Wracając do naszego zadania należy doprecyzować - będziemy szukać świecy o długości ciała 20 punktów dla 4-cyfrowego notowania lub 200 punktów dla 5-cyfrowego.


Pod pojęciem "długość ciała świecy" będziemy rozumieć różnicę pomiędzy jej ceną otwarcia (open) a ceną zamknięcia (close).

Rys. 3. Świeca notowania ceny.


Część 1


Kod 1
#property strict
#property script_show_inputs

input uint InpPoints = 20; // Ilość punktów dla 4-cyfrowego notowania.

void OnStart()
  {
//--- CZĘŚĆ 1
   uint RealPoints = 0;
//--- obliczamy prawidłową ilość punktów w zależności od dokładności notowania
   switch(_Digits)
     {
      case 2:  RealPoints = InpPoints;      break;
      case 4:  RealPoints = InpPoints;      break;
      case 3:  RealPoints = InpPoints * 10; break;
      case 5:  RealPoints = InpPoints * 10; break;
      default: Print("Dokładność notowania nie jest znana."); return;
     }
//--- CZĘŚĆ 2
//--- CZĘŚĆ 3
//--- CZĘŚĆ 4
  }

Tutaj jak zwykle przed OnStart() wpisem #property strict prosimy program o zastosowanie nowego kompilatora, a wpisem #property script_show_inputs o pokazanie okienka z zewnętrznym parametrem InpPoints.

Dalej w ciele OnStart() na początku należy ustalić prawidłową ilość punktów w zależności od dokładności notowania ceny. W tym celu tworzymy zmienną RealPoints, która będzie przechowywać prawidłową ilość punktów. W nagłówku operatora switch podstawiamy predefiniowaną zmienną _Digits, która zawiera ilość znaków po przecinku. Jeśli ten skrypt zostanie uruchomiony na wykresie z 2- lub 4-cyfrowym notowaniem wtedy RealPoints = InpPoints (20 punktów). Jeśli notowania ceny będą miały 3 lub 5 znaków po kropce, wtedy switch przekieruje program na etykietę case 3 lub case 5. W tej sytuacji szukana świeca będzie musiała mieć długość ciała o 10 razy więcej punktów RealPoints = InpPoints * 10 (200 punktów).

A co jeśli skrypt zostanie uruchomiony na wykresie z inną dokładnością notowania ceny? W tym przypadku działanie programu zostanie przekierowane na etykietę default i dalej na dwie instrukcje w tej etykiecie. Funkcja Print() poinformuje użytkownika o zaistniałej sytuacji, a operator return przerwie działanie całego skryptu. Jeśli zamiast return wpiszemy tu break, który przerwie działanie tylko switch a nie skryptu, wtedy dalej w programie RealPoints będzie równe 0 i skrypt zacznie szukać świecy o długości ciała większej lub równej 0, co nie jest zgodne z zadaniem.


Teraz popracujemy nad tuningiem switch-a. W etykietach case 2 i case 4 są zapisane identyczne działania. To samo się dzieje i w etykieatch case 3 i case 5. W rozdziale 2.7. Operator wyboru wielowariantowego switch ... case zaznaczyłem, że w przypadku braku break na końcu etykiety program przechodzi do kolejnej etykiety. A spróbujmy wykorzystać ten trik.

Kod 2
   switch(_Digits)
     {
      case 2:
      case 4:  RealPoints = InpPoints;      break;
      case 3:
      case 5:  RealPoints = InpPoints * 10; break;
      default: Print("Dokładność notowania nie jest znana."); return;
     }

Tutaj po prostu po case 2 i case 3 wszystko zostało usunięte. Teraz jeśli _Digits będzie równe 2, program przejdzie do odpowiedniej etykiety, nic tam nie zrobi i ponieważ nie napotka się na break przejdzie do kolejnej etykiety case 4 i tam zostanie zrealizowana instrukcja RealPoints = InpPoints. Jeśli _Digits będzie równe 4, to program od razu przejdzie do case 4. Dla wariantów 3 i 5 logika jest taka sama.

I jeszcze jeden dodatkowy tuning.

Kod 3
   switch(_Digits)
     {
      case 2: case 4: RealPoints = InpPoints;      break;
      case 3: case 5: RealPoints = InpPoints * 10; break;
      default: Print("Dokładność notowania nie jest znana."); return;
     }

Część 2


Kod 4
void OnStart()
  {
//--- CZĘŚĆ 1

//--- CZĘŚĆ 2
   int    i = 0;             // indeks świecy
   double BodyDiff    = 0.0; // różnica open i close
   double BodyDiffAbs = 0.0; // wartość bezwzględna różnicy open i close
   double CalcDiff    = 0.0; // poszukiwana długość ciała świecy
//---
   BodyDiff    = Open[i] - Close[i];
   BodyDiffAbs = MathAbs(BodyDiff);
   CalcDiff    = RealPoints / MathPow(10 , _Digits);
  
//--- CZĘŚĆ 3
//--- CZĘŚĆ 4
  }

W tej części kodu tworzymy 4 zmienne, którym od razu przypisujemy zerowe wartości: i - licznik świec, BodyDiff - w tej zmiennej będzie zapisywana różnica między ceną open a close, BodyDiffAbs - wartość bezwzględna różnicy open i close oraz CalcDiff - poszukiwana długość ciała świecy. Jeśli nie wiesz dlaczego w zmiennych typu double zapisałem 0.0 a nie samo 0, to odpowiedź znajdziesz w rozdziale 3.7 Typy zmiennych: typy liczb zmiennoprzecinkowych.


W kolejnej instrukcji zmiennej BodyDiff przypisujemy różnicę między cenami otwarcia (open) i zamknięcia (close) świecy z indeksem 0, ponieważ kilka linijek wyżej zmiennej i przypisaliśmy 0.

Kod 5
   BodyDiff    = Open[i] - Close[i];

Na marginesie powiem, że do póki ta świeca jeszcze nie została uformowana, to jej close zawsze będzie równe bieżącej cenie notowania. Dopiero w momencie kiedy pojawi się nowa świeca można mówić, że jej close już nie będzie się zmieniać. Dlatego jeśli uruchomisz ten skrypt zaraz po pojawieniu się świeżej świecy, może się okazać że jej ciało jest mniejsze niż N punktów, ale bliżej końca czasu jej uformowania może ona mieć już ponad N punktów. Po prostu ciało świecy z indeksem 0 cały czas się zmienia. Gdybyś chciał z poszukiwań wyeliminować tę świecę, to należało by wcześniej zapisać i = 1 albo utworzyć zewnętrzny parametr, gdzie użytkownik mógłby sam zadecydować od jakiej świecy rozpocząć poszukiwania. Nie będziemy tu tego robić, chciałem tylko wyjaśnić taki szczegół.


Są świece, dla których open będzie mniejsze niż close i dla nich różnica (kod 5) będzie ujemna. Ponieważ interesuje nas wartość bezwzględna, użyjemy funkcji MathAbs(). W jej nagłówku zapisaliśmy BodyDiff a wynik przypisujemy BodyDiffAbs, który zawsze będzie miał wartość dodatnią (kod 6).

Kod 6
   BodyDiffAbs = MathAbs(BodyDiff);

Teraz zastanówmy się, co by było gdybyśmy porównywali BodyDiffAbs bezpośrednio z RealPoints. Załóżmy, że dla jakiejś świecy open była by np. 1.12001 a close 1.12501. Wtedy wartość bezwzględna ich różnicy będzie 0.00500. Każdy trader powie, że to różnica 500 punktów dla 5-cyfrowego notowania. Ale jeśli taką liczbę 0.00500 porówna się bezpośrednio z liczbą 200 punktów no to 0.00500 na pewno będzie mniejsza niż 200. W związku z tym należy albo 0.00500 przekształcić na ilość punktów (tj. na 500), albo 200 przekształcić na rzeczywistą różnicę (tj. na 0.00200). W naszym kodzie 200 przerobimy na 0.00200.

Jak tego dokonać? 200 podzielimy na 100000. A 100000 to 10 do potęgi 5. A skąd wziąć cyfrę 5? A to właśnie jest dokładność notowania ceny (rys. 4), która jest przechowywana w predefiniowanej zmiennej _Digits (kod 7).

Kod 7
   CalcDiff    = RealPoints / MathPow(10 , _Digits);

Użyjemy funkcji MathPow(), która obliczy wynik jako 1-szy argument do potęgi 2-ego argumentu. Zapis MathPow(10 , _Digits) należy rozumieć tak: 10 - to 1-szy argument, a _Digits - to 2-gi argument. Ponieważ dla 5-cyfrowego notowania _Digits jest równe 5, to funkcja obliczy 10 do potęgi 5 co da nam 100000. W powyższej formule (kod 7) widzimy matematyczne działanie: RealPoints dzielimy przez wynik obliczenia funkcji, tj. 200 / 100000 = 0.00200 i już ta cyfra zostanie przypisana zmiennej CalcDiff.

Jeśli dobrze znasz MQL4 to pewnie chciałbyś mnie zapytać: czemu nie użyłem predefiniowanej zmiennej _Point ? Tak, stosując ją można było by nawet nie robić żadnych obliczeń w switch. Jednak celowo tego nie zrobiłem, dlatego że chciałem pokazać trik ze switch-em.

Jak myślisz, a czy dałoby się dla 2 części kodu zaaplikować kurację odchudzającą? Tak, można. Pokażę to na poniższym przykładzie.

Kod 8
//--- CZĘŚĆ 2 - było
   int    i = 0;             // indeks świecy
   double BodyDiff    = 0.0; // różnica open i close
   double BodyDiffAbs = 0.0; // wartość bezwzględna różnicy open i close
   double CalcDiff    = 0.0; // poszukiwana długość ciała świecy
//---
   BodyDiff    = Open[i] - Close[i];
   BodyDiffAbs = MathAbs(BodyDiff);
   CalcDiff    = RealPoints / MathPow(10 , _Digits);


//--- CZĘŚĆ 2 - zmiana 1
   int    i = 0;             // indeks świecy
   double BodyDiffAbs = 0.0; // wartość bezwzględna różnicy open i close
   double CalcDiff    = 0.0; // poszukiwana długość ciała świecy
//---
   BodyDiffAbs = MathAbs( Open[i] - Close[i] );
   CalcDiff    = RealPoints / MathPow(10 , _Digits);


//--- CZĘŚĆ 2 - zmiana 2
   int    i = 0;             // indeks świecy
//---
   double BodyDiffAbs = MathAbs( Open[i] - Close[i] );      // wartość bezwzględna różnicy open i close
   double CalcDiff    = RealPoints / MathPow(10 , _Digits); // poszukiwana długość ciała świecy

Zmiana 1. Nie tworzymy zmienną BodyDiff, a obliczenie róźnicy między open a close przenieśliśmy do nagłówka funkcji MathAbs().

Zmiana 2. Deklarując BodyDiffAbs i CalcDiff przypisujemy im nie zerowe wartości, a od razu wynik obliczenia i przy tym zostajemy.


Część 3


Kod 9
void OnStart()
  {
//--- CZĘŚĆ 1
//--- CZĘŚĆ 2

//--- CZĘŚĆ 3
   while(BodyDiffAbs < CalcDiff)
     {
      //--- zwiększamy indeks 'i' o 1
      i++;
      //--- sprawdzamy czy istnieje świeca o takim indeksem
      if(i > Bars - 1)
        {
         Print("Świecy nie znaleziono."); return;
        }
      //--- obliczamy długość ciała świecy z indeksem 'i'
      BodyDiffAbs = MathAbs( Open[i] - Close[i] );
     }

//--- CZĘŚĆ 4
  }

Jak działa operator pętli while już wiemy z rozdziału 2.8. Operator pętli while. Kiedy program dojdzie do tego miejsca to na samym początku w jego nagłówku, tj. między (), porówna on obliczoną wcześniej długość ciała świecy o indeksie 0 (BodyDiffAbs) ze zmienną CalcDiff. W przypadku jeśli już w tym momencie BodyDiffAbs nie będzie mniejsze od CalcDiff, tj. wynik porównania - fałsz, wtedy żadne działania zapisane w ciele operatora, tj. między {}, nie zostaną uruchomione i skrypt przejdzie do części 4.

Jeśli wynik porównania BodyDiffAbs < CalcDiff będzie prawdą, to dopiero wtedy program przejdzie do realizacji działań zapisanych wewnątrz {}. Najpierw inkrementujemy, tj. zwiększamy, i o 1 (i++) . Następnie operatorem if sprawdzamy czy świeca o takim indeksie istnieje na wykresie notowań. Co jeśli użytkownik będzie chciał znaleźć świecę o długości ciała 1000000 punktów? Wtedy może się okazać tak, że po przeanalizowaniu wszystkich dostępnych na wykresie świec program zacznie szukać nieistniejącej świecy, a nie możemy do tego dopuścić.

Aby nie wyjść poza obszar przeszukiwanych świec w ciele while zapisaliśmy następujący warunek.

Kod 10
      //--- sprawdzamy czy istnieje świeca o takim indeksie
      if(i > Bars - 1)
        {
         Print("Świecy nie znaleziono."); return;
        }

Predefiniowana zmienna Bars przechowuje ilość świec dostępnych na wykresie. Pamiętając o tym, że pierwsza świeca ma indeks 0, wiemy że ostatnia będzie równać się jako ich ogólna ilość minus 1. Np. jeśli trader w MetaTrader 4 ograniczy ilość świec na wykresie do 5000, wtedy ostatnia będzie miała indeks 4999. Uruchomiając skrypt na takim wykresie, Bars bedzie równe 5000 i właśnie dlatego od niej trzeba odjąć 1, aby znać indeks ostatniej świecy. Po każdej inkrementacji i będzie ona porównywana z indeksem ostatniej świecy if(i > Bars - 1) i jeśli wynik porównania - fałsz, wtedy żadne z działań znajdujące się w ciele operatora if, ani Print() ani return, nie zostaną zrealizowane.

Wyobraźmy sobie sytuację, że na wykresie nie będzie żadnej świecy o żądanej długości ciała. Po analizie świecy z ostatnim indeksem 4999 zmienna i stanie się równa 5000 i wtedy warunek w nagłówku if będzie prawdą. Wtedy Print() poinformuje "Świecy nie znaleziono", a operator przerywania return od tego miejsca przerwie działanie całego skryptu.

Jeśli if przepuści program dalej to zmiennej BodyDiffAbs zostanie przypisana wartość bezwzględna różnicy open i close świecy z indeksem i. Jest to ostania instrukcja w ciele operatora while i potem rozpocznie się kolejna iteracja, gdzie BodyDiffAbs zawierająca długość ciała świecy 1 znowu będzie porównywana z CalcDiff i tak w kółko. Jeśli warunek w nagłówku while będzie prawdziwy to i zostanie zwiększone o 1, if sprawdzi czy to nie jest ostatnia świeca na wykresie i jeśli nie to pod koniec BodyDiffAbs uzyska różnicę open i close świecy z indeksem 2. Takie BodyDiffAbs z nową wartością zostanie podstawione do nagłowka while aby znowu zostać porównane z CalcDiff.

Załóżmy, że dla indeksu 2 w nagłówku while warunek będzie fałszywy, tj. BodyDiffAbs nie będzie mniejsze od CalcDiff. Wtedy pętla zostanie przerwana. Nic co jest zapisane w ciele operatora while nie zostanie wykonane, i nie zostanie zwiększone o 1 i nadal będzie równe 2. Program przejdzie do części 4.


Część 4


Kod 11
void OnStart()
  {
//--- CZĘŚĆ 1
//--- CZĘŚĆ 2
//--- CZĘŚĆ 3

//--- CZĘŚĆ 4
//--- zapamiętujemy datę i czas utworzenia świecy z indeksem 'i'
   datetime TimeValue = Time[i];
//--- wyświetlić informację o świecy
   Print("Świeca " , TimeToString(TimeValue) ,
         " ma długość ciała równe lub większe niż " , IntegerToString(InpPoints) ,
         " punktów.");
  }

Interpretacja tej części nie powinna być skomplikowana. Tworzymy zmienną typu datetime o nazwie TimeValue, której od razu przypisujemy wartość z predefiniowanej tablicy Time[] dla indeksu i. Następnie funkcja Print() w logach terminala wyświetli stosowne informacje. Dla skrócenia kodu Time[i] od razu podstawimy do funkcji TimeToString() (kod 12).

Kod 12
//--- CZĘŚĆ 4
//--- wyświetlić informację o świecy
   Print("Świeca " , TimeToString( Time[i] ) ,
         " ma długość ciała równe lub większe niż " , IntegerToString(InpPoints) ,
         " punktów.");

Teraz pozbierajmy w jedną całość kod skryptu.

Kod 13
#property strict
#property script_show_inputs

input uint InpPoints = 20; // Ilość punktów dla 4-cyfrowego notowania.

void OnStart()
  {
//--- CZĘŚĆ 1
   uint RealPoints = 0;
//--- obliczamy prawidłową ilość punktów w zależności od dokładności notowania
   switch(_Digits)
     {
      case 2: case 4: RealPoints = InpPoints;      break;
      case 3: case 5: RealPoints = InpPoints * 10; break;
      default: Print("Dokładność notowania nie jest znana."); return;
     }

//--- CZĘŚĆ 2
   int    i = 0;             // indeks świecy
//---
   double BodyDiffAbs = MathAbs( Open[i] - Close[i] );      // wartość bezwzględna różnicy open i close
   double CalcDiff    = RealPoints / MathPow(10 , _Digits); // poszukiwana długość ciała świecy
  
//--- CZĘŚĆ 3
   while(BodyDiffAbs < CalcDiff)
     {
      //--- zwiększamy indeks  'i' o 1
      i++;
      //--- sprawdzamy czy istnieje świeca o takim indeksie
      if(i > Bars - 1)
        {
         Print("Świecy nie znaleziono."); return;
        }
      //--- obliczamy długość ciała świecy z indeksem 'i'
      BodyDiffAbs = MathAbs( Open[i] - Close[i] );
     }

//--- CZĘŚĆ 4
//--- wyświetlić informację o świecy
   Print("Świeca " , TimeToString( Time[i] ) ,
         " ma długość ciała równe lub większe niż " , IntegerToString(InpPoints) ,
         " punktów.");
  }

ZAKOŃCZENIE

To był ostatni rozdział lekcji z serii "MQL4 dla początkujących", gdzie postarałem się przedstawić podstawy programowania w języku MetaQuote Language 4. Jeśli z nadmiaru nowych informacji, wrażeń i adrenaliny masz teraz w głowie lekki zamęt, to przypomnij sobie początki swojej nauki matematyki w szkole. Zakładam, że nie były one łatwe, ale po tym jak opanowałeś dodawanie i odejmowanie mogłeś przejść do mnożenia i dzielenia, a następnie do coraz to bardziej skomplikowanych działań matematycznych. Tak samo i tutaj. Liczę na to, że zachęciłem Cię do kontynuowania dalszej nauki MQL4. W kolejnych lekcjach znajdziesz już bardziej zaawansowane informacje, dlatego jeśli masz jeszcze trudności ze zrozumieniem poprzedniego materiału, proponuję jeszcze raz wrócić do odpowiedniego rozdziału.