MQL5 Trade Journal: Code & Analytics Dashboard Guide

Build a custom MQL5 trade journal EA that reads terminal history and renders a live analytics dashboard. Full code, metrics, and honest limitations.

mql5-trade-journal-code-analytics-dashboard-guide

The built-in MetaTrader 5 Trade tab and History tab are fine for a quick look, but they're not a serious analytical tool. You can't see your profit factor by day of week, your average win by session, or your drawdown per strategy. The "Export to XML" button gives you raw data, but then you're stuck in Excel doing manual work every time you want an updated view.

I've been there. For years I exported MT5 trade reports and built spreadsheets until I realized I was spending more time maintaining the spreadsheet than actually trading. The fix was writing a custom MQL5 trade journal EA that reads the terminal's trade history directly and renders analytics on a chart panel. No external software, no manual export, no CSV parsing. Just attach the EA to a chart, and it builds your performance dashboard from the actual MT5 trade history.

This guide walks you through building exactly that. You'll learn which MQL5 history functions matter, how to structure the data, and how to render a clean analytics panel. I'll also be honest about the limitations — because there are a few, and you should know them before you invest the time.

Why Build a Custom Trade Journal in MQL5?

MetaTrader 5 does give you basic statistics in the History tab — profit factor, expected payoff, drawdown, and the equity curve. But it's static. You can't filter by symbol, by magic number, by day of week, or by position holding time. You can't compare your EURUSD scalping results against your GBPJPY swing trades without manually separating them.

MQL5's history functions solve this. The platform stores every deal and order in its internal database, and you can query it programmatically. That means you can build a custom trade journal that shows exactly the metrics you care about, filtered exactly the way you want, updated automatically on every tick or on demand.

For me, the killer feature is per-magic-number analytics. I run multiple EAs on the same account, and I need to know which strategy is actually making money after costs. The built-in panel lumps everything together. My custom journal breaks it out.

There's another angle too: consistency. When you rely on the built-in History tab, you're at the mercy of whatever aggregation MetaTrader decided to give you. The numbers are correct, sure, but they're computed one way and one way only. A custom journal lets you define what a "trade" means in your context. Maybe you want to treat a partial close as a separate trade for R-multiple purposes. Maybe you want to exclude swap from your win/loss calculation because you're testing a pure price strategy. The built-in tab won't let you do that. Your own code will.

Core MQL5 History Functions You Need

Before writing any panel code, you need to understand the data layer. MQL5 has two separate history collections: deals and orders. They're not the same thing, and confusing them is the most common beginner mistake.

  • Deals — executed transactions that actually change your balance or position. Buying 0.5 lots of EURUSD creates a deal. Closing that position creates another deal (or two, if it's partially closed).
  • Orders — the instructions that may or may not have resulted in deals. A pending order that gets filled creates an order record plus a deal. A limit order that gets cancelled creates only an order record.

For a trade journal, you almost always want deals. Deals represent real money movement. Orders are useful for analyzing things like slippage on pending orders or fill rates, but they'll inflate your trade count if you include them in performance metrics.

Here are the key functions you'll use:

// Get total number of deals in history
ulong totalDeals = HistoryDealsTotal();

// Get a deal ticket and its properties
ulong dealTicket = HistoryDealGetTicket(index);
double dealProfit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
double dealVolume = HistoryDealGetDouble(dealTicket, DEAL_VOLUME);
long dealEntry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
long dealType = HistoryDealGetInteger(dealTicket, DEAL_TYPE);
long dealMagic = HistoryDealGetInteger(dealTicket, DEAL_MAGIC);
string dealSymbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL);
datetime dealTime = (datetime)HistoryDealGetInteger(dealTicket, DEAL_TIME);

The critical detail: you must call HistorySelect() before any of these functions work. This function loads the history range into memory. If you skip it, you'll get zeros and empty strings, and you'll waste an hour debugging code that looks correct.

// Select history from the beginning of time to now
datetime fromTime = 0; // 0 means from the earliest available
datetime toTime = TimeCurrent();
HistorySelect(fromTime, toTime);

You can filter by symbol and time range in the select call, but I prefer to load everything and filter in code. It's simpler and more flexible, especially when you want multiple views of the same data.

One thing that trips people up: HistorySelect() has an overload that takes a symbol parameter. If you call HistorySelect(symbol, fromTime, toTime), you only load history for that symbol. That's useful for performance if you're running on a huge account with years of tick data, but it means you'll need to call it again if you want a different symbol's history. I almost always use the two-argument version and filter in my loop. The memory footprint is negligible for most retail accounts — even 10,000 deals is just a few MB of RAM.

Understanding DEAL_ENTRY — the Key to Profit Calculation

Here's where most custom journal attempts fall apart. A deal's profit alone doesn't tell you whether a trade was a winner or a loser. You need to understand DEAL_ENTRY:

DEAL_ENTRY ValueMeaningContributes to P/L?
DEAL_ENTRY_IN (0)Position opening dealNo — opens the position, no profit realized
DEAL_ENTRY_OUT (1)Position closing deal (full or partial)Yes — this is where profit/loss appears
DEAL_ENTRY_INOUT (2)Reverse deal (closes one position, opens opposite)Yes — the closing part carries the P/L
DEAL_ENTRY_OUT_BY (3)Position closed by an opposite positionYes — closing leg carries the P/L

If you simply sum DEAL_PROFIT across all deals, you'll get the right total but you can't compute per-trade statistics. To count winners and losers properly, you need to group deals by position ID.

Every position in MT5 has a unique POSITION_IDENTIFIER. All deals belonging to the same position share this identifier. So a buy-to-open deal and its corresponding sell-to-close deal will have the same position ID. That's your grouping key.

There's a subtlety with DEAL_ENTRY_INOUT that I want to flag. This happens when you use the "Close by" feature or when a hedging strategy closes a position and opens the opposite one in a single operation. The deal's profit includes the closing leg's P/L, but the position ID might be for the new position, not the one being closed. In practice, this is rare for most EAs, but if you're building a journal for a hedging EA, you'll need to handle it carefully. I usually treat DEAL_ENTRY_INOUT as a closing event for the position it references, and I don't try to reconstruct the "new" position from it.

Building the Trade Journal EA — Step by Step

Let's build this properly. I'll use a chart panel approach — we'll draw text labels on the chart itself, which is the simplest way to display data without external DLLs or GUI libraries.

Step 1: Define the Data Structure

First, define a structure to hold one trade (one complete position cycle):

struct TradeRecord
{
   long      positionId;
   string    symbol;
   long      magic;
   datetime  openTime;
   datetime  closeTime;
   double    openPrice;
   double    closePrice;
   double    volume;
   double    profit;
   double    swap;
   double    commission;
   long      dealType; // DEAL_TYPE_BUY or DEAL_TYPE_SELL
};

// Global array to hold all trades
TradeRecord g_trades[];
int g_tradeCount = 0;

You might wonder why I include both openTime and closeTime. Holding time is one of those metrics that separates serious traders from casual ones. If you're scalping, you want to see if your average holding time is actually matching your strategy design. If you're swinging, you want to verify you're not cutting winners early. The built-in tab gives you none of this.

Step 2: Load and Process History

Now the core function. It iterates through all deals, groups them by position ID, and builds the trade array:

void LoadTradeHistory()
{
   // Reset
   g_tradeCount = 0;
   ArrayResize(g_trades, 0);

   // Select full history
   HistorySelect(0, TimeCurrent());

   ulong totalDeals = HistoryDealsTotal();

   // Temporary map: positionId -> index in g_trades
   // MQL5 doesn't have built-in maps, so we use two parallel arrays
   long posIds[];
   int  posIndexes[];
   ArrayResize(posIds, 0);
   ArrayResize(posIndexes, 0);

   for(ulong i = 0; i < totalDeals; i++)
   {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket == 0) continue;

      long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
      long positionId = HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
      long magic = HistoryDealGetInteger(ticket, DEAL_MAGIC);
      string symbol = HistoryDealGetString(ticket, DEAL_SYMBOL);
      double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
      double swap = HistoryDealGetDouble(ticket, DEAL_SWAP);
      double commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      double volume = HistoryDealGetDouble(ticket, DEAL_VOLUME);
      double price = HistoryDealGetDouble(ticket, DEAL_PRICE);
      datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
      long dealType = HistoryDealGetInteger(ticket, DEAL_TYPE);

      // Skip if no position ID (shouldn't happen for normal trades)
      if(positionId == 0) continue;

      // Find if we already have this position
      int idx = -1;
      for(int j = 0; j < g_tradeCount; j++)
      {
         if(g_trades[j].positionId == positionId)
         {
            idx = j;
            break;
         }
      }

      if(idx == -1)
      {
         // New position — create a record
         idx = g_tradeCount;
         g_tradeCount++;
         ArrayResize(g_trades, g_tradeCount);

         g_trades[idx].positionId = positionId;
         g_trades[idx].symbol = symbol;
         g_trades[idx].magic = magic;
         g_trades[idx].volume = volume;
         g_trades[idx].profit = 0;
         g_trades[idx].swap = 0;
         g_trades[idx].commission = 0;
         g_trades[idx].openTime = dealTime;
         g_trades[idx].openPrice = price;
         g_trades[idx].dealType = dealType;
      }

      // Update the record based on entry type
      if(entry == DEAL_ENTRY_IN)
      {
         g_trades[idx].openTime = dealTime;
         g_trades[idx].openPrice = price;
      }
      else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_INOUT || entry == DEAL_ENTRY_OUT_BY)
      {
         g_trades[idx].closeTime = dealTime;
         g_trades[idx].closePrice = price;
         g_trades[idx].profit += profit;
         g_trades[idx].swap += swap;
         g_trades[idx].commission += commission;
      }
   }
}

This is a simplified version — in production you'd also handle partial closes properly. When a position is partially closed, you get multiple OUT deals with the same position ID. The code above accumulates profit correctly, but the close price and close time will reflect the last partial close. For most journals that's acceptable, but be aware of it.

The linear search for position IDs is fine for a few thousand trades, but if you're running this on a multi-year history with 50,000+ deals, it will be slow. I've seen it take 10-15 seconds on accounts with heavy scalping history. The fix is to sort the position IDs and use binary search, or better yet, use a simple hash table. MQL5 doesn't have a built-in dictionary, but you can implement one with two arrays and a custom hash function. For this guide, the linear search keeps things readable — just know the limitation.

Step 3: Compute the Analytics

Now that we have clean trade records, computing metrics is straightforward:

void ComputeAnalytics(double &totalProfit, double &profitFactor,
                      int &winCount, int &lossCount, double &avgWin,
                      double &avgLoss, double &maxDrawdown)
{
   totalProfit = 0;
   profitFactor = 0;
   winCount =

Want to build your own version?

Recreate similar entry logic, risk rules, and filters in TradingBotMaker—no MQL coding. Start free.

Community

Clap for the article and open comments only when you want to read them.

0 claps0 comments

Related articles