Dziennik, który da się czytać
Print bez dyscypliny daje tysiąc linii, z których nic nie wynika. Poziomy, kontekst w jednej linii i zapis do pliku, który otworzysz w arkuszu.
Kod z tej strony skompilowany i uruchomiony: MetaEditor build 6090, sprawdzone 2026-08-02.
Dziennik eksperta to zwykle jedno z dwóch: pusty, bo autor usunął wszystkie Print po debugowaniu, albo zapchany, bo zostawił je wszystkie. Oba warianty są bezużyteczne dokładnie wtedy, gdy są potrzebne — po tygodniu pracy, kiedy próbujesz zrozumieć, dlaczego ekspert zrobił coś dziwnego w środę o 14:20.
Trzy zasady
Jedna linia to jedno zdarzenie z kompletem kontekstu. Wpis "Kupuję" nie mówi nic. Musi zawierać wszystko, co potrzebne, żeby zrozumieć sytuację bez zaglądania w sąsiednie linie: instrument, cenę, wolumen, powód.
Poziom ważności. Bez tego nie da się odsiać. Cztery wystarczą.
Format stały. Jeśli wszystkie wpisy mają ten sam układ, da się je przefiltrować, policzyć i wczytać do arkusza. Jeśli każdy jest inny, zostaje czytanie oczami.
Minimalny dziennik
enum ENUM_POZIOM
{
POZIOM_BLAD = 0,
POZIOM_UWAGA = 1,
POZIOM_INFO = 2,
POZIOM_DEBUG = 3
};
input ENUM_POZIOM Szczegolowosc = POZIOM_INFO;
void Zapisz(const ENUM_POZIOM poziom, const string zdarzenie, const string szczegoly)
{
if(poziom > Szczegolowosc)
return;
string etykieta = "";
switch(poziom)
{
case POZIOM_BLAD: etykieta = "BLAD "; break;
case POZIOM_UWAGA: etykieta = "UWAGA"; break;
case POZIOM_INFO: etykieta = "INFO "; break;
default: etykieta = "DEBUG"; break;
}
PrintFormat("%s | %s | %s | %s",
etykieta, _Symbol, zdarzenie, szczegoly);
}Użycie wygląda tak:
Zapisz(POZIOM_INFO, "wejscie",
StringFormat("kierunek=%d wolumen=%.2f cena=%.5f sl=%.5f powod=przeciecie",
kierunek, wolumen, cena, sl));Wpis w dzienniku:
INFO | EURUSD | wejscie | kierunek=1 wolumen=0.03 cena=1.09325 sl=1.09125 powod=przecieciePo tygodniu da się z tego policzyć, ile było wejść, w jakich cenach i z jakiego powodu — bez czytania.
Zapis do pliku, który otworzysz w arkuszu
Zakładka Eksperci nadaje się do zerknięcia. Do analizy nadaje się plik CSV.
void ZapiszDoPliku(const string zdarzenie, const double cena, const double wolumen)
{
string nazwa = StringFormat("dziennik_%s.csv", _Symbol);
//--- FILE_COMMON zapisuje w katalogu wspólnym wszystkich terminali
int uchwyt = FileOpen(nazwa, FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI, ';');
if(uchwyt == INVALID_HANDLE)
{
PrintFormat("Nie mogę otworzyć %s, błąd %d", nazwa, GetLastError());
return;
}
//--- dopisujemy na końcu, nie nadpisujemy
FileSeek(uchwyt, 0, SEEK_END);
FileWrite(uchwyt,
TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS),
_Symbol, zdarzenie,
DoubleToString(cena, _Digits),
DoubleToString(wolumen, 2));
FileClose(uchwyt);
}Plik ląduje w MQL5\Files w katalogu danych terminala. Średnik jako separator i FILE_ANSI sprawiają, że polski arkusz kalkulacyjny otworzy go bez pytań o kodowanie i o to, czy przecinek jest separatorem dziesiętnym.
Czego nie zapisywać
Hasła i numeru rachunku. Dziennik bywa wysyłany do brokera albo wklejany na forum. Numer rachunku w każdej linii to kiepski pomysł.
Danych, które i tak masz. Historia transakcji jest w terminalu i da się ją odczytać przez HistorySelect. Dziennik ma zapisywać to, czego w niej nie ma: dlaczego ekspert podjął decyzję, jakie wartości miały wskaźniki i które warunki nie zostały spełnione.
Ta ostatnia rzecz jest najważniejsza i najczęściej pomijana. Wpis o zawartej transakcji da się odtworzyć z historii. Wpis o transakcji, której nie zawarłeś, bo spread był za szeroki — nie da się odtworzyć znikąd.
if(!SpreadDopuszczalny(_Symbol, MaksymalnySpread))
{
Zapisz(POZIOM_UWAGA, "sygnal-odrzucony", "powod=spread");
return;
}Po miesiącu takie wpisy odpowiadają na pytanie, czy filtr w ogóle miał sens — i to jest jedyne źródło tej odpowiedzi.