Iluzja dokładności: arytmetyka cen a odrzucone zlecenia
Dlaczego zlecenie oczekujące nie aktywuje się mimo przebicia ceny i skąd biorą się odrzucenia poziomów. Typ double, normalizacja i bezpieczne porównania.
3 fragmenty kodu z tej strony przeszły przez kompilator MetaEditor build 6090, 2026-08-05.
Każdy programista MQL5 prędzej czy później napotyka mistyczny błąd 10016 (Invalid stops) lub sytuację, w której zlecenie oczekujące (Buy Limit/Sell Stop) nie zostaje aktywowane, mimo że wykres wyraźnie "przebił" cenę docelową. Innym częstym objawem jest Trailing Stop, który modyfikuje Stop Loss kilkadziesiąt razy na sekundę, a potem nagle przestaje działać. Winowajcą w 90% przypadków nie jest błąd brokera ani terminala, lecz arytmetyka zmiennoprzecinkowa IEEE 754 i fundamentalne niezrozumienie, jak komputery przechowują ułamki.
W tym artykule przeprowadzimy dogłębną analizę tego, jak MQL5 przetwarza ceny, dlaczego funkcja NormalizeDouble nie jest magiczną różdżką, i jak skonstruować w 100% bezpieczne porównania cenowe.
1. Problem Reprezentacji Dziesiętnej w Systemie Binarnym
Ludzie liczą w systemie dziesiętnym. Komputery w binarnym. Ułamek 1/2 (0.5) daje się łatwo zapisać w binarnym systemie (2^-1). Ale ułamek 1/5 (0.2) nie ma dokładnego, skończonego odpowiednika w systemie binarnym. Zostaje zapisany jako nieskończony ułamek okresowy. Typ double w MQL5 (64-bitowa liczba zmiennoprzecinkowa) posiada 15-17 znaczących cyfr dziesiętnych. Oznacza to, że liczba 1.10000 w pamięci terminala to w rzeczywistości coś w stylu 1.1000000000000001 lub 1.0999999999999999.
Problem eskaluje, gdy zaczynamy wykonywać operacje matematyczne. Jeśli do 1.10000 dodasz 0.00010 (10 punktów), wynikiem w pamięci może być 1.1001000000000001. Kiedy wyślesz tę wartość jako nowy Stop Loss do brokera, serwer (który działa na własnej architekturze) może odrzucić zlecenie, uznając je za nieważne ze względu na przekroczenie limitu miejsc po przecinku (choć z punktu widzenia programisty jest to 5. miejsce po przecinku).
2. Dlaczego NormalizeDouble() Nie Rozwiązuje Wszystkiego?
Funkcja NormalizeDouble(price, digits) zaokrągla liczbę do określonej liczby miejsc po przecinku. Jest niezbędna przed wysłaniem jakiegokolwiek zlecenia do brokera. Jednak nie gwarantuje ona idealnej równości matematycznej.
double a = 1.1;
double b = 0.2;
double c = a + b; // Często nie wynosi dokładnie 1.3
// Po NormalizeDouble wynik to nadal double, a więc nadal może mieć błędy zaokrągleń na 15. cyfrze!
double c_norm = NormalizeDouble(c, 5);
// ZŁE PODEJŚCIE:
if(c_norm == 1.3) { // Ten warunek może się NIE spełnić!
Print("Równe");
}
// NAJGORSZY ANTYWZORZEC W TRADINGU (Trailing Stop):
MqlTick tick;
if(!SymbolInfoTick(_Symbol, tick))
return;
double newSL = NormalizeDouble(tick.bid - 10 * _Point, _Digits);
if(newSL != currentSL) { // To porównanie zawiedzie przy każdym ticku!
// MQL5 widzi różnicę na 15. miejscu po przecinku i modyfikuje zlecenie w kółko.
trade.PositionModify(_Symbol, newSL, currentTP);
}3. Właściwe Podejście: Tolerancja (Epsilon) i Matematyka Punktów
Aby rozwiązać ten problem, w informatycznej finansowej stosuje się podejście oparte na tolerancji (tzw. epsilon). Zamiast sprawdzać, czy dwie ceny są równe, sprawdzamy, czy różnica między nimi jest mniejsza niż akceptowalny margines błędu. W MQL5 naturalnym i najbezpieczniejszym marginesem jest _Point (najmniejszy możliwy ruch ceny na danym instrumencie).
Zamiast modyfikować zlecenie przy każdej zmianie zmiennoprzecinkowej, powinniśmy modyfikować je tylko wtedy, gdy cena zmieniła się o co najmniej jeden pełny punkt.
// Bezpieczne porównanie cen:
bool IsPriceEqual(double price1, double price2) {
return MathAbs(price1 - price2) < _Point / 2.0;
}
// Bezpieczny Trailing Stop:
void ManageTrailingStop() {
if(!PositionSelect(_Symbol)) return;
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
int trailingPoints = 50; // 50 punktów (5 pipów)
double newSL = NormalizeDouble(bid - trailingPoints * _Point, _Digits);
// Jeśli nowy SL jest wyższy niż obecny o CO NAJMNIEJ 1 punkt
if(newSL > currentSL + _Point) {
// Modyfikujemy tylko wtedy, gdy różnica jest znacząca makroekonomicznie,
// a nie tylko na 15. miejscu po przecinku.
if(trade.PositionModify(_Symbol, newSL, currentTP)) {
Print("Trailing Stop przesunięty na: ", newSL);
}
}
}4. Prawdziwa Pułapka: Stoplevels i Błąd 10016
Nawet po zastosowaniu NormalizeDouble i poprawnego porównania, EA wciąż może otrzymywać błąd 10016 (Invalid stops). Dlaczego? Ponieważ serwer brokera narzuca minimalny dystans (w punktach) między bieżącą ceną (Bid/Ask) a poziomami Stop Loss / Take Profit / zleceniami oczekującymi. Nazywa się to SYMBOL_TRADE_STOPS_LEVEL.
Wielu programistów ustawia SL np. na 10 punktów od ceny rynkowej. Jeśli broker ma StopsLevel ustawiony na 20, serwer odrzuci zlecenie. Co gorsza, na rynkach o dużej zmienności lub u brokerów ECN, Stops Level potrafi być zmienny (dynamiczny) i zmienia się z minuty na minutę w zależności od spreadu!
// Profesjonalne obliczenie bezpiecznego Stop Lossu
double CalculateSafeStopLoss(bool isBuy, int desiredStopPoints) {
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
// 1. Pobieramy aktualny minimalny dystans SL/TP od brokera
double stopsLevel = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minStopDistance = stopsLevel * _Point;
// 2. Ustalamy pożądany dystans
double desiredDistance = desiredStopPoints * _Point;
// 3. Wybieramy większy z nich (zawsze bezpieczny wybór)
// Dodajemy 1 punkt marginesu bezpieczeństwa, aby uniknąć błędów zaokrągleń.
double finalDistance = MathMax(desiredDistance, minStopDistance) + _Point;
double sl = 0.0;
if(isBuy) {
sl = bid - finalDistance;
} else {
sl = ask + finalDistance;
}
// 4. Zawsze Normalizujemy przed zwrotem/wysłaniem
return NormalizeDouble(sl, _Digits);
}Podsumowanie
Ceny na rynkach finansowych to ciągłe ułamki, a komputery to maszyny stanów skończonych. Zderzenie tych dwóch światów tworzy przestrzeń dla subtelnych, trudnych do wykrycia błędów logicznych. Pamiętaj: liczby zmiennoprzecinkowe w MQL5 to zawsze przybliżenia. Nigdy nie ufaj operatorowi równości (==) przy cenach. Zawsze stosuj tolerancję opartą na _Point, rygorystycznie używaj NormalizeDouble wyłącznie na samym końcu tuż przed wysłaniem zlecenia, a odczytuj StopsLevel w czasie rzeczywistym. Przejście na ten sposób myślenia to moment, w którym przestajesz pisać skrypty, a zaczynasz budować niezawodne systemy handlowe.