DEV Community

Omais Ali
Omais Ali

Posted on

Writing an MQL5 trade journal to CSV with FileOpen so every EA fill is on record

The Experts log scrolls away, and the terminal history does not record why the EA took a trade. A small CSV journal fixes both. Every time a deal is added, the EA writes one line with the time, symbol, direction, volume, price, profit, and a reason string. You can open the file in a spreadsheet, compare it with the backtest, and answer questions like "why did it buy here" a week later.

Where the file goes

  1. FileOpen without FILE_COMMON writes to MQL5/Files inside the terminal data folder. In the Strategy Tester that becomes the agent's own sandbox folder, which is deleted or hard to find.
  2. With FILE_COMMON the file lands in the shared Terminal/Common/Files folder. Every terminal on the machine and the tester agents can reach it, so it is the easiest place for a journal.
  3. MQL5 cannot write outside these sandboxes. Paths like C:\Logs are refused by design.

The journal code

input string JournalName = "ea_journal.csv";

string g_lastReason = "";

bool JournalWrite(const string &fields[])
{
   int h = FileOpen(JournalName, FILE_READ | FILE_WRITE | FILE_CSV | FILE_COMMON | FILE_SHARE_READ, ',');
   if(h == INVALID_HANDLE)
   {
      Print("Journal open failed, error ", GetLastError());
      return false;
   }

   bool isNew = (FileSize(h) == 0);
   FileSeek(h, 0, SEEK_END);

   if(isNew)
      FileWrite(h, "time", "symbol", "type", "volume", "price", "profit", "magic", "reason");

   FileWrite(h, fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6], fields[7]);
   FileClose(h);
   return true;
}

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
{
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
      return;
   if(!HistoryDealSelect(trans.deal))
      return;

   string fields[8];
   fields[0] = TimeToString((datetime)HistoryDealGetInteger(trans.deal, DEAL_TIME), TIME_DATE | TIME_SECONDS);
   fields[1] = HistoryDealGetString(trans.deal, DEAL_SYMBOL);
   fields[2] = (HistoryDealGetInteger(trans.deal, DEAL_TYPE) == DEAL_TYPE_BUY) ? "buy" : "sell";
   fields[3] = DoubleToString(HistoryDealGetDouble(trans.deal, DEAL_VOLUME), 2);
   fields[4] = DoubleToString(HistoryDealGetDouble(trans.deal, DEAL_PRICE), (int)SymbolInfoInteger(fields[1], SYMBOL_DIGITS));
   fields[5] = DoubleToString(HistoryDealGetDouble(trans.deal, DEAL_PROFIT), 2);
   fields[6] = IntegerToString(HistoryDealGetInteger(trans.deal, DEAL_MAGIC));
   fields[7] = g_lastReason;

   JournalWrite(fields);
}
Enter fullscreen mode Exit fullscreen mode

Before calling OrderSend, set g_lastReason to something short and specific, such as "breakout above 20 bar high" or "time exit after 12 bars". That string is the part the terminal history will never give you.

Practical points

  1. Open, append, and close on each write. Holding the handle open for days risks losing buffered lines if the terminal crashes, and it blocks other programs from reading the file.
  2. FILE_SHARE_READ lets you open the CSV in a spreadsheet while the EA is still running. Close the spreadsheet before the EA writes, or use a copy, because some editors lock the file.
  3. Use a comma delimiter and avoid commas inside the reason text, or wrap the reason in quotes yourself.
  4. Put the symbol and magic number in the file name if several EAs or charts share the Common folder, so two instances never append to the same file at once.
  5. In the tester, write a different file name per run so optimization passes do not interleave lines.
  6. Check FileOpen for INVALID_HANDLE and print the error. Error 5004 usually means a wrong path or a file locked by another program.

A journal that records the reason next to each fill turns "the EA did something odd" into a line you can read, sort, and compare with the backtest.

Top comments (0)