Advertisement IC Markets

The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

Key Differences for MT4/MT5

PrintFormat vs Print: PrintFormat is used here instead of standard concatenation. It allows formatting floating-point numbers to exactly two decimal places (%.2f), which keeps your log files clean and readable when evaluating spread variance over the session.

Exact Spread Calculation: Instead of using SYMBOL_SPREAD (which returns a rounded integer in points), calculating Ask - Bid dynamically ensures you capture the exact fractional pip spread at the millisecond the signal fires.
Recommended broker for automated trading & scalping IC Markets
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

Here are the updated scripts for both MT4 and MT5.

To determine if the M15 candle is "too small to trade," the most mathematically robust price-action method is calculating the Body-to-Range Ratio. Instead of measuring raw pips (which fluctuate with market volatility), this function calculates the absolute size of the candle body and divides it by the total wick-to-wick range.

If the body represents less than 25% of the total range (input as 0.25), the candle is objectively classified as a doji or spinning top—a clear mathematical signature of indecision and trapping flow.

MQL4 (MetaTrader 4)

Code: Select all

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

input double MaxSpreadPips = 1.0;       // Max Spread Limit (Pips)
input double MinM15BodyRatio = 0.25;    // Min M15 Body-to-Range Ratio (0.25 = 25%)

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    bool entrySignalGenerated = false; 
    int signalDirection = OP_BUY;

    if (entrySignalGenerated)
    {
        AttemptExecution(signalDirection);
    }
}

//+------------------------------------------------------------------+
//| Execution Gatekeeper                                             |
//+------------------------------------------------------------------+
void AttemptExecution(int cmd)
{
    string tradeDir = (cmd == OP_BUY) ? "BUY" : "SELL";

    // 1. The M15 Structural Circuit Breaker
    if (!IsM15StructureValid())
    {
        PrintFormat("[REJECTED] %s blocked. M15 prints indecision (Body ratio < %.0f%%).", tradeDir, MinM15BodyRatio * 100);
        return;
    }

    // 2. The Spread Circuit Breaker
    double spreadPips = (Ask - Bid) / GetPipSize();
    if (spreadPips > MaxSpreadPips)
    {
        PrintFormat("[REJECTED] %s blocked. Live Spread: %.2f pips exceeds limit of %.1f.", tradeDir, spreadPips, MaxSpreadPips);
        return; 
    }

    // 3. Execution
    PrintFormat("[ACCEPTED] %s valid. M15 clear. Spread: %.2f. Executing...", tradeDir, spreadPips);
}

//+------------------------------------------------------------------+
//| Evaluates previous M15 candle structure                          |
//+------------------------------------------------------------------+
bool IsM15StructureValid()
{
    double open  = iOpen(Symbol(), PERIOD_M15, 1);
    double close = iClose(Symbol(), PERIOD_M15, 1);
    double high  = iHigh(Symbol(), PERIOD_M15, 1);
    double low   = iLow(Symbol(), PERIOD_M15, 1);

    double range = high - low;
    if (range == 0) return false; // Prevent divide-by-zero on flat candles

    double body = MathAbs(open - close);
    double ratio = body / range;

    return (ratio >= MinM15BodyRatio);
}

//+------------------------------------------------------------------+
//| Normalizes Points to standard Pips                               |
//+------------------------------------------------------------------+
double GetPipSize()
{
    return (Digits == 3 || Digits == 5) ? Point * 10.0 : Point;
}
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

MQL5 (MetaTrader 5)

MQL5 requires using the CopyRates array to safely pull historical timeframe data rather than MQL4's direct iOpen shortcuts.

Code: Select all

//+------------------------------------------------------------------+
//|                                        M15_CircuitBreaker.mq5    |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>

input double MaxSpreadPips = 1.0;       // Max Spread Limit (Pips)
input double MinM15BodyRatio = 0.25;    // Min M15 Body-to-Range Ratio (0.25 = 25%)

CTrade trade;

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    bool entrySignalGenerated = false; 
    ENUM_ORDER_TYPE signalDirection = ORDER_TYPE_BUY;

    if (entrySignalGenerated)
    {
        AttemptExecution(signalDirection);
    }
}

//+------------------------------------------------------------------+
//| Execution Gatekeeper                                             |
//+------------------------------------------------------------------+
void AttemptExecution(ENUM_ORDER_TYPE cmd)
{
    string tradeDir = (cmd == ORDER_TYPE_BUY) ? "BUY" : "SELL";

    // 1. The M15 Structural Circuit Breaker
    if (!IsM15StructureValid())
    {
        PrintFormat("[REJECTED] %s blocked. M15 prints indecision (Body ratio < %.0f%%).", tradeDir, MinM15BodyRatio * 100);
        return;
    }

    // 2. The Spread Circuit Breaker
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double spreadPips = (ask - bid) / GetPipSize();
    
    if (spreadPips > MaxSpreadPips)
    {
        PrintFormat("[REJECTED] %s blocked. Live Spread: %.2f pips exceeds limit of %.1f.", tradeDir, spreadPips, MaxSpreadPips);
        return; 
    }

    // 3. Execution
    PrintFormat("[ACCEPTED] %s valid. M15 clear. Spread: %.2f. Executing...", tradeDir, spreadPips);
}

//+------------------------------------------------------------------+
//| Evaluates previous M15 candle structure using CopyRates          |
//+------------------------------------------------------------------+
bool IsM15StructureValid()
{
    MqlRates rates[];
    ArraySetAsSeries(rates, true); // Index 0 becomes the current candle
    
    // Copy 1 candle, starting from index 1 (the last fully closed candle)
    if(CopyRates(_Symbol, PERIOD_M15, 1, 1, rates) <= 0)
    {
        Print("[ERROR] Failed to retrieve M15 historical data.");
        return false; 
    }

    double range = rates[0].high - rates[0].low;
    if (range == 0) return false; 

    double body = MathAbs(rates[0].open - rates[0].close);
    double ratio = body / range;

    return (ratio >= MinM15BodyRatio);
}

//+------------------------------------------------------------------+
//| Normalizes Points to standard Pips                               |
//+------------------------------------------------------------------+
double GetPipSize()
{
    int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
    double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
    return (digits == 3 || digits == 5) ? point * 10.0 : point;
}
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

Why this structural check is powerful

By reading raw price action directly off the M15 timeframe without relying on moving averages, the bot prevents execution during accumulation and distribution chop. If the M1 triggers a "perfect" setup but the preceding 15 minutes of volume settled back into the middle of its own range (a wick on both sides), the bot locks down. It ensures you only pay the spread when higher-timeframe displacement is actively unfolding.
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

Adding a daily loss circuit breaker requires the script to read your account history dynamically. To do this safely, we must introduce a Magic Number—a unique ID assigned to the bot's trades so it only counts its own losses and ignores any discretionary trades or other algorithms running on the same account.

We also must factor in commissions and swaps into the profit calculation. A scratch trade that technically closed at breakeven but lost money to the broker's commission is mathematically a loss and must count toward the circuit breaker.

Here is how to integrate the consecutive loss lock for both platforms.
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

MQL4 (MetaTrader 4)

Code: Select all

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

input double MaxSpreadPips = 1.0;       
input double MinM15BodyRatio = 0.25;    
input int    MaxConsecutiveLosses = 2;  // Session kill-switch limit
input int    MagicNumber = 12345;       // Bot's unique ID

//+------------------------------------------------------------------+
//| Execution Gatekeeper                                             |
//+------------------------------------------------------------------+
void AttemptExecution(int cmd)
{
    string tradeDir = (cmd == OP_BUY) ? "BUY" : "SELL";

    // 1. The Consecutive Loss Circuit Breaker
    if (IsSessionLockedOut())
    {
        Print("[REJECTED] Execution locked. Consecutive daily loss limit reached.");
        return;
    }

    // 2. The M15 Structural Circuit Breaker
    if (!IsM15StructureValid())
    {
        PrintFormat("[REJECTED] %s blocked. M15 prints indecision.", tradeDir);
        return;
    }

    // 3. The Spread Circuit Breaker
    double spreadPips = (Ask - Bid) / GetPipSize();
    if (spreadPips > MaxSpreadPips)
    {
        PrintFormat("[REJECTED] %s blocked. Spread: %.2f.", tradeDir, spreadPips);
        return; 
    }

    Print("[ACCEPTED] All parameters clear. Executing...");
    // OrderSend(Symbol(), cmd, 0.1, (cmd==OP_BUY?Ask:Bid), 3, 0, 0, "Scalp_Entry", MagicNumber, 0, clrNONE);
}

//+------------------------------------------------------------------+
//| Checks history for consecutive losses today                      |
//+------------------------------------------------------------------+
bool IsSessionLockedOut()
{
    int lossCount = 0;
    datetime startOfDay = iTime(Symbol(), PERIOD_D1, 0); // Midnight of current day
    int total = OrdersHistoryTotal();

    // Iterate backward from the most recent closed trade
    for (int i = total - 1; i >= 0; i--)
    {
        if (OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
        {
            // Only evaluate trades from this bot on this pair
            if (OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
            {
                // If we reach a trade from yesterday, stop checking
                if (OrderCloseTime() < startOfDay) 
                    break; 

                // Calculate Net Profit
                double netProfit = OrderProfit() + OrderSwap() + OrderCommission();

                if (netProfit < 0)
                {
                    lossCount++;
                    if (lossCount >= MaxConsecutiveLosses) 
                        return true; // Lockout triggered
                }
                else if (netProfit > 0)
                {
                    break; // Streak broken by a winner
                }
            }
        }
    }
    return false; // Safe to trade
}

// (GetPipSize and IsM15StructureValid functions remain exactly the same as previous)
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

MQL5 (MetaTrader 5)

MQL5 handles history much differently. It separates trades into "Orders" (requests) and "Deals" (actual executions). To check PnL, we must query the history of Deals that explicitly exited the market (DEAL_ENTRY_OUT).

Code: Select all

//+------------------------------------------------------------------+
//|                                        DailyLockout_MT5.mq5      |
//+------------------------------------------------------------------+
#property strict
#include <Trade\Trade.mqh>

input double MaxSpreadPips = 1.0;       
input double MinM15BodyRatio = 0.25;    
input int    MaxConsecutiveLosses = 2;  // Session kill-switch limit
input ulong  MagicNumber = 12345;       // Bot's unique ID

CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    trade.SetExpertMagicNumber(MagicNumber); // Assign magic number to trade class
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Execution Gatekeeper                                             |
//+------------------------------------------------------------------+
void AttemptExecution(ENUM_ORDER_TYPE cmd)
{
    string tradeDir = (cmd == ORDER_TYPE_BUY) ? "BUY" : "SELL";

    // 1. The Consecutive Loss Circuit Breaker
    if (IsSessionLockedOut())
    {
        Print("[REJECTED] Execution locked. Consecutive daily loss limit reached.");
        return;
    }

    // 2. The M15 Structural Circuit Breaker
    if (!IsM15StructureValid())
    {
        PrintFormat("[REJECTED] %s blocked. M15 prints indecision.", tradeDir);
        return;
    }

    // 3. The Spread Circuit Breaker
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double spreadPips = (ask - bid) / GetPipSize();
    
    if (spreadPips > MaxSpreadPips)
    {
        PrintFormat("[REJECTED] %s blocked. Spread: %.2f.", tradeDir, spreadPips);
        return; 
    }

    Print("[ACCEPTED] All parameters clear. Executing...");
}

//+------------------------------------------------------------------+
//| Checks history for consecutive losses today using Deals          |
//+------------------------------------------------------------------+
bool IsSessionLockedOut()
{
    datetime startOfDay = iTime(_Symbol, PERIOD_D1, 0);
    
    // Load history from the start of the day to right now
    if (!HistorySelect(startOfDay, TimeCurrent())) return false;
    
    int lossCount = 0;
    int totalDeals = HistoryDealsTotal();

    // Iterate backward from the most recent deal
    for (int i = totalDeals - 1; i >= 0; i--)
    {
        ulong ticket = HistoryDealGetTicket(i);
        if (ticket > 0)
        {
            // Only look at deals that closed a position (OUT)
            long entryType = HistoryDealGetInteger(ticket, DEAL_ENTRY);
            if (entryType == DEAL_ENTRY_OUT || entryType == DEAL_ENTRY_INOUT)
            {
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == _Symbol &&
                    HistoryDealGetInteger(ticket, DEAL_MAGIC) == MagicNumber)
                {
                    double netProfit = HistoryDealGetDouble(ticket, DEAL_PROFIT) + 
                                       HistoryDealGetDouble(ticket, DEAL_COMMISSION) + 
                                       HistoryDealGetDouble(ticket, DEAL_SWAP);
                                       
                    if (netProfit < 0)
                    {
                        lossCount++;
                        if (lossCount >= MaxConsecutiveLosses) 
                            return true; // Lockout triggered
                    }
                    else if (netProfit > 0)
                    {
                        break; // Streak broken by a winner
                    }
                }
            }
        }
    }
    return false; // Safe to trade
}

// (GetPipSize and IsM15StructureValid functions remain exactly the same as previous)
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

By placing the IsSessionLockedOut() check at the very top of AttemptExecution, you save processing power. If the session is burned, the bot won't bother calculating M15 body ratios or querying live spreads—it simply shuts the door on the first line.
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

Yes, resetting at the actual session open is vastly superior. If you leave the bot running overnight, a rogue spread-spike loss during the Asian session shouldn't penalize your risk budget for the London open.

To do this, we replace the hardcoded midnight check (iTime(..., PERIOD_D1, 0)) with a dynamic function that calculates the exact broker timestamp for your session start.

Here is the modular update you can drop into both the MT4 and MT5 scripts.
FTtrader
Posts: 954
Joined: Mon Aug 03, 2026 2:43 pm

Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart

Post by FTtrader »

1. Add the New Input Parameter

Add this to the top of your script with the other inputs. Note: MetaTrader uses Broker Server Time, not your local time. If the London open is 08:00 GMT, you must set this to match your broker's timezone (often UTC+2 or UTC+3).

Code: Select all

input string SessionResetTime = "08:00"; // Session Reset (Broker Time HH:MM)
Post Reply