Advertisement IC Markets

Calibrating my spread filter that blocks revenge during CPI week

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

Calibrating my spread filter that blocks revenge during CPI week

Post by LondonScalper »

My spread filter started life as an execution tool and turned into a behaviour tool by accident.

The original rule was simple: no EURUSD entry if the live spread is above 0.6 pips on my raw account. During a normal week that blocks almost nothing between 07:00 and 11:00 London. The surprise came when I tagged every blocked attempt for a quarter. Most of them weren't at random times. They clustered in the twenty minutes after a stop-out, and especially in CPI weeks, when I was annoyed and the spread was still wobbling after the release.

In other words the filter was catching me trying to get my money back while conditions were poor.

For CPI week I now run a tighter version. From the Monday of a US CPI week the threshold drops to 0.5 pips, and on release day the platform script refuses any EURUSD or GBPUSD order from 13:20 to 13:50 London regardless of spread. After 13:50 the normal filter applies, but any order placed within ten minutes of a losing exit needs the spread at or below 0.4.

Last CPI week that blocked nine attempts. I went back over the charts afterwards. Six of those nine would have lost, two would have scratched, one would have made about 4 pips.

I can live with missing that one. The point is that the number stays the same whatever mood I'm in, which I can't say for my judgement after a loss.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

LondonScalper wrote: Mon Oct 05, 2026 12:30 am My spread filter started life as an execution tool and turned into a behaviour tool by accident.

The original rule was simple: no EURUSD entry if the live spread is above 0.6 pips on my raw account. During a normal week that blocks almost nothing between 07:00 and 11:00 London. The surprise came when I tagged every blocked attempt for a quarter. Most of them weren't at random times. They clustered in the twenty minutes after a stop-out, and especially in CPI weeks, when I was annoyed and the spread was still wobbling after the release.

In other words the filter was catching me trying to get my money back while conditions were poor.

For CPI week I now run a tighter version. From the Monday of a US CPI week the threshold drops to 0.5 pips, and on release day the platform script refuses any EURUSD or GBPUSD order from 13:20 to 13:50 London regardless of spread. After 13:50 the normal filter applies, but any order placed within ten minutes of a losing exit needs the spread at or below 0.4.

Last CPI week that blocked nine attempts. I went back over the charts afterwards. Six of those nine would have lost, two would have scratched, one would have made about 4 pips.

I can live with missing that one. The point is that the number stays the same whatever mood I'm in, which I can't say for my judgement after a loss.
Hi,

This is honestly one of the best "accidental discoveries" I've read in a while, and it perfectly highlights the intersection of market mechanics and trading psychology.

I build custom execution scripts and automated order rejection logging tools in MQL5 and cAlgo, and my terminal data reflects the exact same pattern. We initially code these spread filters thinking we are protecting our entries from bad liquidity or slippage, but the reality is we are usually building a circuit breaker to protect ourselves from our own impulsive behavior.

What you're experiencing is heavily tied to market microstructure. Right after a major stop-out (which often aligns with a broader market liquidity sweep) or during the turbulent aftermath of a CPI print, liquidity providers temporarily pull their limit orders. The order book thins out, which inherently widens the spread. When you feel the urge to revenge trade in that 20-minute window, you are essentially trying to force an entry into a market that is still actively sweeping liquidity and hasn't established a clean structure.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

By enforcing that strict 0.4 pip threshold for ten minutes after a loss, you aren't just filtering out bad pricing—you are mechanically forcing yourself to wait until the order book repopulates and the post-sweep volatility settles.

Your CPI week adjustments are spot on, too. I trade raw price action on the 15-minute and Daily charts, and CPI consistently creates massive structural anomalies that invalidate standard setups. A hard block from 13:20 to 13:50 London is the ultimate defense mechanism. Missing a single 4-pip winner out of nine blocked trades is an incredibly cheap premium to pay for avoiding six guaranteed losses and the emotional drain that comes with them. The script doesn't have an ego, and it doesn't try to get its money back.

Out of curiosity, how are you capturing the data on those blocked attempts? Are you just printing the rejected orders to the terminal journal, or do you have the script exporting the spread, time, and structural variables to a separate file for your quarterly reviews?
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

Here is the Pine Script v5 implementation of your execution filter.

Because TradingView is a charting platform rather than a dedicated execution terminal like MetaTrader or cTrader, Pine Script cannot physically disable the "Buy/Sell" buttons for manual trading. To solve this, this script acts as a visual traffic light. It calculates the live spread in real-time, evaluates your CPI and post-loss rules, and paints the chart background harsh red while generating an on-chart dashboard to warn you when conditions dictate you should sit on your hands.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

Pine script v1.0

Code: Select all

//@version=5
indicator("Execution Filter: Spread & CPI", overlay=true)

// --- Inputs ---
grp_spread = "Spread Thresholds (Pips)"
baseSpreadMax = input.float(0.6, title="Base Max Spread", group=grp_spread)
cpiWeekSpreadMax = input.float(0.5, title="CPI Week Max Spread", group=grp_spread)
postLossSpreadMax = input.float(0.4, title="Post-Loss Max Spread", group=grp_spread)

grp_cpi = "CPI Schedule"
isCpiWeek = input.bool(false, title="Is it CPI Week?", group=grp_cpi)
isCpiDay = input.bool(false, title="Is it CPI Release Day?", group=grp_cpi)
cpiSession = input.session("1320-1350", title="CPI Block Window", group=grp_cpi)

grp_loss = "Post-Loss State"
recentLoss = input.bool(false, title="Recent Loss (Active)?", tooltip="Toggle on if you just took a loss.", group=grp_loss)

// --- Spread Calculation ---
// Converts ticks to pips for standard 5-digit forex brokers
pipMultiplier = syminfo.type == "forex" ? 10 : 1

// Real-time spread calculation (defaults to 0 on historical bars)
float liveSpreadPips = 0.0
if barstate.isrealtime
    liveSpreadPips := (syminfo.ask - syminfo.bid) / (syminfo.mintick * pipMultiplier)

// --- Rule Evaluation ---
// 1. Time Window (Mapped to London Time)
inCpiBlockWindow = time(timeframe.period, cpiSession, "Europe/London")

// 2. Determine Active Threshold
currentThreshold = baseSpreadMax
if isCpiWeek
    currentThreshold := cpiWeekSpreadMax
if recentLoss
    currentThreshold := postLossSpreadMax

// 3. Block Logic
bool timeBlock = isCpiDay and not na(inCpiBlockWindow)
bool spreadBlock = liveSpreadPips > currentThreshold and barstate.isrealtime

bool isBlocked = timeBlock or spreadBlock

// --- Visual Output ---
// Traffic light background: Red when blocked (only executes on real-time bars)
bgcolor(barstate.isrealtime and isBlocked ? color.new(color.red, 85) : na, title="Block Active")

// Real-time Dashboard
var table dashboard = table.new(position.top_right, 2, 3, border_width=1, border_color=color.gray, bgcolor=color.new(color.black, 20))
if barstate.islast
    table.cell(dashboard, 0, 0, "Filter Status:", text_color=color.white)
    table.cell(dashboard, 1, 0, isBlocked ? "BLOCKED" : "CLEAR", text_color=isBlocked ? color.red : color.green, text_weight=text.weight_bold)
    
    table.cell(dashboard, 0, 1, "Live Spread:", text_color=color.white)
    table.cell(dashboard, 1, 1, str.tostring(liveSpreadPips, "#.0") + " pips", text_color=spreadBlock ? color.red : color.green)
    
    table.cell(dashboard, 0, 2, "Max Allowed:", text_color=color.white)
    table.cell(dashboard, 1, 2, str.tostring(currentThreshold, "#.0") + " pips", text_color=color.yellow)
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

How it operates in practice:

Live Ask/Bid limitation: The syminfo.ask and syminfo.bid functions only stream data during real-time bars. The indicator won't show historical spread blocks on your back-charting, ensuring your historical price action analysis remains completely clean.

London Timezone Lock: The time() function explicitly calls "Europe/London". Even if you change your local TradingView chart timezone to Habartov local time (CET/CEST), the 13:20 to 13:50 block will always perfectly align with London market hours.

Manual Toggles: Because an indicator cannot directly read your account history to know you just took a loss, you manage the state via the settings cog. During CPI week, you tick the box on Monday. When you take a loss, you tick the "Recent Loss" box to immediately drop the threshold to 0.4 and turn the chart red until liquidity returns.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

Because native MetaTrader 5 order panels bypass Expert Advisors directly to the trade server, an EA cannot intercept and block a native "One-Click" button press. To physically reject orders, the EA itself must handle the execution.

This MQL5 implementation is structured as an object-oriented execution script using keyboard hotkeys (B for Buy, S for Sell). Because MQL5 has direct access to account history, it completely automates the "Recent Loss" detection by polling your closed deals, eliminating the need for the manual toggle used in the Pine Script version.

Code: Select all

//+------------------------------------------------------------------+
//|                                             Execution Filter.mq5 |
//|                                    Hard-blocking Spread & CPI EA |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>

// --- Inputs ---
input group "Spread Thresholds (Pips)"
input double   InpBaseSpreadMax     = 0.6;   // Base Max Spread
input double   InpCpiWeekSpreadMax  = 0.5;   // CPI Week Max Spread
input double   InpPostLossSpreadMax = 0.4;   // Post-Loss Max Spread
input int      InpLossWindowMinutes = 10;    // Post-Loss Monitoring Window

input group "CPI Schedule"
input bool     InpIsCpiWeek         = false; // Is it CPI Week?
input bool     InpIsCpiDay          = false; // Is it CPI Release Day?
input int      InpCpiStartHour      = 15;    // Block Start Hour (Broker Time)
input int      InpCpiStartMinute    = 20;    // Block Start Minute
input int      InpCpiEndHour        = 15;    // Block End Hour (Broker Time)
input int      InpCpiEndMinute      = 50;    // Block End Minute

input group "Execution"
input double   InpLotSize           = 1.0;   // Default Lot Size

CTrade trade;

//+------------------------------------------------------------------+
//| Filter Class Definition                                          |
//+------------------------------------------------------------------+
class CExecutionFilter
  {
private:
   double      GetLiveSpreadPips();
   bool        HasRecentLoss();
   bool        IsInCpiBlockWindow();

public:
   bool        ValidateConditions();
  };

//+------------------------------------------------------------------+
//| Method: Calculate live spread in true pips                       |
//+------------------------------------------------------------------+
double CExecutionFilter::GetLiveSpreadPips()
  {
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double pip_multiplier = (digits == 3 || digits == 5) ? 10.0 : 1.0;
   return (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) / pip_multiplier;
  }

//+------------------------------------------------------------------+
//| Method: Scan history for losses in the defined minute window     |
//+------------------------------------------------------------------+
bool CExecutionFilter::HasRecentLoss()
  {
   datetime end_time = TimeCurrent();
   datetime start_time = end_time - (InpLossWindowMinutes * 60);

   if(!HistorySelect(start_time, end_time))
      return false;

   int total_deals = HistoryDealsTotal();
   for(int i = total_deals - 1; i >= 0; i--)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket > 0)
        {
         long entry_type = HistoryDealGetInteger(ticket, DEAL_ENTRY);
         // Check only deals that closed a position
         if(entry_type == DEAL_ENTRY_OUT || entry_type == DEAL_ENTRY_INOUT)
           {
            double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT);
            if(profit < 0.0) 
               return true; // Found a losing exit within the window
           }
        }
     }
   return false;
  }

//+------------------------------------------------------------------+
//| Method: Check if current broker time is within the block window  |
//+------------------------------------------------------------------+
bool CExecutionFilter::IsInCpiBlockWindow()
  {
   if(!InpIsCpiDay) return false;

   MqlDateTime dt;
   TimeCurrent(dt);
   
   int current_mins = (dt.hour * 60) + dt.min;
   int start_mins   = (InpCpiStartHour * 60) + InpCpiStartMinute;
   int end_mins     = (InpCpiEndHour * 60) + InpCpiEndMinute;

   return (current_mins >= start_mins && current_mins <= end_mins);
  }

//+------------------------------------------------------------------+
//| Method: Master validation logic                                  |
//+------------------------------------------------------------------+
bool CExecutionFilter::ValidateConditions()
  {
   // 1. Time Block Check
   if(IsInCpiBlockWindow())
     {
      Print("🔴 REJECTED: CPI Time Block is active.");
      return false;
     }

   // 2. Determine Active Spread Threshold
   double active_threshold = InpBaseSpreadMax;
   string threshold_reason = "Base Rule";
   
   if(InpIsCpiWeek)
     {
      active_threshold = InpCpiWeekSpreadMax;
      threshold_reason = "CPI Week";
     }
     
   if(HasRecentLoss())
     {
      active_threshold = InpPostLossSpreadMax;
      threshold_reason = "Post-Loss Recovery";
     }

   // 3. Spread Check
   double current_spread = GetLiveSpreadPips();
   if(current_spread > active_threshold)
     {
      PrintFormat("🔴 REJECTED: Spread is %.1f pips. Limit is %.1f (%s).", 
                  current_spread, active_threshold, threshold_reason);
      return false;
     }

   PrintFormat("🟢 ACCEPTED: Spread is %.1f pips. Limit is %.1f (%s).", 
               current_spread, active_threshold, threshold_reason);
   return true;
  }

CExecutionFilter ExecutionFilter;

//+------------------------------------------------------------------+
//| Chart Event Handler for Hotkeys                                  |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(id == CHARTEVENT_KEYDOWN)
     {
      // Keycode 66 = 'B' for Buy
      if(lparam == 66)
        {
         if(ExecutionFilter.ValidateConditions())
           {
            double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            trade.Buy(InpLotSize, _Symbol, ask, 0.0, 0.0, "Filter Accepted");
           }
        }
        
      // Keycode 83 = 'S' for Sell
      else if(lparam == 83)
        {
         if(ExecutionFilter.ValidateConditions())
           {
            double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            trade.Sell(InpLotSize, _Symbol, bid, 0.0, 0.0, "Filter Accepted");
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   Print("Execution Filter Loaded. Press 'B' to Buy, 'S' to Sell.");
   return(INIT_SUCCEEDED);
  }
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

Implementation Notes

Timezone Conversion: The InpCpiStartHour and InpCpiEndHour default to 15 assuming a standard EET broker (UTC+2/UTC+3) to match the 13:20 London time requirement. Adjust these integer inputs to match your specific broker's server time.

Automatic Loss Polling: The HasRecentLoss() method queries HistorySelect() dynamically. If your stop loss gets hit, the EA will immediately detect the negative DEAL_PROFIT on the DEAL_ENTRY_OUT ticket and drop the acceptable spread threshold to 0.4 for the next 10 minutes without any manual toggling.

Execution: Attach this to your chart, ensure Auto Trading is enabled, and execute entries exclusively using the B and S keys. The PrintFormat() statements will log the exact reason for any rejections directly to the Experts journal for your quarterly reviews.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

Since you have an extensive background in C# and .NET, cTrader’s cAlgo API is the perfect environment for this. We can leverage LINQ to make the historical loss polling incredibly clean, completely eliminating the need for the manual loops required in MQL5.

This implementation uses the Chart.KeyDown event to capture your 'B' and 'S' hotkeys, and intercepts the execution request to evaluate your rules before allowing the ExecuteMarketOrder to fire.

Code: Select all

using System;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo.Robots
{
    [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class ExecutionFilter : Robot
    {
        // --- Spread Thresholds ---
        [Parameter("Base Max Spread", Group = "Spread Thresholds", DefaultValue = 0.6, MinValue = 0)]
        public double BaseSpreadMax { get; set; }

        [Parameter("CPI Week Max", Group = "Spread Thresholds", DefaultValue = 0.5, MinValue = 0)]
        public double CpiWeekSpreadMax { get; set; }

        [Parameter("Post-Loss Max", Group = "Spread Thresholds", DefaultValue = 0.4, MinValue = 0)]
        public double PostLossSpreadMax { get; set; }

        [Parameter("Loss Window (Minutes)", Group = "Spread Thresholds", DefaultValue = 10)]
        public int LossWindowMinutes { get; set; }

        // --- CPI Schedule ---
        [Parameter("Is CPI Week?", Group = "CPI Schedule", DefaultValue = false)]
        public bool IsCpiWeek { get; set; }

        [Parameter("Is CPI Day?", Group = "CPI Schedule", DefaultValue = false)]
        public bool IsCpiDay { get; set; }

        [Parameter("Block Start (Broker Time)", Group = "CPI Schedule", DefaultValue = "15:20")]
        public string CpiStartTimeStr { get; set; }

        [Parameter("Block End (Broker Time)", Group = "CPI Schedule", DefaultValue = "15:50")]
        public string CpiEndTimeStr { get; set; }

        // --- Execution ---
        [Parameter("Volume (Units)", Group = "Execution", DefaultValue = 100000)]
        public double VolumeInUnits { get; set; }

        private TimeSpan _cpiStart;
        private TimeSpan _cpiEnd;

        protected override void OnStart()
        {
            // Parse time strings into TimeSpans for easy comparison
            if (!TimeSpan.TryParse(CpiStartTimeStr, out _cpiStart) || !TimeSpan.TryParse(CpiEndTimeStr, out _cpiEnd))
            {
                Print("🔴 ERROR: Invalid CPI Time format. Use HH:mm");
                Stop();
            }

            Chart.KeyDown += OnChartKeyDown;
            Print("🟢 Execution Filter Loaded. Press 'B' to Buy, 'S' to Sell.");
        }

        private void OnChartKeyDown(ChartKeyboardEventArgs args)
        {
            if (args.Key == Key.B)
            {
                TryExecute(TradeType.Buy);
            }
            else if (args.Key == Key.S)
            {
                TryExecute(TradeType.Sell);
            }
        }

        private void TryExecute(TradeType tradeType)
        {
            if (!ValidateConditions())
                return;

            ExecuteMarketOrder(tradeType, SymbolName, VolumeInUnits, "ExecutionFilter", 0, 0);
        }

        private bool ValidateConditions()
        {
            // 1. Time Block Check
            if (IsCpiDay)
            {
                TimeSpan currentTime = Server.Time.TimeOfDay;
                if (currentTime >= _cpiStart && currentTime <= _cpiEnd)
                {
                    Print("🔴 REJECTED: CPI Time Block is active.");
                    return false;
                }
            }

            // 2. Determine Active Spread Threshold
            double activeThreshold = BaseSpreadMax;
            string thresholdReason = "Base Rule";

            if (IsCpiWeek)
            {
                activeThreshold = CpiWeekSpreadMax;
                thresholdReason = "CPI Week";
            }

            // LINQ query to instantly check for a closing loss within the specified minute window
            bool hasRecentLoss = History.Where(x => 
                x.SymbolName == SymbolName && 
                x.NetProfit < 0 && 
                (Server.TimeInUtc - x.ClosingTime).TotalMinutes <= LossWindowMinutes
            ).Any();

            if (hasRecentLoss)
            {
                activeThreshold = PostLossSpreadMax;
                thresholdReason = "Post-Loss Recovery";
            }

            // 3. Spread Check (cTrader calculates this natively in pips via Symbol.PipSize)
            double currentSpreadPips = Symbol.Spread / Symbol.PipSize;
            
            // Round to 1 decimal place to prevent floating-point micro-failures (e.g. 0.600000001 > 0.6)
            currentSpreadPips = Math.Round(currentSpreadPips, 1);

            if (currentSpreadPips > activeThreshold)
            {
                Print($"🔴 REJECTED: Spread is {currentSpreadPips:F1} pips. Limit is {activeThreshold:F1} ({thresholdReason}).");
                return false;
            }

            Print($"🟢 ACCEPTED: Spread is {currentSpreadPips:F1} pips. Limit is {activeThreshold:F1} ({thresholdReason}).");
            return true;
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 4805
Joined: Mon Jul 20, 2026 1:28 pm

Re: Calibrating my spread filter that blocks revenge during CPI week

Post by PTScalper »

Implementation Notes

Time Block Parsing: Instead of dealing with clunky integer inputs for hours and minutes, C# allows us to expose string parameters (e.g., "15:20") and parse them directly into TimeSpan objects inside OnStart(). You will need to input these based on your specific cTrader broker's server time to match the 13:20 London block.

The LINQ Query: The History.Where(...).Any() completely replaces the manual loop iteration needed in MQL5. It filters your account history for the current symbol, negative net profit, and checks if the total minutes since the ClosingTime are within your 10-minute window, returning a boolean instantly.

Floating-Point Safety: I added Math.Round() to the live spread calculation. Because cTrader uses double-precision floats for raw tick data, a spread that perfectly hits 0.6 pips visually might evaluate as 0.6000000001 under the hood, accidentally triggering your rejection logic. Rounding to 1 decimal place maps it exactly to standard fractional pip measurements.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply