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.
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_PATHis the folder File > Open Data Folder opens; files go to itsMQL5/Files. WithFILE_COMMON, they go to theCommon/Filesfolder 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 asTRADE_TRANSACTION_DEAL_ADDwith its ticket intrans.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_WRITEalone. It empties an existing file on every open, so the CSV only ever holds the last trade. AddFILE_READand 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(orCommon/Files): an absolute path such asC:\trades.csvfails 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. UseFILE_COMMONto 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 overHistoryDealsTotal()exports past trades with the sameAppendRow(). - Decimal commas.
DoubleToString()writes a decimal point. With;as separator, use your spreadsheet's import with "." as decimal separator, or replace the point withStringReplace()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.