Page 1 of 1

Building an execution KPI dashboard for yourself

Posted: Mon Sep 14, 2026 7:03 pm
by LondonScalper
Process note on measuring your own execution -- not a broker advert.

I got tired of arguing from memory about whether fills were "fine." So I built a simple execution KPI sheet I update weekly. Nothing fancy: a spreadsheet and honest timestamps.

What I track
  • Median spread at click vs fill (side-aware)
  • Reject / requote count per pair and session
  • Partial-fill rate
  • Time from click to fill (platform clock)
  • Round-trip cost in R terms for typical scalp size
The point is not a pretty dashboard. It is spotting clusters. If GBPUSD rejects spike only in the first ten minutes of London, that is a process rule, not a mood. If gold slippage clusters around round numbers, I change order type or stand aside -- I do not rewrite my whole edge story.

I review Friday afternoon or Sunday. Twenty minutes, same columns every week. If a metric is noisy, I leave it alone until I have a month of rows.

What would you put on a one-page execution scorecard, and what would you deliberately leave off so it stays usable?

Re: Building an execution KPI dashboard for yourself

Posted: Thu Sep 24, 2026 7:24 pm
by PTScalper
LondonScalper wrote: Mon Sep 14, 2026 7:03 pm Process note on measuring your own execution -- not a broker advert.

I got tired of arguing from memory about whether fills were "fine." So I built a simple execution KPI sheet I update weekly. Nothing fancy: a spreadsheet and honest timestamps.

What I track
  • Median spread at click vs fill (side-aware)
  • Reject / requote count per pair and session
  • Partial-fill rate
  • Time from click to fill (platform clock)
  • Round-trip cost in R terms for typical scalp size
The point is not a pretty dashboard. It is spotting clusters. If GBPUSD rejects spike only in the first ten minutes of London, that is a process rule, not a mood. If gold slippage clusters around round numbers, I change order type or stand aside -- I do not rewrite my whole edge story.

I review Friday afternoon or Sunday. Twenty minutes, same columns every week. If a metric is noisy, I leave it alone until I have a month of rows.

What would you put on a one-page execution scorecard, and what would you deliberately leave off so it stays usable?
Hi LondonScalper,

An effective execution scorecard isolates infrastructure and liquidity from strategy edge. If you are scalping 1-minute and 5-minute price action, the goal is to measure the mechanics of getting into and out of the market, not whether the trade itself was a winner.

What to Deliberately Leave Off

1.) Trade Outcome (PnL / R-Multiple): The fastest way to ruin an execution log is to attach the trade's final result to it. A 50ms fill with zero slippage is a perfect execution, even if the trade hits your stop-loss three minutes later.

2.) Strategy Setup Name: Whether it was a liquidity sweep or a moving average crossover doesn't matter to the broker's order book.

3.) Direction (Long/Short): Unless you are trading hard-to-borrow equities, side usually doesn't impact execution quality in spot forex or metals enough to justify the column.

4.) Emotional State / Conviction: Execution is a mechanical process. Adding subjective feelings turns a clean data sheet into a journal, making it impossible to spot objective clusters.

Re: Building an execution KPI dashboard for yourself

Posted: Thu Sep 24, 2026 7:25 pm
by PTScalper
Pine Script: Execution Risk & Cluster Mapper

Pine Script runs on TradingView's servers, meaning it cannot access your local terminal's actual button clicks or broker rejections. However, you can map the exact execution risk clusters you mentioned (session opens, round numbers) directly onto your chart to visualize where you expect execution quality to degrade.

This script creates an on-chart dashboard for real-time spread tracking and highlights structural risk zones—like the first 15 minutes of London and round-number proximity on assets like Gold.

Code: Select all

//@version=5
indicator("Execution Risk & Cluster Mapper", overlay=true, max_lines_count=500)

// --- Inputs ---
grp_sessions = "Cluster Zones (Time)"
londonOpen   = input.session("0800-0815", title="London Open Risk Window", group=grp_sessions)
nyOpen       = input.session("1330-1345", title="NY Open Risk Window", group=grp_sessions)

grp_levels   = "Cluster Zones (Price)"
showRoundNum = input.bool(true, title="Highlight Round Numbers (Gold/FX)", group=grp_levels)
roundNumStep = input.float(10.0, title="Round Number Step (e.g., 10 for Gold)", group=grp_levels)

grp_spread   = "Microstructure"
maxSpread    = input.float(2.0, title="Spread Alert Threshold (Ticks/Points)", group=grp_spread)

// --- 1. Session Cluster Highlighting ---
// Highlights the background during known high-reject/requote windows
inLondon = time(timeframe.period, londonOpen, "Europe/London") != 0
inNY     = time(timeframe.period, nyOpen, "America/New_York") != 0

bgcolor(inLondon ? color.new(color.blue, 90) : na, title="London Open Cluster")
bgcolor(inNY ? color.new(color.red, 90) : na, title="NY Open Cluster")

// --- 2. Round Number Proximity (Slippage Clusters) ---
// Plots lines at major psychological levels where order books thin out
if showRoundNum and barstate.islast
    float currentPrice = close
    float lowerRound = math.floor(currentPrice / roundNumStep) * roundNumStep
    float upperRound = lowerRound + roundNumStep
    
    // Draw lines only on the visible end of the chart to avoid clutter
    line.new(bar_index - 50, lowerRound, bar_index + 10, lowerRound, color=color.new(color.gray, 50), style=line.style_dotted, width=1)
    line.new(bar_index - 50, upperRound, bar_index + 10, upperRound, color=color.new(color.gray, 50), style=line.style_dotted, width=1)

// --- 3. Real-Time Spread Dashboard ---
// Pine can only track bid/ask on the live, streaming bar
var float rtSpread = na
if barstate.isrealtime
    // Calculate spread in terms of the asset's minimum tick
    rtSpread := (syminfo.ask - syminfo.bid) / syminfo.mintick

var table execTable = table.new(position.bottom_right, 2, 3, border_width=1, border_color=color.new(color.gray, 80))

if barstate.islast
    table.cell(execTable, 0, 0, "Execution Metric", text_color=color.gray, bgcolor=color.new(color.black, 80))
    table.cell(execTable, 1, 0, "Current Status", text_color=color.gray, bgcolor=color.new(color.black, 80))

    // Spread Row
    color spreadColor = na(rtSpread) ? color.gray : (rtSpread > maxSpread ? color.red : color.green)
    string spreadText = na(rtSpread) ? "Waiting for tick..." : str.tostring(rtSpread, "#.##") + " ticks"
    table.cell(execTable, 0, 1, "Live Spread", text_color=color.white, bgcolor=color.new(color.black, 80))
    table.cell(execTable, 1, 1, spreadText, text_color=spreadColor, bgcolor=color.new(color.black, 80))

    // Session Risk Row
    bool isRiskWindow = inLondon or inNY
    table.cell(execTable, 0, 2, "Session Risk", text_color=color.white, bgcolor=color.new(color.black, 80))
    table.cell(execTable, 1, 2, isRiskWindow ? "HIGH (Cluster Zone)" : "Standard", text_color=isRiskWindow ? color.orange : color.green, bgcolor=color.new(color.black, 80))

Re: Building an execution KPI dashboard for yourself

Posted: Thu Sep 24, 2026 7:26 pm
by PTScalper
To get true "click-to-fill" latency and capture the exact spread at the moment of your decision, a passive account monitor isn't enough—it only sees the event after the server processes it.

The cleanest way to log this in cTrader is to build a lightweight execution cBot with its own Buy/Sell buttons. This allows you to start a Stopwatch on the local UI thread the millisecond you click, fire the async order, and calculate exact latency and slippage upon the server's return, writing it directly to a local CSV.

Here is the C# cAlgo implementation. It requires Full Access rights in cTrader to allow System.IO

Code: Select all

using System;
using System.Diagnostics;
using System.IO;
using cAlgo.API;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FullAccess)]
    public class ExecutionKpiLogger : Robot
    {
        [Parameter("Volume (Units)", DefaultValue = 100000, MinValue = 1)]
        public double VolumeInUnits { get; set; }

        private string _csvPath;
        private readonly object _fileLock = new object();

        protected override void OnStart()
        {
            // Initialize CSV on the Desktop
            string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            _csvPath = Path.Combine(desktopPath, "Execution_KPI_Log.csv");
            
            if (!File.Exists(_csvPath))
            {
                File.WriteAllText(_csvPath, "Timestamp,Symbol,Side,SpreadAtClick(Pips),RequestedPrice,FillPrice,Slippage(Pips),Latency(ms),Status,RejectReason\n");
            }

            // Build the on-chart execution panel
            var panel = new StackPanel 
            { 
                Orientation = Orientation.Horizontal, 
                HorizontalAlignment = HorizontalAlignment.Center, 
                VerticalAlignment = VerticalAlignment.Bottom, 
                Margin = 20 
            };
            
            var buyBtn = new Button { Text = "BUY (Track KPI)", BackgroundColor = Color.SeaGreen, Margin = 5, Width = 120, Height = 35 };
            var sellBtn = new Button { Text = "SELL (Track KPI)", BackgroundColor = Color.Firebrick, Margin = 5, Width = 120, Height = 35 };
            
            buyBtn.Click += args => ExecuteAndLogAsync(TradeType.Buy);
            sellBtn.Click += args => ExecuteAndLogAsync(TradeType.Sell);
            
            panel.AddChild(buyBtn);
            panel.AddChild(sellBtn);
            Chart.AddControl(panel);
        }

        private async void ExecuteAndLogAsync(TradeType tradeType)
        {
            // Capture state exactly at the moment of the click
            var stopwatch = Stopwatch.StartNew();
            DateTime clickTime = Server.Time;
            double spreadAtClick = Symbol.Spread / Symbol.PipSize;
            double requestedPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
            
            // Execute the order asynchronously
            var result = await ExecuteMarketOrderAsync(tradeType, SymbolName, Symbol.NormalizeVolumeInUnits(VolumeInUnits), "KPI_Logger");
            
            stopwatch.Stop();

            // Calculate KPIs
            double fillPrice = result.Position?.EntryPrice ?? 0;
            double slippage = 0;
            
            if (result.IsSuccessful)
            {
                // Positive slippage = worse fill. Negative slippage = price improvement.
                slippage = tradeType == TradeType.Buy 
                    ? (fillPrice - requestedPrice) / Symbol.PipSize 
                    : (requestedPrice - fillPrice) / Symbol.PipSize;
            }

            string status = result.IsSuccessful ? "Filled" : "Rejected";
            string reason = result.IsSuccessful ? "" : result.Error.ToString();
            
            // Format and append to CSV
            string logLine = $"{clickTime:yyyy-MM-dd HH:mm:ss.fff},{SymbolName},{tradeType},{spreadAtClick:F1},{requestedPrice},{fillPrice},{slippage:F1},{stopwatch.ElapsedMilliseconds},{status},{reason}\n";

            lock (_fileLock)
            {
                File.AppendAllText(_csvPath, logLine);
            }
            
            Print($"Execution Logged: {status} | Latency: {stopwatch.ElapsedMilliseconds}ms | Slippage: {slippage:F1} pips");
        }
    }
}

Re: Building an execution KPI dashboard for yourself

Posted: Thu Sep 24, 2026 7:27 pm
by PTScalper
Key Architectural Choices:

Thread Safety (lock): If you map hotkeys to this logic or spam the button during high volatility (like a news event sweep), the _fileLock prevents IO write crashes when appending to the CSV.

Pip Normalization: Spread and slippage are divided by Symbol.PipSize so the output is universally comparable across JPY pairs, standard spot forex, and Gold, saving you from doing decimal math in Excel later.

Async Execution: Using ExecuteMarketOrderAsync ensures the UI thread doesn't freeze while waiting for the broker's server, which is critical for accurate local latency measurement via the Stopwatch class.

Re: Building an execution KPI dashboard for yourself

Posted: Thu Sep 24, 2026 7:28 pm
by PTScalper
Because MetaTrader requires an event loop to listen for button clicks (OnChartEvent), these must be compiled as Expert Advisors (EAs) rather than simple scripts.

Both versions use GetMicrosecondCount() wrapping a synchronous OrderSend(). Because the function blocks the thread until the broker's trade server responds, measuring the time before and after the call captures the exact round-trip latency (network transit + broker execution time).

MQL5 Version
Save this in MQL5\Experts. The CSV will output to File -> Open Data Folder -> MQL5\Files\Execution_KPI_Log.csv.

Code: Select all

//+------------------------------------------------------------------+
//|                                             ExecutionKPILogger.mq5|
//+------------------------------------------------------------------+
#property copyright "Execution KPI Tracker"
#property version   "1.00"

input double InpVolume = 0.1; // Volume (Lots)

string csv_filename = "Execution_KPI_Log.csv";

int OnInit()
  {
   // Build on-chart UI
   CreateButton("btnBuy", "BUY (Track KPI)", 20, 20, clrWhite, clrSeaGreen);
   CreateButton("btnSell", "SELL (Track KPI)", 160, 20, clrWhite, clrFireBrick);
   
   // Initialize CSV with headers if it doesn't exist
   if(!FileIsExist(csv_filename))
     {
      int handle = FileOpen(csv_filename, FILE_WRITE|FILE_TXT|FILE_ANSI);
      if(handle != INVALID_HANDLE)
        {
         FileWrite(handle, "Timestamp,Symbol,Side,SpreadAtClick(Pips),RequestedPrice,FillPrice,Slippage(Pips),Latency(ms),Status,RejectReason");
         FileClose(handle);
        }
     }
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   ObjectDelete(0, "btnBuy");
   ObjectDelete(0, "btnSell");
  }

void CreateButton(string name, string text, int x, int y, color txt_color, color bg_color)
  {
   ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, 130);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, 35);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, txt_color);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg_color);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_LOWER);
   ObjectSetInteger(0, name, OBJPROP_STATE, false);
  }

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(sparam == "btnBuy")
        {
         ExecuteAndLog(ORDER_TYPE_BUY);
         ObjectSetInteger(0, "btnBuy", OBJPROP_STATE, false);
        }
      else if(sparam == "btnSell")
        {
         ExecuteAndLog(ORDER_TYPE_SELL);
         ObjectSetInteger(0, "btnSell", OBJPROP_STATE, false);
        }
     }
  }

void ExecuteAndLog(ENUM_ORDER_TYPE type)
  {
   ulong start_mcs = GetMicrosecondCount();
   datetime click_time = TimeCurrent();
   
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   // Normalize pips based on digit count (3/5 digits vs 2/4 digits)
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double pip_size = (digits == 3 || digits == 5) ? point * 10 : point;
   
   double spread_pips = (ask - bid) / pip_size;
   double req_price = (type == ORDER_TYPE_BUY) ? ask : bid;
   
   MqlTradeRequest request={0};
   MqlTradeResult result={0};
   
   request.action = TRADE_ACTION_DEAL;
   request.symbol = _Symbol;
   request.volume = InpVolume;
   request.type = type;
   request.price = req_price;
   request.deviation = 1000; // Intentionally high to ensure fill and measure exact slip
   request.magic = 999111;
   
   // Blocking call to capture latency
   bool success = OrderSend(request, result);
   
   ulong latency_mcs = GetMicrosecondCount() - start_mcs;
   double latency_ms = latency_mcs / 1000.0;
   
   double fill_price = result.price;
   double slippage = 0;
   
   if(success && fill_price > 0)
     {
      slippage = (type == ORDER_TYPE_BUY) 
         ? (fill_price - req_price) / pip_size 
         : (req_price - fill_price) / pip_size;
     }
     
   string status = success ? "Filled" : "Rejected";
   string reason = success ? "" : IntegerToString(result.retcode);
   string side_str = (type == ORDER_TYPE_BUY) ? "Buy" : "Sell";
   
   int handle = FileOpen(csv_filename, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(handle != INVALID_HANDLE)
     {
      FileSeek(handle, 0, SEEK_END);
      string log_line = StringFormat("%s,%s,%s,%.1f,%f,%f,%.1f,%.1f,%s,%s",
                                     TimeToString(click_time, TIME_DATE|TIME_SECONDS),
                                     _Symbol, side_str, spread_pips, req_price, fill_price,
                                     slippage, latency_ms, status, reason);
      FileWrite(handle, log_line);
      FileClose(handle);
     }
     
   PrintFormat("Execution: %s | Latency: %.1fms | Slippage: %.1f pips", status, latency_ms, slippage);
  }

Re: Building an execution KPI dashboard for yourself

Posted: Thu Sep 24, 2026 7:28 pm
by PTScalper
MQL4 Version

Save this in MQL4\Experts. The CSV will output to File -> Open Data Folder -> MQL4\Files\Execution_KPI_Log.csv. MQL4 handles error codes differently (requiring GetLastError()) and relies on OrderSelect post-fill to retrieve the exact entry price.

Code: Select all

//+------------------------------------------------------------------+
//|                                             ExecutionKPILogger.mq4|
//+------------------------------------------------------------------+
#property strict

input double InpVolume = 0.1; // Volume (Lots)

string csv_filename = "Execution_KPI_Log.csv";

int OnInit()
  {
   CreateButton("btnBuy", "BUY (Track KPI)", 20, 20, clrWhite, clrSeaGreen);
   CreateButton("btnSell", "SELL (Track KPI)", 160, 20, clrWhite, clrFireBrick);
   
   if(!FileIsExist(csv_filename))
     {
      int handle = FileOpen(csv_filename, FILE_WRITE|FILE_TXT|FILE_ANSI);
      if(handle != INVALID_HANDLE)
        {
         FileWrite(handle, "Timestamp,Symbol,Side,SpreadAtClick(Pips),RequestedPrice,FillPrice,Slippage(Pips),Latency(ms),Status,RejectReason");
         FileClose(handle);
        }
     }
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   ObjectDelete(0, "btnBuy");
   ObjectDelete(0, "btnSell");
  }

void CreateButton(string name, string text, int x, int y, color txt_color, color bg_color)
  {
   ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, 130);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, 35);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, txt_color);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bg_color);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_LOWER);
   ObjectSetInteger(0, name, OBJPROP_STATE, false);
  }

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(sparam == "btnBuy")
        {
         ExecuteAndLog(OP_BUY);
         ObjectSetInteger(0, "btnBuy", OBJPROP_STATE, false);
        }
      else if(sparam == "btnSell")
        {
         ExecuteAndLog(OP_SELL);
         ObjectSetInteger(0, "btnSell", OBJPROP_STATE, false);
        }
     }
  }

void ExecuteAndLog(int type)
  {
   ulong start_mcs = GetMicrosecondCount();
   datetime click_time = TimeCurrent();
   RefreshRates(); // Vital in MQL4 to ensure Ask/Bid aren't stale before order
   
   double req_price = (type == OP_BUY) ? Ask : Bid;
   
   double pip_size = (Digits == 3 || Digits == 5) ? Point * 10 : Point;
   double spread_pips = (Ask - Bid) / pip_size;
   
   // Blocking execution call
   int ticket = OrderSend(Symbol(), type, InpVolume, req_price, 1000, 0, 0, "KPI_Logger", 999111, 0, (type==OP_BUY)?clrBlue:clrRed);
   
   ulong latency_mcs = GetMicrosecondCount() - start_mcs;
   double latency_ms = latency_mcs / 1000.0;
   
   bool success = (ticket > 0);
   double fill_price = 0;
   double slippage = 0;
   int err_code = 0;
   
   if(success)
     {
      if(OrderSelect(ticket, SELECT_BY_TICKET))
        {
         fill_price = OrderOpenPrice();
         slippage = (type == OP_BUY) 
            ? (fill_price - req_price) / pip_size 
            : (req_price - fill_price) / pip_size;
        }
     }
   else
     {
      err_code = GetLastError();
     }
     
   string status = success ? "Filled" : "Rejected";
   string reason = success ? "" : IntegerToString(err_code);
   string side_str = (type == OP_BUY) ? "Buy" : "Sell";
   
   int handle = FileOpen(csv_filename, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(handle != INVALID_HANDLE)
     {
      FileSeek(handle, 0, SEEK_END);
      string log_line = StringFormat("%s,%s,%s,%.1f,%f,%f,%.1f,%.1f,%s,%s",
                                     TimeToString(click_time, TIME_DATE|TIME_SECONDS),
                                     Symbol(), side_str, spread_pips, req_price, fill_price,
                                     slippage, latency_ms, status, reason);
      FileWrite(handle, log_line);
      FileClose(handle);
     }
     
   PrintFormat("Execution: %s | Latency: %.1fms | Slippage: %.1f pips", status, latency_ms, slippage);
  }