Page 1 of 2

Slippage during CPI: second-by-second fill log analysis

Posted: Thu Sep 10, 2026 10:34 am
by LondonScalper
CPI week reminder: the chart can look tradable while the fill tape is garbage.

I log CPI windows separately:
• Requested price vs fill
• Spread at send / at fill
• Side, size, pair
• Seconds relative to the release (T−60 … T+180)

What I usually find
• Average slippage lies; p95 is the number that murders the week
• First impulse after the print is often a tax, not an edge
• “A+ level” without a spread gate is not A+ during the window

My default
Hard blackout into the release; resume only after spreads normalize and M15 is readable again. If I take anything post-print, size is down and it’s pre-written — not invented because the first M1 candle looked clean.

If you log CPI fills: what’s your worst p95 pair (XAU vs majors), and do you still trade the first 5 minutes?

Desk/process notes — not trade calls.

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Thu Sep 10, 2026 7:03 pm
by Fairman
Spread and Slippage: The Silent Scalping Killer


Scalpers obsess over entry precision and pattern recognition, and then quietly bleed out on costs that have nothing to do with their analysis being wrong. Spread and slippage are the most underrated variable in whether a scalping strategy is actually profitable once it leaves the backtest.


Why This Matters More for Scalpers Than Anyone Else


A swing trader targeting 200+ pips barely notices a 1.5-pip spread. A scalper targeting 15–20 pips per trade is giving up 7–10% of their target to the spread alone before the trade even moves in their favor. Multiply that across dozens of trades a week, and spread costs alone can be the difference between a strategy with a genuine edge and one that only looks profitable until you actually trade it live.


Spread Isn't Fixed — It's Conditional


Spreads widen during low-liquidity periods (late NY session, pre-Asian open) and around news releases, sometimes dramatically. A pair that normally runs a 0.8-pip spread can widen to 4–5 pips in the seconds around a major release. If your backtest used average or static spread assumptions, it's quietly overstating your edge during exactly the volatile conditions scalpers are drawn to trade.


Slippage: The Cousin Nobody Backtests For


Slippage — the gap between your intended entry/exit price and your actual fill — tends to show up specifically during fast-moving conditions, which is disproportionately when scalpers are trying to enter. A backtest that assumes perfect fills at your signal price is testing a strategy that doesn't exist in live execution.


What to Actually Do About It


Check your broker's typical spread by session, not just the marketed "as low as" figure — trade during the windows where your specific pairs run tight, consistent spreads.

Build spread cost directly into your risk-to-reward math, not as an afterthought. If your average spread eats 3 pips and your stop is 15 pips, your real risk is 18, not 15.

Avoid entering directly into major news releases where spread widening is most extreme (see the earlier post on scalping news spikes).

Test your strategy with realistic — not optimistic — cost assumptions, or better yet, forward-test on a demo with your actual broker and conditions before trusting a backtest's raw numbers.

Ignoring this is one of the most common reasons a strategy that looks great on paper quietly loses money once real execution costs enter the picture.

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Tue Sep 15, 2026 2:49 pm
by LondonScalper
Fairman wrote:Scalpers obsess over entry precision and pattern recognition, and then quietly bleed out on costs that have nothing to do with their analysis being wrong. Spread and slippage are the most underrated variable in whether a scalping strategy is actually profitable.
Exactly — the fill log is the audit trail most people skip.

I keep a simple CPI / high-impact sheet: planned entry, requested price, fill price, spread at click, and seconds from release. After a few prints you stop arguing with the chart and start arguing with execution reality. A clean M1 signal that fills four points worse is not a “strategy loss”; it is a cost that should have blocked the ticket.

Desk rule for CPI: either flat into the print, or a pre-written reaction plan with half size and a hard time stop. I do not “fade the first spike” unless the fill log from prior releases says that path survives costs. Second-by-second notes sound pedantic until you see how often the edge died between intent and confirmation.

If the strategy only works on perfect fills, it does not work. Costs are part of the edge definition, not an afterthought.

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:21 pm
by PTScalper
LondonScalper wrote: Tue Sep 15, 2026 2:49 pm
Fairman wrote:Scalpers obsess over entry precision and pattern recognition, and then quietly bleed out on costs that have nothing to do with their analysis being wrong. Spread and slippage are the most underrated variable in whether a scalping strategy is actually profitable.
Exactly — the fill log is the audit trail most people skip.

I keep a simple CPI / high-impact sheet: planned entry, requested price, fill price, spread at click, and seconds from release. After a few prints you stop arguing with the chart and start arguing with execution reality. A clean M1 signal that fills four points worse is not a “strategy loss”; it is a cost that should have blocked the ticket.

Desk rule for CPI: either flat into the print, or a pre-written reaction plan with half size and a hard time stop. I do not “fade the first spike” unless the fill log from prior releases says that path survives costs. Second-by-second notes sound pedantic until you see how often the edge died between intent and confirmation.

If the strategy only works on perfect fills, it does not work. Costs are part of the edge definition, not an afterthought.
Hi LondonScalper,

You are absolutely right. Arguing with the chart is a fool’s errand; the fill log is the only ground truth. If a setup cannot survive a four-point slip on an M1 breakout, it isn’t a viable high-impact strategy—it’s a theoretical artifact.

Your desk rules represent the kind of pragmatic, defensive trading that actually survives long-term. Treating execution costs as a core component of the edge definition, rather than an anomaly, is what separates professionals from amateurs who wonder why their backtests never match their brokerage statements.

To mirror your desk rules, I have written a Pine Script v5 backtesting strategy. It is designed to stress-test your M1 signals against "execution reality" rather than perfect conditions.

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:22 pm
by PTScalper
The "Execution Reality" Pine Strategy

This script explicitly models the hostile environment of a CPI print.

Stress-Tested Slippage: Hardcoded with a 40-tick (4-point) slippage penalty to ensure the edge survives terrible fills.

Time-Isolated Window: Only allows the engine to look for signals during your defined release window (e.g., 08:30–08:35 EST).

Fractional Sizing: Automatically cuts position sizing in half during the news window.

Hard Time Stop: Tracks bars since entry and automatically flattens the position after N minutes to prevent holding through post-print chop.

Code: Select all

//@version=5
strategy("CPI Execution Reality", overlay=true, slippage=40, commission_type=strategy.commission.cash_per_contract, commission_value=2.0)

// --- Desk Rule Inputs ---
newsTime = input.session("0830-0835", "News Window (EST)")
timeStopBars = input.int(3, "Time Stop (Bars/Minutes)")
riskPerTrade = input.float(2.0, "Standard Risk (%)")
halfSizeNews = input.bool(true, "Half Size for News Print?")

// --- Time & Sizing Logic ---
// Convert chart time to EST to catch the CPI print accurately
inNewsWindow = not na(time(timeframe.period, newsTime, "America/New_York"))

// Halve the risk if trading inside the news window
actualRisk = inNewsWindow and halfSizeNews ? (riskPerTrade / 2) : riskPerTrade
qty = (strategy.equity * (actualRisk / 100)) / close 

// --- Simulated M1 Signal ---
// Replace this with your actual edge/trigger logic
volatilitySpike = ta.atr(14) > ta.sma(ta.atr(14), 50) * 1.5
longSignal = inNewsWindow and close > open and volatilitySpike
shortSignal = inNewsWindow and close < open and volatilitySpike

// --- Execution ---
if longSignal and strategy.position_size == 0
    strategy.entry("CPI_Long", strategy.long, qty=qty)

if shortSignal and strategy.position_size == 0
    strategy.entry("CPI_Short", strategy.short, qty=qty)

// --- Hard Time Stop Management ---
var int barsInTrade = 0
if strategy.position_size != 0
    barsInTrade += 1
else
    barsInTrade := 0

if barsInTrade >= timeStopBars
    strategy.close_all(comment="Time Stop - Flat")

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:22 pm
by PTScalper
Run your standard entry logic through this framework. If the equity curve collapses once the 40-tick slippage and 3-minute time stop are applied, the strategy doesn't work.

Do you currently use a custom historical data feed to backtest tick-level bid/ask spreads, or are you building your fill logs purely from live, small-size forward testing?

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:24 pm
by PTScalper
To upgrade this to a professional, desk-level standard, the architecture needs to move away from basic script logic and adopt a modular, object-oriented structure.

A "pro" implementation in Pine Script v5 utilizes custom types to isolate state, dynamic risk sizing based on precise stop-loss distances rather than arbitrary percentages, and most importantly, an on-chart "Fill Log" table to audit the execution latency and simulated slippage in real-time.

Here is an institutional-grade framework designed to stress-test your market microstructure setups.

Institutional Execution & Audit Model

This version introduces a strict separation of concerns: Event Parsing (News Window), Signal Generation, Risk Sizing, and Trade State Auditing.

Code: Select all

//@version=5
strategy("Institutional Execution Reality & Audit", overlay=true, calc_on_every_tick=true, max_labels_count=50, max_lines_count=50, slippage=40, commission_type=strategy.commission.cash_per_contract, commission_value=2.0)

// =========================================================================
// 1. DESK PARAMETERS & CONFIGURATION
// =========================================================================
var string G_RISK  = "Risk & Sizing (Desk Rules)"
var string G_EXEC  = "Execution & Time Constraints"

riskPerTradePct    = input.float(1.0, "Standard Risk (%)", step=0.1, group=G_RISK)
newsRiskModifier   = input.float(0.5, "News Risk Multiplier (e.g., 0.5 = Half Size)", step=0.1, group=G_RISK)
minStopDistance    = input.float(10.0, "Minimum Stop Distance (Ticks)", group=G_RISK)

newsSession        = input.session("0830-0835", "High-Impact Release Window (EST)", group=G_EXEC)
hardTimeStop       = input.int(3, "Hard Time Stop (Minutes/Bars)", group=G_EXEC)
maxSpreadTolerated = input.float(5.0, "Max Acceptable Spread Blowout (Ticks)", group=G_EXEC)

// =========================================================================
// 2. TYPES & STATE MANAGEMENT
// =========================================================================
// Custom type to manage the lifecycle and audit of a single trade
type ExecutionState
    float intendedEntry
    float actualFill
    float stopLoss
    int   entryBarIndex
    float spreadAtClick
    bool  isActive

var ExecutionState currentTrade = ExecutionState.new(na, na, na, na, na, false)

// =========================================================================
// 3. CORE LOGIC & MICROSTRUCTURE SIZING
// =========================================================================
// Timezone conversion to catch the exact print regardless of local exchange time
inNewsWindow = not na(time(timeframe.period, newsSession, "America/New_York"))

// Dynamic Risk Sizing Method
method calculatePositionSize(float riskPct, float stopDistanceTicks) =>
    adjustedRisk = inNewsWindow ? (riskPct * newsRiskModifier) : riskPct
    riskAmount   = (strategy.equity * (adjustedRisk / 100))
    tickValue    = syminfo.mintick * syminfo.pointvalue
    qty          = riskAmount / math.max(stopDistanceTicks * tickValue, 1e-5)
    math.floor(qty)

// Simulated Spread Blowout (Since Pine lacks historical Bid/Ask)
// We use a fraction of the M1 ATR to simulate the spread widening exactly at 08:30:00
simulatedSpread = ta.atr(5) * 0.2 
spreadViolated  = (simulatedSpread / syminfo.mintick) > maxSpreadTolerated

// =========================================================================
// 4. SIGNAL GENERATION (REPLACE WITH RAW PRICE ACTION LOGIC)
// =========================================================================
// Mock setup: 1-minute liquidity sweep and structural shift
bullishSweep = low < ta.lowest(low[1], 10) and close > open
bearishSweep = high > ta.highest(high[1], 10) and close < open

longTrigger  = inNewsWindow and bullishSweep and not spreadViolated
shortTrigger = inNewsWindow and bearishSweep and not spreadViolated

// =========================================================================
// 5. EXECUTION & AUDITING
// =========================================================================
float slDistanceTicks = math.max(ta.atr(14) / syminfo.mintick, minStopDistance)

if longTrigger and not currentTrade.isActive
    currentTrade.intendedEntry := close
    currentTrade.spreadAtClick := simulatedSpread / syminfo.mintick
    currentTrade.stopLoss      := close - (slDistanceTicks * syminfo.mintick)
    currentTrade.entryBarIndex := bar_index
    currentTrade.isActive      := true
    
    qty = calculatePositionSize(riskPerTradePct, slDistanceTicks)
    strategy.entry("Desk_Long", strategy.long, qty=qty)

if shortTrigger and not currentTrade.isActive
    currentTrade.intendedEntry := close
    currentTrade.spreadAtClick := simulatedSpread / syminfo.mintick
    currentTrade.stopLoss      := close + (slDistanceTicks * syminfo.mintick)
    currentTrade.entryBarIndex := bar_index
    currentTrade.isActive      := true
    
    qty = calculatePositionSize(riskPerTradePct, slDistanceTicks)
    strategy.entry("Desk_Short", strategy.short, qty=qty)

// Update actual fill price on the next tick/bar
if currentTrade.isActive and currentTrade.entryBarIndex == bar_index[1]
    currentTrade.actualFill := strategy.opentrades.entry_price(strategy.opentrades - 1)

// =========================================================================
// 6. HARD EXITS & TIME STOPS
// =========================================================================
barsHeld = currentTrade.isActive ? (bar_index - currentTrade.entryBarIndex) : 0

if currentTrade.isActive
    // Time Stop
    if barsHeld >= hardTimeStop
        strategy.close_all(comment="Time Stop")
        currentTrade.isActive := false
    // Hard Stop Loss
    else
        strategy.exit("Risk_Out", from_entry="Desk_Long", stop=currentTrade.stopLoss)
        strategy.exit("Risk_Out", from_entry="Desk_Short", stop=currentTrade.stopLoss)
        if strategy.position_size == 0
            currentTrade.isActive := false

// =========================================================================
// 7. DASHBOARD: THE FILL LOG
// =========================================================================
var table auditTable = table.new(position.bottom_right, 2, 5, bgcolor=color.new(color.black, 20), border_width=1, border_color=color.gray)

if barstate.islast
    table.cell(auditTable, 0, 0, "LAST AUDIT", text_color=color.white, text_halign=text.align_left)
    table.cell(auditTable, 1, 0, "VALUE", text_color=color.white, text_halign=text.align_right)
    
    table.cell(auditTable, 0, 1, "Intended Entry", text_color=color.gray, text_halign=text.align_left)
    table.cell(auditTable, 1, 1, str.tostring(currentTrade.intendedEntry, format.mintick), text_color=color.white, text_halign=text.align_right)
    
    table.cell(auditTable, 0, 2, "Actual Fill (w/ Slip)", text_color=color.gray, text_halign=text.align_left)
    table.cell(auditTable, 1, 2, str.tostring(currentTrade.actualFill, format.mintick), text_color=color.white, text_halign=text.align_right)
    
    slipTicks = math.abs(currentTrade.intendedEntry - currentTrade.actualFill) / syminfo.mintick
    slipColor = slipTicks > 10 ? color.red : color.green
    table.cell(auditTable, 0, 3, "Slippage INCURRED", text_color=color.gray, text_halign=text.align_left)
    table.cell(auditTable, 1, 3, str.tostring(slipTicks) + " Ticks", text_color=slipColor, text_halign=text.align_right)
    
    table.cell(auditTable, 0, 4, "Spread at Click", text_color=color.gray, text_halign=text.align_left)
    table.cell(auditTable, 1, 4, str.tostring(currentTrade.spreadAtClick, "#.#") + " Ticks", text_color=color.white, text_halign=text.align_right)

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:25 pm
by PTScalper
Key Architectural Upgrades

State Management via Types: Moving away from scattered var variables, the ExecutionState object cleanly tracks the lifecycle of the trade, mimicking a proper internal ledger.

Dynamic Tick-Based Risk (calculatePositionSize method): Instead of a generic equity percentage based on asset price, risk is calculated precisely against the hard stop distance and the asset's tick value (syminfo.pointvalue). It automatically halves the size (newsRiskModifier) if executed inside the specified time window.

Simulated Spread Rejection: Pine Script historical data does not contain historical Level 2 Bid/Ask data. To simulate the liquidity void at 08:30:00, the script calculates a simulatedSpread using fractional ATR. If the simulated spread exceeds your maxSpreadTolerated, the script forcefully rejects the ticket, mimicking a desk-level circuit breaker.

The Live Audit Dashboard: It prints a table in the bottom right corner tracking your "Fill Log" variables (Intended Entry vs. Actual Fill, Slippage Incurred in ticks, and Spread at Click).

When you are satisfied with a prototype's survival rate in Pine Script, do you rewrite the verified edge entirely in C# for your live execution environments, or do you bridge the signal generation out of TradingView?

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:26 pm
by PTScalper
Moving this framework into MetaTrader is where you stop simulating execution reality and start measuring it. Pine Script is great for testing the theoretical edge, but MQL is where you build the actual audit trail because it gives you direct access to the live Level 2 spread, broker execution latency, and exact deal history.

Here is the implementation split for both platforms. MT5 is the priority here, as its modern OOP structure (CTrade) and distinct separation between Orders (intent) and Deals (actual fills) makes auditing significantly cleaner.

The MT5 (MQL5) Implementation

This Expert Advisor framework handles the live spread filter, calculates tick-based risk sizing, logs the execution latency in milliseconds, and prints the exact slippage in points by querying the HistoryDealSelect post-fill.

Code: Select all

//+------------------------------------------------------------------+
//|                                     CPI_Execution_Audit_MT5.mq5  |
//|                    Institutional Market Microstructure Framework |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>

// --- DESK PARAMETERS ---
input int    NewsHourServerTime = 15;      // Server Time Hour for Print (e.g., 15 for 08:30 EST on EET)
input int    NewsMinuteStart    = 30;
input int    NewsMinuteEnd      = 35;
input double RiskPercent        = 1.0;     // Standard Risk (%)
input double NewsRiskModifier   = 0.5;     // 0.5 = Half Size
input int    MaxSpreadPoints    = 50;      // Max Acceptable Spread Blowout (Points)
input int    HardTimeStopBars   = 3;       // Time Stop (M1 Bars)
input int    MinStopDistancePts = 100;     // Minimum SL (Points)
input ulong  SlippageTolerance  = 40;      // Max allowed deviation (Points)

CTrade trade;
int    entryBarTime = 0;

int OnInit() {
    trade.SetExpertMagicNumber(1001);
    trade.SetDeviationInPoints(SlippageTolerance);
    
    // NDD/ECN brokers generally require Market Execution
    trade.SetTypeFilling(ORDER_FILLING_FOK); 
    return(INIT_SUCCEEDED);
}

void OnTick() {
    // 1. Time Constraints
    MqlDateTime dt;
    TimeCurrent(dt);
    bool inNewsWindow = (dt.hour == NewsHourServerTime && dt.min >= NewsMinuteStart && dt.min <= NewsMinuteEnd);

    // 2. Microstructure & Spread Filter
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    int currentSpread = (int)MathRound((ask - bid) / _Point);
    bool spreadViolated = (currentSpread > MaxSpreadPoints);

    // 3. Hard Time Stop Management
    if(PositionsTotal() > 0) {
        if(PositionSelect(_Symbol)) {
            int currentBarTime = (int)iTime(_Symbol, PERIOD_M1, 0);
            int barsHeld = (currentBarTime - entryBarTime) / 60; // 60s per M1 bar
            
            if(barsHeld >= HardTimeStopBars) {
                trade.PositionClose(_Symbol);
                Print("DESK RULE: Hard Time Stop Triggered. Position Flattened.");
            }
        }
        return; // Suspend new signal checks while exposed
    }

    // 4. Signal Engine (Replace with your specific PA sweep logic)
    bool longTrigger = inNewsWindow && !spreadViolated; // Mock trigger

    // 5. Execution & Audit Trail
    if(longTrigger) {
        double intendedEntry = ask;
        double stopLoss = intendedEntry - (MinStopDistancePts * _Point);
        
        // Dynamic Risk Sizing
        double activeRisk = RiskPercent * NewsRiskModifier;
        double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * (activeRisk / 100.0);
        double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
        double pointValue = tickValue / (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / _Point);
        double volume = NormalizeDouble(riskAmount / (MinStopDistancePts * pointValue), 2);

        // Ping the timer for latency tracking
        uint startMS = GetTickCount();
        
        if(trade.Buy(volume, _Symbol, intendedEntry, stopLoss, 0, "CPI_Long")) {
            uint execTime = GetTickCount() - startMS;
            entryBarTime = (int)iTime(_Symbol, PERIOD_M1, 0);
            
            // Audit Log: Query the actual deal history to find true fill
            ulong dealTicket = trade.ResultDeal();
            if(HistoryDealSelect(dealTicket)) {
                double actualFill = HistoryDealGetDouble(dealTicket, DEAL_PRICE);
                int slippagePts = (int)MathRound(MathAbs(actualFill - intendedEntry) / _Point);
                
                Print("\n================ CPI FILL LOG AUDIT ================");
                PrintFormat("Intent (Click): %.5f | Actual Fill: %.5f", intendedEntry, actualFill);
                PrintFormat("Spread at Click: %d pts | Slippage Incurred: %d pts", currentSpread, slippagePts);
                PrintFormat("Broker Latency: %d ms", execTime);
                Print("====================================================");
            }
        } else {
            PrintFormat("Ticket Rejected. Spread: %d. Broker Code: %d", currentSpread, trade.ResultRetcode());
        }
    }
}

Re: Slippage during CPI: second-by-second fill log analysis

Posted: Wed Sep 16, 2026 8:26 pm
by PTScalper
The MT4 (MQL4) Implementation

If you are running this on legacy MT4 environments, the execution logic shifts from CTrade objects to the procedural OrderSend(). MT4 handles order history differently, requiring you to iterate through OrderSelect() rather than selecting deal tickets directly.

Here is the modified execution and audit block for MQL4:

Code: Select all

//+------------------------------------------------------------------+
//| MT4 EXECUTION & AUDIT BLOCK (Place inside OnTick)                |
//+------------------------------------------------------------------+

// 1. Spread Check (Native MT4)
int currentSpread = (int)MarketInfo(Symbol(), MODE_SPREAD);
bool spreadViolated = (currentSpread > MaxSpreadPoints);

// ... (Time Check & Sizing logic remains procedurally similar to MT5) ...

if(longTrigger) {
    double intendedEntry = Ask;
    double stopLoss = intendedEntry - (MinStopDistancePts * Point);
    
    // Ping the timer
    uint startMS = GetTickCount();
    
    // Execute
    int ticket = OrderSend(Symbol(), OP_BUY, volume, intendedEntry, SlippageTolerance, stopLoss, 0, "CPI_Long", 1001, 0, Blue);
    
    // Calculate Latency
    uint execTime = GetTickCount() - startMS;

    if(ticket > 0) {
        entryBarTime = (int)Time[0]; // Anchor time stop
        
        // Audit Log: Select order to get true open price
        if(OrderSelect(ticket, SELECT_BY_TICKET)) {
            double actualFill = OrderOpenPrice();
            int slippagePts = (int)MathRound(MathAbs(actualFill - intendedEntry) / Point);
            
            Print("\n================ CPI FILL LOG AUDIT ================");
            PrintFormat("Intent: %.5f | Actual Fill: %.5f", intendedEntry, actualFill);
            PrintFormat("Spread at Click: %d pts | Slippage Incurred: %d pts", currentSpread, slippagePts);
            PrintFormat("Broker Latency: %d ms", execTime);
            Print("====================================================");
        }
    } else {
        PrintFormat("Ticket Rejected. Spread: %d. Broker Code: %d", currentSpread, GetLastError());
    }
}