1881 lines
70 KiB
Plaintext
1881 lines
70 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| NANDR_ORB_OB_EA.mq5 |
|
|
//| ORB + Order Block Scalping EA for XAUUSD |
|
|
//| Supports M1 / M5 / M15 (default M15). ORB = 15 real minutes. |
|
|
//| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "NANDR"
|
|
#property version "1.35"
|
|
#property strict
|
|
|
|
#include <Trade\Trade.mqh>
|
|
#include <Trade\PositionInfo.mqh>
|
|
#include <Trade\OrderInfo.mqh>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Enumerations |
|
|
//+------------------------------------------------------------------+
|
|
enum ENUM_ORB_TF
|
|
{
|
|
ORB_TF_M1 = 1, // M1 (15 bars = 15min ORB)
|
|
ORB_TF_M5 = 5, // M5 (3 bars = 15min ORB)
|
|
ORB_TF_M15 = 15 // M15 (1 bar = 15min ORB) [Default]
|
|
};
|
|
|
|
enum ENUM_SL_MODE
|
|
{
|
|
SL_OB_BOUNDARY, // OB Boundary
|
|
SL_FIXED_PIPS, // Fixed Pips
|
|
SL_ATR_BASED // ATR-Based
|
|
};
|
|
|
|
enum ENUM_TP_MODE
|
|
{
|
|
TP_FIXED_PIPS, // Fixed Pips
|
|
TP_RR_RATIO // Risk:Reward Ratio
|
|
};
|
|
|
|
enum ENUM_LOT_MODE
|
|
{
|
|
LOT_FIXED, // Fixed Lot Size
|
|
LOT_RISK_PERCENT // Risk % of Balance
|
|
};
|
|
|
|
enum ENUM_ENTRY_MODE
|
|
{
|
|
ENTRY_MARKET, // Market Order (on retest bar close)
|
|
ENTRY_LIMIT // Limit Order (at ORB level)
|
|
};
|
|
|
|
enum ENUM_OB_MITIGATION
|
|
{
|
|
MITIG_WICK, // Wick (high/low penetrates zone)
|
|
MITIG_CLOSE // Close (close penetrates zone)
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Input Parameters |
|
|
//+------------------------------------------------------------------+
|
|
|
|
// --- Symbol ---
|
|
input group "═══ Symbol Settings ═══"
|
|
input string InpSymbolName = "XAUUSD"; // Symbol Name (auto-detected)
|
|
|
|
// --- Timeframe ---
|
|
input group "═══ Timeframe Settings ═══"
|
|
input ENUM_ORB_TF InpOrbTimeframe = ORB_TF_M15; // ORB Timeframe (M1/M5/M15)
|
|
input int InpBreakoutConfBars = 0; // Breakout Confirm Bars (0=auto)
|
|
|
|
// --- Sessions ---
|
|
input group "═══ Session Settings ═══"
|
|
input bool InpUseDailyOpen = true; // Daily Open Session (00:00 UTC)
|
|
input bool InpUseTokyoSession = true; // Tokyo Session (00:00 UTC)
|
|
input bool InpUseLondonSession = true; // London Session (07:00 UTC)
|
|
input bool InpUseNYSession = true; // NY Session (12:00 UTC)
|
|
input bool InpUseNYOrbSession = true; // NY ORB Session (13:30 UTC)
|
|
|
|
// --- Order Block Settings ---
|
|
input group "═══ Order Block Settings ═══"
|
|
input int InpOBPivotLength = 0; // OB Pivot Length (0=auto by TF)
|
|
input int InpOBMaxCount = 5; // Max Active OBs per direction
|
|
input ENUM_OB_MITIGATION InpOBMitig = MITIG_WICK; // Mitigation Method
|
|
input bool InpOBRequireConf = false; // Require OB Confirmation
|
|
input double InpOBProximityPips = 50.0; // OB Proximity (pips)
|
|
|
|
// --- Entry Settings ---
|
|
input group "═══ Entry Settings ═══"
|
|
input ENUM_ENTRY_MODE InpEntryMode = ENTRY_MARKET; // Entry Mode
|
|
input bool InpUseStrictFilter = false; // Use Strict Breakout Filter (false=simple close-cross)
|
|
input bool InpWaitForRetest = true; // Wait for ORB Retest Before Entry
|
|
input int InpBreakoutExpireBars = 20; // Bars Before Breakout Expires (0=never)
|
|
input bool InpUseOBRetestEntry = true; // OB Retest Entry (enter on OB boundary retest)
|
|
input int InpMaxRetestsPerSession = 3; // Max Retest Entries Per Session (per breakout)
|
|
input bool InpUseSessionBiasFilter = true; // Session Bias Filter: block signals against day bias
|
|
|
|
// --- Lot Size & Risk ---
|
|
input group "═══ Lot Size & Risk ═══"
|
|
input ENUM_LOT_MODE InpLotMode = LOT_RISK_PERCENT; // Lot Mode
|
|
input double InpFixedLotSize = 0.05; // Fixed Lot Size
|
|
input double InpRiskPercent = 0.05; // Risk % per Trade
|
|
|
|
// --- Stop Loss ---
|
|
input group "═══ Stop Loss ═══"
|
|
input ENUM_SL_MODE InpSLMode = SL_OB_BOUNDARY; // SL Mode
|
|
input double InpSLPips = 100.0; // SL Fixed Pips
|
|
input double InpATRMultiplier = 1.5; // ATR Multiplier (for SL_ATR)
|
|
input int InpATRPeriod = 14; // ATR Period
|
|
|
|
// --- Take Profit ---
|
|
input group "═══ Take Profit ═══"
|
|
input ENUM_TP_MODE InpTPMode = TP_RR_RATIO; // TP Mode
|
|
input double InpTPPips = 100.0; // TP Fixed Pips
|
|
input double InpRRRatio = 1.0; // Risk:Reward Ratio
|
|
|
|
// --- Trade Management ---
|
|
input group "═══ Trade Management ═══"
|
|
input double InpBreakevenTrigPips = 50.0; // Breakeven Trigger (pips)
|
|
input double InpBreakevenOffPips = 2.0; // Breakeven Offset (pips)
|
|
input double InpPartialCloseRatio = 0.5; // Partial Close Ratio at BE (0=disabled, 0.5=half)
|
|
input double InpTrailingStopPips = 0.0; // Trailing Stop Pips (0=off)
|
|
input int InpMaxTradesPerDay = 8; // Max Trades per Day
|
|
input int InpMaxPosPerSession = 2; // Max Positions per Session
|
|
|
|
// --- Risk Management ---
|
|
input group "═══ Risk Management ═══"
|
|
input double InpMaxDailyLossUSD = 150.0; // Max Daily Loss USD (0=off)
|
|
input double InpMaxDailyLossPct = 5.0; // Max Daily Loss % (0=off)
|
|
|
|
// --- Display ---
|
|
input group "═══ Display Settings ═══"
|
|
input bool InpShowDashboard = true; // Show Dashboard
|
|
input bool InpShowORBLines = true; // Show ORB Lines
|
|
input bool InpShowOBZones = true; // Show OB Zones
|
|
input bool InpShowTradeLabels = true; // Show Trade Labels
|
|
|
|
// --- Magic ---
|
|
input int InpMagicNumber = 202601; // Magic Number
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Session structure |
|
|
//+------------------------------------------------------------------+
|
|
struct SSession
|
|
{
|
|
string name;
|
|
int startHour;
|
|
int startMin;
|
|
bool enabled;
|
|
|
|
double orbHigh;
|
|
double orbLow;
|
|
int orbBarCount;
|
|
bool orbComplete;
|
|
datetime orbStartTime;
|
|
|
|
int breakoutDir; // 0=none, 1=bull, -1=bear
|
|
bool inBreakout;
|
|
bool inRetest;
|
|
int breakoutBarsAgo;
|
|
|
|
int tradesThisSession;
|
|
bool retestFiredThisBar; // blocks duplicate entries on consecutive ticks within same bar
|
|
|
|
void Reset()
|
|
{
|
|
orbHigh = 0;
|
|
orbLow = DBL_MAX;
|
|
orbBarCount = 0;
|
|
orbComplete = false;
|
|
orbStartTime = 0;
|
|
breakoutDir = 0;
|
|
inBreakout = false;
|
|
inRetest = false;
|
|
breakoutBarsAgo = 0;
|
|
tradesThisSession = 0;
|
|
retestFiredThisBar = false;
|
|
}
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Order Block structure |
|
|
//+------------------------------------------------------------------+
|
|
struct SOrderBlock
|
|
{
|
|
double top;
|
|
double bottom;
|
|
double mid;
|
|
datetime time;
|
|
bool active;
|
|
bool traded; // Prevents re-entering the same OB zone twice in one day
|
|
string objName;
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Global variables |
|
|
//+------------------------------------------------------------------+
|
|
CTrade g_Trade;
|
|
CPositionInfo g_Position;
|
|
COrderInfo g_Order;
|
|
|
|
string g_Symbol;
|
|
int g_OrbBarsNeeded;
|
|
int g_BreakoutConfBars;
|
|
int g_OBPivotLength;
|
|
double g_PipSize;
|
|
double g_PointSize;
|
|
|
|
// Sessions array (max 5)
|
|
SSession g_Sessions[5];
|
|
int g_SessionCount = 0;
|
|
|
|
// Order blocks
|
|
SOrderBlock g_BullOBs[];
|
|
SOrderBlock g_BearOBs[];
|
|
int g_BullOBCount = 0;
|
|
int g_BearOBCount = 0;
|
|
|
|
// Day bias: direction of the first confirmed ORB breakout of the day (0=none, 1=bull, -1=bear).
|
|
// When InpUseSessionBiasFilter=true, subsequent session breakouts and all OB entries that
|
|
// contradict this bias are suppressed, preventing counter-trend trades.
|
|
int g_GlobalBiasDir = 0;
|
|
|
|
// Tracks tickets that have already had partial close executed so it only fires once per position
|
|
#define MAX_PARTIAL_TRACKED 20
|
|
ulong g_PartialClosedTickets[MAX_PARTIAL_TRACKED];
|
|
int g_PartialClosedCount = 0;
|
|
|
|
bool IsPartialClosed(ulong ticket)
|
|
{
|
|
for(int i = 0; i < g_PartialClosedCount; i++)
|
|
if(g_PartialClosedTickets[i] == ticket) return true;
|
|
return false;
|
|
}
|
|
|
|
void MarkPartialClosed(ulong ticket)
|
|
{
|
|
if(g_PartialClosedCount < MAX_PARTIAL_TRACKED)
|
|
{
|
|
g_PartialClosedTickets[g_PartialClosedCount] = ticket;
|
|
g_PartialClosedCount++;
|
|
}
|
|
}
|
|
|
|
// Daily stats
|
|
datetime g_LastDayReset = 0;
|
|
int g_TodayTrades = 0;
|
|
int g_TodayWins = 0;
|
|
int g_TodayLosses = 0;
|
|
double g_TodayPnL = 0;
|
|
double g_DayStartEquity = 0;
|
|
bool g_TradingHalted = false;
|
|
double g_TotalPnL = 0;
|
|
|
|
// Bar tracking
|
|
datetime g_LastBarTime = 0;
|
|
bool g_IsNewBar = false;
|
|
|
|
// ATR handle
|
|
int g_ATRHandle = INVALID_HANDLE;
|
|
|
|
// History tracking for price buffer (need N bars lookback)
|
|
#define MAX_BARS_LOOKBACK 20
|
|
double g_HighBuf[];
|
|
double g_LowBuf[];
|
|
double g_CloseBuf[];
|
|
double g_VolBuf[];
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Utility: resolve actual symbol |
|
|
//+------------------------------------------------------------------+
|
|
string ResolveSymbol(const string base)
|
|
{
|
|
// Exact match first
|
|
if(SymbolInfoDouble(base, SYMBOL_BID) > 0)
|
|
return base;
|
|
|
|
// Scan all symbols for XAUUSD variants
|
|
int total = SymbolsTotal(false);
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
string sym = SymbolName(i, false);
|
|
if(StringFind(sym, "XAUUSD") >= 0 || StringFind(sym, "GOLD") >= 0 || StringFind(sym, "XAU") >= 0)
|
|
{
|
|
if(SymbolInfoDouble(sym, SYMBOL_BID) > 0)
|
|
{
|
|
PrintFormat("NANDR EA: Symbol '%s' not found. Using '%s' instead.", base, sym);
|
|
return sym;
|
|
}
|
|
}
|
|
}
|
|
Print("NANDR EA: WARNING — could not resolve symbol '", base, "'. Using as-is.");
|
|
return base;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Utility: pip value |
|
|
//+------------------------------------------------------------------+
|
|
double PipsToPrice(double pips)
|
|
{
|
|
return pips * g_PipSize;
|
|
}
|
|
|
|
double PriceToPips(double price)
|
|
{
|
|
return price / g_PipSize;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Utility: normalize lot size |
|
|
//+------------------------------------------------------------------+
|
|
double NormalizeLot(double lots)
|
|
{
|
|
double step = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_STEP);
|
|
double minL = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MIN);
|
|
double maxL = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MAX);
|
|
lots = MathFloor(lots / step) * step;
|
|
lots = MathMax(lots, minL);
|
|
lots = MathMin(lots, maxL);
|
|
return NormalizeDouble(lots, 2);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate lot size from risk |
|
|
//+------------------------------------------------------------------+
|
|
double CalcLotSize(double slPips)
|
|
{
|
|
if(InpLotMode == LOT_FIXED)
|
|
return NormalizeLot(InpFixedLotSize);
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
double riskUSD = balance * InpRiskPercent / 100.0;
|
|
double tickVal = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
double pipValue = tickVal * (g_PipSize / tickSize);
|
|
|
|
if(pipValue <= 0 || slPips <= 0)
|
|
return NormalizeLot(InpFixedLotSize);
|
|
|
|
double lots = riskUSD / (slPips * pipValue);
|
|
|
|
// Safety cap: never risk more than 5% of balance per trade regardless of settings.
|
|
// This prevents runaway lot sizes caused by an incorrect SL distance (e.g. pip size bug,
|
|
// misconfigured pips, or SL that is too tight).
|
|
double maxRiskUSD = balance * 5.0 / 100.0;
|
|
double maxSafeLots = maxRiskUSD / (slPips * pipValue);
|
|
if(lots > maxSafeLots)
|
|
{
|
|
PrintFormat("NANDR EA: WARNING — Computed lots %.2f exceeds 5%% risk cap. "
|
|
"Clamped to %.2f. Check SL pips (%.1f) and pip size (%.5f).",
|
|
lots, maxSafeLots, slPips, g_PipSize);
|
|
lots = maxSafeLots;
|
|
}
|
|
|
|
return NormalizeLot(lots);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate SL price |
|
|
//+------------------------------------------------------------------+
|
|
double CalcSL(int dir, double entry, double obBottom, double obTop)
|
|
{
|
|
double sl = 0;
|
|
if(InpSLMode == SL_FIXED_PIPS)
|
|
{
|
|
sl = (dir > 0) ? entry - PipsToPrice(InpSLPips)
|
|
: entry + PipsToPrice(InpSLPips);
|
|
}
|
|
else if(InpSLMode == SL_OB_BOUNDARY)
|
|
{
|
|
if(dir > 0 && obBottom > 0)
|
|
sl = obBottom - PipsToPrice(2.0); // 2 pip buffer below OB bottom
|
|
else if(dir < 0 && obTop > 0)
|
|
sl = obTop + PipsToPrice(2.0); // 2 pip buffer above OB top
|
|
else
|
|
sl = (dir > 0) ? entry - PipsToPrice(InpSLPips)
|
|
: entry + PipsToPrice(InpSLPips);
|
|
}
|
|
else // ATR
|
|
{
|
|
double atr[1];
|
|
if(CopyBuffer(g_ATRHandle, 0, 0, 1, atr) > 0)
|
|
sl = (dir > 0) ? entry - atr[0] * InpATRMultiplier
|
|
: entry + atr[0] * InpATRMultiplier;
|
|
else
|
|
sl = (dir > 0) ? entry - PipsToPrice(InpSLPips)
|
|
: entry + PipsToPrice(InpSLPips);
|
|
}
|
|
return NormalizeDouble(sl, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate TP price |
|
|
//+------------------------------------------------------------------+
|
|
double CalcTP(int dir, double entry, double sl)
|
|
{
|
|
double tp = 0;
|
|
double slDist = MathAbs(entry - sl);
|
|
if(InpTPMode == TP_FIXED_PIPS)
|
|
{
|
|
tp = (dir > 0) ? entry + PipsToPrice(InpTPPips)
|
|
: entry - PipsToPrice(InpTPPips);
|
|
}
|
|
else // RR ratio
|
|
{
|
|
tp = (dir > 0) ? entry + slDist * InpRRRatio
|
|
: entry - slDist * InpRRRatio;
|
|
}
|
|
return NormalizeDouble(tp, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Initialize sessions |
|
|
//+------------------------------------------------------------------+
|
|
void InitSessions()
|
|
{
|
|
g_SessionCount = 0;
|
|
|
|
// Daily Open
|
|
if(InpUseDailyOpen)
|
|
{
|
|
g_Sessions[g_SessionCount].name = "DailyOpen";
|
|
g_Sessions[g_SessionCount].startHour = 0;
|
|
g_Sessions[g_SessionCount].startMin = 0;
|
|
g_Sessions[g_SessionCount].enabled = true;
|
|
g_Sessions[g_SessionCount].Reset();
|
|
g_SessionCount++;
|
|
}
|
|
// Tokyo
|
|
if(InpUseTokyoSession)
|
|
{
|
|
g_Sessions[g_SessionCount].name = "Tokyo";
|
|
g_Sessions[g_SessionCount].startHour = 0;
|
|
g_Sessions[g_SessionCount].startMin = 0;
|
|
g_Sessions[g_SessionCount].enabled = true;
|
|
g_Sessions[g_SessionCount].Reset();
|
|
g_SessionCount++;
|
|
}
|
|
// London
|
|
if(InpUseLondonSession)
|
|
{
|
|
g_Sessions[g_SessionCount].name = "London";
|
|
g_Sessions[g_SessionCount].startHour = 7;
|
|
g_Sessions[g_SessionCount].startMin = 0;
|
|
g_Sessions[g_SessionCount].enabled = true;
|
|
g_Sessions[g_SessionCount].Reset();
|
|
g_SessionCount++;
|
|
}
|
|
// NY
|
|
if(InpUseNYSession)
|
|
{
|
|
g_Sessions[g_SessionCount].name = "NY";
|
|
g_Sessions[g_SessionCount].startHour = 12;
|
|
g_Sessions[g_SessionCount].startMin = 0;
|
|
g_Sessions[g_SessionCount].enabled = true;
|
|
g_Sessions[g_SessionCount].Reset();
|
|
g_SessionCount++;
|
|
}
|
|
// NY ORB (stock market open)
|
|
if(InpUseNYOrbSession)
|
|
{
|
|
g_Sessions[g_SessionCount].name = "NYORB";
|
|
g_Sessions[g_SessionCount].startHour = 13;
|
|
g_Sessions[g_SessionCount].startMin = 30;
|
|
g_Sessions[g_SessionCount].enabled = true;
|
|
g_Sessions[g_SessionCount].Reset();
|
|
g_SessionCount++;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if current bar is the session open bar |
|
|
//+------------------------------------------------------------------+
|
|
bool IsSessionOpenBar(const SSession &sess, datetime barTime)
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(barTime, dt);
|
|
return (dt.hour == sess.startHour && dt.min == sess.startMin);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if current bar is within session ORB accumulation window |
|
|
//+------------------------------------------------------------------+
|
|
bool IsWithinOrbWindow(const SSession &sess, datetime barTime)
|
|
{
|
|
if(sess.orbStartTime == 0) return false;
|
|
int elapsed = (int)(barTime - sess.orbStartTime);
|
|
return elapsed < g_OrbBarsNeeded * (int)InpOrbTimeframe * 60;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update ORB for all sessions on new bar |
|
|
//| IMPORTANT: always reads bar[1] (the just-CLOSED candle) so the |
|
|
//| true high/low is captured. bar[0] at new-bar time = open only. |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateORBSessions()
|
|
{
|
|
datetime bar1Time = iTime(g_Symbol, PERIOD_CURRENT, 1);
|
|
double bar1High = iHigh(g_Symbol, PERIOD_CURRENT, 1);
|
|
double bar1Low = iLow(g_Symbol, PERIOD_CURRENT, 1);
|
|
|
|
// New bar: clear per-bar retest flag so the next wick can trigger a fresh entry
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
g_Sessions[s].retestFiredThisBar = false;
|
|
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
{
|
|
if(!g_Sessions[s].enabled) continue;
|
|
|
|
// Session open bar just closed: bar[1] time matches session start
|
|
if(IsSessionOpenBar(g_Sessions[s], bar1Time))
|
|
{
|
|
g_Sessions[s].orbHigh = bar1High;
|
|
g_Sessions[s].orbLow = bar1Low;
|
|
g_Sessions[s].orbBarCount = 1;
|
|
g_Sessions[s].orbStartTime = bar1Time;
|
|
g_Sessions[s].orbComplete = (g_OrbBarsNeeded == 1);
|
|
g_Sessions[s].breakoutDir = 0;
|
|
g_Sessions[s].inBreakout = false;
|
|
g_Sessions[s].inRetest = false;
|
|
g_Sessions[s].breakoutBarsAgo = 0;
|
|
g_Sessions[s].tradesThisSession= 0;
|
|
|
|
if(g_Sessions[s].orbComplete)
|
|
PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f",
|
|
g_Sessions[s].name, bar1High, bar1Low);
|
|
if(InpShowORBLines)
|
|
DrawORBLines(s);
|
|
continue;
|
|
}
|
|
|
|
// Still accumulating (M5 or M1): append bar[1] closed data to range
|
|
if(!g_Sessions[s].orbComplete && g_Sessions[s].orbBarCount > 0
|
|
&& IsWithinOrbWindow(g_Sessions[s], bar1Time))
|
|
{
|
|
g_Sessions[s].orbHigh = MathMax(g_Sessions[s].orbHigh, bar1High);
|
|
g_Sessions[s].orbLow = MathMin(g_Sessions[s].orbLow, bar1Low);
|
|
g_Sessions[s].orbBarCount++;
|
|
if(g_Sessions[s].orbBarCount >= g_OrbBarsNeeded)
|
|
{
|
|
g_Sessions[s].orbComplete = true;
|
|
PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f",
|
|
g_Sessions[s].name, g_Sessions[s].orbHigh, g_Sessions[s].orbLow);
|
|
}
|
|
if(InpShowORBLines)
|
|
DrawORBLines(s);
|
|
}
|
|
|
|
// Track breakout bar age (only while breakout is active)
|
|
// NOTE: incremented here before DetectBreakouts, so newly-detected
|
|
// breakouts start at 0 here but will be seen as 0 in CheckEntrySignals
|
|
// because DetectBreakouts resets it to 0 AFTER this runs.
|
|
// The entry guard uses <=1 to handle both cases safely.
|
|
if(g_Sessions[s].inBreakout)
|
|
g_Sessions[s].breakoutBarsAgo++;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Strict breakout filter (mirrors Pine filteredHighCrossBO/Low) |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckStrictBreakout(int dir, double orbLevel)
|
|
{
|
|
int N = g_BreakoutConfBars;
|
|
if(N < 2) N = 2;
|
|
|
|
// Need at least N+3 closed bars (all indices shifted +1 vs simple version)
|
|
int bars = Bars(g_Symbol, PERIOD_CURRENT);
|
|
if(bars < N + 3) return false;
|
|
|
|
if(dir > 0) // bullish
|
|
{
|
|
if(iLow(g_Symbol, PERIOD_CURRENT, N + 2) >= orbLevel) return false;
|
|
for(int i = 2; i <= N + 1; i++)
|
|
{
|
|
if(iLow(g_Symbol, PERIOD_CURRENT, i) <= orbLevel) return false;
|
|
if(iClose(g_Symbol, PERIOD_CURRENT, i) <= orbLevel) return false;
|
|
}
|
|
if(iClose(g_Symbol, PERIOD_CURRENT, 1) <= iLow(g_Symbol, PERIOD_CURRENT, 2)) return false;
|
|
if(iLow(g_Symbol, PERIOD_CURRENT, 1) <= orbLevel) return false;
|
|
return true;
|
|
}
|
|
else // bearish
|
|
{
|
|
if(iHigh(g_Symbol, PERIOD_CURRENT, N + 2) <= orbLevel) return false;
|
|
for(int i = 2; i <= N + 1; i++)
|
|
{
|
|
if(iHigh(g_Symbol, PERIOD_CURRENT, i) >= orbLevel) return false;
|
|
if(iClose(g_Symbol, PERIOD_CURRENT, i) >= orbLevel) return false;
|
|
}
|
|
if(iClose(g_Symbol, PERIOD_CURRENT, 1) >= iHigh(g_Symbol, PERIOD_CURRENT, 2)) return false;
|
|
if(iHigh(g_Symbol, PERIOD_CURRENT, 1) >= orbLevel) return false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Simple breakout: uses bar[1] (just-closed) vs bar[2] |
|
|
//| Matches Pine Script bar-close evaluation — never checks an |
|
|
//| in-progress candle so no premature signals at bar open. |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckSimpleBreakout(int dir, double orbLevel)
|
|
{
|
|
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar
|
|
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); // bar before it
|
|
if(dir > 0) return (c1 <= orbLevel && c0 > orbLevel);
|
|
else return (c1 >= orbLevel && c0 < orbLevel);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Detect breakout for all sessions |
|
|
//+------------------------------------------------------------------+
|
|
void DetectBreakouts()
|
|
{
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
{
|
|
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
|
|
if(g_Sessions[s].orbHigh <= 0 || g_Sessions[s].orbLow >= DBL_MAX) continue;
|
|
if(g_Sessions[s].inBreakout || g_Sessions[s].inRetest) continue;
|
|
|
|
bool bullBO = InpUseStrictFilter
|
|
? CheckStrictBreakout( 1, g_Sessions[s].orbHigh)
|
|
: CheckSimpleBreakout( 1, g_Sessions[s].orbHigh);
|
|
bool bearBO = InpUseStrictFilter
|
|
? CheckStrictBreakout(-1, g_Sessions[s].orbLow)
|
|
: CheckSimpleBreakout(-1, g_Sessions[s].orbLow);
|
|
|
|
if(bullBO)
|
|
{
|
|
// Bias filter: suppress if day bias is already bearish
|
|
if(InpUseSessionBiasFilter && g_GlobalBiasDir == -1)
|
|
{
|
|
PrintFormat("NANDR EA: [%s] Bullish breakout SUPPRESSED — day bias is BEARISH",
|
|
g_Sessions[s].name);
|
|
continue;
|
|
}
|
|
g_Sessions[s].breakoutDir = 1;
|
|
g_Sessions[s].inBreakout = true;
|
|
g_Sessions[s].breakoutBarsAgo= 0;
|
|
if(g_GlobalBiasDir == 0) g_GlobalBiasDir = 1; // first breakout of the day sets bias
|
|
PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f [DayBias=%d]",
|
|
g_Sessions[s].name, g_Sessions[s].orbHigh, g_GlobalBiasDir);
|
|
}
|
|
else if(bearBO)
|
|
{
|
|
// Bias filter: suppress if day bias is already bullish
|
|
if(InpUseSessionBiasFilter && g_GlobalBiasDir == 1)
|
|
{
|
|
PrintFormat("NANDR EA: [%s] Bearish breakout SUPPRESSED — day bias is BULLISH",
|
|
g_Sessions[s].name);
|
|
continue;
|
|
}
|
|
g_Sessions[s].breakoutDir = -1;
|
|
g_Sessions[s].inBreakout = true;
|
|
g_Sessions[s].breakoutBarsAgo= 0;
|
|
if(g_GlobalBiasDir == 0) g_GlobalBiasDir = -1; // first breakout of the day sets bias
|
|
PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f [DayBias=%d]",
|
|
g_Sessions[s].name, g_Sessions[s].orbLow, g_GlobalBiasDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update retest state on bar close: sweep reversals + invalidation |
|
|
//| Called once per new bar. Does NOT fire any trades. |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateRetestState(int sessIdx)
|
|
{
|
|
if(!g_Sessions[sessIdx].inBreakout) return;
|
|
|
|
// Use bar[1] (just-closed) and bar[2] for bar-close confirmation.
|
|
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1);
|
|
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2);
|
|
double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 1);
|
|
double l0 = iLow(g_Symbol, PERIOD_CURRENT, 1);
|
|
|
|
double orbHigh = g_Sessions[sessIdx].orbHigh;
|
|
double orbLow = g_Sessions[sessIdx].orbLow;
|
|
double orbMid = (orbHigh + orbLow) / 2.0;
|
|
|
|
if(g_Sessions[sessIdx].breakoutDir == 1)
|
|
{
|
|
// Sweep reversal: bullish breakout failed — bar closed below orbHigh → flip to bearish
|
|
bool sweepRev = (c1 > orbHigh) && (l0 < orbHigh) && (c0 < orbHigh) && (c0 >= orbMid);
|
|
if(sweepRev)
|
|
{
|
|
g_Sessions[sessIdx].breakoutDir = -1;
|
|
PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH",
|
|
g_Sessions[sessIdx].name, orbHigh);
|
|
}
|
|
// Full invalidation: closed below mid
|
|
else if(c0 < orbMid && c1 > orbMid)
|
|
{
|
|
PrintFormat("NANDR EA: [%s] Failed bullish retest (closed below mid %.2f)",
|
|
g_Sessions[sessIdx].name, orbMid);
|
|
g_Sessions[sessIdx].inBreakout = false;
|
|
g_Sessions[sessIdx].breakoutDir = 0;
|
|
}
|
|
}
|
|
else if(g_Sessions[sessIdx].breakoutDir == -1)
|
|
{
|
|
// Sweep reversal: bearish breakout failed — bar closed above orbLow → flip to bullish
|
|
bool sweepRev = (c1 < orbLow) && (h0 > orbLow) && (c0 > orbLow) && (c0 < orbHigh);
|
|
if(sweepRev)
|
|
{
|
|
g_Sessions[sessIdx].breakoutDir = 1;
|
|
PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH",
|
|
g_Sessions[sessIdx].name, orbLow);
|
|
}
|
|
// Full invalidation: closed above mid
|
|
else if(c0 > orbMid && c1 < orbMid)
|
|
{
|
|
PrintFormat("NANDR EA: [%s] Failed bearish retest (closed above mid %.2f)",
|
|
g_Sessions[sessIdx].name, orbMid);
|
|
g_Sessions[sessIdx].inBreakout = false;
|
|
g_Sessions[sessIdx].breakoutDir = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Order Block: scan for new OBs on new bar |
|
|
//+------------------------------------------------------------------+
|
|
void ScanOrderBlocks()
|
|
{
|
|
int length = g_OBPivotLength;
|
|
int barsNeeded = length * 2 + 2;
|
|
if(Bars(g_Symbol, PERIOD_CURRENT) < barsNeeded) return;
|
|
|
|
// Check for volume pivot at bar[length]
|
|
// Volume at bar[length] must be highest in [0..2*length]
|
|
long pivotVol = iVolume(g_Symbol, PERIOD_CURRENT, length);
|
|
bool isVolPivot = true;
|
|
for(int i = 0; i < length * 2 + 1; i++)
|
|
{
|
|
if(i == length) continue;
|
|
if(iVolume(g_Symbol, PERIOD_CURRENT, i) >= pivotVol)
|
|
{
|
|
isVolPivot = false;
|
|
break;
|
|
}
|
|
}
|
|
if(!isVolPivot) return;
|
|
|
|
// Market structure: os
|
|
// os=1 (bullish): low[length] is lowest low in the window
|
|
// os=0 (bearish): high[length] is highest high in the window
|
|
double pivotHigh = iHigh(g_Symbol, PERIOD_CURRENT, length);
|
|
double pivotLow = iLow(g_Symbol, PERIOD_CURRENT, length);
|
|
|
|
double highestHigh = pivotHigh;
|
|
double lowestLow = pivotLow;
|
|
for(int i = 0; i < length * 2 + 1; i++)
|
|
{
|
|
highestHigh = MathMax(highestHigh, iHigh(g_Symbol, PERIOD_CURRENT, i));
|
|
lowestLow = MathMin(lowestLow, iLow(g_Symbol, PERIOD_CURRENT, i));
|
|
}
|
|
|
|
int os = -1; // unknown
|
|
if(pivotHigh >= highestHigh) os = 0; // bearish structure
|
|
else if(pivotLow <= lowestLow) os = 1; // bullish structure
|
|
|
|
if(os < 0) return;
|
|
|
|
datetime obTime = iTime(g_Symbol, PERIOD_CURRENT, length);
|
|
double hl2 = (pivotHigh + pivotLow) / 2.0;
|
|
|
|
if(os == 1) // Bullish OB: zone [low[length], hl2]
|
|
{
|
|
if(g_BullOBCount < InpOBMaxCount)
|
|
{
|
|
// Check not duplicate
|
|
for(int i = 0; i < g_BullOBCount; i++)
|
|
if(g_BullOBs[i].time == obTime) return;
|
|
|
|
g_BullOBs[g_BullOBCount].top = hl2;
|
|
g_BullOBs[g_BullOBCount].bottom = pivotLow;
|
|
g_BullOBs[g_BullOBCount].mid = (hl2 + pivotLow) / 2.0;
|
|
g_BullOBs[g_BullOBCount].time = obTime;
|
|
g_BullOBs[g_BullOBCount].active = true;
|
|
g_BullOBs[g_BullOBCount].traded = false;
|
|
g_BullOBs[g_BullOBCount].objName= "NANDR_BullOB_" + IntegerToString(obTime);
|
|
g_BullOBCount++;
|
|
if(InpShowOBZones) DrawOBZone(g_BullOBCount - 1, true);
|
|
PrintFormat("NANDR EA: Bullish OB detected. Zone [%.2f - %.2f]", pivotLow, hl2);
|
|
}
|
|
}
|
|
else // Bearish OB: zone [hl2, high[length]]
|
|
{
|
|
if(g_BearOBCount < InpOBMaxCount)
|
|
{
|
|
for(int i = 0; i < g_BearOBCount; i++)
|
|
if(g_BearOBs[i].time == obTime) return;
|
|
|
|
g_BearOBs[g_BearOBCount].top = pivotHigh;
|
|
g_BearOBs[g_BearOBCount].bottom = hl2;
|
|
g_BearOBs[g_BearOBCount].mid = (pivotHigh + hl2) / 2.0;
|
|
g_BearOBs[g_BearOBCount].time = obTime;
|
|
g_BearOBs[g_BearOBCount].active = true;
|
|
g_BearOBs[g_BearOBCount].traded = false;
|
|
g_BearOBs[g_BearOBCount].objName= "NANDR_BearOB_" + IntegerToString(obTime);
|
|
g_BearOBCount++;
|
|
if(InpShowOBZones) DrawOBZone(g_BearOBCount - 1, false);
|
|
PrintFormat("NANDR EA: Bearish OB detected. Zone [%.2f - %.2f]", hl2, pivotHigh);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Remove mitigated OBs (Wick or Close method) |
|
|
//+------------------------------------------------------------------+
|
|
void MitigateOrderBlocks()
|
|
{
|
|
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 0);
|
|
double l0 = iLow(g_Symbol, PERIOD_CURRENT, 0);
|
|
double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 0);
|
|
|
|
// Bullish OBs: mitigated if price goes below bottom
|
|
for(int i = g_BullOBCount - 1; i >= 0; i--)
|
|
{
|
|
if(!g_BullOBs[i].active) continue;
|
|
double target = (InpOBMitig == MITIG_WICK) ? l0 : c0;
|
|
if(target < g_BullOBs[i].bottom)
|
|
{
|
|
g_BullOBs[i].active = false;
|
|
if(InpShowOBZones) RemoveOBZone(g_BullOBs[i].objName);
|
|
// Shift array left
|
|
for(int j = i; j < g_BullOBCount - 1; j++)
|
|
g_BullOBs[j] = g_BullOBs[j+1];
|
|
g_BullOBCount--;
|
|
}
|
|
}
|
|
|
|
// Bearish OBs: mitigated if price goes above top
|
|
for(int i = g_BearOBCount - 1; i >= 0; i--)
|
|
{
|
|
if(!g_BearOBs[i].active) continue;
|
|
double target = (InpOBMitig == MITIG_WICK) ? h0 : c0;
|
|
if(target > g_BearOBs[i].top)
|
|
{
|
|
g_BearOBs[i].active = false;
|
|
if(InpShowOBZones) RemoveOBZone(g_BearOBs[i].objName);
|
|
for(int j = i; j < g_BearOBCount - 1; j++)
|
|
g_BearOBs[j] = g_BearOBs[j+1];
|
|
g_BearOBCount--;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Find nearest OB to a given price and direction |
|
|
//+------------------------------------------------------------------+
|
|
bool IsOBNearLevel(double price, int dir, double &obTop, double &obBottom)
|
|
{
|
|
double proxPrice = PipsToPrice(InpOBProximityPips);
|
|
|
|
if(dir > 0) // Look for bullish OB near or below entry
|
|
{
|
|
double nearest = DBL_MAX;
|
|
int bestIdx = -1;
|
|
for(int i = 0; i < g_BullOBCount; i++)
|
|
{
|
|
if(!g_BullOBs[i].active) continue;
|
|
double dist = MathAbs(price - g_BullOBs[i].top);
|
|
if(dist <= proxPrice && dist < nearest)
|
|
{
|
|
nearest = dist;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
if(bestIdx >= 0)
|
|
{
|
|
obTop = g_BullOBs[bestIdx].top;
|
|
obBottom = g_BullOBs[bestIdx].bottom;
|
|
return true;
|
|
}
|
|
}
|
|
else // Look for bearish OB near or above entry
|
|
{
|
|
double nearest = DBL_MAX;
|
|
int bestIdx = -1;
|
|
for(int i = 0; i < g_BearOBCount; i++)
|
|
{
|
|
if(!g_BearOBs[i].active) continue;
|
|
double dist = MathAbs(price - g_BearOBs[i].bottom);
|
|
if(dist <= proxPrice && dist < nearest)
|
|
{
|
|
nearest = dist;
|
|
bestIdx = i;
|
|
}
|
|
}
|
|
if(bestIdx >= 0)
|
|
{
|
|
obTop = g_BearOBs[bestIdx].top;
|
|
obBottom = g_BearOBs[bestIdx].bottom;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
obTop = obBottom = 0;
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Count open positions by magic |
|
|
//+------------------------------------------------------------------+
|
|
int CountOpenPositions()
|
|
{
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(g_Position.SelectByIndex(i))
|
|
if(g_Position.Magic() == InpMagicNumber && g_Position.Symbol() == g_Symbol)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Count pending orders by magic |
|
|
//+------------------------------------------------------------------+
|
|
int CountPendingOrders()
|
|
{
|
|
int count = 0;
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(g_Order.SelectByIndex(i))
|
|
if(g_Order.Magic() == InpMagicNumber && g_Order.Symbol() == g_Symbol)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Cancel all pending orders |
|
|
//+------------------------------------------------------------------+
|
|
void CancelAllPending()
|
|
{
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(g_Order.SelectByIndex(i))
|
|
if(g_Order.Magic() == InpMagicNumber && g_Order.Symbol() == g_Symbol)
|
|
g_Trade.OrderDelete(g_Order.Ticket());
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Open trade |
|
|
//+------------------------------------------------------------------+
|
|
void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBottom)
|
|
{
|
|
if(g_TradingHalted) return;
|
|
if(g_TodayTrades >= InpMaxTradesPerDay) return;
|
|
if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return;
|
|
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) return;
|
|
|
|
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
|
|
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
|
|
double entry = (dir > 0) ? ask : bid;
|
|
|
|
// For limit orders, entry is at the ORB level
|
|
if(InpEntryMode == ENTRY_LIMIT)
|
|
entry = orbLevel;
|
|
|
|
double sl = CalcSL(dir, entry, obBottom, obTop);
|
|
double tp = CalcTP(dir, entry, sl);
|
|
double slPips = PriceToPips(MathAbs(entry - sl));
|
|
double lots = CalcLotSize(slPips);
|
|
|
|
string comment = StringFormat("NANDR|%s|%s", g_Sessions[sessIdx].name, (dir > 0 ? "BUY" : "SELL"));
|
|
|
|
bool result = false;
|
|
if(InpEntryMode == ENTRY_MARKET)
|
|
{
|
|
if(dir > 0)
|
|
result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp, comment);
|
|
else
|
|
result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp, comment);
|
|
}
|
|
else // Limit
|
|
{
|
|
ENUM_ORDER_TYPE otype = (dir > 0) ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT;
|
|
result = g_Trade.OrderOpen(g_Symbol, otype, lots, orbLevel, orbLevel, sl, tp, ORDER_TIME_DAY, 0, comment);
|
|
}
|
|
|
|
if(result)
|
|
{
|
|
g_TodayTrades++;
|
|
g_Sessions[sessIdx].tradesThisSession++;
|
|
// Do NOT clear inBreakout — the ORB level remains valid for subsequent retests
|
|
// until the breakout expires, fails, or the daily reset. This allows the EA to
|
|
// take multiple retest entries on the same level (e.g. 18:45 AND 20:45 retests).
|
|
// Re-entry is naturally gated by CountOpenPositions() checked at the top of OpenTrade.
|
|
PrintFormat("NANDR EA: Trade opened. Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s",
|
|
(dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name);
|
|
if(InpShowTradeLabels)
|
|
DrawTradeLabel(dir, entry, sl, tp);
|
|
}
|
|
else
|
|
{
|
|
PrintFormat("NANDR EA: OrderSend failed. Error=%d Session=%s",
|
|
GetLastError(), g_Sessions[sessIdx].name);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Tick-based retest entries — fires at the wick, not on bar close |
|
|
//| Covers both ORB session retests and OB boundary retests. |
|
|
//| Uses live bar[0] high/low + last closed bar[1] for confirmation. |
|
|
//| Direction: determined by breakoutDir for ORB, OB type for OBs. |
|
|
//| Sweep reversals: detected via ask/bid relative to the level. |
|
|
//+------------------------------------------------------------------+
|
|
void CheckRetestEntriesTick()
|
|
{
|
|
if(g_TradingHalted) return;
|
|
if(g_TodayTrades >= InpMaxTradesPerDay) return;
|
|
|
|
double h_cur = iHigh(g_Symbol, PERIOD_CURRENT, 0); // current bar live high so far
|
|
double l_cur = iLow(g_Symbol, PERIOD_CURRENT, 0); // current bar live low so far
|
|
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); // last closed bar close
|
|
double open0 = iOpen(g_Symbol, PERIOD_CURRENT, 0); // current bar open
|
|
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
|
|
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
|
|
double tol = PipsToPrice(1.0); // 1-pip tolerance for direction detection
|
|
|
|
// ── ORB Session Retest Entries ──────────────────────────────────
|
|
// Fires at the moment the wick touches the ORB level, not one bar later.
|
|
// Direction uses ask/bid to distinguish continuation vs sweep reversal:
|
|
// bid <= level + tol → price is AT or below the level → continuation in breakout dir
|
|
// ask >= level - tol → price has passed back through the level → sweep reversal, flip dir
|
|
if(InpWaitForRetest)
|
|
{
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
{
|
|
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
|
|
if(!g_Sessions[s].inBreakout) continue;
|
|
if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue;
|
|
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) continue;
|
|
if(InpBreakoutExpireBars > 0 && g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars) continue;
|
|
|
|
double orbHigh = g_Sessions[s].orbHigh;
|
|
double orbLow = g_Sessions[s].orbLow;
|
|
double orbMid = (orbHigh + orbLow) / 2.0;
|
|
int dir = 0;
|
|
double orbLevel = 0;
|
|
|
|
if(g_Sessions[s].breakoutDir == 1) // Bullish
|
|
{
|
|
// Standard: prev close above orbHigh, wick dipped to orbHigh
|
|
if(c1 > orbHigh && l_cur <= orbHigh)
|
|
{
|
|
orbLevel = orbHigh;
|
|
// ask still above orbHigh → support held → BUY
|
|
// ask dropped below orbHigh → sweep failed → SELL, flip to bearish
|
|
if(ask >= orbHigh - tol)
|
|
dir = 1;
|
|
else
|
|
{
|
|
dir = -1;
|
|
g_Sessions[s].breakoutDir = -1;
|
|
PrintFormat("NANDR EA: [%s] Tick sweep reversal below orbHigh %.2f → SELL",
|
|
g_Sessions[s].name, orbHigh);
|
|
}
|
|
}
|
|
// Mid retest: prev close above mid, wick dipped to mid
|
|
else if(c1 > orbMid && l_cur <= orbMid)
|
|
{
|
|
orbLevel = orbMid;
|
|
if(ask >= orbMid - tol)
|
|
dir = 1;
|
|
else
|
|
{
|
|
dir = -1;
|
|
g_Sessions[s].breakoutDir = -1;
|
|
}
|
|
}
|
|
}
|
|
else if(g_Sessions[s].breakoutDir == -1) // Bearish
|
|
{
|
|
// Standard: prev close below orbLow, wick came back up to orbLow
|
|
if(c1 < orbLow && h_cur >= orbLow)
|
|
{
|
|
orbLevel = orbLow;
|
|
// bid still below orbLow → resistance held → SELL (continuation)
|
|
// bid moved above orbLow → sweep reversal → BUY, flip to bullish
|
|
if(bid <= orbLow + tol)
|
|
dir = -1;
|
|
else
|
|
{
|
|
dir = 1;
|
|
g_Sessions[s].breakoutDir = 1;
|
|
PrintFormat("NANDR EA: [%s] Tick sweep reversal above orbLow %.2f → BUY",
|
|
g_Sessions[s].name, orbLow);
|
|
}
|
|
}
|
|
// Mid retest: prev close below mid, wick came back up to mid
|
|
else if(c1 < orbMid && h_cur >= orbMid)
|
|
{
|
|
orbLevel = orbMid;
|
|
if(bid <= orbMid + tol)
|
|
dir = -1;
|
|
else
|
|
{
|
|
dir = 1;
|
|
g_Sessions[s].breakoutDir = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(dir == 0) continue;
|
|
|
|
// One entry per wick touch per bar — prevents 3 duplicate trades on consecutive ticks
|
|
if(g_Sessions[s].retestFiredThisBar) continue;
|
|
g_Sessions[s].retestFiredThisBar = true;
|
|
|
|
double obTop = 0, obBottom = 0;
|
|
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
|
|
if(InpOBRequireConf && obTop == 0) continue;
|
|
OpenTrade(dir, s, orbLevel, obTop, obBottom);
|
|
}
|
|
}
|
|
|
|
if(!InpUseOBRetestEntry) return;
|
|
|
|
// ── Bearish OB Retest → SELL at wick touch ───────────────────────
|
|
// Suppressed when day bias is bullish (session takes priority).
|
|
if(InpUseSessionBiasFilter && g_GlobalBiasDir == 1) return;
|
|
|
|
for(int i = 0; i < g_BearOBCount; i++)
|
|
{
|
|
if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue;
|
|
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break;
|
|
if(g_TodayTrades >= InpMaxTradesPerDay) break;
|
|
|
|
double obBottom = g_BearOBs[i].bottom;
|
|
double obTop2 = g_BearOBs[i].top;
|
|
|
|
bool prevBelow = (c1 <= obBottom || open0 <= obBottom);
|
|
bool wickTouched = (h_cur >= obBottom);
|
|
if(!prevBelow || !wickTouched) continue;
|
|
|
|
double entry = bid;
|
|
double sl = NormalizeDouble(obTop2 + PipsToPrice(2.0),
|
|
(int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
double slPips = PriceToPips(MathAbs(entry - sl));
|
|
double tp = CalcTP(-1, entry, sl);
|
|
double lots = CalcLotSize(slPips);
|
|
|
|
bool result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp,
|
|
StringFormat("NANDR|OBRetest|SELL|%.2f", obBottom));
|
|
if(result)
|
|
{
|
|
g_TodayTrades++;
|
|
g_BearOBs[i].traded = true;
|
|
PrintFormat("NANDR EA: OB Retest SELL. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]",
|
|
entry, sl, tp, obBottom, obTop2);
|
|
if(InpShowTradeLabels) DrawTradeLabel(-1, entry, sl, tp);
|
|
}
|
|
else
|
|
PrintFormat("NANDR EA: OB Retest SELL failed. Error=%d", GetLastError());
|
|
break;
|
|
}
|
|
|
|
// ── Bullish OB Retest → BUY at wick touch ────────────────────────
|
|
// Suppressed when day bias is bearish (session takes priority).
|
|
if(InpUseSessionBiasFilter && g_GlobalBiasDir == -1) return;
|
|
|
|
for(int i = 0; i < g_BullOBCount; i++)
|
|
{
|
|
if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue;
|
|
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break;
|
|
if(g_TodayTrades >= InpMaxTradesPerDay) break;
|
|
|
|
double obTop3 = g_BullOBs[i].top;
|
|
double obBottom2 = g_BullOBs[i].bottom;
|
|
|
|
bool prevAbove = (c1 >= obTop3 || open0 >= obTop3);
|
|
bool wickTouched = (l_cur <= obTop3);
|
|
if(!prevAbove || !wickTouched) continue;
|
|
|
|
double entry = ask;
|
|
double sl = NormalizeDouble(obBottom2 - PipsToPrice(2.0),
|
|
(int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
double slPips = PriceToPips(MathAbs(entry - sl));
|
|
double tp = CalcTP(1, entry, sl);
|
|
double lots = CalcLotSize(slPips);
|
|
|
|
bool result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp,
|
|
StringFormat("NANDR|OBRetest|BUY|%.2f", obTop3));
|
|
if(result)
|
|
{
|
|
g_TodayTrades++;
|
|
g_BullOBs[i].traded = true;
|
|
PrintFormat("NANDR EA: OB Retest BUY. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]",
|
|
entry, sl, tp, obBottom2, obTop3);
|
|
if(InpShowTradeLabels) DrawTradeLabel(1, entry, sl, tp);
|
|
}
|
|
else
|
|
PrintFormat("NANDR EA: OB Retest BUY failed. Error=%d", GetLastError());
|
|
break;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check all sessions for entry signals |
|
|
//+------------------------------------------------------------------+
|
|
void CheckEntrySignals()
|
|
{
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
{
|
|
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
|
|
// Guard: allow up to InpMaxRetestsPerSession retest entries on the same breakout level.
|
|
// Previously used InpMaxPosPerSession (=1) which permanently blocked the second retest.
|
|
if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue;
|
|
if(!g_Sessions[s].inBreakout) continue;
|
|
|
|
// --- Breakout expiry ---
|
|
if(InpBreakoutExpireBars > 0
|
|
&& g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars)
|
|
{
|
|
PrintFormat("NANDR EA: [%s] Breakout expired after %d bars. Resetting.",
|
|
g_Sessions[s].name, g_Sessions[s].breakoutBarsAgo);
|
|
g_Sessions[s].inBreakout = false;
|
|
g_Sessions[s].breakoutDir = 0;
|
|
g_Sessions[s].inRetest = false;
|
|
continue;
|
|
}
|
|
|
|
int dir = g_Sessions[s].breakoutDir;
|
|
double orbLevel = (dir > 0) ? g_Sessions[s].orbHigh : g_Sessions[s].orbLow;
|
|
|
|
// --- LIMIT mode: place order immediately on breakout bar ---
|
|
if(InpEntryMode == ENTRY_LIMIT)
|
|
{
|
|
// Place once — on the bar breakout is first detected (age 0 or 1 due to update order)
|
|
if(g_Sessions[s].breakoutBarsAgo <= 1)
|
|
{
|
|
double obTop = 0, obBottom = 0;
|
|
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
|
|
if(InpOBRequireConf && obTop == 0) continue;
|
|
OpenTrade(dir, s, orbLevel, obTop, obBottom);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- MARKET mode ---
|
|
if(!InpWaitForRetest)
|
|
{
|
|
// Direct entry on the bar the breakout is confirmed (age 0 or 1)
|
|
if(g_Sessions[s].breakoutBarsAgo <= 1)
|
|
{
|
|
double obTop = 0, obBottom = 0;
|
|
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
|
|
if(InpOBRequireConf && obTop == 0) continue;
|
|
OpenTrade(dir, s, orbLevel, obTop, obBottom);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Retest entries fire tick-based in CheckRetestEntriesTick().
|
|
// Here we only update state: sweep reversals and failed retest cancellation.
|
|
UpdateRetestState(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Manage open positions: breakeven + trailing stop |
|
|
//+------------------------------------------------------------------+
|
|
void ManageOpenTrades()
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!g_Position.SelectByIndex(i)) continue;
|
|
if(g_Position.Magic() != InpMagicNumber) continue;
|
|
if(g_Position.Symbol() != g_Symbol) continue;
|
|
|
|
double entry = g_Position.PriceOpen();
|
|
double sl = g_Position.StopLoss();
|
|
double currentSL= sl;
|
|
double price = g_Position.PriceCurrent();
|
|
ulong ticket = g_Position.Ticket();
|
|
int posDir = (g_Position.PositionType() == POSITION_TYPE_BUY) ? 1 : -1;
|
|
|
|
// Breakeven + Partial Close
|
|
if(InpBreakevenTrigPips > 0)
|
|
{
|
|
double trigDist = PipsToPrice(InpBreakevenTrigPips);
|
|
double beDist = PipsToPrice(InpBreakevenOffPips);
|
|
double beLevel = (posDir > 0) ? entry + beDist : entry - beDist;
|
|
bool triggered = (posDir > 0)
|
|
? (price >= entry + trigDist && sl < beLevel)
|
|
: (price <= entry - trigDist && (sl > beLevel || sl == 0));
|
|
|
|
if(triggered)
|
|
{
|
|
// Step 1: partial close — fire once per position
|
|
if(InpPartialCloseRatio > 0 && !IsPartialClosed(ticket))
|
|
{
|
|
double fullLots = g_Position.Volume();
|
|
double closeLots = NormalizeLot(fullLots * InpPartialCloseRatio);
|
|
double minLot = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MIN);
|
|
if(closeLots >= minLot)
|
|
{
|
|
bool partResult = (posDir > 0)
|
|
? g_Trade.Sell(closeLots, g_Symbol, 0, 0, 0,
|
|
StringFormat("NANDR|PartialClose|%.0fpips", InpBreakevenTrigPips))
|
|
: g_Trade.Buy(closeLots, g_Symbol, 0, 0, 0,
|
|
StringFormat("NANDR|PartialClose|%.0fpips", InpBreakevenTrigPips));
|
|
if(partResult)
|
|
{
|
|
MarkPartialClosed(ticket);
|
|
PrintFormat("NANDR EA: Partial close %.2f lots at %.2f (%.0f pips profit). Ticket=%llu",
|
|
closeLots, price, InpBreakevenTrigPips, ticket);
|
|
}
|
|
else
|
|
PrintFormat("NANDR EA: Partial close failed. Error=%d Ticket=%llu",
|
|
GetLastError(), ticket);
|
|
}
|
|
}
|
|
// Step 2: move SL to breakeven
|
|
currentSL = NormalizeDouble(beLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
}
|
|
}
|
|
|
|
// Trailing stop
|
|
if(InpTrailingStopPips > 0)
|
|
{
|
|
double trailDist = PipsToPrice(InpTrailingStopPips);
|
|
if(posDir > 0)
|
|
{
|
|
double newSL = price - trailDist;
|
|
if(newSL > currentSL) currentSL = NormalizeDouble(newSL, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
}
|
|
else
|
|
{
|
|
double newSL = price + trailDist;
|
|
if(currentSL == 0 || newSL < currentSL)
|
|
currentSL = NormalizeDouble(newSL, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
|
|
}
|
|
}
|
|
|
|
if(currentSL != sl && currentSL > 0)
|
|
g_Trade.PositionModify(ticket, currentSL, g_Position.TakeProfit());
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Daily reset logic |
|
|
//+------------------------------------------------------------------+
|
|
void CheckDailyReset()
|
|
{
|
|
datetime now = TimeCurrent();
|
|
MqlDateTime dt, dtLast;
|
|
TimeToStruct(now, dt);
|
|
TimeToStruct(g_LastDayReset, dtLast);
|
|
|
|
if(dt.day != dtLast.day || dt.mon != dtLast.mon || dt.year != dtLast.year)
|
|
{
|
|
g_LastDayReset = now;
|
|
g_TodayTrades = 0;
|
|
g_TodayWins = 0;
|
|
g_TodayLosses = 0;
|
|
g_TodayPnL = 0;
|
|
g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
g_TradingHalted = false;
|
|
g_GlobalBiasDir = 0; // clear day bias — first session breakout will re-establish it
|
|
g_PartialClosedCount = 0; // clear partial close tracker for the new day
|
|
|
|
// Reset all session ORB data for the new day
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
g_Sessions[s].Reset();
|
|
|
|
// Reset OB traded flags so the same zones can re-trigger on the new day
|
|
for(int i = 0; i < g_BullOBCount; i++) g_BullOBs[i].traded = false;
|
|
for(int i = 0; i < g_BearOBCount; i++) g_BearOBs[i].traded = false;
|
|
|
|
Print("NANDR EA: Daily reset. Equity=", g_DayStartEquity);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check risk limits |
|
|
//+------------------------------------------------------------------+
|
|
void CheckRiskLimits()
|
|
{
|
|
if(g_TradingHalted) return;
|
|
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
bool halt = false;
|
|
|
|
if(InpMaxDailyLossUSD > 0 && (g_DayStartEquity - equity) >= InpMaxDailyLossUSD)
|
|
{
|
|
PrintFormat("NANDR EA: Max daily loss USD reached (%.2f). Trading halted.", InpMaxDailyLossUSD);
|
|
halt = true;
|
|
}
|
|
if(InpMaxDailyLossPct > 0 && g_DayStartEquity > 0)
|
|
{
|
|
double lossPct = (g_DayStartEquity - equity) / g_DayStartEquity * 100.0;
|
|
if(lossPct >= InpMaxDailyLossPct)
|
|
{
|
|
PrintFormat("NANDR EA: Max daily loss %% reached (%.2f%%). Trading halted.", InpMaxDailyLossPct);
|
|
halt = true;
|
|
}
|
|
}
|
|
|
|
if(halt)
|
|
{
|
|
g_TradingHalted = true;
|
|
CancelAllPending();
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update PnL stats from closed trades |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateClosedTrades()
|
|
{
|
|
// Scan deal history for today
|
|
datetime dayStart = iTime(g_Symbol, PERIOD_D1, 0);
|
|
HistorySelect(dayStart, TimeCurrent());
|
|
|
|
int deals = HistoryDealsTotal();
|
|
double newPnL = 0;
|
|
int newWins = 0, newLosses = 0;
|
|
|
|
for(int i = 0; i < deals; i++)
|
|
{
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagicNumber) continue;
|
|
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != g_Symbol) continue;
|
|
if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
|
|
|
|
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
|
|
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
|
|
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
|
|
newPnL += profit;
|
|
if(profit >= 0) newWins++;
|
|
else newLosses++;
|
|
}
|
|
|
|
g_TodayPnL = newPnL;
|
|
g_TodayWins = newWins;
|
|
g_TodayLosses = newLosses;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw ORB lines for a session |
|
|
//+------------------------------------------------------------------+
|
|
void DrawORBLines(int sessIdx)
|
|
{
|
|
if(sessIdx < 0 || sessIdx >= g_SessionCount) return;
|
|
|
|
color lineColor = clrWhite;
|
|
if(g_Sessions[sessIdx].name == "Tokyo") lineColor = clrDodgerBlue;
|
|
else if(g_Sessions[sessIdx].name == "London") lineColor = clrTomato;
|
|
else if(g_Sessions[sessIdx].name == "NY"
|
|
|| g_Sessions[sessIdx].name == "NYORB") lineColor = clrGold;
|
|
else if(g_Sessions[sessIdx].name == "DailyOpen") lineColor = clrSilver;
|
|
|
|
string prefix = "NANDR_ORB_" + g_Sessions[sessIdx].name + "_";
|
|
|
|
// High line
|
|
string hName = prefix + "High";
|
|
ObjectDelete(0, hName);
|
|
if(g_Sessions[sessIdx].orbHigh > 0)
|
|
{
|
|
ObjectCreate(0, hName, OBJ_HLINE, 0, 0, g_Sessions[sessIdx].orbHigh);
|
|
ObjectSetInteger(0, hName, OBJPROP_COLOR, lineColor);
|
|
ObjectSetInteger(0, hName, OBJPROP_STYLE, STYLE_SOLID);
|
|
ObjectSetInteger(0, hName, OBJPROP_WIDTH, 1);
|
|
ObjectSetString(0, hName, OBJPROP_TOOLTIP,
|
|
g_Sessions[sessIdx].name + " ORB High: " +
|
|
DoubleToString(g_Sessions[sessIdx].orbHigh, 2));
|
|
}
|
|
|
|
// Low line
|
|
string lName = prefix + "Low";
|
|
ObjectDelete(0, lName);
|
|
if(g_Sessions[sessIdx].orbLow < DBL_MAX && g_Sessions[sessIdx].orbLow > 0)
|
|
{
|
|
ObjectCreate(0, lName, OBJ_HLINE, 0, 0, g_Sessions[sessIdx].orbLow);
|
|
ObjectSetInteger(0, lName, OBJPROP_COLOR, lineColor);
|
|
ObjectSetInteger(0, lName, OBJPROP_STYLE, STYLE_SOLID);
|
|
ObjectSetInteger(0, lName, OBJPROP_WIDTH, 1);
|
|
ObjectSetString(0, lName, OBJPROP_TOOLTIP,
|
|
g_Sessions[sessIdx].name + " ORB Low: " +
|
|
DoubleToString(g_Sessions[sessIdx].orbLow, 2));
|
|
}
|
|
|
|
// Mid line (dotted)
|
|
if(g_Sessions[sessIdx].orbHigh > 0
|
|
&& g_Sessions[sessIdx].orbLow < DBL_MAX && g_Sessions[sessIdx].orbLow > 0)
|
|
{
|
|
string mName = prefix + "Mid";
|
|
ObjectDelete(0, mName);
|
|
double mid = (g_Sessions[sessIdx].orbHigh + g_Sessions[sessIdx].orbLow) / 2.0;
|
|
ObjectCreate(0, mName, OBJ_HLINE, 0, 0, mid);
|
|
ObjectSetInteger(0, mName, OBJPROP_COLOR, lineColor);
|
|
ObjectSetInteger(0, mName, OBJPROP_STYLE, STYLE_DOT);
|
|
ObjectSetInteger(0, mName, OBJPROP_WIDTH, 1);
|
|
}
|
|
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw OB zone rectangle |
|
|
//+------------------------------------------------------------------+
|
|
void DrawOBZone(int idx, bool isBull)
|
|
{
|
|
SOrderBlock ob;
|
|
if(isBull) ob = g_BullOBs[idx];
|
|
else ob = g_BearOBs[idx];
|
|
if(!ob.active) return;
|
|
|
|
color bgColor = isBull ? (color)ColorToARGB(clrGreen, 40) : (color)ColorToARGB(clrRed, 40);
|
|
color borderColor = isBull ? clrGreen : clrRed;
|
|
|
|
datetime t1 = ob.time;
|
|
datetime t2 = ob.time + PeriodSeconds(PERIOD_CURRENT) * 200; // extend right
|
|
|
|
ObjectDelete(0, ob.objName);
|
|
ObjectCreate(0, ob.objName, OBJ_RECTANGLE, 0, t1, ob.top, t2, ob.bottom);
|
|
ObjectSetInteger(0, ob.objName, OBJPROP_COLOR, borderColor);
|
|
ObjectSetInteger(0, ob.objName, OBJPROP_BGCOLOR, isBull ? clrGreen : clrRed);
|
|
ObjectSetInteger(0, ob.objName, OBJPROP_FILL, true);
|
|
ObjectSetInteger(0, ob.objName, OBJPROP_BACK, true);
|
|
ObjectSetInteger(0, ob.objName, OBJPROP_STYLE, STYLE_SOLID);
|
|
ObjectSetInteger(0, ob.objName, OBJPROP_WIDTH, 1);
|
|
ObjectSetString(0, ob.objName, OBJPROP_TOOLTIP,
|
|
(isBull ? "Bull OB" : "Bear OB") + " [" +
|
|
DoubleToString(ob.bottom, 2) + " - " + DoubleToString(ob.top, 2) + "]");
|
|
|
|
// Mid line
|
|
string midName = ob.objName + "_mid";
|
|
ObjectDelete(0, midName);
|
|
ObjectCreate(0, midName, OBJ_TREND, 0, t1, ob.mid, t2, ob.mid);
|
|
ObjectSetInteger(0, midName, OBJPROP_COLOR, isBull ? clrLimeGreen : clrOrangeRed);
|
|
ObjectSetInteger(0, midName, OBJPROP_STYLE, STYLE_DASH);
|
|
ObjectSetInteger(0, midName, OBJPROP_RAY_RIGHT, true);
|
|
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Remove OB zone objects |
|
|
//+------------------------------------------------------------------+
|
|
void RemoveOBZone(const string name)
|
|
{
|
|
ObjectDelete(0, name);
|
|
ObjectDelete(0, name + "_mid");
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw trade entry label |
|
|
//+------------------------------------------------------------------+
|
|
void DrawTradeLabel(int dir, double entry, double sl, double tp)
|
|
{
|
|
string name = "NANDR_Trade_" + IntegerToString(TimeCurrent());
|
|
datetime t = iTime(g_Symbol, PERIOD_CURRENT, 0);
|
|
|
|
ObjectCreate(0, name, OBJ_ARROW, 0, t, entry);
|
|
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, (dir > 0) ? 233 : 234);
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, (dir > 0) ? clrLime : clrRed);
|
|
ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);
|
|
|
|
// SL line
|
|
string slName = name + "_SL";
|
|
ObjectCreate(0, slName, OBJ_HLINE, 0, t, sl);
|
|
ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed);
|
|
ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DASH);
|
|
|
|
// TP line
|
|
string tpName = name + "_TP";
|
|
ObjectCreate(0, tpName, OBJ_HLINE, 0, t, tp);
|
|
ObjectSetInteger(0, tpName, OBJPROP_COLOR, clrLime);
|
|
ObjectSetInteger(0, tpName, OBJPROP_STYLE, STYLE_DASH);
|
|
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
// Dashboard helper — module-level state set by DrawDashboard before each call
|
|
string g_DBPrefix = "NANDR_DB_";
|
|
int g_DBX = 15;
|
|
int g_DBY = 30;
|
|
int g_DBDY = 18;
|
|
int g_DBLine = 0;
|
|
|
|
void DBLine(string txt, color col, int fontSize = 9)
|
|
{
|
|
string name = g_DBPrefix + IntegerToString(g_DBLine);
|
|
ObjectDelete(0, name);
|
|
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
|
|
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
|
|
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, g_DBX);
|
|
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, g_DBY + g_DBLine * g_DBDY);
|
|
ObjectSetInteger(0, name, OBJPROP_COLOR, col);
|
|
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
|
|
ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
|
|
ObjectSetString(0, name, OBJPROP_TEXT, txt);
|
|
g_DBLine++;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw dashboard |
|
|
//+------------------------------------------------------------------+
|
|
void DrawDashboard()
|
|
{
|
|
if(!InpShowDashboard) return;
|
|
|
|
color title = clrGold;
|
|
color normal = clrSilver;
|
|
color good = clrLimeGreen;
|
|
color bad = clrTomato;
|
|
|
|
g_DBPrefix = "NANDR_DB_";
|
|
g_DBX = 15;
|
|
g_DBY = 30;
|
|
g_DBDY = 18;
|
|
g_DBLine = 0;
|
|
|
|
DBLine("== NANDR ORB+OB EA ==", title, 10);
|
|
DBLine("-------------------------", normal);
|
|
DBLine(StringFormat("Symbol : %s TF: %s", g_Symbol, EnumToString(Period())), normal);
|
|
DBLine(StringFormat("OrbTF : M%d BarsNeeded: %d", (int)InpOrbTimeframe, g_OrbBarsNeeded), normal);
|
|
DBLine("-------------------------", normal);
|
|
|
|
// Sessions
|
|
for(int s = 0; s < g_SessionCount; s++)
|
|
{
|
|
string status;
|
|
if(g_Sessions[s].orbComplete)
|
|
status = "ARMED";
|
|
else if(g_Sessions[s].orbBarCount > 0)
|
|
status = "ACCUM";
|
|
else
|
|
status = "WAIT ";
|
|
if(g_Sessions[s].inBreakout)
|
|
status = (g_Sessions[s].breakoutDir > 0 ? "BULL+" : "BEAR-");
|
|
color sc = g_Sessions[s].orbComplete ? good : normal;
|
|
if(g_TradingHalted) sc = bad;
|
|
|
|
DBLine(StringFormat("%-8s H:%.2f L:%.2f [%s]",
|
|
g_Sessions[s].name,
|
|
g_Sessions[s].orbHigh > 0 ? g_Sessions[s].orbHigh : 0,
|
|
g_Sessions[s].orbLow < DBL_MAX ? g_Sessions[s].orbLow : 0,
|
|
status), sc);
|
|
}
|
|
|
|
DBLine("-------------------------", normal);
|
|
DBLine(StringFormat("BullOBs: %d BearOBs: %d", g_BullOBCount, g_BearOBCount), normal);
|
|
DBLine("-------------------------", normal);
|
|
|
|
// Daily stats
|
|
double wr = (g_TodayWins + g_TodayLosses > 0)
|
|
? (double)g_TodayWins / (g_TodayWins + g_TodayLosses) * 100.0 : 0;
|
|
DBLine(StringFormat("Trades : %d / %d", g_TodayTrades, InpMaxTradesPerDay), normal);
|
|
DBLine(StringFormat("W/L : %d / %d WR: %.0f%%", g_TodayWins, g_TodayLosses, wr),
|
|
g_TodayWins >= g_TodayLosses ? good : normal);
|
|
DBLine(StringFormat("Today PnL: %+.2f USD", g_TodayPnL),
|
|
g_TodayPnL >= 0 ? good : bad);
|
|
DBLine(StringFormat("Equity : %.2f", AccountInfoDouble(ACCOUNT_EQUITY)), normal);
|
|
|
|
if(g_TradingHalted)
|
|
DBLine("! TRADING HALTED - Daily limit", bad, 10);
|
|
else if(g_SessionCount == 0)
|
|
DBLine("! No sessions enabled", bad);
|
|
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Remove all EA objects from chart |
|
|
//+------------------------------------------------------------------+
|
|
void RemoveAllObjects()
|
|
{
|
|
int total = ObjectsTotal(0, 0, -1);
|
|
for(int i = total - 1; i >= 0; i--)
|
|
{
|
|
string name = ObjectName(0, i, 0, -1);
|
|
if(StringFind(name, "NANDR_") >= 0)
|
|
ObjectDelete(0, name);
|
|
}
|
|
ChartRedraw(0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnInit |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
// Resolve symbol
|
|
g_Symbol = ResolveSymbol(InpSymbolName);
|
|
|
|
// Validate timeframe
|
|
ENUM_TIMEFRAMES chartTF = Period();
|
|
int chartMins = (int)(PeriodSeconds(chartTF) / 60);
|
|
if(chartMins != (int)InpOrbTimeframe)
|
|
{
|
|
PrintFormat("NANDR EA: WARNING — Chart TF is M%d but ORB Timeframe input is M%d. "
|
|
"Attach EA to M%d chart for best results.",
|
|
chartMins, (int)InpOrbTimeframe, (int)InpOrbTimeframe);
|
|
}
|
|
|
|
// Bars needed for 15min ORB at current TF
|
|
switch(InpOrbTimeframe)
|
|
{
|
|
case ORB_TF_M1: g_OrbBarsNeeded = 15; break;
|
|
case ORB_TF_M5: g_OrbBarsNeeded = 3; break;
|
|
case ORB_TF_M15: g_OrbBarsNeeded = 1; break;
|
|
default: g_OrbBarsNeeded = 1; break;
|
|
}
|
|
|
|
// Breakout confirmation bars
|
|
if(InpBreakoutConfBars > 0)
|
|
g_BreakoutConfBars = InpBreakoutConfBars;
|
|
else
|
|
{
|
|
switch(InpOrbTimeframe)
|
|
{
|
|
case ORB_TF_M1: g_BreakoutConfBars = 5; break;
|
|
case ORB_TF_M5: g_BreakoutConfBars = 3; break;
|
|
case ORB_TF_M15: g_BreakoutConfBars = 2; break;
|
|
default: g_BreakoutConfBars = 2; break;
|
|
}
|
|
}
|
|
|
|
// OB pivot length
|
|
if(InpOBPivotLength > 0)
|
|
g_OBPivotLength = InpOBPivotLength;
|
|
else
|
|
{
|
|
switch(InpOrbTimeframe)
|
|
{
|
|
case ORB_TF_M1: g_OBPivotLength = 10; break;
|
|
default: g_OBPivotLength = 5; break;
|
|
}
|
|
}
|
|
|
|
// Pip size: 1 pip = 10 points for all standard decimal counts (2, 3, 5).
|
|
// digits==2 (XAUUSD): point=0.01 → pip=0.10 → 100 pips = $10 price distance
|
|
// digits==3 (USDJPY): point=0.001 → pip=0.01
|
|
// digits==5 (EURUSD): point=0.00001 → pip=0.0001
|
|
// BUG FIX: digits==2 previously used g_PointSize directly (0.01) making 100 pips = $1.00,
|
|
// causing a near-zero SL and massively oversized lot on XAUUSD.
|
|
double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
int digits = (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS);
|
|
g_PointSize = SymbolInfoDouble(g_Symbol, SYMBOL_POINT);
|
|
g_PipSize = (digits == 2 || digits == 3 || digits == 5) ? g_PointSize * 10 : g_PointSize;
|
|
PrintFormat("NANDR EA: PipSize=%.5f (digits=%d). 100 pips = %.2f price units.",
|
|
g_PipSize, digits, PipsToPrice(100));
|
|
|
|
// Initialize OB arrays
|
|
ArrayResize(g_BullOBs, InpOBMaxCount);
|
|
ArrayResize(g_BearOBs, InpOBMaxCount);
|
|
g_BullOBCount = 0;
|
|
g_BearOBCount = 0;
|
|
|
|
// ATR indicator
|
|
g_ATRHandle = iATR(g_Symbol, PERIOD_CURRENT, InpATRPeriod);
|
|
if(g_ATRHandle == INVALID_HANDLE)
|
|
Print("NANDR EA: WARNING — could not create ATR indicator handle.");
|
|
|
|
// Trade object
|
|
g_Trade.SetExpertMagicNumber(InpMagicNumber);
|
|
g_Trade.SetDeviationInPoints(20);
|
|
g_Trade.SetTypeFilling(ORDER_FILLING_IOC);
|
|
|
|
// Init sessions
|
|
InitSessions();
|
|
if(g_SessionCount == 0)
|
|
Print("NANDR EA: WARNING — No sessions enabled. EA will not trade. Enable at least one session.");
|
|
|
|
// Daily init
|
|
g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
g_LastDayReset = TimeCurrent();
|
|
|
|
PrintFormat("NANDR EA: Initialized. Symbol=%s OrbBarsNeeded=%d BreakoutConf=%d OBPivot=%d Sessions=%d",
|
|
g_Symbol, g_OrbBarsNeeded, g_BreakoutConfBars, g_OBPivotLength, g_SessionCount);
|
|
|
|
DrawDashboard();
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnDeinit |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
RemoveAllObjects();
|
|
if(g_ATRHandle != INVALID_HANDLE)
|
|
IndicatorRelease(g_ATRHandle);
|
|
Comment("");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Returns true if current server time is Saturday or Sunday |
|
|
//+------------------------------------------------------------------+
|
|
bool IsWeekend()
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(TimeCurrent(), dt);
|
|
return (dt.day_of_week == 0 || dt.day_of_week == 6); // 0=Sunday, 6=Saturday
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnTick |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
// Silence all processing during weekend market closure
|
|
if(IsWeekend()) return;
|
|
|
|
// New bar detection
|
|
datetime barTime = iTime(g_Symbol, PERIOD_CURRENT, 0);
|
|
g_IsNewBar = (barTime != g_LastBarTime);
|
|
if(g_IsNewBar)
|
|
g_LastBarTime = barTime;
|
|
|
|
// Per-tick management
|
|
ManageOpenTrades();
|
|
CheckRiskLimits();
|
|
|
|
// Retest entries fire on tick (at wick touch), not on bar close
|
|
CheckRetestEntriesTick();
|
|
|
|
if(!g_IsNewBar) return;
|
|
|
|
// Daily reset
|
|
CheckDailyReset();
|
|
|
|
if(g_TradingHalted)
|
|
{
|
|
DrawDashboard();
|
|
return;
|
|
}
|
|
|
|
// ORB accumulation
|
|
UpdateORBSessions();
|
|
|
|
// Order Block updates
|
|
ScanOrderBlocks();
|
|
MitigateOrderBlocks();
|
|
|
|
// Breakout detection
|
|
DetectBreakouts();
|
|
|
|
// Entry signals (direct breakout / limit orders on new bar)
|
|
CheckEntrySignals();
|
|
|
|
// Update closed trade stats & dashboard
|
|
UpdateClosedTrades();
|
|
DrawDashboard();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| OnTradeTransaction — handle position close events |
|
|
//+------------------------------------------------------------------+
|
|
void OnTradeTransaction(const MqlTradeTransaction &trans,
|
|
const MqlTradeRequest &request,
|
|
const MqlTradeResult &result)
|
|
{
|
|
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
|
|
{
|
|
ulong ticket = trans.deal;
|
|
if(HistoryDealSelect(ticket))
|
|
{
|
|
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) == InpMagicNumber &&
|
|
HistoryDealGetString(ticket, DEAL_SYMBOL) == g_Symbol &&
|
|
HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT)
|
|
{
|
|
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
|
|
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
|
|
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
|
|
g_TotalPnL += profit;
|
|
PrintFormat("NANDR EA: Trade closed. Profit=%.2f TotalPnL=%.2f", profit, g_TotalPnL);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|