Page 3 of 5
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:57 am
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.
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:57 am
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;
}
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:57 am
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;
}
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:58 am
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.
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:58 am
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.
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:59 am
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)
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:59 am
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)
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 7:59 am
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.
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 8:06 am
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.
Re: The Scalper's Secret Sauce: Why I Ditched the 1-Minute Chart
Posted: Fri Sep 25, 2026 8:07 am
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)