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

4.1. Przykład kombinacji operatorów for oraz if

W oparciu o wiedzę, którą uzyskałeś w poprzednich rozdziałach tej serii lekcji, teraz spróbujmy utrwalić nasze umiejętności na konkretnych przykładach. Stwórzmy skrypt, który na odcinku 100 świec znajdzie świecę z najwyższą ceną high. Następnie, wartość tego high oraz czas utworzenia tej świecy wyświetlimy w logach terminala MT4. Niżej przedstawiam kod tego skryptu, a dalej rozłożymy go na czynniki pierwsze.

Kod 1
#property strict
#property script_show_inputs
//---
input uchar BarsNumber = 100; // Ilość świec.
//---
void OnStart()
  {
   double   Value     = 0.0;
   datetime TimeValue = 0;
//---
   for(int i = 0; i < BarsNumber; i++)
     {
      if(High[i] > Value)
        {
         Value     = High[i];
         TimeValue = Time[i];
        }
     }
//---
   string NumberBar    = IntegerToString(BarsNumber);
   string TimeBar      = TimeToString(TimeValue);
   string HighestValue = DoubleToString(Value , _Digits);
//---
   Print("Wśród " , NumberBar ,
         " świec, świeca z datą utworzenia " , TimeBar ,
         " ma najwyższy high " , HighestValue);
  }

Najpierw za pomocą komendy #property strict określamy zastosowanie nowego kompilatora. Komenda #property script_show_inputs pozwoli użytkownikowi wpisać ilości świec po uruchomieniu skryptu w terminale MetaTrader 4.

Zapis //--- to są komentarze w źródłowym kodzie, które są ignorowane przez kompilator i nie wpływają na działanie programu. Umieściłem te jednowierszowe komentarze, żeby po prostu wizualnie oddzielić od siebie poszczególne bloki kodu.

Za pomocą input zmienna o nazwie BarsNumber stanie się zewnętrznym parametrem, gdzie użytkownik będzie mógł określić ilość przeszukiwanych świec. Typ uchar określa typ tej zmiennej, której od razu przypisano domyślną wartość 100. W tej samej linijce na końcu znajduje się komentarz // Ilość świec, dzięki czemu w oknie wyboru wartości zewnętrznej zmiennej zobaczymy ten zapis zamiast nazwy zmiennej (rys. 1).

Rys. 1. Okienko z zewnętrznym parametrem.


OnStart() to funkcja główna skryptu, a void, zapisany przed nią, mówi nam, że jest to typ pusty. Wszystkie działania, które ten skrypt ma zrealizować, należy umieścić pomiędzy klamrami {}, tj. w ciele funkcji.

Kod 2
void OnStart()
  {
   // to co ma wykonać skrypt
  }

W pierwszej kolejności w funkcji głównej skryptu utworzono dwie zmienne. Pierwsza ma typ double , która będzie służyć do przechowywania wartości najwyższego znalezionego high. Druga ma typ datetime , która będzie służyć do przechowywania czasu utworzenia świecy o najwyższym high. Od razu przypisujemy im zerowe wartości. W rozdziale 3.7. Typy zmiennych: typy liczb zmiennoprzecinkowych opisałem powód dlaczego w zmiennych typu double po kropce należy pisać cyfrę, a co najmniej 0.

Kod 3
   double   Value     = 0.0;
   datetime TimeValue = 0;

Zaraz po nich idzie operator pętli for .

Kod 4
   for(int i = 0; i < BarsNumber; i++)
     {
      // powtarzalne działania wewnątrz pętli
     }

W ciele tego operatora jako pierwszy argument idzie inicjalizacja zmiennej i typu int, która będzie odgrywać rolę licznika w pętli i jednocześnie indeksu w predefiniowanej tablicy najwyższych cen świec High[]. Zaczynamy od 0 dzięki czemu skrypt zacznie sprawdzać od świecy o takim indeksie (int i = 0).

W drugim argumencie (i < BarsNumber) ograniczamy ilość sprawdzanych świec do 100. Pamiętamy, że indeksacja świec zaczyna się od 0 dla świecy, która znajduje się jako pierwsza po prawej stronie w oknie notowań. Tym zapisem ograniczamy licznik do 99, ponieważ dla warunku i < 100 maksymalna wartość licznika może być właśnie 99. Gdybyśmy użyli znaku <= wtedy warunek i <= 100 nadal był by prawdziwy i skrypt musiał by ocenić świecę z takim indeksem. Z kolei licząc od 0 do 100 jest to już 101 elementów a nie 100.

Wewnątrz tej pętli widzimy operator if, gdzie najwyższa cena każdej świecy High[i] porównuje się ze zmienną Value. Predefiniowana tablica High[] zawiera ceny high wszystkich świec wykresu, na którym zostanie uruchomiony skrypt, dlatego wystarczy tylko podstawić odpowiedni indeks świecy aby znaleźć jej high.

Podczas pierwszej iteracji, gdy i = 0, obliczone High[0] da nam high świecy z indeksem 0. Przy i = 0 dla świecy z indeksem 1 itd, aż do 99.

Kod 5
   for(int i = 0; i < BarsNumber; i++)
     {
      if(High[i] > Value)
        {
         // działania realizowane jeśli porównanie w 'íf' jest prawdą
        }
     }

W przypadku jeśli wartość high świecy z indeksim i okaże się większa niż wartość Value, tj. porównanie High[i] > Value będzie prawdą, wtedy program przejdzie do realizacji dwóch instrukcji zapisanych w ciele operatora if. Potem licznik i zostanie powiększony o 1 (i++) i zacznie się kolejna pętla (iteracja). Jeśli w if będzie fałsz to program od razu przejdzie do nowej iteracji.

Kod 6
      if(High[i] > Value)
        {
         Value     = High[i];
         TimeValue = Time[i];
        }

Pierwsza iteracja (i = 0)

W nagłówku if do tablicy High[] zamiast i zostanie podstawiony indeks 0. W tej tablice skrypt znajdzie wartość high dla świecy o takim indeksie i ta wartość zostanie porównana z 0.0, gdyż podczas utworzenia zmiennej Value przypisaliśmy jej 0.0. Ponieważ cena dowolnego instrumentu finansowego zawsze jest większa niż 0, na pierwszej iteracji warunek High[0] > 0.0 - to prawda i program przejdzie do dwóch instrukcji: Value = High[i] oraz TimeValue = Time[i]. Teraz zmiennej Value zostanie przypisana nowa wartość, a do TimeValue nowa wartość czasu utworzenia świecy z indeksem 0 z predefiniowanej tablicy Time[]. Koniec iteracji.

Druga iteracja (i = 1)

Przed jej rozpoczęciem for sprawdzi czy licznik i nie jest większy niż BarsNumber. Jest ok (1 < 100 - prawda) i dopiero potem program przejdzie do if, gdzie porówna cenę high świecy z indeksem 1 z wartością Value. Ponieważ na poprzedniej iteracji zmiennej Value przypisano wartość High[0] teraz nie wiemy czy warunek High[1] > Value to prawda czy fałsz. Jeśli fałsz - to ani Value ani TimeValue nie zostaną zmienione i program rozpocznie kolejną iterację. Jeśli prawda - to Value zostanie zamienione na High[1], a TimeValue na Time[1] a dopiero potem rozpocznie się kolejna iteracja.

I tak na każdej iteracji. Wartość high sprawdzanej świecy będzie porównywana z maksymalnym high jaki udało się wcześniej znaleźć. Jeśli sprawdzany high okaże się większy, to jego wartość i czas tej świecy zostaną zapamiętane. Jeśli mniejszy lub równy, to nie.


Kiedy wszystkie iteracji w for się zakończą, są tworzone 3 tekstowe zmienne NumberBar, TimeBar oraz HighestValue.

Kod 7
   string NumberBar    = IntegerToString(BarsNumber);
   string TimeBar      = TimeToString(TimeValue);
   string HighestValue = DoubleToString(Value , _Digits);

Ponieważ dalej za pomocą funkcji Print() przewiduje się wyświetlenie informacji o ilości przeszukanych świec, o czasie utworzenia świecy oraz wartości high, to najpierw te wartości należy przekonwertować do string. W tym celu funkcja IntegerToString() konwertuje typ liczby całkowitej uchar na string, TimeToString() konwertuje typ daty i czasu datetime na string, a DoubleToString() konwertuje typ liczby zmiennoprzecinkowej double na string (kod 7).

W rozdziale 3.9. Przekształcenie double do string opisałem sposób działania DoubleToString(). Logika działania pozostałych dwóch funkcji jest identyczna. Dodam, że w nagłówku tej funkcji jako drugi argument stoi predefiniowana zmienna _Digits. Zawiera ona ilość znaków po przecinku w notowaniu instrumentu finansowego.

Na poniższym obrazku widać przykład działania tego skryptu na wykresie EURUSD. W logach terminala w zakładce Strategie widzimy, że najwyższy high ma 5 znaków po kropce. Uruchom skrypt na parze walutowej z inną dokładnością pomiaru ceny, np. na USDJPY z dokładnością 3 znaków, a zobaczysz że skrypt wyświetli high tylko z 3 znakami po kropce. Jeśli usuniesz _Digits z funkcji DoubleToString(), wtedy zostanie dopisane tyle zer aby ilość znaków po kropce stanowiła 8 cyfr, co nie jest nam potrzebne.

Rys. 2. Wynik działania skryptu.


A co w przypadku gdyby na badanym odcinku znajdowałoby się dwie lub więcej świec z takimi samymi najwyższymi cenami high? Czas jakiej świecy pokaże nam skrypt? Odpowiedź - czas świecy bliżej początku przeszukiwanego odcinka, tj. bliżej prawej strony na wykresie. To dlatego, że w nagłówku operatora if zapisano znak porównania > (więcej). Jeśli program napotka się na drugą świecę z takim samym high to warunek High[i] > Value będzie fałsz i program rozpocznie kolejną iterację nie zmieniając ani Value ani TimeValue. Gdybyśmy zapisali >= (więcej lub równe) to skrypt pokaze czas świecy znajdującej się bliżej końca, tj. lewej strony, przeszukiwanego odcinka.


Choć skrypt działa prawidłowo i można było by go zostawić w spokoju, to jednak posiada on jeszcze potencjał do modernizacji. Uczmy się pisać programy profesjonalnie, bez zbędnych operacji, skracając ilość linijek kodu co z kolej przekłada się na szybkość działania programu.

Wiemy już, że jeśli po jakimś operatorze przewiduje się tylko jedno działanie uzależnione od tego operatora, to klamry można nie pisać. Tutaj tylko jeden operator if zależy od for. W tym przypadku klamry dla pętli (zaznaczono na źółto) można opuścić. Z kolej dwie instrukcje, dotyczące Value i TimeValue, są uzależnione od if, dlatego klamry należące od if muszą pozostać (zaznaczono na niebiesko). Tę część kodu można poprawić w następujący sposób:

Kod 8
//--- było
   for(int i = 0; i < BarsNumber; i++)
     {
      if(High[i] > Value)
        {
         Value     = High[i];
         TimeValue = Time[i];
        }
     }

//--- zamieniono na
   for(int i = 0; i < BarsNumber; i++)
      if(High[i] > Value)
        {
         Value = High[i]; TimeValue = Time[i];
        }

Funkcje, które konwertują uchar, datetime i double na string można przenieść w nagłówek funkcji Print() bez tworzenia dodatkowych 3 zmiennych. Tę część kodu można zamienić na:

Kod 9
//--- było
   string NumberBar    = IntegerToString(BarsNumber);
   string TimeBar      = TimeToString(TimeValue);
   string HighestValue = DoubleToString(Value , _Digits);

   Print("Wśród " , NumberBar ,
         " świec, świeca z datą utworzenia " , TimeBar ,
         " ma najwyższy high " , HighestValue);

//--- zamieniono na
   Print("Wśród " , IntegerToString(BarsNumber) ,
         " świec, świeca z datą utworzenia " , TimeToString(TimeValue) ,
         " ma najwyższy high " , DoubleToString(Value , _Digits));

Popatrz, teraz kod skryptu wygląda trochę zgrabniej.

Kod 10
#property strict
#property script_show_inputs
//---
input uchar BarsNumber = 100; // Ilość świec.
//---
void OnStart()
  {
   double   Value     = 0.0;
   datetime TimeValue = 0;
//---
   for(int i = 0; i < BarsNumber; i++)
      if(High[i] > Value)
        {
         Value = High[i]; TimeValue = Time[i];
        }
//---
   Print("Wśród " , IntegerToString(BarsNumber) ,
         " świec, świeca z datą utworzenia " , TimeToString(TimeValue) ,
         " ma najwyższy high " , DoubleToString(Value , _Digits));
  }