Edgoro
Expert Advisor · MQL5

Write closed trades to a CSV file in MQL5

Short answer

Catch each new deal in OnTradeTransaction() (TRADE_TRANSACTION_DEAL_ADD), keep the closing ones (DEAL_ENTRY_OUT), read them with HistoryDealGet*(), and append a row with FileOpen(name, FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI, ';'), FileSeek(h, 0, SEEK_END), FileWrite() and FileClose(). The file is written to the terminal's MQL5/Files folder.

UsesOnTradeTransactionHistoryDealSelectHistorySelectByPositionFileOpenFileSeekFileWrite

The code

An Expert Advisor that trades nothing: attach it to any one chart and it appends a row for every position closed on the account, with the file's full path printed at start.

Save it as MQL5/Experts/write-trades-to-csv.mq5 and compile it in MetaEditor (F7): 0 errors, 0 warnings.

MQL5
input string InpFile      = "closed_trades.csv"; // File name, in MQL5\Files
input bool   InpCommon    = false;               // Write to the Common\Files folder shared by all terminals
input string InpSeparator = ";";                 // Column separator (";" for Excel with a decimal comma)
input long   InpMagic     = -1;                  // Trades to log (-1 = all, 0 = manual)

int OnInit()
  {
   string folder = InpCommon ? TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files"
                             : TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files";
   PrintFormat("Closed trades are written to %s\\%s", folder, InpFile);
   return INIT_SUCCEEDED;
  }

// Opens the file for appending, writes the header if it's new, adds one row
bool AppendRow(const string time, const long position, const string symbol, const string side, const double volume,
               const double openPrice, const double closePrice, const double profit, const double commission,
               const double swap, const long magic, const string comment)
  {
   int flags = FILE_READ | FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_SHARE_READ;
   if(InpCommon)
      flags |= FILE_COMMON;
   ResetLastError();
   int h = FileOpen(InpFile, flags, (short)StringGetCharacter(InpSeparator, 0));
   if(h == INVALID_HANDLE)
     {
      PrintFormat("FileOpen %s failed: error %d (is it open in Excel?)", InpFile, GetLastError());
      return false;
     }
   if(FileSize(h) == 0)
      FileWrite(h, "close_time", "position", "symbol", "side", "volume", "open_price", "close_price",
                "profit", "commission", "swap", "magic", "comment");
   FileSeek(h, 0, SEEK_END);
   int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
   FileWrite(h, time, position, symbol, side, DoubleToString(volume, 2), DoubleToString(openPrice, digits),
             DoubleToString(closePrice, digits), DoubleToString(profit, 2), DoubleToString(commission, 2),
             DoubleToString(swap, 2), magic, comment);
   FileClose(h);   // closed after every row: nothing is lost if the terminal stops
   return true;
  }

// Every new deal arrives here. Closing deals (OUT, INOUT, OUT_BY) are written to the file.
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result)
  {
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD || !HistoryDealSelect(trans.deal))
      return;
   long entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
   if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT && entry != DEAL_ENTRY_OUT_BY)
      return;
   long magic = HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
   if(InpMagic >= 0 && magic != InpMagic)
      return;

   // Read everything about the closing deal before HistorySelectByPosition() replaces the selection
   long     position   = HistoryDealGetInteger(trans.deal, DEAL_POSITION_ID);
   string   symbol     = HistoryDealGetString(trans.deal, DEAL_SYMBOL);
   datetime time       = (datetime)HistoryDealGetInteger(trans.deal, DEAL_TIME);
   double   volume     = HistoryDealGetDouble(trans.deal, DEAL_VOLUME);
   double   closePrice = HistoryDealGetDouble(trans.deal, DEAL_PRICE);
   double   profit     = HistoryDealGetDouble(trans.deal, DEAL_PROFIT);
   double   commission = HistoryDealGetDouble(trans.deal, DEAL_COMMISSION);
   double   swap       = HistoryDealGetDouble(trans.deal, DEAL_SWAP);
   string   comment    = HistoryDealGetString(trans.deal, DEAL_COMMENT);
   // A closing SELL deal closes a long position, and the other way round
   string   side       = HistoryDealGetInteger(trans.deal, DEAL_TYPE) == DEAL_TYPE_SELL ? "long" : "short";

   // Entry price and entry commission come from the position's opening deal
   double openPrice = 0.0;
   if(HistorySelectByPosition(position))
      for(int i = 0; i < HistoryDealsTotal(); i++)
        {
         ulong deal = HistoryDealGetTicket(i);
         if(deal > 0 && HistoryDealGetInteger(deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
           {
            openPrice   = HistoryDealGetDouble(deal, DEAL_PRICE);
            commission += HistoryDealGetDouble(deal, DEAL_COMMISSION);
            break;
           }
        }
   AppendRow(TimeToString(time, TIME_DATE | TIME_SECONDS), position, symbol, side, volume, openPrice, closePrice,
             profit, commission, swap, magic, comment);
  }

How it works

  • The path in OnInit(). TERMINAL_DATA_PATH is the folder File > Open Data Folder opens; files go to its MQL5/Files. With FILE_COMMON, they go to the Common/Files folder shared by every terminal on the computer.
  • FILE_READ | FILE_WRITE. Opens the file without emptying it. FileSeek(h, 0, SEEK_END) then moves to the end, so each row is appended.
  • FILE_ANSI. A CSV file is written as Unicode (UTF-16) unless you ask for ANSI, and spreadsheets then show everything in one column or as odd characters.
  • The header. Written only when FileSize() is 0, that is when the file is new.
  • OnTradeTransaction(). Called for every change on the account. A new deal arrives as TRADE_TRANSACTION_DEAL_ADD with its ticket in trans.deal; HistoryDealSelect() makes its properties readable.
  • Read first, then HistorySelectByPosition(). Selecting the position's history replaces the current selection, so the closing deal's properties are read before the entry deal is looked up for the entry price and commission.

Pitfalls

  • FILE_WRITE alone. It empties an existing file on every open, so the CSV only ever holds the last trade. Add FILE_READ and seek to the end.
  • The file is open in Excel. Excel locks the file, and FileOpen() fails with error 5004 (ERR_CANNOT_OPEN_FILE) until it's closed. Copy the file to read it while the EA runs.
  • Paths outside the sandbox. MQL5 can only write inside MQL5/Files (or Common/Files): an absolute path such as C:\trades.csv fails with error 5002 (ERR_WRONG_FILENAME).
  • Where the Strategy Tester writes. During a test the file goes to the testing agent's own sandbox, not to your terminal's MQL5/Files. Use FILE_COMMON to find it in one place.
  • Duplicate rows. OnTradeTransaction() receives every deal of the account, whichever program made it. Run one logger per account, or filter by magic number, or each copy writes the same trade.

Variations

  • The whole history at once. A script with HistorySelect(0, TimeCurrent()) and a loop over HistoryDealsTotal() exports past trades with the same AppendRow().
  • Decimal commas. DoubleToString() writes a decimal point. With ; as separator, use your spreadsheet's import with "." as decimal separator, or replace the point with StringReplace() before writing.
  • A journal. The columns match what a trading journal needs; the free trading journal template takes them from there.
  • Send instead of write. Post each closed trade to a server or a chat with WebRequest().

Questions

Where does MetaTrader 5 save files written by an EA?

In the MQL5/Files folder of the terminal's data folder (File > Open Data Folder), or in the Common/Files folder shared by all terminals when the file is opened with FILE_COMMON. During a Strategy Tester run, in the testing agent's own folder.

Why does my CSV file only contain the last trade?

Because the file is opened with FILE_WRITE without FILE_READ, which empties it each time. Open it with FILE_READ | FILE_WRITE and call FileSeek(handle, 0, SEEK_END) before writing.

Why does Excel show my CSV file in one column?

Either the file is Unicode (opened without FILE_ANSI), or the separator doesn't match your regional settings. Use FILE_ANSI, and a semicolon in regions where the comma is the decimal separator.

This program is a minimal example written to show one technique, not a trading system, and none of it is a recommendation to trade. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.

Want it inside a complete EA?

Edgoro's MT5 EA generator can add trade logging like this to the EA it writes, so every closed trade lands in a file you can analyse. A clean build proves the code compiles, not that a strategy works: test it on a demo account.