Advertisement IC Markets

Broker scorecard quarterly update: spreads, rejects, and uptime

Compare ECN/Raw spread brokers, analyze execution speeds, report slippage, and evaluate commission structures for high-frequency traders.
LondonScalper
Posts: 404
Joined: Sat Sep 05, 2026 7:54 am

Broker scorecard quarterly update: spreads, rejects, and uptime

Post by LondonScalper »

I stopped arguing about brokers from memory. Once a quarter I update a simple scorecard from my logs.

Columns that matter for scalping
• Median spread by pair + session (London / overlap)
• p95 slippage on market orders
• Reject / requote count
• Time-to-fill feel (qualitative is OK if you don’t have timestamps)
• Outages / disconnects
• Cost per 100 round-turns (spread + commission + adverse slip)

Rule
A pretty marketing page loses to an ugly spreadsheet. If a broker wins on raw spread but loses on p95 slip + rejects, it gets demoted for my M1 work.

I’m refreshing my Q3→Q4 card now (majors + XAU). If you keep something similar, what metrics actually changed your broker choice last time — not what you wish mattered?

No account numbers. Comparison talk welcome. Not financial advice.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

LondonScalper wrote: Thu Sep 10, 2026 10:31 am I stopped arguing about brokers from memory. Once a quarter I update a simple scorecard from my logs.

Columns that matter for scalping
• Median spread by pair + session (London / overlap)
• p95 slippage on market orders
• Reject / requote count
• Time-to-fill feel (qualitative is OK if you don’t have timestamps)
• Outages / disconnects
• Cost per 100 round-turns (spread + commission + adverse slip)

Rule
A pretty marketing page loses to an ugly spreadsheet. If a broker wins on raw spread but loses on p95 slip + rejects, it gets demoted for my M1 work.

I’m refreshing my Q3→Q4 card now (majors + XAU). If you keep something similar, what metrics actually changed your broker choice last time — not what you wish mattered?

No account numbers. Comparison talk welcome. Not financial advice.
Hello traders, LondonScalper,

o answer your question on what actually changed my broker choice last time: Asymmetric slippage on market orders during the London/NY overlap.

Broker A had a 0.2 pip median spread, but my logs showed a p95 slippage of 0.8 pips. Worse, it was asymmetric—limit orders never got positive slippage, but market stops were slipped heavily. Broker B had a wider median spread (0.5 pips) but true DMA execution. Their p95 slippage was barely 0.1 pips, and positive slippage actually occurred.

The other breaking point was the Time-to-fill vs. Reject Rate tradeoff. Broker A rejected 8% of my orders during the 8:30 AM EST liquidity sweeps. Missing a high-probability trade costs way more expected value (EV) than paying an extra 0.3 pips in spread. While the broader price action and structure might be mapped clearly on D1 and M15 charts, the actual M1 execution is where those hidden friction costs destroy an edge.
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

The Pine Script Tracker

Logging actual reject counts and exact fill slippage is straightforward when coding natively in MQL5 or C# cAlgo. However, TradingView doesn't natively store historical bid and ask arrays, so plotting historical spread distributions on TV is heavily restricted.

To get around this in Pine Script, you have to run a real-time monitor. You can slap this dashboard on your chart during active sessions; it continuously reads the live spread and calculates your theoretical "Cost per 100 Round Turns" on the fly based on your inputs.

Code: Select all

//@version=5
indicator("Live Execution Cost Monitor", overlay=true)

// --- Inputs ---
var string GRP = "Broker Cost Parameters"
commRT    = input.float(7.0, title="Commission per Round Turn ($)", group=GRP)
slipEst   = input.float(0.2, title="Est. p95 Slippage (Pips)", group=GRP)
pipValue  = input.float(10.0, title="Pip Value per Lot ($)", group=GRP, tooltip="Usually $10 for standard FX lot")
tradesVol = input.int(100, title="Volume (Round Turns)", group=GRP)

// --- Spread Calculation ---
// TV exposes live ask/bid, but historical bars return 'na'.
liveSpreadPips = syminfo.mintick > 0 ? (ask - bid) / (syminfo.mintick * 10) : na

// --- Cost Projections ---
spreadCost = liveSpreadPips * pipValue * tradesVol
commCost   = commRT * tradesVol
slipCost   = slipEst * pipValue * tradesVol
totalCost  = spreadCost + commCost + slipCost

// --- Dashboard Rendering ---
var table dash = table.new(position.top_right, 2, 5, bgcolor=color.new(color.black, 80), border_color=color.gray, border_width=1)

if barstate.islast
    table.cell(dash, 0, 0, "Metric (" + str.tostring(tradesVol) + " Trades)", text_color=color.gray, text_halign=text.align_left)
    table.cell(dash, 1, 0, "Cost Impact", text_color=color.gray, text_halign=text.align_right)
    
    table.cell(dash, 0, 1, "Live Spread (" + str.tostring(liveSpreadPips, "#.##") + " pips)", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 1, "$" + str.tostring(spreadCost, "#.##"), text_color=color.yellow, text_halign=text.align_right)
    
    table.cell(dash, 0, 2, "Commission", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 2, "$" + str.tostring(commCost, "#.##"), text_color=color.white, text_halign=text.align_right)
    
    table.cell(dash, 0, 3, "Est. Slippage", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 3, "$" + str.tostring(slipCost, "#.##"), text_color=color.red, text_halign=text.align_right)
    
    table.cell(dash, 0, 4, "Total Cost", text_color=color.white, text_halign=text.align_left)
    table.cell(dash, 1, 4, "$" + str.tostring(totalCost, "#.##"), text_color=color.red, text_halign=text.align_right)
It updates on every real-time tick, giving you a live look at exactly how much the current liquidity conditions are eating into your bottom line.
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

Institutional Execution Friction Monitor (Pine Script)

Native environments like C# cAlgo or MQL5 are vastly superior for logging granular bid/ask arrays and precise millisecond fill times. TradingView’s Pine Script lacks robust historical tick data access, meaning you cannot backtest spread distributions accurately.

To bypass this architectural limitation in TradingView, you must run a real-time monitor. The following script computes theoretical execution drag dynamically by sampling live liquidity.

Note: The script is optimized to initialize the UI framework only on the first bar, executing computational updates strictly on real-time ticks to minimize resource consumption.

Code: Select all

//@version=5
indicator("Execution Friction Monitor", overlay=true)

// --- Execution Parameters ---
var string GRP = "Cost Engine Engine"
float commRT    = input.float(7.0, title="Commission (Round Turn)", group=GRP)
float slipEst   = input.float(0.2, title="p95 Slippage Estimate (Pips)", group=GRP)
float pipValue  = input.float(10.0, title="Pip Value ($)", group=GRP, tooltip="Standard lot = $10")
int   tradesVol = input.int(100, title="Sample Size (Trades)", group=GRP)

// --- Spread Calculation ---
// Real-time ask/bid sampling; returns 'na' on historical bars
float liveSpreadPips = syminfo.mintick > 0 ? (ask - bid) / (syminfo.mintick * 10) : na

// --- Cost Projections ---
float spreadCost = liveSpreadPips * pipValue * tradesVol
float commCost   = commRT * tradesVol
float slipCost   = slipEst * pipValue * tradesVol
float totalCost  = spreadCost + commCost + slipCost

// --- Dashboard UI Architecture ---
var table dash = table.new(position.top_right, 2, 5, bgcolor=color.new(color.black, 85), border_color=color.rgb(60, 60, 60), border_width=1)

// Initialize headers optimally on first bar
if barstate.isfirst
    table.cell(dash, 0, 0, "METRIC (" + str.tostring(tradesVol) + " TRADES)", text_color=color.gray, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 1, 0, "IMPACT ($)", text_color=color.gray, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 0, 1, "Real-Time Spread", text_color=color.white, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 0, 2, "Commission Base", text_color=color.white, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 0, 3, "Asymmetric Slippage", text_color=color.white, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 0, 4, "TOTAL FRICTION", text_color=color.white, text_halign=text.align_left, text_size=size.small)

// Push dynamic updates on live ticks
if barstate.islast
    table.cell(dash, 1, 1, str.tostring(spreadCost, "#.##"), text_color=color.yellow, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 1, 2, str.tostring(commCost, "#.##"), text_color=color.white, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 1, 3, str.tostring(slipCost, "#.##"), text_color=color.red, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 1, 4, str.tostring(totalCost, "#.##"), text_color=color.red, text_halign=text.align_right, text_size=size.small)
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

Relying on empirical data over broker marketing is the only sustainable approach to scaling an edge. While mapping macro structure and liquidity sweeps on D1 and M15 charts provides a statistically sound directional bias, the actual execution on the M1 timeframe is strictly a function of order book microstructure. If that microstructure is compromised, a verified edge mathematically decays into a net-negative expected value (EV).

Regarding the metrics that forced my last infrastructure migration, the primary catalyst was toxic flow profiling resulting in asymmetric slippage.

The Slippage Asymmetry Profile: Broker A aggressively marketed a 0.1 to 0.2 pip median spread. However, my execution logs revealed a severe synthetic delay. During high-probability liquidity sweeps, p95 slippage on market orders consistently spiked to 0.8+ pips. More critically, the slippage was strictly asymmetric: limit orders never received price improvement (positive slippage), while market stops were subjected to maximum adverse excursion. A true DMA/STP environment yields a normal distribution of slippage; asymmetry confirms B-book latency injection.

Fill-Probability vs. Execution Drag: The secondary metric was the order rejection ratio during macroeconomic volatility (e.g., 08:30 EST data drops). Broker A exhibited an 8% rejection rate under the guise of "insufficient liquidity." Missing the fat-tail distribution of a high-conviction sweep destroys more portfolio EV than absorbing a marginally wider, but mathematically guaranteed, spread from a prime-of-prime liquidity provider.
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

Real-Time Microstructure Cost Engine (Pine Script)

Native environments like C# cAlgo and MQL5 are fundamentally superior for this analysis, as they allow direct access to historical bid/ask tick arrays and millisecond-level execution logging. TradingView’s architecture isolates Pine Script from historical spread data, rendering traditional backtesting of execution costs impossible.

To circumvent this limitation, the engine must be built as a real-time sampling monitor. The script below computes theoretical execution drag dynamically by evaluating live liquidity conditions on the current tick.

Code: Select all

//@version=5
//@description Real-time execution friction and liquidity spread monitor for M1/M5 scalping environments.
indicator("Microstructure Cost Engine", overlay=true, display=display.all)

// =============================================================================
// PARAMETERS & CONSTANTS
// =============================================================================
var string GRP_COST = "Execution Drag Parameters"
float commRT    = input.float(7.0, title="Commission per Round Turn ($)", group=GRP_COST)
float slipEst   = input.float(0.2, title="p95 Adverse Slippage Est. (Pips)", group=GRP_COST)
float pipValue  = input.float(10.0, title="Standard Pip Value ($)", group=GRP_COST, tooltip="Defaults to $10 for standard 1.0 FX lot")
int   tradesVol = input.int(100, title="Sample Size (Round Turns)", group=GRP_COST)

// =============================================================================
// LIQUIDITY SAMPLING
// =============================================================================
// TV exposes live ask/bid strictly on the real-time bar. Historical references return 'na'.
float liveSpreadPips = syminfo.mintick > 0 ? (ask - bid) / (syminfo.mintick * 10) : na

// =============================================================================
// EV DEGRADATION CALCULATION
// =============================================================================
float spreadCost = liveSpreadPips * pipValue * tradesVol
float commCost   = commRT * tradesVol
float slipCost   = slipEst * pipValue * tradesVol
float totalCost  = spreadCost + commCost + slipCost

// =============================================================================
// UI ARCHITECTURE
// =============================================================================
var color bgCol     = color.new(#000000, 85)
var color borderCol = color.rgb(45, 45, 45)
var color textMain  = color.rgb(200, 200, 200)
var color textWarn  = color.rgb(255, 82, 82)
var color textAlert = color.rgb(255, 215, 0)

var table dash = table.new(position.top_right, 2, 5, bgcolor=bgCol, border_color=borderCol, border_width=1)

// Initialize matrix architecture strictly on the first bar to optimize rendering overhead
if barstate.isfirst
    table.cell(dash, 0, 0, "METRIC (" + str.tostring(tradesVol) + " RTs)", text_color=color.gray, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 1, 0, "CAPITAL DRAG", text_color=color.gray, text_halign=text.align_right, text_size=size.small)
    
    table.cell(dash, 0, 1, "Real-Time Spread", text_color=textMain, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 0, 2, "Base Commission", text_color=textMain, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 0, 3, "Synthetic Slippage", text_color=textMain, text_halign=text.align_left, text_size=size.small)
    table.cell(dash, 0, 4, "TOTAL FRICTION", text_color=color.white, text_halign=text.align_left, text_size=size.small)

// Push state updates exclusively on real-time ticks
if barstate.islast
    table.cell(dash, 1, 1, str.tostring(spreadCost, format.mintick), text_color=textAlert, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 1, 2, str.tostring(commCost, format.mintick), text_color=textMain, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 1, 3, str.tostring(slipCost, format.mintick), text_color=textWarn, text_halign=text.align_right, text_size=size.small)
    table.cell(dash, 1, 4, str.tostring(totalCost, format.mintick), text_color=textWarn, text_halign=text.align_right, text_size=size.small)
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

In MT4 and MT5, you have direct access to millisecond/microsecond hardware timers, tick-level spread sampling, and post-trade execution event loops (OnTradeTransaction in MQL5). This allows you to output hard numbers directly into a CSV for your quarterly scorecard instead of estimating.

Here is how to set up the empirical execution tracker in both environments.

1. MetaTrader 5 (MQL5) – Execution & Microstructure Logger

MT5 provides native transaction event hooks via OnTradeTransaction(). This Expert Advisor samples live spread into dynamic memory arrays, measures round-trip order execution latency in microseconds (GetMicrosecondCount()), calculates actual realized slippage against your pre-submission quote, and appends the raw audit trail to MQL5/Files/execution_scorecard.csv.

Code: Select all

//+------------------------------------------------------------------+
//|                                     ExecutionQualityAuditor.mq5  |
//|                                  Microstructure Scorecard Logger |
//+------------------------------------------------------------------+
#property copyright "Proprietary Institutional Scalping Tool"
#property link      ""
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

input group "=== Logging Parameters ==="
input string InpFileName        = "execution_scorecard.csv"; // Audit CSV File
input double InpCommissionPerLot = 7.0;                      // Round-Turn Commission ($)

// State Tracking for Latency & Slippage
struct PendingExecution
{
   ulong    ticket;
   ulong    sendTimeMicroseconds;
   double   intendedPrice;
   ENUM_ORDER_TYPE orderType;
};

PendingExecution lastOrder;
CTrade trade;

// Tick Spread Metrics
double spreadBuffer[];
int    spreadSampleCount = 0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   ArrayResize(spreadBuffer, 0, 10000);
   
   // Initialize CSV Header if newly created
   int fileHandle = FileOpen(InpFileName, FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON, ",");
   if(fileHandle != INVALID_HANDLE)
   {
      if(FileSize(fileHandle) == 0)
      {
         FileWrite(fileHandle, "Timestamp", "Symbol", "Action", "ExecutionTime_ms", 
                               "IntendedPrice", "ExecutedPrice", "Slippage_Pts", 
                               "Spread_Pts", "Comment");
      }
      FileClose(fileHandle);
   }
   
   Print("[AUDITOR] Initialized. Logging to Common/Files/", InpFileName);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function (Spread Sampling)                          |
//+------------------------------------------------------------------+
void OnTick()
{
   MqlTick tick;
   if(!SymbolInfoTick(_Symbol, tick)) return;
   
   double currentSpreadPts = (tick.ask - tick.bid) / _Point;
   
   spreadSampleCount++;
   ArrayResize(spreadBuffer, spreadSampleCount);
   spreadBuffer[spreadSampleCount - 1] = currentSpreadPts;
   
   // Lightweight On-Chart Telemetry
   Comment(StringFormat("=== EXECUTION QUALITY AUDITOR ===\n" +
                        "Symbol: %s\n" +
                        "Live Spread: %.1f pts\n" +
                        "Tick Samples: %d\n" +
                        "Log File: Common/Files/%s",
                        _Symbol, currentSpreadPts, spreadSampleCount, InpFileName));
}

//+------------------------------------------------------------------+
//| Execution Wrapper: Call this to track send -> fill metrics       |
//+------------------------------------------------------------------+
bool ExecuteAuditedMarketOrder(ENUM_ORDER_TYPE orderType, double volume)
{
   MqlTick tick;
   SymbolInfoTick(_Symbol, tick);
   
   lastOrder.orderType            = orderType;
   lastOrder.intendedPrice        = (orderType == ORDER_TYPE_BUY) ? tick.ask : tick.bid;
   lastOrder.sendTimeMicroseconds = GetMicrosecondCount();
   
   bool result = false;
   if(orderType == ORDER_TYPE_BUY)
      result = trade.Buy(volume, _Symbol);
   else if(orderType == ORDER_TYPE_SELL)
      result = trade.Sell(volume, _Symbol);
      
   return result;
}

//+------------------------------------------------------------------+
//| Trade Transaction Hook (Catches Actual Deal Fills)               |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
{
   if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
   {
      ulong dealTicket = trans.deal;
      if(dealTicket > 0)
      {
         ulong latencyMicroseconds = GetMicrosecondCount() - lastOrder.sendTimeMicroseconds;
         double executionTimeMs    = (double)latencyMicroseconds / 1000.0;
         
         double fillPrice = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
         double slippagePts = 0.0;
         
         if(trans.deal_type == DEAL_TYPE_BUY)
            slippagePts = (fillPrice - lastOrder.intendedPrice) / _Point;
         else if(trans.deal_type == DEAL_TYPE_SELL)
            slippagePts = (lastOrder.intendedPrice - fillPrice) / _Point;
            
         MqlTick tick;
         SymbolInfoTick(_Symbol, tick);
         double liveSpread = (tick.ask - tick.bid) / _Point;

         // Append to Scorecard CSV
         int fileHandle = FileOpen(InpFileName, FILE_READ|FILE_WRITE|FILE_CSV|FILE_COMMON, ",");
         if(fileHandle != INVALID_HANDLE)
         {
            FileSeek(fileHandle, 0, SEEK_END);
            FileWrite(fileHandle, 
                      TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS),
                      _Symbol,
                      EnumToString(trans.deal_type),
                      DoubleToString(executionTimeMs, 2),
                      DoubleToString(lastOrder.intendedPrice, _Digits),
                      DoubleToString(fillPrice, _Digits),
                      DoubleToString(slippagePts, 1),
                      DoubleToString(liveSpread, 1),
                      "Deal #" + IntegerToString(dealTicket));
            FileClose(fileHandle);
         }
         
         PrintFormat("[EXECUTION LOG] Fill: %s | Time: %.2f ms | Slippage: %.1f pts | Spread: %.1f pts",
                     _Symbol, executionTimeMs, slippagePts, liveSpread);
      }
   }
}
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

2. MetaTrader 4 (MQL4) – Execution Wrapper & Latency Logger

Because MT4 lacks the asynchronous OnTradeTransaction architecture, execution timing must be measured synchronously around OrderSend(). This script measures millisecond round-trip response time, verifies whether requotes (ERR_REQUOTE / 138) occurred, logs the exact price deviation, and flushes to disk.

Code: Select all

//+------------------------------------------------------------------+
//|                                     ExecutionQualityAuditor.mq4  |
//|                                  Microstructure Scorecard Logger |
//+------------------------------------------------------------------+
#property copyright "Proprietary Institutional Scalping Tool"
#property link      ""
#property version   "1.00"
#property strict

input string InpFileName         = "execution_scorecard_mt4.csv";
input double InpCommissionPerLot = 7.0;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   int fileHandle = FileOpen(InpFileName, FILE_READ|FILE_WRITE|FILE_CSV, ",");
   if(fileHandle != INVALID_HANDLE)
   {
      if(FileSize(fileHandle) == 0)
      {
         FileWrite(fileHandle, "Timestamp", "Symbol", "Type", "ExecutionTime_ms", 
                               "IntendedPrice", "ExecutedPrice", "Slippage_Pts", 
                               "Spread_Pts", "ResultCode");
      }
      FileClose(fileHandle);
   }
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Synchronous Execution & Latency Hook                             |
//+------------------------------------------------------------------+
int SendAuditedMarketOrder(int cmd, double volume, int slippageAllowed)
{
   RefreshRates();
   double intendedPrice = (cmd == OP_BUY) ? Ask : Bid;
   double currentSpread = (Ask - Bid) / Point;
   
   uint startTick = GetTickCount();
   int ticket = OrderSend(Symbol(), cmd, volume, intendedPrice, slippageAllowed, 0, 0, "ScorecardAudit", 0, 0, clrNONE);
   uint durationMs = GetTickCount() - startTick;
   
   int lastErr = GetLastError();
   double executedPrice = 0.0;
   double realizedSlippagePts = 0.0;
   
   if(ticket > 0)
   {
      if(OrderSelect(ticket, SELECT_BY_TICKET))
      {
         executedPrice = OrderOpenPrice();
         if(cmd == OP_BUY)
            realizedSlippagePts = (executedPrice - intendedPrice) / Point;
         else if(cmd == OP_SELL)
            realizedSlippagePts = (intendedPrice - executedPrice) / Point;
      }
   }
   
   // Write to Audit Log
   int fileHandle = FileOpen(InpFileName, FILE_READ|FILE_WRITE|FILE_CSV, ",");
   if(fileHandle != INVALID_HANDLE)
   {
      FileSeek(fileHandle, 0, SEEK_END);
      FileWrite(fileHandle, 
                TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS),
                Symbol(),
                (cmd == OP_BUY ? "BUY" : "SELL"),
                IntegerToString(durationMs),
                DoubleToString(intendedPrice, Digits),
                DoubleToString(executedPrice, Digits),
                DoubleToString(realizedSlippagePts, 1),
                DoubleToString(currentSpread, 1),
                (ticket > 0 ? "SUCCESS" : "ERROR_" + IntegerToString(lastErr)));
      FileClose(fileHandle);
   }
   
   return ticket;
}

//+------------------------------------------------------------------+
//| OnTick Display                                                   |
//+------------------------------------------------------------------+
void OnTick()
{
   double spread = (Ask - Bid) / Point;
   Comment(StringFormat("MT4 Microstructure Logger\nSpread: %.1f pts\nFile: MQL4/Files/%s", 
                        spread, InpFileName));
}
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

How to Pipe This into Your Quarterly Scorecard

Calculate True p95 Slippage: In your CSV sheet, run =PERCENTILE.INC(FILTER(Slippage_Pts, Action="BUY"), 0.95). Compare this against the median spread column. If your median spread is 0.2 pips (2 pts) but p95 slippage is 0.9 pips (9 pts), your actual execution cost is 1.1 pips per entry before commission.

Rejection / Requote Frequency: Filter ResultCode for error codes (in MT4: 138 Requote, 136 Off quotes; in MT5: TRADE_RETCODE_REQUOTE, TRADE_RETCODE_PRICE_OFF).

Execution Latency Profiling: Check if ExecutionTime_ms correlates with trade direction or market volatility bursts. If fills are 20ms during calm sessions but jump to 450ms+ on news sweeps, the broker is routing your flow into a virtual dealer plugin to inject delay.
FTtrader
Posts: 442
Joined: Mon Aug 03, 2026 2:43 pm

Re: Broker scorecard quarterly update: spreads, rejects, and uptime

Post by FTtrader »

Moving this execution tracker into cTrader provides a massive architectural advantage. Because cAlgo runs natively on the .NET framework, you can bypass proprietary platform timers entirely and utilize OS-level hardware counters for high-resolution latency tracking.

By leveraging System.Diagnostics.Stopwatch, we can measure the exact execution drag from the moment the method is called to the moment the FIX API returns the deal confirmation. Furthermore, native System.IO access makes flushing the telemetry to a local CSV completely seamless.

Here is the institutional execution wrapper configured as a cBot. You can run this alongside your manual trading routines to intercept, time, and log every market execution.

cTrader (C# cAlgo) – .NET Microstructure Auditor

Note: You must set the bot’s access rights to AccessRights.FileSystem to allow it to write the CSV to your Documents folder.

Code: Select all

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

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.FileSystem)]
    public class ExecutionQualityAuditor : Robot
    {
        [Parameter("Audit Log Filename", DefaultValue = "cTrader_Execution_Scorecard.csv")]
        public string LogFileName { get; set; }

        private string _filePath;

        protected override void OnStart()
        {
            // Route log to the standard Windows Documents folder
            string docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            _filePath = Path.Combine(docPath, LogFileName);

            // Initialize CSV header if the file is new
            if (!File.Exists(_filePath))
            {
                string header = "Timestamp,Symbol,Direction,Latency_ms,IntendedPrice,ExecutedPrice,Slippage_Pips,Spread_Pips,Status\n";
                File.WriteAllText(_filePath, header);
            }
            
            Print($"[AUDITOR] Tracking initialized. Writing to: {_filePath}");
        }

        // --- Execution Wrapper ---
        // Call this method instead of native ExecuteMarketOrder when entering positions
        public void SendAuditedMarketOrder(TradeType tradeType, double volumeInUnits)
        {
            // Sample exact liquidity state immediately prior to submission
            double intendedPrice = tradeType == TradeType.Buy ? Symbol.Ask : Symbol.Bid;
            double currentSpreadPips = (Symbol.Ask - Symbol.Bid) / Symbol.PipSize;
            
            // Utilize .NET high-resolution timer for strict latency profiling
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            
            // Synchronous execution dispatch
            TradeResult result = ExecuteMarketOrder(tradeType, SymbolName, volumeInUnits, "ScorecardAudit");
            
            stopwatch.Stop();
            double latencyMs = stopwatch.Elapsed.TotalMilliseconds;
            
            if (result.IsSuccessful)
            {
                double executedPrice = result.Position.EntryPrice;
                double slippagePips = 0;
                
                if (tradeType == TradeType.Buy)
                    slippagePips = (executedPrice - intendedPrice) / Symbol.PipSize;
                else
                    slippagePips = (intendedPrice - executedPrice) / Symbol.PipSize;

                AppendToScorecard(tradeType.ToString(), latencyMs, intendedPrice, executedPrice, slippagePips, currentSpreadPips, "FILLED");
            }
            else
            {
                // Capture the specific FIX rejection reason
                AppendToScorecard(tradeType.ToString(), latencyMs, intendedPrice, 0, 0, currentSpreadPips, $"REJECTED_{result.Error}");
            }
        }

        private void AppendToScorecard(string direction, double latency, double intended, double executed, double slippage, double spread, string status)
        {
            // Format telemetry for easy quarter-end aggregation in Excel/Sheets
            string record = $"{Server.Time:yyyy-MM-dd HH:mm:ss.fff},{SymbolName},{direction},{latency:F2},{intended},{executed},{slippage:F2},{spread:F2},{status}\n";
            File.AppendAllText(_filePath, record);
            
            // Push immediate terminal feedback
            Print($"[EXECUTION] {direction} | Latency: {latency:F2} ms | Slippage: {slippage:F2} pips | Spread: {spread:F2} pips");
        }

        protected override void OnTick()
        {
            // Maintain a live telemetry overlay on the active chart
            double spread = (Symbol.Ask - Symbol.Bid) / Symbol.PipSize;
            string dashText = $"cTrader Execution Auditor\nLive Spread: {spread:F1} pips\nTarget: {LogFileName}";
            
            Chart.DrawStaticText("audit_dash", dashText, VerticalAlignment.Top, HorizontalAlignment.Right, Color.DimGray);
        }
    }
}
Post Reply