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 (2)

Collapse
 
edgelogtrading profile image
Edgelog •

Useful pattern, especially the reason string, which the terminal history never keeps. One thing worth adding: a single position can produce several deals (partial closes, or an IN plus an OUT), so writing DEAL_ENTRY and DEAL_POSITION_ID as columns lets you group the CSV back into positions later. Commission and swap are separate deal properties too (DEAL_COMMISSION, DEAL_SWAP), so the profit column alone understates costs on short trades. And if a balance or credit deal ever reaches that handler, the buy/sell ternary will label it 'sell'.

Collapse
 
omaisali profile image
Omais Ali •

All three are fair, thanks for reading it that closely.

  1. DEAL_POSITION_ID and DEAL_ENTRY belong in the file. With partial closes a single position can easily produce three or four lines, and without the position ID you end up matching them back by time and volume, which breaks as soon as two positions overlap.
  2. Agreed on costs. I would add DEAL_COMMISSION, DEAL_SWAP and DEAL_FEE as their own columns and leave DEAL_PROFIT untouched, so the net result is a sum you can check in the spreadsheet rather than a number the EA already adjusted.
  3. Good catch on the ternary. The cleaner fix is to skip anything that is not DEAL_TYPE_BUY or DEAL_TYPE_SELL right at the top of the handler, since balance, credit and other service deals are not trades and do not belong in a fill journal anyway.

I will fold these into the example.

⌂ 换网址