How to keep a trading journal that fills itself in

6 min readMetaKit

Every trading coach tells you to keep a journal, and almost nobody keeps one past week three. The reason is always the same: copying entry price, exit price, lots and P&L out of MetaTrader by hand is tedious, so the journal falls behind, and a journal that's behind is a journal you stop opening.

The fix is to split the work. Half of a good journal is facts the platform already knows. Half is judgement only you can supply. Automate the first half completely, and the second half takes thirty seconds per trade.

What a trading journal should record

The facts (automatable):

  • Open and close time
  • Symbol, side (long or short), volume in lots
  • Entry and exit price
  • Gross profit, commission and swap, and net profit
  • How it closed: stop loss, take profit, manual, stop-out
  • Hold time

The judgement (yours):

  • Setup: which of your named setups was this? Keep the list short; if every trade is a different setup, you don't have setups yet.
  • Planned risk and reward: where was the stop and target before you entered, in R? Comparing planned R to realised R is where most lessons are.
  • Followed the plan? Yes or no. Not "mostly".
  • Emotional state: one word. Bored, fearful, revenge, confident, rushed.
  • Note: one sentence. What would you tell yourself next time?
  • Screenshot link: the chart at entry, if you take one.

That's it. A journal with forty columns gets abandoned as fast as one with none. The point is to answer questions later, like "do my revenge trades lose?" or "is setup B worth keeping?", and those only need a few honest columns, filled every time.

The free template

Make a Google Sheet with a tab called journal and this header row:

deal_ticket · position_id · opened · closed · symbol · side · volume · entry · exit · profit · commission · swap · net · close_reason · hold_min · setup · planned_R · followed_plan · emotion · note · screenshot

The first fifteen columns are filled by the script below. The last six are yours. Freeze the header row, and add data validation on setup, followed_plan and emotion so they're dropdowns: consistent labels are what make a pivot table useful later.

Auto-import closed MT5 trades into the sheet

MT5 doesn't push trades anywhere by itself, so something has to fetch them. The approach below uses Google Apps Script (free, no server) to pull new closed deals from your account through the MetaKit API every fifteen minutes. It needs a connected account and a read-only API key; a journal never needs to trade, so don't give it a key that can.

Open Extensions → Apps Script and paste:

const BASE = 'https://api.metakit.cloud/v1';
const ACCOUNT_ID = 2;
 
function api(path, params) {
  const key = PropertiesService.getScriptProperties().getProperty('METAKIT_KEY');
  const qs = Object.entries(params || {}).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
  const res = UrlFetchApp.fetch(`${BASE}${path}${qs ? '?' + qs : ''}`, {
    headers: { 'x-api-key': key },
    muteHttpExceptions: true,
  });
  const body = JSON.parse(res.getContentText());
  if (res.getResponseCode() !== 200) throw new Error(`${body.error.code}: ${body.error.message}`);
  return body;
}
 
/** Every deal in [from, now], across all pages. */
function dealsSince(from) {
  const out = [];
  for (let page = 1; ; page++) {
    const r = api(`/accounts/${ACCOUNT_ID}/deals`, { from: from.toISOString(), limit: 100, page });
    out.push(...r.data);
    if (page >= r.total_pages) return out;
  }
}
 
function syncJournal() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('journal');
  const props = PropertiesService.getScriptProperties();
  // Look back a full day past the last sync: MT5 stamps deals with the broker's
  // server clock, which can be hours off UTC. Duplicates are skipped by deal
  // ticket below, so the overlap costs nothing.
  const last = new Date(props.getProperty('LAST_SYNC') || '2026-01-01T00:00:00Z');
  const from = new Date(last.getTime() - 24 * 60 * 60 * 1000);
 
  const existing = new Set(sheet.getRange('A2:A').getValues().flat().filter(String).map(String));
  const deals = dealsSince(from);
 
  // Entry deals ("in") carry the open time and price; exit deals ("out") carry the result.
  const entries = {};
  for (const d of deals) if (d.entry === 'in') entries[d.positionId] = d;
 
  const rows = [];
  for (const d of deals) {
    if (d.entry !== 'out' || existing.has(String(d.ticket))) continue;
    const open = entries[d.positionId];
    const opened = open ? new Date(open.time) : null;
    const closed = new Date(d.time);
    // Share the entry commission across partial closes by volume, so closing
    // a position in two halves doesn't count the entry commission twice.
    const entryCommission = open ? open.commission * (d.volume / open.volume) : 0;
    const commission = d.commission + entryCommission;
    rows.push([
      d.ticket, d.positionId,
      opened || '', closed, d.symbol,
      // The closing deal is the opposite side of the position.
      d.type === 'sell' ? 'long' : 'short',
      d.volume, open ? open.price : '', d.price,
      d.profit, commission, d.swap,
      d.profit + commission + d.swap,
      d.reason,
      opened ? Math.round((closed - opened) / 60000) : '',
    ]);
  }
  if (rows.length) sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length).setValues(rows);
  props.setProperty('LAST_SYNC', new Date().toISOString());
}

Put your key in Project Settings → Script Properties as METAKIT_KEY, run syncJournal once by hand to authorise it, then add a time-driven trigger every 15 minutes.

A few details worth knowing:

  • Why exit deals. MT5 records a trade as two or more deals: one opening the position, one or more closing it. The profit lands on the closing deal. The orders, deals and positions post explains why there are three different objects.
  • Partial closes appear as separate rows, one per closing deal, which is what you want in a journal: closing half at 1R and the rest at 3R are two decisions.
  • Positions opened before the sync window won't have their entry deal in the fetched batch, so entry price and open time come up blank. Widen from on the first run (the LAST_SYNC default does this) and it fills in.
  • Times are the broker's server clock, not UTC: MT5 stamps deals that way and the API passes them through. That's fine for a journal as long as you're consistent; the server time post shows how to convert if you want real UTC.
  • Commission is charged on both sides on many accounts, so the script adds the entry deal's commission to the exit row (split by volume across partial closes) to get a true net figure. Swap only exists on the closing deal. The costs post covers what each field includes.
  • reason tells you whether the trade hit its stop, its target, or was closed by hand, which is the single most useful automatic column: a journal where most losers are "client" (manual) closes is telling you something.

If you'd rather have trades arrive the moment they close instead of every 15 minutes, the position.closed webhook carries the same information; the trade grading post builds a handler for it.

The weekly review is the point

A journal that's never read is a log. Once a week, take fifteen minutes and build three pivot tables:

  1. Net by setup. Which setups pay? Cut the one that doesn't.
  2. Net by followed_plan. For almost everyone, the "no" rows are where the losses are. This number is the most persuasive argument for discipline you'll ever see, because it's your money.
  3. Net by emotion. Revenge and boredom trades usually lose. Seeing it in your own numbers is different from reading it in a book.

Then compare planned_R with what you actually got. If you plan 1:2 and realise 1:0.8, the issue isn't your entries, it's that you move targets or close early. The risk-reward post shows what that does to the win rate you need.

For the account-level numbers a journal doesn't give you, like Sharpe, max drawdown and profit factor, see profit factor, win rate and expectancy. Those tell you whether the system works; the journal tells you why.