1906 satır
64 KiB
Text
1906 satır
64 KiB
Text
#property strict
|
|
#property version "1.00"
|
|
#include <Trade\Trade.mqh>
|
|
#include <Trade\PositionInfo.mqh>
|
|
#include <Trade\DealInfo.mqh>
|
|
#property description "High/Low Detection Bot: Detects Highs (Bull->Bear) and Lows (Bear->Bull) with Visuals"
|
|
|
|
|
|
//==================== INPUTS ====================
|
|
// Symbol
|
|
input string InpSymbol = ""; // Symbol (empty = current)
|
|
|
|
|
|
|
|
|
|
// Visuals
|
|
input bool InpDrawHighLow = true; // Draw High/Low Points
|
|
input bool InpDrawBOS = true; // Draw BOS Points
|
|
input int InpBOSThresholdPoints = 5; // BOS Break Threshold in Points
|
|
input bool InpDrawFVG = true; // Draw FVG (Fair Value Gap)
|
|
input int InpFVGThresholdPoints = 5; // FVG Threshold in Points
|
|
input bool InpDrawiFVG = true; // Draw iFVG (FVG Mitigation)
|
|
input bool InpDrawOrderBlock = true; // Draw Order Blocks
|
|
input int InpOrderBlockCandles = 5; // Number of candles to check for Order Block
|
|
|
|
// Detection Parameters
|
|
input int InpLookback = 500; // Bars to Scan (History on Init)
|
|
input int InpHistoryLimit = 100; // Max Swing Points to Keep in Memory
|
|
|
|
// Session Settings
|
|
input bool InpUseSessions = false; // Master Session Switch
|
|
input bool InpTradeLondon = true; // Trade London Session
|
|
input bool InpTradeNY = true; // Trade NY Session
|
|
input bool InpTradeAsia = true; // Trade Asia Session
|
|
|
|
|
|
input int InpLondonStart = 8; // London Start Hour
|
|
input int InpLondonEnd = 16; // London End Hour
|
|
input int InpNYStart = 13; // NY Start Hour
|
|
input int InpNYStartMin = 0; // NY Start Minute
|
|
input int InpNYEnd = 21; // NY End Hour
|
|
input int InpAsiaStart = 3; // Asia Start Hour
|
|
input int InpAsiaEnd = 8; // Asia End Hour
|
|
|
|
// Trading Settings
|
|
input double InpLotSize = 0.1; // Lot Size
|
|
input int InpStopLoss = 200; // Stop Loss (Points) - Backup/Safety
|
|
input int InpSLBufferPoints = 10; // Stop Loss Buffer (Points) from OB/FVG
|
|
input int InpMaxSLPoints = 1000; // Max Stop Loss Distance (Points)
|
|
input double InpRiskRewardRatio = 2.0; // Risk:Reward Ratio
|
|
input int InpMaxTradesPerSetup = 1; // Max Trades Per M5 Setup
|
|
input int InpMagicNumber = 123456; // Magic Number
|
|
|
|
|
|
//==================== STRUCTURES ====================
|
|
struct TradeSetup {
|
|
bool active; // Is there a valid M5 setup waiting?
|
|
int direction; // 1 = Buy, -1 = Sell
|
|
double slPrice; // The Stop Loss price determined by M5 OB/FVG
|
|
double initialSL;
|
|
datetime setupTime; // When the M5 BOS/iFVG happened
|
|
string setupDesc; // Description of the M5 Setup (Trigger)
|
|
string confDesc; // Description of the M5 Confirmation
|
|
string initialDesc;
|
|
datetime lastCheckedTime;
|
|
datetime m5FvgTime; // Time of the original M5 FVG (for iFVG setups)
|
|
double m5FvgTop; // Top of the original M5 FVG (for iFVG setups)
|
|
double m5FvgBottom; // Bottom of the original M5 FVG (for iFVG setups)
|
|
};
|
|
|
|
struct SwingPoint {
|
|
datetime time;
|
|
double price;
|
|
int type; // 1=High, -1=Low
|
|
};
|
|
|
|
struct BOSPoint {
|
|
datetime time;
|
|
double price;
|
|
int type; // 1=Bullish BOS (break above high), -1=Bearish BOS (break below low)
|
|
datetime swingTime; // Time of the swing point that was broken
|
|
double swingPrice; // Price of the swing point that was broken
|
|
};
|
|
|
|
struct FVGPoint {
|
|
datetime time; // Time of the middle candle (candle 2)
|
|
double top; // Top of the FVG gap
|
|
double bottom; // Bottom of the FVG gap
|
|
int type; // 1=Bullish FVG (gap up), -1=Bearish FVG (gap down)
|
|
double size; // Size of the gap in points
|
|
bool mitigated; // True if FVG has been mitigated (iFVG)
|
|
};
|
|
|
|
struct iFVGPoint {
|
|
datetime time; // Time when FVG was mitigated
|
|
datetime fvgTime; // Time of the original FVG
|
|
double price; // Price where mitigation occurred
|
|
int type; // 1=Bullish FVG mitigated (close below), -1=Bearish FVG mitigated (close above)
|
|
double fvgTop; // Original FVG top
|
|
double fvgBottom; // Original FVG bottom
|
|
};
|
|
|
|
struct OrderBlock {
|
|
datetime time; // Time of the order block (first candle in the sequence)
|
|
double top; // Top of the order block
|
|
double bottom; // Bottom of the order block
|
|
int type; // 1=Bullish Order Block (before bearish BOS), -1=Bearish Order Block (before bullish BOS)
|
|
int candleCount; // Number of consecutive candles in the order block
|
|
};
|
|
|
|
|
|
//==================== GLOBALS ====================
|
|
string SYM = "";
|
|
CTrade m_trade;
|
|
double point = 0.0;
|
|
int g_dayOfYear = -1;
|
|
TradeSetup g_pendingSetup;
|
|
datetime g_lastTradedSetupTime = 0; // Track the last M5 setup that was traded
|
|
int g_tradesExecutedForCurrentSetup = 0; // Count trades executed for current M5 setup
|
|
datetime g_lastLoggedSetupTime = 0; // Track the last M5 setup that was logged to prevent duplicate prints
|
|
datetime g_lastCheckedBarTime = 0;
|
|
datetime g_lastCheckedBarTimeM1 = 0;
|
|
datetime g_lastCheckedBarTimeM5 = 0;
|
|
|
|
// Dynamic Arrays to store detected levels "on the run"
|
|
// Arrays for specific timeframes only
|
|
SwingPoint g_swingHighsM1[];
|
|
SwingPoint g_swingLowsM1[];
|
|
BOSPoint g_bosPointsM1[];
|
|
FVGPoint g_fvgPointsM1[];
|
|
iFVGPoint g_ifvgPointsM1[];
|
|
OrderBlock g_orderBlocksM1[];
|
|
|
|
SwingPoint g_swingHighsM5[];
|
|
SwingPoint g_swingLowsM5[];
|
|
BOSPoint g_bosPointsM5[];
|
|
FVGPoint g_fvgPointsM5[];
|
|
iFVGPoint g_ifvgPointsM5[];
|
|
OrderBlock g_orderBlocksM5[];
|
|
|
|
// Latest Swing Variables (Always Available)
|
|
double g_LatestHighPrice = 0;
|
|
datetime g_LatestHighTime = 0;
|
|
double g_LatestLowPrice = 0;
|
|
datetime g_LatestLowTime = 0;
|
|
|
|
//==================== TRADING STRATEGY ====================
|
|
|
|
|
|
//==================== TIME HELPERS ====================
|
|
// Helper function to find chart by symbol and timeframe
|
|
long FindChartBySymbolAndTimeframe(string symbol, ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
long currChart = ChartFirst();
|
|
int limit = 100; // Safety limit to prevent infinite loop
|
|
|
|
for(int i = 0; i < limit; i++)
|
|
{
|
|
if(currChart < 0) break; // End of chart list
|
|
|
|
string chartSymbol = ChartSymbol(currChart);
|
|
ENUM_TIMEFRAMES chartPeriod = ChartPeriod(currChart);
|
|
|
|
if(chartSymbol == symbol && chartPeriod == timeframe)
|
|
{
|
|
return currChart; // Found the chart
|
|
}
|
|
|
|
currChart = ChartNext(currChart); // Get next chart
|
|
}
|
|
|
|
return 0; // Chart not found
|
|
}
|
|
|
|
// Helper function to get or create chart for specific timeframe
|
|
long GetOrCreateChart(string symbol, ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
// First try to find existing chart
|
|
long chartId = FindChartBySymbolAndTimeframe(symbol, timeframe);
|
|
if(chartId > 0)
|
|
{
|
|
return chartId;
|
|
}
|
|
|
|
// If not found, try to create new chart with retry logic
|
|
for(int attempt = 0; attempt < 3; attempt++)
|
|
{
|
|
chartId = ChartOpen(symbol, timeframe);
|
|
if(chartId > 0)
|
|
{
|
|
// Apply "hadi" template to the newly created chart
|
|
ChartApplyTemplate(chartId, "hadi");
|
|
|
|
// Give chart time to initialize
|
|
Sleep(100);
|
|
return chartId;
|
|
}
|
|
Sleep(200); // Wait before retry
|
|
}
|
|
|
|
return 0; // Return 0 if all attempts fail
|
|
}
|
|
|
|
int DayOfYearServer()
|
|
{
|
|
MqlDateTime tm;
|
|
TimeToStruct(TimeCurrent(), tm);
|
|
return tm.day_of_year;
|
|
}
|
|
|
|
// Function to check if we are in allowed sessions
|
|
bool InTradingSession()
|
|
{
|
|
if(!InpUseSessions) return true; // Master Switch OFF = 24/7 trading
|
|
|
|
datetime now = TimeCurrent();
|
|
MqlDateTime tm;
|
|
TimeToStruct(now, tm);
|
|
|
|
int h = tm.hour;
|
|
int m = tm.min;
|
|
|
|
// Asia (03:00 - 08:00)
|
|
bool asia = false;
|
|
if(InpTradeAsia)
|
|
{
|
|
if(h >= InpAsiaStart && h < InpAsiaEnd) asia = true;
|
|
}
|
|
|
|
// London (08:00 - 16:00)
|
|
bool london = false;
|
|
if(InpTradeLondon)
|
|
{
|
|
if(h >= InpLondonStart && h < InpLondonEnd) london = true;
|
|
}
|
|
|
|
// New York (13:30? - 21:00)
|
|
bool ny = false;
|
|
if(InpTradeNY)
|
|
{
|
|
if(h > InpNYStart && h < InpNYEnd) ny = true;
|
|
else if(h == InpNYStart && m >= InpNYStartMin) ny = true;
|
|
}
|
|
|
|
return (asia || london || ny);
|
|
}
|
|
|
|
// Check if a position is already open for this symbol and magic number
|
|
bool IsPositionOpen()
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == SYM && PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ResetDailyCountersIfNeeded()
|
|
{
|
|
int d = DayOfYearServer();
|
|
if(d != g_dayOfYear)
|
|
{
|
|
g_dayOfYear = d;
|
|
}
|
|
}
|
|
|
|
|
|
//==================== HELPER FUNCTIONS ====================
|
|
// Find the newest M5 FVG that formed AFTER the setup time
|
|
// Returns the SL level (Bottom for Buy, Top for Sell) or 0.0 if not found
|
|
double FindNewerM5FVG(int direction, datetime setupTime, string &outDescription)
|
|
{
|
|
double bestSL = 0.0;
|
|
datetime bestTime = 0; // Earliest FVG time after setupTime
|
|
string foundType = "";
|
|
|
|
int fvgCount = ArraySize(g_fvgPointsM5);
|
|
for(int i = fvgCount - 1; i >= 0; i--)
|
|
{
|
|
// FVG can be AT or slightly before setup time (e.g. formed by the BOS/Confirmation candle sequence)
|
|
// FVG time is usually middle candle, BOS time is closing candle (3rd). So FVG is 1 bar "older" than BOS.
|
|
if(g_fvgPointsM5[i].time < setupTime - PeriodSeconds(PERIOD_M5))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check direction: Buy (1) needs Bullish FVG (1), Sell (-1) needs Bearish FVG (-1)
|
|
if(g_fvgPointsM5[i].type == direction)
|
|
{
|
|
// Find the FIRST one after setupTime (earliest time)
|
|
if(bestTime == 0 || g_fvgPointsM5[i].time < bestTime)
|
|
{
|
|
bestTime = g_fvgPointsM5[i].time;
|
|
// For Buy (1), SL is Bottom. For Sell (-1), SL is Top.
|
|
bestSL = (direction == 1) ? g_fvgPointsM5[i].bottom : g_fvgPointsM5[i].top;
|
|
foundType = (direction == 1) ? "Bullish FVG" : "Bearish FVG";
|
|
}
|
|
}
|
|
}
|
|
|
|
if(bestSL > 0)
|
|
{
|
|
outDescription = foundType + " (New) at " + TimeToString(bestTime);
|
|
}
|
|
return bestSL;
|
|
}
|
|
|
|
// Find the initial confirmation (OB or FVG) at the moment of setup creation
|
|
// For BOS: Priority is OB.
|
|
// For iFVG: Priority is older FVG (unlikely for initial, but we might just use iFVG itself or look for support)
|
|
double FindInitialConfirmation(int type, datetime triggerTime, string &outDescription, datetime minTime = 0)
|
|
{
|
|
double bestSL = 0.0;
|
|
datetime bestTime = 0;
|
|
string foundType = "";
|
|
|
|
// 1. Check Order Blocks (Priority for BOS)
|
|
int obCount = ArraySize(g_orderBlocksM5);
|
|
for(int i = obCount - 1; i >= 0; i--)
|
|
{
|
|
if(g_orderBlocksM5[i].time > triggerTime) continue; // Future relative to trigger
|
|
if(minTime > 0 && g_orderBlocksM5[i].time < minTime) continue;
|
|
|
|
// Logic: Bullish BOS (1) needs Bearish OB (-1) support
|
|
int targetOBType = (type == 1) ? -1 : 1;
|
|
|
|
if(g_orderBlocksM5[i].type == targetOBType)
|
|
{
|
|
if(g_orderBlocksM5[i].time > bestTime)
|
|
{
|
|
bestTime = g_orderBlocksM5[i].time;
|
|
bestSL = (type == 1) ? g_orderBlocksM5[i].bottom : g_orderBlocksM5[i].top;
|
|
foundType = (g_orderBlocksM5[i].type == 1) ? "Bullish OB" : "Bearish OB";
|
|
}
|
|
break; // Found recent OB
|
|
}
|
|
}
|
|
|
|
// 2. If no OB found, check for FVG (Fallback)
|
|
if(bestSL == 0.0)
|
|
{
|
|
int fvgCount = ArraySize(g_fvgPointsM5);
|
|
for(int i = fvgCount - 1; i >= 0; i--)
|
|
{
|
|
if(g_fvgPointsM5[i].time > triggerTime) continue;
|
|
if(minTime > 0 && g_fvgPointsM5[i].time < minTime) continue;
|
|
|
|
if(g_fvgPointsM5[i].type == type)
|
|
{
|
|
if(g_fvgPointsM5[i].time > bestTime)
|
|
{
|
|
bestTime = g_fvgPointsM5[i].time;
|
|
bestSL = (type == 1) ? g_fvgPointsM5[i].bottom : g_fvgPointsM5[i].top;
|
|
foundType = (type == 1) ? "Bullish FVG" : "Bearish FVG";
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(bestSL > 0)
|
|
{
|
|
outDescription = foundType + " at " + TimeToString(bestTime);
|
|
}
|
|
return bestSL;
|
|
}
|
|
|
|
// Execute Trade on M1 Confirmation
|
|
void ExecuteM1Entry(int direction, double entryPrice, datetime time, string m1TriggerDesc, datetime fvgTime = 0, double fvgTop = 0, double fvgBottom = 0)
|
|
{
|
|
if(!InTradingSession()) return; // Respect Session Times
|
|
if(IsPositionOpen()) return; // Only one trade at a time
|
|
|
|
if(!g_pendingSetup.active) return; // Must have valid M5 Setup
|
|
if(g_pendingSetup.direction != direction) return; // Must match direction
|
|
if(time <= g_pendingSetup.setupTime) return; // Must be AFTER the M5 setup
|
|
|
|
// Check if max trades per setup has been reached
|
|
if(g_tradesExecutedForCurrentSetup >= InpMaxTradesPerSetup)
|
|
{
|
|
Print("Max trades (", InpMaxTradesPerSetup, ") already executed for current M5 setup. Skipping entry.");
|
|
return;
|
|
}
|
|
|
|
// Validate M1 iFVG: Original FVG must be formed AFTER the M5 setup
|
|
// M5 Setup Time is the open time of the M5 bar. Confirmation happens at Close (Time + 5min).
|
|
// We want the M1 FVG to form strictly AFTER the M5 setup is confirmed.
|
|
if(StringFind(m1TriggerDesc, "iFVG") >= 0 && fvgTime > 0)
|
|
{
|
|
datetime m5ConfirmationTime = g_pendingSetup.setupTime + PeriodSeconds(PERIOD_M5);
|
|
|
|
if(fvgTime <= m5ConfirmationTime)
|
|
{
|
|
Print("Ignored M1 iFVG trigger: Original FVG (", TimeToString(fvgTime), ") formed before/at M5 setup confirmation (", TimeToString(m5ConfirmationTime), ")");
|
|
return;
|
|
}
|
|
}
|
|
|
|
double sl = g_pendingSetup.slPrice; // Already includes buffer from CheckM5Handover
|
|
string effectiveConfDesc = g_pendingSetup.confDesc; // Use initial desc by default
|
|
|
|
// Check for a NEWER M5 FVG (formed after setup) to use as a tighter/better SL
|
|
string newFVGDesc = "";
|
|
double newSL = FindNewerM5FVG(direction, g_pendingSetup.setupTime, newFVGDesc);
|
|
|
|
// Strict rule: if the M5 setup is iFVG, we do NOT allow an entry unless a newer M5 FVG exists
|
|
if(StringFind(g_pendingSetup.setupDesc, "iFVG") >= 0 && newSL <= 0)
|
|
{
|
|
Print("Entry blocked: M5 setup is iFVG but no newer M5 FVG formed yet for SL. Waiting...");
|
|
return;
|
|
}
|
|
|
|
if(newSL > 0)
|
|
{
|
|
// Apply Buffer to the new SL
|
|
double slBuffer = InpSLBufferPoints * point;
|
|
if(direction == 1) newSL -= slBuffer; else newSL += slBuffer;
|
|
|
|
sl = newSL;
|
|
effectiveConfDesc = newFVGDesc; // Update description for comments/logs
|
|
|
|
// Update global state for record keeping (optional, but good for logs)
|
|
g_pendingSetup.slPrice = newSL;
|
|
g_pendingSetup.confDesc = newFVGDesc;
|
|
}
|
|
|
|
double tp = 0;
|
|
double risk = 0;
|
|
|
|
// Construct Short Comment: "5[ConfType]1[TriggerType] MM:SS"
|
|
string m5ConfType = "";
|
|
if(StringFind(effectiveConfDesc, "OB") >= 0)
|
|
m5ConfType = "ob";
|
|
else if(StringFind(effectiveConfDesc, "FVG") >= 0)
|
|
m5ConfType = "fv";
|
|
|
|
string m1TrigType = "";
|
|
if(StringFind(m1TriggerDesc, "BOS") >= 0)
|
|
m1TrigType = "bs";
|
|
else if(StringFind(m1TriggerDesc, "iFVG") >= 0)
|
|
m1TrigType = "if";
|
|
|
|
MqlDateTime dt;
|
|
TimeToStruct(time, dt);
|
|
string comment = StringFormat("5%s1%s %02d:%02d", m5ConfType, m1TrigType, dt.hour, dt.min);
|
|
|
|
// Calculate Risk & Reward
|
|
if(direction == 1) // Buy
|
|
{
|
|
// Buffer already applied in CheckM5Handover
|
|
|
|
if(sl >= entryPrice)
|
|
{
|
|
Print("Invalid Buy SL: SL ", sl, " >= Price ", entryPrice);
|
|
return;
|
|
}
|
|
risk = entryPrice - sl;
|
|
|
|
// Check Max SL Distance
|
|
if((risk / point) > InpMaxSLPoints)
|
|
{
|
|
Print("Trade Skipped: Buy SL Distance ", DoubleToString(risk/point, 1), " pts > Max ", InpMaxSLPoints, " pts");
|
|
return;
|
|
}
|
|
|
|
tp = entryPrice + (risk * InpRiskRewardRatio);
|
|
|
|
m_trade.Buy(InpLotSize, SYM, entryPrice, sl, tp, comment);
|
|
}
|
|
else // Sell
|
|
{
|
|
// Buffer already applied in CheckM5Handover
|
|
|
|
if(sl <= entryPrice)
|
|
{
|
|
Print("Invalid Sell SL: SL ", sl, " <= Price ", entryPrice);
|
|
return;
|
|
}
|
|
risk = sl - entryPrice;
|
|
|
|
// Check Max SL Distance
|
|
if((risk / point) > InpMaxSLPoints)
|
|
{
|
|
Print("Trade Skipped: Sell SL Distance ", DoubleToString(risk/point, 1), " pts > Max ", InpMaxSLPoints, " pts");
|
|
return;
|
|
}
|
|
|
|
tp = entryPrice - (risk * InpRiskRewardRatio);
|
|
|
|
m_trade.Sell(InpLotSize, SYM, entryPrice, sl, tp, comment);
|
|
}
|
|
|
|
// Detailed Logging to Terminal
|
|
Print("========================================");
|
|
Print("TRADE EXECUTED: ", comment);
|
|
Print("========================================");
|
|
Print("M5 SETUP:");
|
|
Print(" Type: ", g_pendingSetup.setupDesc);
|
|
Print(" Time: ", TimeToString(g_pendingSetup.setupTime));
|
|
Print(" Direction: ", (g_pendingSetup.direction == 1 ? "BULLISH" : "BEARISH"));
|
|
if(StringFind(g_pendingSetup.setupDesc, "iFVG") >= 0 && g_pendingSetup.m5FvgTime > 0)
|
|
{
|
|
Print(" Original FVG (Broken/Mitigated):");
|
|
Print(" FVG Time: ", TimeToString(g_pendingSetup.m5FvgTime));
|
|
Print(" FVG Top: ", DoubleToString(g_pendingSetup.m5FvgTop, _Digits));
|
|
Print(" FVG Bottom: ", DoubleToString(g_pendingSetup.m5FvgBottom, _Digits));
|
|
}
|
|
Print("M5 CONFIRMATION:");
|
|
Print(" Type: ", g_pendingSetup.confDesc);
|
|
Print(" SL Level: ", DoubleToString(g_pendingSetup.slPrice, _Digits));
|
|
Print("M1 TRIGGER:");
|
|
Print(" Type: ", m1TriggerDesc);
|
|
Print(" Time: ", TimeToString(time));
|
|
Print(" Price: ", DoubleToString(entryPrice, _Digits));
|
|
if(StringFind(m1TriggerDesc, "iFVG") >= 0 && fvgTime > 0)
|
|
{
|
|
Print(" Original FVG (Broken/Mitigated):");
|
|
Print(" FVG Time: ", TimeToString(fvgTime));
|
|
Print(" FVG Top: ", DoubleToString(fvgTop, _Digits));
|
|
Print(" FVG Bottom: ", DoubleToString(fvgBottom, _Digits));
|
|
}
|
|
Print("TRADE DETAILS:");
|
|
Print(" Direction: ", (direction == 1 ? "BUY" : "SELL"));
|
|
Print(" Entry: ", DoubleToString(entryPrice, _Digits));
|
|
Print(" SL: ", DoubleToString(sl, _Digits), " (Buffer: ", InpSLBufferPoints, " pts)");
|
|
Print(" TP: ", DoubleToString(tp, _Digits));
|
|
Print(" Risk: ", DoubleToString(risk, _Digits), " | Reward: ", DoubleToString(risk * InpRiskRewardRatio, _Digits));
|
|
Print(" RR Ratio: 1:", DoubleToString(InpRiskRewardRatio, 1));
|
|
Print(" Trade #", g_tradesExecutedForCurrentSetup + 1, " of ", InpMaxTradesPerSetup, " for this setup");
|
|
Print("========================================");
|
|
|
|
// Increment trade counter for current setup
|
|
g_tradesExecutedForCurrentSetup++;
|
|
|
|
// If max trades reached, deactivate setup
|
|
if(g_tradesExecutedForCurrentSetup >= InpMaxTradesPerSetup)
|
|
{
|
|
g_lastTradedSetupTime = g_pendingSetup.setupTime; // Mark this setup as fully traded
|
|
g_pendingSetup.active = false;
|
|
Print("Max trades reached for this M5 setup. Setup deactivated.");
|
|
}
|
|
}
|
|
|
|
//==================== BOS DETECTION ====================
|
|
// Internal function to register a detected BOS point
|
|
void RegisterBOSPoint(datetime time, double price, int type, datetime swingTime, double swingPrice, ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT)
|
|
{
|
|
int digits = (int)SymbolInfoInteger(SYM, SYMBOL_DIGITS);
|
|
double normPrice = NormalizeDouble(price, digits);
|
|
double normSwingPrice = NormalizeDouble(swingPrice, digits);
|
|
|
|
// Determine chart ID based on timeframe
|
|
long chartId = 0; // Default to current chart
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
// M1 data is analyzed but not drawn (no chart created)
|
|
chartId = 0;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
chartId = GetOrCreateChart(SYM, PERIOD_M5);
|
|
}
|
|
else
|
|
{
|
|
return; // Only process M1 and M5 timeframes
|
|
}
|
|
|
|
if(timeframe == PERIOD_M5 && chartId == 0)
|
|
{
|
|
return; // Don't proceed if no valid chart for M5
|
|
}
|
|
|
|
// Check uniqueness to avoid duplicates
|
|
int s = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
s = ArraySize(g_bosPointsM1);
|
|
if(s > 0 && g_bosPointsM1[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_bosPointsM1, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_bosPointsM1, s+1);
|
|
g_bosPointsM1[s].time = time;
|
|
g_bosPointsM1[s].price = normPrice;
|
|
g_bosPointsM1[s].type = type;
|
|
g_bosPointsM1[s].swingTime = swingTime;
|
|
g_bosPointsM1[s].swingPrice = normSwingPrice;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
s = ArraySize(g_bosPointsM5);
|
|
if(s > 0 && g_bosPointsM5[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_bosPointsM5, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_bosPointsM5, s+1);
|
|
g_bosPointsM5[s].time = time;
|
|
g_bosPointsM5[s].price = normPrice;
|
|
g_bosPointsM5[s].type = type;
|
|
g_bosPointsM5[s].swingTime = swingTime;
|
|
g_bosPointsM5[s].swingPrice = normSwingPrice;
|
|
}
|
|
|
|
if(type == 1) // Bullish BOS
|
|
{
|
|
// Bullish BOS Detected
|
|
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
string triggerDesc = "Bullish BOS at " + TimeToString(time);
|
|
ExecuteM1Entry(1, price, time, triggerDesc);
|
|
}
|
|
|
|
if(InpDrawBOS && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_BOS_Bull_";
|
|
DrawBOSMarker(chartId, prefix + TimeToString(time), time, normPrice, swingTime, normSwingPrice, clrGreen);
|
|
}
|
|
}
|
|
else // Bearish BOS
|
|
{
|
|
// Bearish BOS Detected
|
|
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
string triggerDesc = "Bearish BOS at " + TimeToString(time);
|
|
ExecuteM1Entry(-1, price, time, triggerDesc);
|
|
}
|
|
|
|
if(InpDrawBOS && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_BOS_Bear_";
|
|
DrawBOSMarker(chartId, prefix + TimeToString(time), time, normPrice, swingTime, normSwingPrice, clrRed);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to detect BOS on specific timeframe
|
|
void DetectBOS(ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
// Only process M1 and M5 timeframes
|
|
if(timeframe != PERIOD_M1 && timeframe != PERIOD_M5) return;
|
|
|
|
// Get current completed candle data (wait for candle close)
|
|
MqlRates currentRates[];
|
|
ArraySetAsSeries(currentRates, true);
|
|
if(CopyRates(SYM, timeframe, 1, 2, currentRates) < 2) return; // Need last completed candle and previous one
|
|
|
|
double currentClose = currentRates[0].close; // Close of last completed candle
|
|
double currentHigh = currentRates[0].high;
|
|
double currentLow = currentRates[0].low;
|
|
datetime currentTime = currentRates[0].time;
|
|
|
|
// Get the latest swing high and low only (not all historical)
|
|
double latestSwingHigh = 0;
|
|
datetime latestSwingHighTime = 0;
|
|
double latestSwingLow = 999999;
|
|
datetime latestSwingLowTime = 0;
|
|
|
|
// Get the last BOS type to prevent consecutive same-direction BOS
|
|
int lastBOSType = 0; // 0 = none, 1 = bullish, -1 = bearish
|
|
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
int highCount = ArraySize(g_swingHighsM1);
|
|
if(highCount > 0)
|
|
{
|
|
latestSwingHigh = g_swingHighsM1[highCount-1].price;
|
|
latestSwingHighTime = g_swingHighsM1[highCount-1].time;
|
|
}
|
|
|
|
int lowCount = ArraySize(g_swingLowsM1);
|
|
if(lowCount > 0)
|
|
{
|
|
latestSwingLow = g_swingLowsM1[lowCount-1].price;
|
|
latestSwingLowTime = g_swingLowsM1[lowCount-1].time;
|
|
}
|
|
|
|
// Get last BOS type for M1
|
|
int bosCount = ArraySize(g_bosPointsM1);
|
|
if(bosCount > 0)
|
|
{
|
|
lastBOSType = g_bosPointsM1[bosCount-1].type;
|
|
}
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
int highCount = ArraySize(g_swingHighsM5);
|
|
if(highCount > 0)
|
|
{
|
|
latestSwingHigh = g_swingHighsM5[highCount-1].price;
|
|
latestSwingHighTime = g_swingHighsM5[highCount-1].time;
|
|
}
|
|
|
|
int lowCount = ArraySize(g_swingLowsM5);
|
|
if(lowCount > 0)
|
|
{
|
|
latestSwingLow = g_swingLowsM5[lowCount-1].price;
|
|
latestSwingLowTime = g_swingLowsM5[lowCount-1].time;
|
|
}
|
|
|
|
// Get last BOS type for M5
|
|
int bosCount = ArraySize(g_bosPointsM5);
|
|
if(bosCount > 0)
|
|
{
|
|
lastBOSType = g_bosPointsM5[bosCount-1].type;
|
|
}
|
|
}
|
|
|
|
// Check for Bullish BOS: Candle BODY closes above latest swing high
|
|
// ONLY if last BOS was not bullish (prevent consecutive bullish BOS)
|
|
if(latestSwingHigh > 0 && currentClose > latestSwingHigh && lastBOSType != 1)
|
|
{
|
|
// Check if this BOS was already detected
|
|
bool alreadyDetected = false;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
int bosCount = ArraySize(g_bosPointsM1);
|
|
for(int j = 0; j < bosCount; j++)
|
|
{
|
|
if(g_bosPointsM1[j].type == 1 && g_bosPointsM1[j].swingTime == latestSwingHighTime)
|
|
{
|
|
alreadyDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
int bosCount = ArraySize(g_bosPointsM5);
|
|
for(int j = 0; j < bosCount; j++)
|
|
{
|
|
if(g_bosPointsM5[j].type == 1 && g_bosPointsM5[j].swingTime == latestSwingHighTime)
|
|
{
|
|
alreadyDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!alreadyDetected)
|
|
{
|
|
RegisterBOSPoint(currentTime, currentClose, 1, latestSwingHighTime, latestSwingHigh, timeframe);
|
|
}
|
|
}
|
|
|
|
// Check for Bearish BOS: Candle BODY closes below latest swing low
|
|
// ONLY if last BOS was not bearish (prevent consecutive bearish BOS)
|
|
if(latestSwingLow < 999999 && currentClose < latestSwingLow && lastBOSType != -1)
|
|
{
|
|
// Check if this BOS was already detected
|
|
bool alreadyDetected = false;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
int bosCount = ArraySize(g_bosPointsM1);
|
|
for(int j = 0; j < bosCount; j++)
|
|
{
|
|
if(g_bosPointsM1[j].type == -1 && g_bosPointsM1[j].swingTime == latestSwingLowTime)
|
|
{
|
|
alreadyDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
int bosCount = ArraySize(g_bosPointsM5);
|
|
for(int j = 0; j < bosCount; j++)
|
|
{
|
|
if(g_bosPointsM5[j].type == -1 && g_bosPointsM5[j].swingTime == latestSwingLowTime)
|
|
{
|
|
alreadyDetected = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!alreadyDetected)
|
|
{
|
|
RegisterBOSPoint(currentTime, currentClose, -1, latestSwingLowTime, latestSwingLow, timeframe);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to detect FVG (Fair Value Gap) on specific timeframe
|
|
void DetectFVG(ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
// Only process M1 and M5 timeframes
|
|
if(timeframe != PERIOD_M1 && timeframe != PERIOD_M5) return;
|
|
|
|
// Get 3 most recent completed candles
|
|
MqlRates rates[];
|
|
ArraySetAsSeries(rates, true);
|
|
if(CopyRates(SYM, timeframe, 1, 3, rates) < 3) return; // Need 3 completed candles
|
|
|
|
// Candle 1 (oldest), Candle 2 (middle), Candle 3 (newest)
|
|
double candle1Low = rates[2].low;
|
|
double candle1High = rates[2].high;
|
|
double candle2Low = rates[1].low;
|
|
double candle2High = rates[1].high;
|
|
double candle3Low = rates[0].low;
|
|
double candle3High = rates[0].high;
|
|
datetime candle2Time = rates[1].time;
|
|
|
|
double pointValue = SymbolInfoDouble(SYM, SYMBOL_POINT);
|
|
double threshold = InpFVGThresholdPoints * pointValue;
|
|
|
|
// Check for Bearish FVG: Candle 1 Low > Candle 3 High + threshold (gap down)
|
|
if(candle1Low > candle3High + threshold)
|
|
{
|
|
double fvgTop = candle1Low;
|
|
double fvgBottom = candle3High;
|
|
double fvgSize = (fvgTop - fvgBottom) / pointValue;
|
|
|
|
// Register Bearish FVG (gap down)
|
|
RegisterFVGPoint(candle2Time, fvgTop, fvgBottom, -1, fvgSize, timeframe);
|
|
}
|
|
|
|
// Check for Bullish FVG: Candle 1 High < Candle 3 Low - threshold (gap up)
|
|
if(candle1High < candle3Low - threshold)
|
|
{
|
|
double fvgTop = candle3Low;
|
|
double fvgBottom = candle1High;
|
|
double fvgSize = (fvgTop - fvgBottom) / pointValue;
|
|
|
|
// Register Bullish FVG (gap up)
|
|
RegisterFVGPoint(candle2Time, fvgTop, fvgBottom, 1, fvgSize, timeframe);
|
|
}
|
|
}
|
|
|
|
// Internal function to register a detected FVG point
|
|
void RegisterFVGPoint(datetime time, double top, double bottom, int type, double size, ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
int digits = (int)SymbolInfoInteger(SYM, SYMBOL_DIGITS);
|
|
double normTop = NormalizeDouble(top, digits);
|
|
double normBottom = NormalizeDouble(bottom, digits);
|
|
|
|
// Determine chart ID based on timeframe
|
|
long chartId = 0; // Default to current chart
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
// M1 data is analyzed but not drawn (no chart created)
|
|
chartId = 0;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
chartId = GetOrCreateChart(SYM, PERIOD_M5);
|
|
}
|
|
else
|
|
{
|
|
return; // Only process M1 and M5 timeframes
|
|
}
|
|
|
|
if(timeframe == PERIOD_M5 && chartId == 0)
|
|
{
|
|
return; // Don't proceed if no valid chart for M5
|
|
}
|
|
|
|
// Check uniqueness to avoid duplicates
|
|
int s = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
s = ArraySize(g_fvgPointsM1);
|
|
if(s > 0 && g_fvgPointsM1[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_fvgPointsM1, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_fvgPointsM1, s+1);
|
|
g_fvgPointsM1[s].time = time;
|
|
g_fvgPointsM1[s].top = normTop;
|
|
g_fvgPointsM1[s].bottom = normBottom;
|
|
g_fvgPointsM1[s].type = type;
|
|
g_fvgPointsM1[s].size = size;
|
|
g_fvgPointsM1[s].mitigated = false;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
s = ArraySize(g_fvgPointsM5);
|
|
if(s > 0 && g_fvgPointsM5[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_fvgPointsM5, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_fvgPointsM5, s+1);
|
|
g_fvgPointsM5[s].time = time;
|
|
g_fvgPointsM5[s].top = normTop;
|
|
g_fvgPointsM5[s].bottom = normBottom;
|
|
g_fvgPointsM5[s].type = type;
|
|
g_fvgPointsM5[s].size = size;
|
|
g_fvgPointsM5[s].mitigated = false;
|
|
}
|
|
|
|
if(type == 1) // Bullish FVG
|
|
{
|
|
// Bullish FVG Detected
|
|
|
|
if(InpDrawFVG && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_FVG_Bull_";
|
|
DrawFVGRectangle(chartId, prefix + TimeToString(time), time, normTop, normBottom, clrBlue);
|
|
}
|
|
}
|
|
else // Bearish FVG
|
|
{
|
|
// Bearish FVG Detected
|
|
|
|
if(InpDrawFVG && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_FVG_Bear_";
|
|
DrawFVGRectangle(chartId, prefix + TimeToString(time), time, normTop, normBottom, clrWhite);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draws FVG rectangle on the specified chart
|
|
void DrawFVGRectangle(long chartId, string name, datetime time, double top, double bottom, color clr)
|
|
{
|
|
if(!InpDrawFVG) return;
|
|
|
|
if(ObjectFind(chartId, name) >= 0) return; // Avoid flickering
|
|
|
|
// Calculate rectangle time range (extend to the right)
|
|
datetime endTime = time + (PeriodSeconds() * 5); // Extend 5 bars to the right (quarter of previous length)
|
|
|
|
// Create rectangle for FVG
|
|
if(ObjectCreate(chartId, name, OBJ_RECTANGLE, 0, time, top, endTime, bottom))
|
|
{
|
|
// Custom Styles based on Color (User Request)
|
|
// Bullish (Blue) = Solid, Filled
|
|
// Bearish (White) = Dotted, No Fill (Rectangle Outline)
|
|
ENUM_LINE_STYLE style = (clr == clrWhite) ? STYLE_DOT : STYLE_SOLID;
|
|
bool fill = (clr == clrWhite) ? false : true;
|
|
|
|
ObjectSetInteger(chartId, name, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, name, OBJPROP_STYLE, style);
|
|
ObjectSetInteger(chartId, name, OBJPROP_WIDTH, 1);
|
|
ObjectSetInteger(chartId, name, OBJPROP_BACK, true); // Draw behind price
|
|
ObjectSetInteger(chartId, name, OBJPROP_FILL, fill); // Fill the rectangle
|
|
ObjectSetInteger(chartId, name, OBJPROP_HIDDEN, false); // Visible in object list
|
|
ObjectSetInteger(chartId, name, OBJPROP_SELECTABLE, true); // Selectable
|
|
|
|
// Set semi-transparent background for shadow effect (only if filled)
|
|
if(fill)
|
|
{
|
|
color bgColor = clr;
|
|
if(clr == clrGreen)
|
|
bgColor = C'0,255,0,50'; // Semi-transparent green
|
|
else if(clr == clrRed)
|
|
bgColor = C'255,0,0,50'; // Semi-transparent red
|
|
else if(clr == clrWhite)
|
|
bgColor = C'255,255,255,50'; // Semi-transparent white
|
|
else if(clr == clrBlue)
|
|
bgColor = C'0,0,255,50'; // Semi-transparent blue
|
|
|
|
ObjectSetInteger(chartId, name, OBJPROP_BGCOLOR, bgColor);
|
|
}
|
|
|
|
ChartRedraw(chartId); // Force update
|
|
}
|
|
}
|
|
|
|
// Function to detect iFVG (FVG Mitigation) on specific timeframe
|
|
void DetectiFVG(ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
// Only process M1 and M5 timeframes
|
|
if(timeframe != PERIOD_M1 && timeframe != PERIOD_M5) return;
|
|
|
|
// Get current completed candle data
|
|
MqlRates currentRates[];
|
|
ArraySetAsSeries(currentRates, true);
|
|
if(CopyRates(SYM, timeframe, 1, 2, currentRates) < 2) return; // Need last completed candle
|
|
|
|
double currentClose = currentRates[0].close;
|
|
|
|
double currentOpen = currentRates[0].open;
|
|
datetime currentTime = currentRates[0].time;
|
|
|
|
// Determine if current candle is bullish or bearish
|
|
bool isCurrentBullish = currentClose > currentOpen;
|
|
bool isCurrentBearish = currentClose < currentOpen;
|
|
|
|
// Check for FVG mitigation in existing FVGs
|
|
int fvgCount = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
fvgCount = ArraySize(g_fvgPointsM1);
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
fvgCount = ArraySize(g_fvgPointsM5);
|
|
}
|
|
|
|
for(int i = 0; i < fvgCount; i++)
|
|
{
|
|
FVGPoint fvg;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
fvg = g_fvgPointsM1[i];
|
|
}
|
|
else
|
|
{
|
|
fvg = g_fvgPointsM5[i];
|
|
}
|
|
|
|
// Skip if already mitigated
|
|
if(fvg.mitigated) continue;
|
|
|
|
// Check for Bearish FVG mitigation: BULLISH candle body closes ABOVE FVG top
|
|
if(fvg.type == -1 && isCurrentBullish && currentClose > fvg.top)
|
|
{
|
|
// Mark FVG as mitigated
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
g_fvgPointsM1[i].mitigated = true;
|
|
}
|
|
else
|
|
{
|
|
g_fvgPointsM5[i].mitigated = true;
|
|
}
|
|
|
|
// Register iFVG (FVG mitigation)
|
|
RegisteriFVGPoint(currentTime, fvg.time, currentClose, -1, fvg.top, fvg.bottom, timeframe);
|
|
}
|
|
|
|
// Check for Bullish FVG mitigation: BEARISH candle body closes BELOW FVG bottom
|
|
if(fvg.type == 1 && isCurrentBearish && currentClose < fvg.bottom)
|
|
{
|
|
// Mark FVG as mitigated
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
g_fvgPointsM1[i].mitigated = true;
|
|
}
|
|
else
|
|
{
|
|
g_fvgPointsM5[i].mitigated = true;
|
|
}
|
|
|
|
// Register iFVG (FVG mitigation)
|
|
RegisteriFVGPoint(currentTime, fvg.time, currentClose, 1, fvg.top, fvg.bottom, timeframe);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Internal function to register a detected iFVG (FVG mitigation)
|
|
void RegisteriFVGPoint(datetime time, datetime fvgTime, double price, int type, double fvgTop, double fvgBottom, ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
int digits = (int)SymbolInfoInteger(SYM, SYMBOL_DIGITS);
|
|
double normPrice = NormalizeDouble(price, digits);
|
|
|
|
// Determine chart ID based on timeframe
|
|
long chartId = 0; // Default to current chart
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
// M1 data is analyzed but not drawn (no chart created)
|
|
chartId = 0;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
chartId = GetOrCreateChart(SYM, PERIOD_M5);
|
|
}
|
|
else
|
|
{
|
|
return; // Only process M1 and M5 timeframes
|
|
}
|
|
|
|
if(timeframe == PERIOD_M5 && chartId == 0)
|
|
{
|
|
return; // Don't proceed if no valid chart for M5
|
|
}
|
|
|
|
// Check uniqueness to avoid duplicates
|
|
int s = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
s = ArraySize(g_ifvgPointsM1);
|
|
if(s > 0 && g_ifvgPointsM1[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_ifvgPointsM1, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_ifvgPointsM1, s+1);
|
|
g_ifvgPointsM1[s].time = time;
|
|
g_ifvgPointsM1[s].fvgTime = fvgTime;
|
|
g_ifvgPointsM1[s].price = normPrice;
|
|
g_ifvgPointsM1[s].type = type;
|
|
g_ifvgPointsM1[s].fvgTop = fvgTop;
|
|
g_ifvgPointsM1[s].fvgBottom = fvgBottom;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
s = ArraySize(g_ifvgPointsM5);
|
|
if(s > 0 && g_ifvgPointsM5[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_ifvgPointsM5, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_ifvgPointsM5, s+1);
|
|
g_ifvgPointsM5[s].time = time;
|
|
g_ifvgPointsM5[s].fvgTime = fvgTime;
|
|
g_ifvgPointsM5[s].price = normPrice;
|
|
g_ifvgPointsM5[s].type = type;
|
|
g_ifvgPointsM5[s].fvgTop = fvgTop;
|
|
g_ifvgPointsM5[s].fvgBottom = fvgBottom;
|
|
}
|
|
|
|
if(type == -1) // Bearish FVG mitigated (bullish candle closes above)
|
|
{
|
|
// iFVG Bearish Mitigated (Actually a Bullish Signal?)
|
|
// Wait:
|
|
// iFVG Point Type -1 means "Bearish FVG mitigated".
|
|
// Original FVG was Bearish (Gap Down).
|
|
// Mitigation means price closed ABOVE it (Bullish move).
|
|
// So this is a BULLISH signal (Inversion FVG).
|
|
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
string triggerDesc = "Bullish iFVG at " + TimeToString(time);
|
|
ExecuteM1Entry(1, price, time, triggerDesc, fvgTime, fvgTop, fvgBottom);
|
|
}
|
|
|
|
if(InpDrawiFVG && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
// Construct the name of the ORIGINAL FVG that was mitigated
|
|
// Type -1 iFVG means original FVG was Bearish (-1)
|
|
string originalName = "M5_FVG_Bear_" + TimeToString(fvgTime);
|
|
string markerName = "M5_iFVG_Bear_" + TimeToString(time); // Use mitigation time for marker
|
|
|
|
DrawiFVGVisuals(chartId, originalName, markerName, time, fvgTop, fvgBottom, clrBlue); // Bullish Signal = Blue
|
|
}
|
|
}
|
|
else // Bullish FVG mitigated (bearish candle closes below)
|
|
{
|
|
// iFVG Bullish Mitigated (Actually a Bearish Signal?)
|
|
// Original FVG was Bullish (Gap Up).
|
|
// Mitigation means price closed BELOW it (Bearish move).
|
|
// So this is a BEARISH signal (Inversion FVG).
|
|
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
string triggerDesc = "Bearish iFVG at " + TimeToString(time);
|
|
ExecuteM1Entry(-1, price, time, triggerDesc, fvgTime, fvgTop, fvgBottom);
|
|
}
|
|
|
|
if(InpDrawiFVG && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
// Construct the name of the ORIGINAL FVG that was mitigated
|
|
// Type 1 iFVG means original FVG was Bullish (1)
|
|
string originalName = "M5_FVG_Bull_" + TimeToString(fvgTime);
|
|
string markerName = "M5_iFVG_Bull_" + TimeToString(time); // Use mitigation time for marker
|
|
|
|
DrawiFVGVisuals(chartId, originalName, markerName, time, fvgTop, fvgBottom, clrWhite); // Bearish Signal = White
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draws iFVG Visuals (Modify Zone + Marker)
|
|
void DrawiFVGVisuals(long chartId, string originalFVGName, string markerName, datetime time, double top, double bottom, color clr)
|
|
{
|
|
if(!InpDrawiFVG) return;
|
|
|
|
// 1. REMOVED: Modification of Original FVG Zone (User request: "ifvg is just the x sign")
|
|
|
|
// 2. Draw the Marker (X) at mitigation time
|
|
string arrowName = markerName + "_Marker";
|
|
if(ObjectFind(chartId, arrowName) < 0)
|
|
{
|
|
double midPrice = (top + bottom) / 2.0;
|
|
if(ObjectCreate(chartId, arrowName, OBJ_ARROW, 0, time, midPrice))
|
|
{
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_ARROWCODE, 251); // X symbol
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_WIDTH, 2);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_ANCHOR, ANCHOR_CENTER);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_HIDDEN, false);
|
|
|
|
ChartRedraw(chartId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to detect Order Blocks on specific timeframe
|
|
void DetectOrderBlock(ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
// Only process M1 and M5 timeframes
|
|
if(timeframe != PERIOD_M1 && timeframe != PERIOD_M5) return;
|
|
|
|
// Get recent BOS to check for order blocks
|
|
int bosCount = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
bosCount = ArraySize(g_bosPointsM1);
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
bosCount = ArraySize(g_bosPointsM5);
|
|
}
|
|
|
|
// Check the most recent BOS for order block detection
|
|
if(bosCount == 0) return;
|
|
|
|
BOSPoint lastBOS;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
lastBOS = g_bosPointsM1[bosCount-1];
|
|
}
|
|
else
|
|
{
|
|
lastBOS = g_bosPointsM5[bosCount-1];
|
|
}
|
|
|
|
// Get candle data for order block analysis
|
|
MqlRates rates[];
|
|
ArraySetAsSeries(rates, true);
|
|
|
|
// Get candles before BOS point (going backwards in time)
|
|
int barsToGet = InpOrderBlockCandles; // Get extra bars to find order block
|
|
if(CopyRates(SYM, timeframe, lastBOS.time, barsToGet, rates) < barsToGet) return;
|
|
|
|
// Find the BOS candle in the rates array (should be at index 0 or close to it)
|
|
int bosCandleIndex = -1;
|
|
for(int i = 0; i < ArraySize(rates); i++)
|
|
{
|
|
if(rates[i].time == lastBOS.time)
|
|
{
|
|
bosCandleIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(bosCandleIndex == -1) return;
|
|
|
|
// Look backwards from BOS to find consecutive opposite-direction candles
|
|
int consecutiveCount = 0;
|
|
double highestHigh = 0;
|
|
double lowestLow = 999999;
|
|
datetime firstCandleTime = 0;
|
|
bool foundOrderBlock = false;
|
|
|
|
if(lastBOS.type == 1) // Bullish BOS - look for consecutive bearish candles
|
|
{
|
|
// Start looking backwards from BOS for first bearish candle
|
|
int startIdx = bosCandleIndex + 1;
|
|
|
|
// Skip same-direction candles (blue for bullish BOS) to find first opposite (red)
|
|
while(startIdx < ArraySize(rates) && rates[startIdx].close > rates[startIdx].open)
|
|
{
|
|
startIdx++; // Skip bullish candles
|
|
}
|
|
|
|
// Now collect consecutive bearish candles
|
|
for(int i = startIdx; i < ArraySize(rates) && consecutiveCount < InpOrderBlockCandles; i++)
|
|
{
|
|
if(rates[i].close < rates[i].open) // Bearish candle
|
|
{
|
|
if(consecutiveCount == 0) firstCandleTime = rates[i].time;
|
|
consecutiveCount++;
|
|
if(rates[i].high > highestHigh) highestHigh = rates[i].high;
|
|
if(rates[i].low < lowestLow) lowestLow = rates[i].low;
|
|
foundOrderBlock = true;
|
|
}
|
|
else
|
|
{
|
|
break; // Stop at first non-bearish candle
|
|
}
|
|
}
|
|
}
|
|
else if(lastBOS.type == -1) // Bearish BOS - look for consecutive bullish candles
|
|
{
|
|
// Start looking backwards from BOS for first bullish candle
|
|
int startIdx = bosCandleIndex + 1;
|
|
|
|
// Skip same-direction candles (red for bearish BOS) to find first opposite (green)
|
|
while(startIdx < ArraySize(rates) && rates[startIdx].close < rates[startIdx].open)
|
|
{
|
|
startIdx++; // Skip bearish candles
|
|
}
|
|
|
|
// Now collect consecutive bullish candles
|
|
for(int i = startIdx; i < ArraySize(rates) && consecutiveCount < InpOrderBlockCandles; i++)
|
|
{
|
|
if(rates[i].close > rates[i].open) // Bullish candle
|
|
{
|
|
if(consecutiveCount == 0) firstCandleTime = rates[i].time;
|
|
consecutiveCount++;
|
|
if(rates[i].high > highestHigh) highestHigh = rates[i].high;
|
|
if(rates[i].low < lowestLow) lowestLow = rates[i].low;
|
|
foundOrderBlock = true;
|
|
}
|
|
else
|
|
{
|
|
break; // Stop at first non-bullish candle
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register order block if found
|
|
if(foundOrderBlock && consecutiveCount >= 1)
|
|
{
|
|
int obType = (lastBOS.type == 1) ? -1 : 1; // Opposite of BOS type
|
|
RegisterOrderBlock(firstCandleTime, highestHigh, lowestLow, obType, consecutiveCount, timeframe);
|
|
}
|
|
}
|
|
|
|
// Internal function to register a detected Order Block
|
|
void RegisterOrderBlock(datetime time, double top, double bottom, int type, int candleCount, ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
int digits = (int)SymbolInfoInteger(SYM, SYMBOL_DIGITS);
|
|
double normTop = NormalizeDouble(top, digits);
|
|
double normBottom = NormalizeDouble(bottom, digits);
|
|
|
|
// Determine chart ID based on timeframe
|
|
long chartId = 0; // Default to current chart
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
// M1 data is analyzed but not drawn (no chart created)
|
|
chartId = 0;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
chartId = GetOrCreateChart(SYM, PERIOD_M5);
|
|
}
|
|
else
|
|
{
|
|
return; // Only process M1 and M5 timeframes
|
|
}
|
|
|
|
if(timeframe == PERIOD_M5 && chartId == 0)
|
|
{
|
|
return; // Don't proceed if no valid chart for M5
|
|
}
|
|
|
|
// Check uniqueness to avoid duplicates
|
|
int s = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
s = ArraySize(g_orderBlocksM1);
|
|
if(s > 0 && g_orderBlocksM1[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_orderBlocksM1, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_orderBlocksM1, s+1);
|
|
g_orderBlocksM1[s].time = time;
|
|
g_orderBlocksM1[s].top = normTop;
|
|
g_orderBlocksM1[s].bottom = normBottom;
|
|
g_orderBlocksM1[s].type = type;
|
|
g_orderBlocksM1[s].candleCount = candleCount;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
s = ArraySize(g_orderBlocksM5);
|
|
if(s > 0 && g_orderBlocksM5[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_orderBlocksM5, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_orderBlocksM5, s+1);
|
|
g_orderBlocksM5[s].time = time;
|
|
g_orderBlocksM5[s].top = normTop;
|
|
g_orderBlocksM5[s].bottom = normBottom;
|
|
g_orderBlocksM5[s].type = type;
|
|
g_orderBlocksM5[s].candleCount = candleCount;
|
|
}
|
|
|
|
if(type == -1) // Bearish Order Block (before bullish BOS)
|
|
{
|
|
// Bearish Order Block
|
|
|
|
if(InpDrawOrderBlock && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_OB_Bear_";
|
|
DrawOrderBlockRectangle(chartId, prefix + TimeToString(time), time, normTop, normBottom, clrRed);
|
|
}
|
|
}
|
|
else // Bullish Order Block (before bearish BOS)
|
|
{
|
|
// Bullish Order Block
|
|
|
|
if(InpDrawOrderBlock && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_OB_Bull_";
|
|
DrawOrderBlockRectangle(chartId, prefix + TimeToString(time), time, normTop, normBottom, clrGreen);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DrawOrderBlockRectangle(long chartId, string name, datetime time, double top, double bottom, color clr)
|
|
{
|
|
if(!InpDrawOrderBlock) return;
|
|
|
|
if(ObjectFind(chartId, name) >= 0) return; // Avoid flickering
|
|
|
|
// Calculate rectangle time range (extend to the right)
|
|
datetime endTime = time + (PeriodSeconds() * 5); // Extend 30 bars to the right
|
|
|
|
// Create rectangle for Order Block
|
|
if(ObjectCreate(chartId, name, OBJ_RECTANGLE, 0, time, top, endTime, bottom))
|
|
{
|
|
ObjectSetInteger(chartId, name, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, name, OBJPROP_STYLE, STYLE_SOLID);
|
|
ObjectSetInteger(chartId, name, OBJPROP_WIDTH, 2);
|
|
ObjectSetInteger(chartId, name, OBJPROP_BACK, true); // Draw behind price
|
|
ObjectSetInteger(chartId, name, OBJPROP_FILL, true); // Fill the rectangle
|
|
ObjectSetInteger(chartId, name, OBJPROP_HIDDEN, false); // Visible in object list
|
|
ObjectSetInteger(chartId, name, OBJPROP_SELECTABLE, true); // Selectable
|
|
|
|
// Set semi-transparent background
|
|
color bgColor = clr;
|
|
if(clr == clrGreen)
|
|
bgColor = C'0,255,0,30'; // Very transparent green
|
|
else if(clr == clrRed)
|
|
bgColor = C'255,0,0,30'; // Very transparent red
|
|
|
|
ObjectSetInteger(chartId, name, OBJPROP_BGCOLOR, bgColor);
|
|
|
|
ChartRedraw(chartId); // Force update
|
|
}
|
|
}
|
|
|
|
|
|
//==================== DETECTION & VISUALS ====================
|
|
// Draws a short horizontal line from wick to the right for swing points
|
|
void DrawHighLow(long chartId, string name, datetime time, double price, color clr)
|
|
{
|
|
if(!InpDrawHighLow) return;
|
|
|
|
if(ObjectFind(chartId, name) >= 0) return; // Avoid flickering
|
|
|
|
// Calculate end time (extend a few bars to the right)
|
|
datetime endTime = time + (PeriodSeconds() * 2); // Extend 2 bars to the right (half the previous length)
|
|
|
|
// Create short horizontal trendline instead of arrow
|
|
if(ObjectCreate(chartId, name, OBJ_TREND, 0, time, price, endTime, price))
|
|
{
|
|
ObjectSetInteger(chartId, name, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, name, OBJPROP_WIDTH, 2);
|
|
ObjectSetInteger(chartId, name, OBJPROP_STYLE, STYLE_SOLID);
|
|
ObjectSetInteger(chartId, name, OBJPROP_HIDDEN, false); // Visible in object list
|
|
ObjectSetInteger(chartId, name, OBJPROP_SELECTABLE, true); // Selectable
|
|
ObjectSetInteger(chartId, name, OBJPROP_BACK, false); // Bring to front
|
|
ObjectSetInteger(chartId, name, OBJPROP_RAY_RIGHT, false); // Don't extend infinitely
|
|
|
|
ChartRedraw(chartId); // Force update
|
|
}
|
|
}
|
|
|
|
// Draws BOS marker with line connecting to broken swing point
|
|
void DrawBOSMarker(long chartId, string name, datetime bosTime, double bosPrice, datetime swingTime, double swingPrice, color clr)
|
|
{
|
|
if(!InpDrawBOS) return;
|
|
|
|
if(ObjectFind(chartId, name) >= 0) return; // Avoid flickering
|
|
|
|
// Create BOS arrow
|
|
string arrowName = name + "_Arrow";
|
|
if(ObjectCreate(chartId, arrowName, OBJ_ARROW, 0, bosTime, bosPrice))
|
|
{
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_ARROWCODE, 236); // Diamond symbol
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_WIDTH, 3);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_ANCHOR, ANCHOR_CENTER);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_HIDDEN, false);
|
|
ObjectSetInteger(chartId, arrowName, OBJPROP_SELECTABLE, true);
|
|
}
|
|
|
|
// Create trendline from swing point to BOS point
|
|
string lineName = name + "_Line";
|
|
if(ObjectCreate(chartId, lineName, OBJ_TREND, 0, swingTime, swingPrice, bosTime, bosPrice))
|
|
{
|
|
ObjectSetInteger(chartId, lineName, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, lineName, OBJPROP_WIDTH, 2);
|
|
ObjectSetInteger(chartId, lineName, OBJPROP_STYLE, STYLE_DASH);
|
|
ObjectSetInteger(chartId, lineName, OBJPROP_HIDDEN, false);
|
|
ObjectSetInteger(chartId, lineName, OBJPROP_SELECTABLE, true);
|
|
ObjectSetInteger(chartId, lineName, OBJPROP_RAY_RIGHT, false);
|
|
}
|
|
|
|
// Create label at swing point
|
|
string labelName = name + "_Label";
|
|
string labelText = StringFormat("BOS %.5f", swingPrice);
|
|
if(ObjectCreate(chartId, labelName, OBJ_TEXT, 0, swingTime, swingPrice))
|
|
{
|
|
ObjectSetString(chartId, labelName, OBJPROP_TEXT, labelText);
|
|
ObjectSetInteger(chartId, labelName, OBJPROP_COLOR, clr);
|
|
ObjectSetInteger(chartId, labelName, OBJPROP_FONTSIZE, 8);
|
|
ObjectSetInteger(chartId, labelName, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
|
|
ObjectSetInteger(chartId, labelName, OBJPROP_HIDDEN, false);
|
|
ObjectSetInteger(chartId, labelName, OBJPROP_SELECTABLE, true);
|
|
}
|
|
|
|
ChartRedraw(chartId); // Force update
|
|
}
|
|
|
|
// Internal function to register a detected point
|
|
void RegisterSwingPoint(datetime time, double price, int type, ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT)
|
|
{
|
|
int digits = (int)SymbolInfoInteger(SYM, SYMBOL_DIGITS);
|
|
double normPrice = NormalizeDouble(price, digits);
|
|
|
|
// Determine chart ID based on timeframe
|
|
long chartId = 0; // Default to current chart
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
// M1 data is analyzed but not drawn (no chart created)
|
|
chartId = 0;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
chartId = GetOrCreateChart(SYM, PERIOD_M5);
|
|
}
|
|
else
|
|
{
|
|
return; // Only process M1 and M5 timeframes
|
|
}
|
|
|
|
if(timeframe == PERIOD_M5 && chartId == 0)
|
|
{
|
|
return; // Don't proceed if no valid chart for M5
|
|
}
|
|
|
|
// Check uniqueness to avoid duplicates (especially during live runs)
|
|
if(type == 1) // High
|
|
{
|
|
int s = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
s = ArraySize(g_swingHighsM1);
|
|
if(s > 0 && g_swingHighsM1[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_swingHighsM1, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_swingHighsM1, s+1);
|
|
g_swingHighsM1[s].time = time;
|
|
g_swingHighsM1[s].price = normPrice;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
s = ArraySize(g_swingHighsM5);
|
|
if(s > 0 && g_swingHighsM5[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_swingHighsM5, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_swingHighsM5, s+1);
|
|
g_swingHighsM5[s].time = time;
|
|
g_swingHighsM5[s].price = normPrice;
|
|
}
|
|
|
|
// High Detected
|
|
|
|
if(InpDrawHighLow && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_High_";
|
|
DrawHighLow(chartId, prefix + TimeToString(time), time, normPrice, clrLime);
|
|
}
|
|
}
|
|
else // Low
|
|
{
|
|
int s = 0;
|
|
if(timeframe == PERIOD_M1)
|
|
{
|
|
s = ArraySize(g_swingLowsM1);
|
|
if(s > 0 && g_swingLowsM1[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_swingLowsM1, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_swingLowsM1, s+1);
|
|
g_swingLowsM1[s].time = time;
|
|
g_swingLowsM1[s].price = normPrice;
|
|
}
|
|
else if(timeframe == PERIOD_M5)
|
|
{
|
|
s = ArraySize(g_swingLowsM5);
|
|
if(s > 0 && g_swingLowsM5[s-1].time == time) return; // Already exists
|
|
|
|
// Enforce History Limit (FIFO)
|
|
if(s >= InpHistoryLimit){
|
|
ArrayRemove(g_swingLowsM5, 0, 1);
|
|
s--; // Decrement size after removal
|
|
}
|
|
|
|
ArrayResize(g_swingLowsM5, s+1);
|
|
g_swingLowsM5[s].time = time;
|
|
g_swingLowsM5[s].price = normPrice;
|
|
}
|
|
|
|
// Low Detected
|
|
|
|
if(InpDrawHighLow && timeframe == PERIOD_M5) // Only draw for M5
|
|
{
|
|
string prefix = "M5_Low_";
|
|
DrawHighLow(chartId, prefix + TimeToString(time), time, normPrice, clrRed);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to scan HISTORY logic for specific timeframe
|
|
void InitHistoryTimeframe(ENUM_TIMEFRAMES timeframe)
|
|
{
|
|
int Lookback = InpLookback;
|
|
MqlRates rates[];
|
|
ArraySetAsSeries(rates, true); // Index 0 is newest
|
|
|
|
int copied = CopyRates(SYM, timeframe, 1, Lookback + 2, rates); // Start from Bar 1 (skip forming)
|
|
if(copied < Lookback + 2) return;
|
|
|
|
for(int i = Lookback - 1; i >= 0; i--)
|
|
{
|
|
bool bullRight = rates[i].close > rates[i].open;
|
|
bool bearRight = rates[i].close < rates[i].open;
|
|
|
|
bool bullLeft = rates[i+1].close > rates[i+1].open;
|
|
bool bearLeft = rates[i+1].close < rates[i+1].open;
|
|
|
|
// Low: Bearish (i+1) -> Bullish (i)
|
|
if(bearLeft && bullRight)
|
|
{
|
|
double lowPrice = MathMin(rates[i].low, rates[i+1].low);
|
|
datetime lowTime = (rates[i].low <= rates[i+1].low) ? rates[i].time : rates[i+1].time;
|
|
|
|
RegisterSwingPoint(lowTime, lowPrice, -1, timeframe);
|
|
}
|
|
|
|
// High: Bullish (i+1) -> Bearish (i)
|
|
if(bullLeft && bearRight)
|
|
{
|
|
double highPrice = MathMax(rates[i].high, rates[i+1].high);
|
|
datetime highTime = (rates[i].high >= rates[i+1].high) ? rates[i].time : rates[i+1].time;
|
|
|
|
RegisterSwingPoint(highTime, highPrice, 1, timeframe);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Function to scan HISTORY logic (runs once on Init)
|
|
void InitHistory()
|
|
{
|
|
// Initialize history for M1 and M5 timeframes only
|
|
InitHistoryTimeframe(PERIOD_M1);
|
|
InitHistoryTimeframe(PERIOD_M5);
|
|
}
|
|
|
|
// Function to check LATEST signals for specific timeframe
|
|
void CheckLivePatternTimeframe(ENUM_TIMEFRAMES timeframe, datetime &lastCheckedBarTime)
|
|
{
|
|
MqlRates rates[];
|
|
ArraySetAsSeries(rates, true);
|
|
if(CopyRates(SYM, timeframe, 1, 3, rates) < 3) return; // Need Bar 1 and Bar 2
|
|
|
|
// Dedup: Only process if we haven't checked this confirmation bar yet
|
|
if(rates[0].time == lastCheckedBarTime) return;
|
|
lastCheckedBarTime = rates[0].time; // Mark confirmation bar as processed
|
|
|
|
bool bullRight = rates[0].close > rates[0].open;
|
|
bool bearRight = rates[0].close < rates[0].open;
|
|
|
|
bool bullLeft = rates[1].close > rates[1].open;
|
|
bool bearLeft = rates[1].close < rates[1].open;
|
|
|
|
// Low: Bearish (Left/Bar2) -> Bullish (Right/Bar1)
|
|
if(bearLeft && bullRight)
|
|
{
|
|
double lowPrice = MathMin(rates[0].low, rates[1].low);
|
|
datetime lowTime = (rates[0].low <= rates[1].low) ? rates[0].time : rates[1].time;
|
|
|
|
RegisterSwingPoint(lowTime, lowPrice, -1, timeframe);
|
|
}
|
|
|
|
// High: Bullish (Left/Bar2) -> Bearish (Right/Bar1)
|
|
if(bullLeft && bearRight)
|
|
{
|
|
double highPrice = MathMax(rates[0].high, rates[1].high);
|
|
datetime highTime = (rates[0].high >= rates[1].high) ? rates[0].time : rates[1].time;
|
|
|
|
RegisterSwingPoint(highTime, highPrice, 1, timeframe);
|
|
}
|
|
}
|
|
|
|
//==================== INIT ====================
|
|
int OnInit()
|
|
{
|
|
SYM = (StringLen(InpSymbol) == 0 ? _Symbol : InpSymbol);
|
|
|
|
point = SymbolInfoDouble(SYM, SYMBOL_POINT);
|
|
|
|
m_trade.SetExpertMagicNumber(InpMagicNumber);
|
|
|
|
g_dayOfYear = DayOfYearServer();
|
|
|
|
// Reset Global Setup
|
|
g_pendingSetup.active = false;
|
|
g_pendingSetup.direction = 0;
|
|
g_pendingSetup.slPrice = 0;
|
|
g_pendingSetup.initialSL = 0;
|
|
g_pendingSetup.setupTime = 0;
|
|
g_pendingSetup.initialDesc = "";
|
|
g_pendingSetup.lastCheckedTime = 0;
|
|
|
|
// Initialize chart for M5 timeframe only (M1 data will be analyzed but no chart created)
|
|
long m5Chart = GetOrCreateChart(SYM, PERIOD_M5);
|
|
|
|
// Clear Arrays
|
|
ArrayResize(g_swingHighsM1, 0);
|
|
ArrayResize(g_swingLowsM1, 0);
|
|
ArrayResize(g_bosPointsM1, 0);
|
|
ArrayResize(g_fvgPointsM1, 0);
|
|
ArrayResize(g_ifvgPointsM1, 0);
|
|
ArrayResize(g_orderBlocksM1, 0);
|
|
ArrayResize(g_swingHighsM5, 0);
|
|
ArrayResize(g_swingLowsM5, 0);
|
|
ArrayResize(g_bosPointsM5, 0);
|
|
ArrayResize(g_fvgPointsM5, 0);
|
|
ArrayResize(g_ifvgPointsM5, 0);
|
|
ArrayResize(g_orderBlocksM5, 0);
|
|
|
|
// Populate History Once
|
|
InitHistory();
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
void OnDeinit(const int reason)
|
|
{
|
|
// Clean up M5 chart if it exists (M1 data was analyzed but no chart was created)
|
|
long m5ChartId = FindChartBySymbolAndTimeframe(SYM, PERIOD_M5);
|
|
if(m5ChartId > 0)
|
|
{
|
|
ObjectsDeleteAll(m5ChartId, "M5_High_");
|
|
ObjectsDeleteAll(m5ChartId, "M5_Low_");
|
|
ObjectsDeleteAll(m5ChartId, "M5_BOS_Bull_");
|
|
ObjectsDeleteAll(m5ChartId, "M5_BOS_Bear_");
|
|
ObjectsDeleteAll(m5ChartId, "M5_FVG_");
|
|
ObjectsDeleteAll(m5ChartId, "M5_iFVG_");
|
|
ObjectsDeleteAll(m5ChartId, "M5_OB_");
|
|
}
|
|
}
|
|
|
|
// Check M5 Handover at the end of the tick
|
|
// This ensures that Order Blocks detected *after* the BOS are included in the confirmation search
|
|
void CheckM5Handover()
|
|
{
|
|
datetime lastM5Time = iTime(SYM, PERIOD_M5, 1);
|
|
|
|
// Check M5 BOS
|
|
int bosCount = ArraySize(g_bosPointsM5);
|
|
if(bosCount > 0)
|
|
{
|
|
BOSPoint bos = g_bosPointsM5[bosCount-1];
|
|
if(bos.time == lastM5Time) // New BOS
|
|
{
|
|
// Prevent re-activating a setup we just traded
|
|
if(bos.time == g_lastTradedSetupTime) return;
|
|
|
|
int dir = (bos.type == 1) ? 1 : -1;
|
|
string confDesc = "";
|
|
double confSL = FindInitialConfirmation(dir, bos.time, confDesc);
|
|
|
|
if(confSL > 0)
|
|
{
|
|
// Apply SL Buffer immediately
|
|
double slBuffer = InpSLBufferPoints * point;
|
|
if(dir == 1) confSL -= slBuffer; else confSL += slBuffer;
|
|
|
|
g_pendingSetup.active = true;
|
|
g_pendingSetup.direction = dir;
|
|
g_pendingSetup.slPrice = confSL; // Initial Best Price
|
|
g_pendingSetup.initialSL = confSL; // Fallback
|
|
g_pendingSetup.setupTime = bos.time;
|
|
g_pendingSetup.setupDesc = (dir==1?"Bullish":"Bearish") + " BOS at " + TimeToString(bos.time);
|
|
g_pendingSetup.confDesc = confDesc;
|
|
g_pendingSetup.initialDesc = confDesc;
|
|
g_pendingSetup.lastCheckedTime = bos.time;
|
|
g_tradesExecutedForCurrentSetup = 0; // Reset trade counter for new setup
|
|
|
|
// Only print if this setup hasn't been logged yet
|
|
if(bos.time != g_lastLoggedSetupTime)
|
|
{
|
|
Print("M5 Setup Found: BOS ", (dir==1?"Bull":"Bear"), " at ", TimeToString(bos.time), ". SL: ", confSL);
|
|
g_lastLoggedSetupTime = bos.time;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check M5 iFVG
|
|
int ifvgCount = ArraySize(g_ifvgPointsM5);
|
|
if(ifvgCount > 0)
|
|
{
|
|
iFVGPoint ifvg = g_ifvgPointsM5[ifvgCount-1];
|
|
if(ifvg.time == lastM5Time) // New iFVG
|
|
{
|
|
// Prevent re-activating a setup we just traded
|
|
if(ifvg.time == g_lastTradedSetupTime) return;
|
|
|
|
// iFVG logic: type -1 (Bearish Mitigated) -> Bullish Signal (1)
|
|
// type 1 (Bullish Mitigated) -> Bearish Signal (-1)
|
|
int dir = (ifvg.type == -1) ? 1 : -1;
|
|
string confDesc = "";
|
|
// Pass ifvg.time as minTime to ensure confirmation is at or after the iFVG mitigation time
|
|
double confSL = FindInitialConfirmation(dir, ifvg.time, confDesc, ifvg.time);
|
|
|
|
if(confSL > 0)
|
|
{
|
|
// Apply SL Buffer immediately
|
|
double slBuffer = InpSLBufferPoints * point;
|
|
if(dir == 1) confSL -= slBuffer; else confSL += slBuffer;
|
|
|
|
g_pendingSetup.active = true;
|
|
g_pendingSetup.direction = dir;
|
|
g_pendingSetup.slPrice = confSL; // Initial Best Price
|
|
g_pendingSetup.initialSL = confSL; // Fallback
|
|
g_pendingSetup.setupTime = ifvg.time;
|
|
g_pendingSetup.setupDesc = (dir==1?"Bullish":"Bearish") + " iFVG at " + TimeToString(ifvg.time);
|
|
g_pendingSetup.confDesc = confDesc;
|
|
g_pendingSetup.initialDesc = confDesc;
|
|
g_pendingSetup.lastCheckedTime = ifvg.time;
|
|
g_pendingSetup.m5FvgTime = ifvg.fvgTime;
|
|
g_pendingSetup.m5FvgTop = ifvg.fvgTop;
|
|
g_pendingSetup.m5FvgBottom = ifvg.fvgBottom;
|
|
g_tradesExecutedForCurrentSetup = 0; // Reset trade counter for new setup
|
|
|
|
// Only print if this setup hasn't been logged yet
|
|
if(ifvg.time != g_lastLoggedSetupTime)
|
|
{
|
|
Print("M5 Setup Found: iFVG ", (dir==1?"Bull":"Bear"), " at ", TimeToString(ifvg.time), ". SL: ", confSL);
|
|
g_lastLoggedSetupTime = ifvg.time;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//==================== TICK ====================
|
|
void OnTick()
|
|
{
|
|
ResetDailyCountersIfNeeded();
|
|
|
|
// -- Real-Time Check for all timeframes --
|
|
CheckLivePatternTimeframe(PERIOD_M1, g_lastCheckedBarTimeM1);
|
|
CheckLivePatternTimeframe(PERIOD_M5, g_lastCheckedBarTimeM5);
|
|
|
|
// -- BOS Detection on 1min and 5min timeframes --
|
|
DetectBOS(PERIOD_M1); // 1-minute timeframe
|
|
DetectBOS(PERIOD_M5); // 5-minute timeframe
|
|
|
|
// -- FVG Detection on 1min and 5min timeframes --
|
|
DetectFVG(PERIOD_M1); // 1-minute timeframe
|
|
DetectFVG(PERIOD_M5); // 5-minute timeframe
|
|
|
|
// -- iFVG Detection on 1min and 5min timeframes --
|
|
DetectiFVG(PERIOD_M1); // 1-minute timeframe
|
|
DetectiFVG(PERIOD_M5); // 5-minute timeframe
|
|
|
|
// -- Order Block Detection on 1min and 5min timeframes --
|
|
DetectOrderBlock(PERIOD_M1); // 1-minute timeframe
|
|
DetectOrderBlock(PERIOD_M5); // 5-minute timeframe
|
|
|
|
// -- Check for M5 Handover (Late Update to catch Order Blocks) --
|
|
CheckM5Handover();
|
|
}
|
|
//==================== END OF EA ====================
|