Files
2026-06-08 10:36:15 +02:00

1204 lines
50 KiB
Plaintext

//+------------------------------------------------------------------+
//| HedgeGuard_EA_v5_2.mq4 |
//| Smart Hedge Bot for XAUUSD Grid/Averaging EA |
//| |
//| HYBRID TRIGGERS: |
//| 1. Drawdown+Grid Count: DD >= X% AND >= N open trades |
//| 2. Emergency Momentum: Spike >= P pts -- fires when grid has |
//| >= 3 positions AND drawdown >= 50% of trigger threshold |
//| 3. ATR Expansion: Strong trend confirmed (same guards as T2) |
//| (Note: internally numbered as Trigger 3 — S3 group) |
//| SAFETY: Will NOT hedge during blocked news/session hours |
//| Will NOT place any order when market is closed |
//| ORPHAN: Prioritises closing highest-lot orphan positions first |
//| RECOVERY: Adopts ALL hedge positions from before EA restart |
//| |
//| v5.2 ORPHAN FIXES: |
//| Bug 2 — Orphan block now runs while grid is active when hedges |
//| are detached (hedgeIsOpen=false); previously the |
//| tradeCount==0 gate blocked it entirely. |
//| Bug 3 — Funding close (Phase A) promoted to a dedicated pre- |
//| pass so it always targets bestProfitOrphanTicket, |
//| not a random position found by the reverse main loop. |
//| Bug 4 — Bank-pool assassination no longer requires tradeCount |
//| ==0; detached orphans can be assassinated while the |
//| grid is still active. |
//| Bug 5 — realizedHedgePL anchor uses g_OrphanCycleStartTime |
//| when hedges are detached + grid is active, preventing |
//| oldestGridTime drift from understating the pool. |
//| g_OrphanCycleStartTime is no longer reset while any |
//| hedge positions exist regardless of grid state. |
//| Bug 6 — CheckPartialOverlapClose skips same-direction hedge |
//| positions in detached mode; prevents false pairing of |
//| two losing same-side positions as an "overlap". |
//| |
//| v5.3 CRITICAL FIXES (2026.04.23): |
//| Bug 7 — OVERLAP CLOSE: Now REFUSES to close any position with |
//| a LOSS unless OnlyCloseInProfit=false. Previously the |
//| code closed grid trades at losses if hedge was profit. |
//| FIX: CheckPartialOverlapClose() now validates: |
//| - BOTH hedge & grid losses must be acceptable (loss OK |
//| only if hedge profit >= losing position's abs loss) |
//| - OR both must be in profit (strict, no net calc) |
//| - MinHedgeProfit applies to NET only when both profit |
//| Bug 8 — OnlyCloseInProfit enforcement: Now checked at EVERY |
//| close attempt in CheckPartialOverlapClose before |
//| proceeding (early return if position in loss). |
//| Bug 9 — Individual position close: Scaled profit check via |
//| BaseLotForProfitTarget to avoid closing micro-lots at |
//| tiny losses masked by profit requirements. |
//+------------------------------------------------------------------+
#property copyright "HedgeGuard EA v5.3"
#property version "5.30"
#property strict
//+------------------------------------------------------------------+
//| INPUT PARAMETERS |
//+------------------------------------------------------------------+
input string S0 = "=== Core Identity ===";
input string TradeSymbol = ""; // Leave BLANK to auto-use chart symbol (safe with any broker suffix/case)
input int HedgeMagicNumber = 88888;
input int GridMagicNumber = 0; // 0 = watch all non-hedge trades
input string TradeComment = "HedgeGuard";
input int Slippage = 30;
// Runtime resolved symbol — handles case differences & broker suffixes (XAUUSDm, xauusdm, XAUUSD., etc.)
string g_Symbol = ""; // UPPERCASE — used only for comparisons
string g_SymbolRaw = ""; // Original case — used for OrderSend, MarketInfo
input string S1 = "=== Trigger 1: Drawdown + Grid Count (Both Required) ===";
input bool UsePrimaryTrigger = true;
input double HedgeTriggerPct = 0.1; // Fire if floating loss >= this % of balance
input int MinGridTrades = 3; // AND grid has >= this many open trades
input string S2 = "=== Trigger 2: Emergency Momentum (Independent) ===";
input bool UseEmergencyMomentum = true;
input int MomentumBars = 3; // Bars to measure momentum over
input double MomentumPipsThresh = 150.0; // Fire immediately if price spikes > this many points against grid
input double MomentumReentryStep = 50.0; // Min points distance for next momentum scalp on the same spike
input int MomentumMinGridTrades = 1; // Minimum grid positions required before T2 can fire
input string S3 = "=== Trigger 3: ATR Expansion (Strong Trend) ===";
input bool UseATRTrigger = true;
input int ATRPeriod = 14; // ATR period
input double ATRMultiplier = 1.5; // Fire if current ATR > X * average ATR
input int ATRAvgPeriod = 50; // Bars to average ATR over
input int ATRMinGridTrades = 1; // Minimum grid positions required before T3 can fire
input double ATRTriggerPct = 0.1; // T3: min drawdown % required before ATR expansion can fire (independent of T1)
input string S5 = "=== News / Session Block (Safety Filter) — Disabled by default: apply blocking to the grid EA, not the hedge EA ===";
input bool UseSessionFilter = false;
input int BlockStartHour = 12; // Block hedge from this hour (server time)
input int BlockEndHour = 14; // Block hedge until this hour (covers NY open/news)
input bool BlockFriday = false; // Block hedging on Friday (illiquid close)
input string S6 = "=== Hedge Lot & Exit ===";
input double HedgeLotMultiplier = 2.0; // 2.0 = double hedge of net grid lots (for faster recovery)
input double MaxHedgeLot = 0.1;
input double MinHedgeLot = 0.01;
input double HedgeExitPct = 2.0; // Close hedge when drawdown recovers to this %
input bool OnlyCloseInProfit = true; // *** CRITICAL: Only close hedge if its net PNL is >= 0 ***
input bool ForceCloseOnFlip = false; // True=Close immediately, False=Detach to allow new hedge
input double MinHedgeProfit = 3.0; // Minimum net profit ($) for overlap or global closes
input bool EnableOverlapClose = true; // Use profitable trades to close losing trades on the other side
input bool EnableIndividualClose = false; // Allow closing individual active hedges early (steals overlap potential)
input bool EnableOrphanIndividualClose = true; // Allow individual close for orphan/detached hedges in profit
input double MinIndividualProfit = 2.0; // Minimum profit ($) required to close a hedge individually
input double BaseLotForProfitTarget = 0.01; // Base lot size used for scaling MinIndividualProfit
input string S7 = "=== Orphan Hedge Recovery ===";
input bool EnableOrphanRecovery = true; // Enable recovery grid for orphaned hedges
input double OrphanRecoveryStep = 2.0; // Distance in ATRs before opening recovery trade
input double OrphanRecoveryMult = 1.5; // Lot multiplier for recovery trades
input int MaxOrphanRecovery = 2; // Max recovery trades to add
input double OrphanProfitTarget = 3.0; // Target profit ($) for orphan group closure
input bool EnableMomentumFlip = true; // Open opposite hedge on momentum spike to recover orphan
input double MomentumFlipMult = 2.0; // Lot multiplier for counter-hedge (double recommended)
input string S8 = "=== Alerts & Control ===";
input bool EnableAlerts = false;
input bool EnablePushNotify = false; // Mobile push notifications
input int ManualBlockSeconds = 60; // Block auto-logic after manual close (buttons)
input string S9 = "=== Dashboard Position ===";
input bool DashboardRightAligned = true; // true = dashboard on RIGHT side of chart (recommended)
input int DashboardXOffset = 20; // Extra pixels to shift dashboard contents RIGHT (increase to move further right)
//+------------------------------------------------------------------+
//| GLOBALS |
//+------------------------------------------------------------------+
bool hedgeIsOpen = false;
int hedgeTicket = -1;
int atrHandle = -1;
// Tracks which trigger opened the current hedge — shown on dashboard while hedge is active
string g_LastTriggerSource = "";
// Latch to prevent instant close if entered at low DD
bool g_DDWasHigh = false;
datetime g_OrphanCycleStartTime = 0;
// Persistent banked-pool for orphan assassination.
// Accumulates the REALISED P&L of each position closed via bank-pool assassination.
// Unlike realizedHedgePL (recomputed each tick from history with a time-anchor that
// can drift or reset when a grid trade re-opens), this counter is never wiped by
// grid restarts, timestamp drift, or orphan-cycle transitions.
// Reset only when ALL hedges are fully cleared (CloseAllOnSymbol / CloseHedge success).
double g_BankedOrphanPool = 0.0;
// Direction-flip confirmation
int g_FlipConfirmTicks = 0;
#define FLIP_CONFIRM_REQUIRED 10
// Recovery close guard
int g_TradesSeen = 0;
#define RECOVERY_MIN_TRADES_SEEN 30
// Dashboard
#define DB_PREFIX "HG_"
#define DB_X 15
#define DB_Y 60
#define DB_W 320
#define DB_ROW_H 24
#define DB_TITLE_H 30
#define DB_FONT "Arial"
#define DB_FS 9
// Palette (unchanged)
#define C_BG C'10,10,10'
#define C_CELL C'25,25,25'
#define C_BRD C'50,50,50'
#define C_ACCENT C'0,102,204'
#define C_TITLE C'255,255,255'
#define C_LBL C'160,160,160'
#define C_VAL C'255,255,255'
#define C_GRN C'46,204,113'
#define C_RED C'231,76,60'
#define C_ORG C'230,126,34'
#define C_YLW C'241,196,15'
int g_DBCorner = CORNER_LEFT_LOWER;
//+------------------------------------------------------------------+
//| Dashboard positioning helpers |
//+------------------------------------------------------------------+
int GetPanelLeftX()
{
return DashboardRightAligned ? (DB_W + 15 + DashboardXOffset) : DB_X;
}
//+------------------------------------------------------------------+
//| Rectangle helper |
//+------------------------------------------------------------------+
void _Rect(string n, int x, int y, int w, int h, color bg, color brd, int brdW=1)
{
if(ObjectFind(0, n) < 0) ObjectCreate(0, n, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, n, OBJPROP_CORNER, g_DBCorner);
ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, n, OBJPROP_XSIZE, w);
ObjectSetInteger(0, n, OBJPROP_YSIZE, h);
ObjectSetInteger(0, n, OBJPROP_BGCOLOR, bg);
ObjectSetInteger(0, n, OBJPROP_COLOR, brd);
ObjectSetInteger(0, n, OBJPROP_WIDTH, brdW);
ObjectSetInteger(0, n, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, n, OBJPROP_BACK, false);
ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, n, OBJPROP_ZORDER, 10);
}
//+------------------------------------------------------------------+
//| Label helper |
//+------------------------------------------------------------------+
void _Lbl(string n, int x, int y, string txt, color clr,
int fs=DB_FS, string fnt=DB_FONT, int anchor=ANCHOR_LEFT)
{
if(ObjectFind(0, n) < 0)
{
ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, n, OBJPROP_BACK, false);
ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, n, OBJPROP_ZORDER, 20);
}
// Always update corner — ensures stale objects from a prior session/corner
// setting are immediately corrected rather than retaining the old corner.
ObjectSetInteger(0, n, OBJPROP_CORNER, g_DBCorner);
ObjectSetInteger(0, n, OBJPROP_ANCHOR, anchor);
ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, n, OBJPROP_COLOR, clr);
ObjectSetInteger(0, n, OBJPROP_FONTSIZE, fs);
ObjectSetString (0, n, OBJPROP_FONT, fnt);
ObjectSetString (0, n, OBJPROP_TEXT, txt);
}
//+------------------------------------------------------------------+
//| Button helper |
//+------------------------------------------------------------------+
void _Btn(string n, int x, int y, int w, int h, string txt, color clr, color bg, int fs=9)
{
if(ObjectFind(0, n) < 0)
{
ObjectCreate(0, n, OBJ_BUTTON, 0, 0, 0);
ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, n, OBJPROP_ZORDER, 30);
}
ObjectSetInteger(0, n, OBJPROP_CORNER, g_DBCorner);
ObjectSetInteger(0, n, OBJPROP_XDISTANCE, x);
ObjectSetInteger(0, n, OBJPROP_YDISTANCE, y);
ObjectSetInteger(0, n, OBJPROP_XSIZE, w);
ObjectSetInteger(0, n, OBJPROP_YSIZE, h);
ObjectSetInteger(0, n, OBJPROP_COLOR, clr);
ObjectSetInteger(0, n, OBJPROP_BGCOLOR, bg);
ObjectSetInteger(0, n, OBJPROP_FONTSIZE, fs);
ObjectSetString (0, n, OBJPROP_TEXT, txt);
ObjectSetInteger(0, n, OBJPROP_STATE, false);
}
//+------------------------------------------------------------------+
//| Row helper — fixed for right alignment |
//+------------------------------------------------------------------+
void _Row(string id, int y, int h, string lbl, string val, color vClr, color bClr=C_CELL)
{
int panelLeft = GetPanelLeftX();
_Rect(DB_PREFIX+id+"_bg", panelLeft, y, DB_W, h, bClr, C_BRD, 1);
int midY = y - h + 7;
int labelX, valueX;
if (DashboardRightAligned)
{
// CORNER_RIGHT_LOWER: X = distance from right edge. panelLeft=355 is the visual left edge.
// To place text inside the panel, subtract from panelLeft.
labelX = panelLeft - 10; // near visual left of panel
valueX = panelLeft - 170; // ~halfway across panel toward the right
}
else
{
labelX = panelLeft + 10;
valueX = panelLeft + 170;
}
_Lbl(DB_PREFIX+id+"_l", labelX, midY, lbl, C_LBL, 9, "Arial");
_Lbl(DB_PREFIX+id+"_v", valueX, midY, val, vClr, 9, "Courier New");
}
//+------------------------------------------------------------------+
//| Clean dashboard |
//+------------------------------------------------------------------+
void DBClean()
{
ObjectsDeleteAll(0, DB_PREFIX);
ObjectsDeleteAll(0, "HG_DIA_");
ObjectsDeleteAll(0, "HG_SIG_");
}
//+------------------------------------------------------------------+
//| Profit probability (heuristic) |
//+------------------------------------------------------------------+
int GetProfitProbability(double hedgeLots)
{
if (hedgeLots <= 0) return 0;
double entryPrice = 0;
int hType = -1;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
entryPrice = OrderOpenPrice();
hType = OrderType();
break;
}
if (entryPrice <= 0) return 0;
double curPrice = (hType == OP_BUY) ? MarketInfo(g_SymbolRaw, MODE_BID) : MarketInfo(g_SymbolRaw, MODE_ASK);
double pipsAway = MathAbs(curPrice - entryPrice) / MarketInfo(g_SymbolRaw, MODE_POINT) / 10.0;
double atr = iATR(g_SymbolRaw, PERIOD_M5, 14, 0) / MarketInfo(g_SymbolRaw, MODE_POINT) / 10.0;
double rsi = iRSI(g_SymbolRaw, PERIOD_M5, 14, PRICE_CLOSE, 0);
int prob = 50;
if (pipsAway < atr) prob += 20;
else if (pipsAway > atr * 3) prob -= 30;
if (hType == OP_SELL)
{
if (rsi > 70) prob += 20;
if (rsi < 30) prob -= 20;
}
else
{
if (rsi < 30) prob += 20;
if (rsi > 70) prob -= 20;
}
return (int)MathMax(5, MathMin(95, prob));
}
//+------------------------------------------------------------------+
//| Draw dashboard — fully fixed right alignment |
//+------------------------------------------------------------------+
void DrawDashboard(
double balance, double gridPL, double hedgePL, double realizedPL, double drawdownPct,
string gridDirLabel, double netLots, int tradeCount,
bool at1, bool at2, bool at3, bool blk,
string hedgeStatus, bool hedgeActive, double coveragePct,
int prob, string triggerSource,
double orphanPool = 0.0, double orphanFloat = 0.0)
{
int rowsCount = 17;
int panelH = DB_TITLE_H + (rowsCount * DB_ROW_H) + (DB_ROW_H * 2) + 40;
int panelTop = DB_Y + panelH;
int panelLeft = GetPanelLeftX();
_Rect(DB_PREFIX+"PANEL", panelLeft, panelTop, DB_W, panelH, C_BG, C_BRD, 1);
_Rect(DB_PREFIX+"TITLE_BG", panelLeft, panelTop, DB_W, DB_TITLE_H, C_ACCENT, C_ACCENT, 0);
color dotClr = hedgeActive ? C_YLW : (blk ? C_ORG : C_GRN);
int titleTxtX = DashboardRightAligned ? (panelLeft - 22) : (panelLeft + 22);
// Dot sits just to the LEFT of the title text, inside the panel left edge.
// Right-corner mode: X is distance from right edge; panelLeft is the visual left edge
// of the panel. To place the dot left-of-text we use a slightly larger X (further from
// right edge = further left visually). Text starts at panelLeft-22, dot sits at panelLeft-8
// (which is 14px to the LEFT of the text anchor in screen space).
int titleDotX = DashboardRightAligned ? (panelLeft - 8) : (panelLeft + 8);
_Lbl(DB_PREFIX+"TITLE_DOT", titleDotX, panelTop - DB_TITLE_H + 9, "O", dotClr, 11, "Arial Bold");
_Lbl(DB_PREFIX+"TITLE_TXT", titleTxtX, panelTop - DB_TITLE_H + 9, "HedgeGuard EA v5.3", C_TITLE, 10, "Arial Bold");
int curY = panelTop - DB_TITLE_H - 1;
double openPL = gridPL + hedgePL;
double totalNetPL = openPL + realizedPL;
_Row("r1", curY, DB_ROW_H, "Balance", StringFormat("$ %.2f", balance), C_VAL); curY -= DB_ROW_H;
_Row("r2", curY, DB_ROW_H, "Grid P/L", StringFormat("$ %.2f", gridPL), (gridPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H;
if (hedgeActive || hedgePL != 0)
{
_Row("r2a", curY, DB_ROW_H, "Hedge P/L", StringFormat("$ %.2f", hedgePL), (hedgePL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H;
}
else
{
_Row("r2a", curY, DB_ROW_H, "Hedge P/L", "$ 0.00", C_VAL); curY -= DB_ROW_H;
}
_Row("r2b", curY, DB_ROW_H, "Open P/L", StringFormat("$ %.2f", openPL), (openPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H;
_Row("r2c", curY, DB_ROW_H, "Banked P/L", StringFormat("$ %.2f", realizedPL), (realizedPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H;
// Orphan Pool row — only shown when grid is flat and orphan hedges are open.
// Shows the combined assassination pool: closed profits (g_BankedOrphanPool)
// + floating profits of profitable open orphans. This is the actual ammunition
// the EA uses to decide whether it can absorb a losing orphan's loss.
// Banked P/L (realizedHedgePL from MT4 history) cannot show this because it
// uses a time-anchored history scan that misses the floating component entirely.
if (tradeCount == 0 && hedgeActive)
{
double combinedOrphanPool = orphanPool + orphanFloat;
color poolClr = (combinedOrphanPool >= OrphanProfitTarget) ? C_GRN : C_YLW;
// Line 1: label + combined total
_Row("r2e", curY, DB_ROW_H, "Orphan Pool",
StringFormat("$ %.2f", combinedOrphanPool),
poolClr);
curY -= DB_ROW_H;
// Line 2: B/F breakdown (no label, indented value)
_Row("r2e2", curY, DB_ROW_H, "",
StringFormat("B:%.2f F:%.2f", orphanPool, orphanFloat),
poolClr);
curY -= DB_ROW_H;
}
else
{
// Clear both rows when not in orphan mode so they don't ghost
_Row("r2e", curY, DB_ROW_H, "", "", C_BG); curY -= DB_ROW_H;
_Row("r2e2", curY, DB_ROW_H, "", "", C_BG); curY -= DB_ROW_H;
}
_Row("r2d", curY, DB_ROW_H, "Cycle Net P/L", StringFormat("$ %.2f", totalNetPL), (totalNetPL >= 0 ? C_GRN : C_RED)); curY -= DB_ROW_H;
_Row("r3", curY, DB_ROW_H, "Drawdown %", StringFormat("%.2f %%", drawdownPct), (drawdownPct >= HedgeTriggerPct ? C_RED : C_VAL)); curY -= DB_ROW_H;
_Row("r3b", curY, DB_ROW_H, "Exit Target", StringFormat("%.2f %% DD", HedgeExitPct), C_YLW); curY -= DB_ROW_H;
_Row("r4", curY, DB_ROW_H, "Grid Dir", StringFormat("%s (%d trades)", gridDirLabel, tradeCount), C_VAL); curY -= DB_ROW_H;
_Row("r5", curY, DB_ROW_H, "Exposure", StringFormat("%.2f Lots", netLots), C_VAL); curY -= DB_ROW_H;
_Row("r6", curY, DB_ROW_H, "Hedge Coverage", StringFormat("%.1f %%", coveragePct), (coveragePct >= 100 ? C_GRN : C_RED)); curY -= DB_ROW_H;
if (hedgeActive)
{
color pClr = (prob > 70 ? C_GRN : (prob > 40 ? C_YLW : C_RED));
_Row("r7", curY, DB_ROW_H, "Profit Prob.", StringFormat("%d %%", prob), pClr); curY -= DB_ROW_H;
}
int centerX = DashboardRightAligned ? (panelLeft - DB_W / 2) : (panelLeft + DB_W / 2);
_Rect(DB_PREFIX+"trig_hdr", panelLeft, curY, DB_W, DB_ROW_H, C'40,40,40', C_BRD, 1);
_Lbl(DB_PREFIX+"trig_lbl", centerX, curY - DB_ROW_H + 8, "SMART TRIGGERS", C_ACCENT, 8, "Arial Bold", ANCHOR_CENTER);
curY -= DB_ROW_H;
// Trigger statuses
// FIRED = trigger condition fully met right now (hedge should open)
// ACTIVE = this trigger was the one that opened the current hedge
// WAIT<N = blocked by minimum trade count guard (shows required count)
// WAIT DD = blocked by drawdown threshold guard
// OK = monitoring normally, no block
// T1: blocked by MinGridTrades count or DD?
string t1Status; color t1Clr;
if (at1) { t1Status = "FIRED"; t1Clr = C_RED; }
else if (UsePrimaryTrigger && tradeCount > 0 && tradeCount < MinGridTrades)
{ t1Status = StringFormat("WAIT<%d", MinGridTrades); t1Clr = C_ORG; }
else { t1Status = "OK"; t1Clr = C_GRN; }
// T2: trade count guard fires first (when MomentumMinGridTrades > 0), then DD guard
string t2Status; color t2Clr;
if (at2) { t2Status = "FIRED"; t2Clr = C_RED; }
else if (UseEmergencyMomentum && MomentumMinGridTrades > 0 && tradeCount < MomentumMinGridTrades)
{ t2Status = StringFormat("WAIT<%d", MomentumMinGridTrades); t2Clr = C_ORG; }
else if (UseEmergencyMomentum && MomentumMinGridTrades > 0 && drawdownPct < HedgeTriggerPct)
{ t2Status = "WAIT DD"; t2Clr = C_ORG; }
else { t2Status = "OK"; t2Clr = C_GRN; }
// T3: trade count guard fires first (when ATRMinGridTrades > 0), then DD guard
string t3Status; color t3Clr;
if (at3) { t3Status = "FIRED"; t3Clr = C_RED; }
else if (UseATRTrigger && ATRMinGridTrades > 0 && tradeCount < ATRMinGridTrades)
{ t3Status = StringFormat("WAIT<%d", ATRMinGridTrades); t3Clr = C_ORG; }
else if (UseATRTrigger && ATRMinGridTrades > 0 && drawdownPct < ATRTriggerPct)
{ t3Status = "WAIT DD"; t3Clr = C_ORG; }
else { t3Status = "OK"; t3Clr = C_GRN; }
if (hedgeActive && triggerSource != "")
{
if (StringFind(triggerSource, "T1:") >= 0) { t1Status = "ACTIVE"; t1Clr = C_ORG; }
else if (StringFind(triggerSource, "T2 EMERGENCY") >= 0) { t2Status = "ACTIVE"; t2Clr = C_ORG; }
else if (StringFind(triggerSource, "ATR expansion") >= 0) { t3Status = "ACTIVE"; t3Clr = C_ORG; }
}
_Row("t1", curY, DB_ROW_H, "T1: DD+Count", t1Status, t1Clr); curY -= DB_ROW_H;
_Row("t2", curY, DB_ROW_H, "T2: Emergency Mom", t2Status, t2Clr); curY -= DB_ROW_H;
_Row("t3", curY, DB_ROW_H, "T3: ATR Expansion", t3Status, t3Clr); curY -= DB_ROW_H;
_Row("t5", curY, DB_ROW_H, "Session Block", blk ? "YES" : "NO", blk ? C_ORG : C_GRN);
curY -= (DB_ROW_H + 5);
string statusTxt = hedgeActive ? (coveragePct < 90 ? "UNDER-HEDGED" : "HEDGING") : (blk ? "SYSTEM BLOCKED" : "MONITORING...");
color statusColor = hedgeActive ? (coveragePct < 90 ? C_RED : C_YLW) : (blk ? C_ORG : C_LBL);
// In CORNER_RIGHT_LOWER, X is distance from the RIGHT edge of the chart.
// panelLeft is already the correct anchor. Use it directly for all footer elements.
int footX = panelLeft;
_Rect(DB_PREFIX+"FOOT_BG", footX, curY, DB_W-10, DB_ROW_H+4, C_CELL, C_ACCENT, 1);
_Lbl(DB_PREFIX+"FOOT_TXT", centerX, curY - (DB_ROW_H+4) + 9, statusTxt, statusColor, 9, "Arial Bold", ANCHOR_CENTER);
curY -= (DB_ROW_H + 8);
int btnW = (DB_W - 15) / 2;
int btn1X = panelLeft;
int btn2X = DashboardRightAligned ? (btn1X - btnW - 5) : (panelLeft + btnW + 10);
_Btn(DB_PREFIX+"CLOSE_HEDGE", btn1X, curY, btnW, DB_ROW_H+6, "CLOSE HEDGE", C_TITLE, C_ORG, 8);
_Btn(DB_PREFIX+"CLOSE_ALL", btn2X, curY, btnW, DB_ROW_H+6, "CLOSE ALL", C_TITLE, C_RED, 8);
ChartRedraw();
}
//+------------------------------------------------------------------+
//| ChartEvent |
//+------------------------------------------------------------------+
datetime g_NextAllowedOrderTime = 0;
datetime g_ManualCloseBlockTime = 0;
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
if (id == CHARTEVENT_OBJECT_CLICK)
{
if (sparam == DB_PREFIX+"CLOSE_HEDGE")
{
Print("[HedgeGuard] Manual 'Close Hedge' requested.");
g_ManualCloseBlockTime = TimeCurrent() + ManualBlockSeconds;
CloseHedge("Manual button click");
ObjectSetInteger(0, DB_PREFIX+"CLOSE_HEDGE", OBJPROP_STATE, false);
}
else if (sparam == DB_PREFIX+"CLOSE_ALL")
{
Print("[HedgeGuard] Manual 'Close All' requested.");
g_ManualCloseBlockTime = TimeCurrent() + ManualBlockSeconds;
CloseAllOnSymbol("Manual button click");
ObjectSetInteger(0, DB_PREFIX+"CLOSE_ALL", OBJPROP_STATE, false);
}
ChartRedraw();
}
}
//+------------------------------------------------------------------+
//| Market open guard |
//| Returns true only when the broker is currently accepting orders |
//| for this symbol. Checks MODE_TRADEALLOWED (server-side flag) |
//| and the chart symbol's trade-allowed status. |
//+------------------------------------------------------------------+
bool IsMarketOpen()
{
// Use chart symbol as fallback if g_SymbolRaw has not been resolved yet
string checkSym = (g_SymbolRaw != "" && g_SymbolRaw != NULL) ? g_SymbolRaw : Symbol();
// Broker/server has suspended trading on this symbol
if ((int)MarketInfo(checkSym, MODE_TRADEALLOWED) == 0) return false;
// MT4 also exposes the global terminal trade-allowed flag
if (!IsTradeAllowed()) return false;
if (!IsConnected()) return false;
// Additional weekend guard: Saturday = 6, Sunday = 0
int dow = TimeDayOfWeek(TimeCurrent());
if (dow == 0 || dow == 6) return false;
return true;
}
//+------------------------------------------------------------------+
//| Safe OrderSend wrapper |
//+------------------------------------------------------------------+
int SafeOrderSend(string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment, int magic, datetime expiration, color arrow_color)
{
static bool s_marketClosedLogged = false;
// Never attempt an order when the market is closed
if (!IsMarketOpen())
{
if (!s_marketClosedLogged)
{
Print("[HedgeGuard] ⚠️ Market is closed — order skipped (", comment, ")");
s_marketClosedLogged = true;
}
return -1;
}
else
{
if (s_marketClosedLogged)
{
Print("[HedgeGuard] ✅ Market is now open — resuming operations.");
s_marketClosedLogged = false;
}
}
if (TimeCurrent() < g_NextAllowedOrderTime) return -1;
int ticket = OrderSend(symbol, cmd, volume, price, slippage, stoploss, takeprofit, comment, magic, expiration, arrow_color);
if (ticket < 0)
{
int err = GetLastError();
if (err == 134)
{
g_NextAllowedOrderTime = TimeCurrent() + 60;
Print(StringFormat("[HedgeGuard] ❌ ERROR 134 (Not enough money) for %.2f lots. Retrying in 60s...", volume));
}
else
{
g_NextAllowedOrderTime = TimeCurrent() + 5;
Print(StringFormat("[HedgeGuard] ❌ ERROR %d opening order. Retrying in 5s...", err));
}
}
else
{
// Apply a 3-second cooldown on successful order placement to prevent
// rapid double-firing before the terminal updates its internal order pool
g_NextAllowedOrderTime = TimeCurrent() + 3;
}
return ticket;
}
//+------------------------------------------------------------------+
//| Normalize lot |
//+------------------------------------------------------------------+
double NormalizeLot(double lot)
{
double lstep = MarketInfo(g_SymbolRaw, MODE_LOTSTEP);
double lmin = MarketInfo(g_SymbolRaw, MODE_MINLOT);
double lmax = MarketInfo(g_SymbolRaw, MODE_MAXLOT);
lot = MathFloor(lot / lstep) * lstep;
lot = MathMax(lot, lmin);
lot = MathMin(lot, lmax);
lot = MathMin(lot, MaxHedgeLot);
lot = MathMax(lot, MinHedgeLot);
return NormalizeDouble(lot, 2);
}
//+------------------------------------------------------------------+
//| Scan grid positions |
//+------------------------------------------------------------------+
void ScanGridPositions(double &netLots, double &floatingPL,
int &direction, int &tradeCount, datetime &oldestTime)
{
double buyLots = 0, sellLots = 0;
floatingPL = 0;
tradeCount = 0;
oldestTime = 0;
string filterSym = "";
if (TradeSymbol != "" && TradeSymbol != NULL)
{
filterSym = TradeSymbol;
StringToUpper(filterSym);
}
if (filterSym == "")
{
string topSym = "";
int maxC = 0;
string sNames[50];
int sCounts[50];
ArrayInitialize(sCounts, 0);
for(int i=0; i<50; i++) sNames[i]="";
int sTotal = 0;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() == HedgeMagicNumber) continue;
if (GridMagicNumber != 0 && OrderMagicNumber() != GridMagicNumber) continue;
string s = OrderSymbol(); StringToUpper(s);
bool found = false;
for (int j = 0; j < sTotal; j++)
if (sNames[j] == s) { sCounts[j]++; found = true; break; }
if (!found && sTotal < 50)
{
sNames[sTotal] = s; sCounts[sTotal] = 1; sTotal++;
}
}
for (int k = 0; k < sTotal; k++)
if (sCounts[k] > maxC) { maxC = sCounts[k]; topSym = sNames[k]; }
if (topSym != "") filterSym = topSym;
else filterSym = g_Symbol;
}
if (filterSym != "" && filterSym != g_Symbol)
{
g_Symbol = filterSym;
g_SymbolRaw = Symbol();
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
string _raw = OrderSymbol(); string _up = _raw; StringToUpper(_up);
if (_up == g_Symbol) { g_SymbolRaw = _raw; break; }
}
Print("[HedgeGuard] Monitoring symbol: ", g_SymbolRaw);
}
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() == HedgeMagicNumber) continue;
if (GridMagicNumber != 0 && OrderMagicNumber() != GridMagicNumber) continue;
string _oSym = OrderSymbol(); StringToUpper(_oSym);
if (_oSym != g_Symbol) continue;
floatingPL += OrderProfit() + OrderSwap() + OrderCommission();
tradeCount++;
if (OrderType() == OP_BUY) buyLots += OrderLots();
if (OrderType() == OP_SELL) sellLots += OrderLots();
if (oldestTime == 0 || OrderOpenTime() < oldestTime)
oldestTime = OrderOpenTime();
}
if (buyLots >= sellLots) { netLots = buyLots - sellLots; direction = OP_BUY; }
else { netLots = sellLots - buyLots; direction = OP_SELL; }
}
//+------------------------------------------------------------------+
//| Trigger 1: DD + Count |
//+------------------------------------------------------------------+
// gridDrawdownPct = MathAbs(floatingPL [grid-only]) / balance * 100.0
// This is intentionally grid-only (no hedge P/L) so T1 fires on raw
// grid stress, independent of whether a hedge is offsetting losses.
// Bug fix: previously recomputed pct internally from floatingPL, which
// could include swap/commission on brand-new positions and fire at near-
// zero real DD. Now the caller computes and passes gridDrawdownPct so
// the same value drives both T1 and the dashboard display.
bool CheckPrimaryTrigger(double floatingPL, double balance, int tradeCount, int gridDirection, string &reason, double gridDrawdownPct = -1.0)
{
if (!UsePrimaryTrigger || balance <= 0) return false;
// Use pre-computed gridDrawdownPct when provided; fall back to
// internal calculation (backwards-compatible default = -1.0).
double pct;
if (gridDrawdownPct >= 0.0)
pct = gridDrawdownPct;
else
{
// Fallback: only treat as loss when floatingPL is genuinely negative
// (not just commission-bleed). Require at least MinHedgeLot worth of
// real loss before computing pct.
if (floatingPL >= 0) return false;
pct = MathAbs(floatingPL) / balance * 100.0;
}
if (pct < HedgeTriggerPct) return false;
if (tradeCount < MinGridTrades) return false;
reason = StringFormat("T1: DD=%.2f%% >= %.1f%% | Grid=%d >= %d trades",
pct, HedgeTriggerPct, tradeCount, MinGridTrades);
return true;
}
//+------------------------------------------------------------------+
//| Trigger 2: Emergency Momentum |
//+------------------------------------------------------------------+
bool CheckEmergencyMomentum(int gridDirection, string &reason, int tradeCount = 0, double drawdownPct = 0.0)
{
if (!UseEmergencyMomentum) return false;
if (Bars < MomentumBars + 2) return false;
if (MomentumMinGridTrades > 0)
{
// Require minimum active grid positions
if (tradeCount < MomentumMinGridTrades) return false;
// Require a meaningful drawdown (using global HedgeTriggerPct) so we don't
// fire the momentum trigger on a healthy grid with no real stress
if (drawdownPct < HedgeTriggerPct) return false;
}
double priceNow = Close[1];
double priceBack = Close[MomentumBars + 1];
double move = MathAbs(priceNow - priceBack) / Point;
if (move >= MomentumPipsThresh)
{
bool movingDown = (priceNow < priceBack);
bool gridIsLong = (gridDirection == OP_BUY);
if ((gridIsLong && movingDown) || (!gridIsLong && !movingDown))
{
reason = StringFormat("T2 EMERGENCY: Spike=%.1f pts >= %.0f pts against grid in %d bars | Grid=%d >= %d positions | DD=%.2f%%",
move, MomentumPipsThresh, MomentumBars, tradeCount, MomentumMinGridTrades, drawdownPct);
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| Orphan momentum trigger |
//+------------------------------------------------------------------+
double g_LastMomScalpPrice = 0;
datetime g_LastMomScalpBar = 0;
bool CheckMomentumTrigger(int hedgeDirection, bool &isSpikingAgainst, string &reason)
{
if (Bars < MomentumBars + 2) return false;
double priceNow = Close[1];
double priceBack = Close[MomentumBars + 1];
double move = MathAbs(priceNow - priceBack) / Point;
if (move < MomentumPipsThresh) return false;
if (Time[1] != g_LastMomScalpBar)
{
g_LastMomScalpPrice = 0;
g_LastMomScalpBar = Time[1];
}
if (g_LastMomScalpPrice != 0 &&
MathAbs(priceNow - g_LastMomScalpPrice) / Point < MomentumReentryStep)
return false;
bool movingDown = (priceNow < priceBack);
bool hedgeIsLong = (hedgeDirection == OP_BUY);
isSpikingAgainst = ((hedgeIsLong && movingDown) || (!hedgeIsLong && !movingDown));
string dirStr = isSpikingAgainst ? "against" : "in favor of";
reason = StringFormat("Orphan Mom=%.1f pts %s hedge in %d bars", move, dirStr, MomentumBars);
return true;
}
//+------------------------------------------------------------------+
//| Trigger 3: ATR Expansion |
//+------------------------------------------------------------------+
bool CheckATRTrigger(string &reason, int gridDirection = -1, int tradeCount = 0, double drawdownPct = 0.0)
{
if (!UseATRTrigger) return false;
if (Bars < ATRAvgPeriod + ATRPeriod + 5) return false;
if (ATRMinGridTrades > 0)
{
// Require minimum active grid positions
if (tradeCount < ATRMinGridTrades) return false;
// Bug fix: was using HedgeTriggerPct (T1's threshold), causing T3 to always
// co-fire with T1. Now uses its own ATRTriggerPct so T3 can be tuned
// independently (e.g. set lower to act as an earlier warning, or higher
// to ensure T3 only fires under severe stress).
if (drawdownPct < ATRTriggerPct) return false;
}
// Optimized: cache ATR values once
double atrBuffer[];
ArrayResize(atrBuffer, ATRAvgPeriod + 2);
for (int i = 1; i <= ATRAvgPeriod + 1; i++)
atrBuffer[i] = iATR(g_SymbolRaw, 0, ATRPeriod, i);
double currentATR = atrBuffer[1];
double sumATR = 0;
for (int i = 2; i <= ATRAvgPeriod + 1; i++)
sumATR += atrBuffer[i];
double avgATR = sumATR / ATRAvgPeriod;
if (avgATR <= 0) return false;
double ratio = currentATR / avgATR;
if (ratio >= ATRMultiplier)
{
if (gridDirection == OP_BUY || gridDirection == OP_SELL)
{
// Bug fix: was using MomentumBars (a T2-specific 3-bar lookback) for the
// directional check, causing T3 to fire on brief 3-bar counter-moves even
// when the ATR expansion was driven by a longer-term trend. Now uses
// ATRAvgPeriod as the lookback so the direction check is consistent with
// the window over which ATR expansion is measured.
int dirLookback = MathMin(ATRAvgPeriod, Bars - 2);
bool movingDown = (Close[1] < Close[dirLookback + 1]);
bool gridIsLong = (gridDirection == OP_BUY);
if ((gridIsLong && !movingDown) || (!gridIsLong && movingDown))
return false;
}
reason = StringFormat("ATR expansion: %.5f = %.2fx avg (threshold %.1fx) against grid | Grid=%d >= %d positions | DD=%.2f%%",
currentATR, ratio, ATRMultiplier, tradeCount, ATRMinGridTrades, drawdownPct);
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Session filter |
//+------------------------------------------------------------------+
bool IsSessionBlocked(string &reason)
{
if (!UseSessionFilter) return false;
datetime now = TimeCurrent();
int hour = TimeHour(now);
int dayOfWeek = TimeDayOfWeek(now);
if (BlockFriday && dayOfWeek == 5)
{
reason = "Friday session block";
return true;
}
if (BlockStartHour < BlockEndHour)
{
if (hour >= BlockStartHour && hour < BlockEndHour)
{
reason = StringFormat("Session block: %02d:00 - %02d:00", BlockStartHour, BlockEndHour);
return true;
}
}
else
{
if (hour >= BlockStartHour || hour < BlockEndHour)
{
reason = StringFormat("Session block: %02d:00 - %02d:00", BlockStartHour, BlockEndHour);
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| Hedge profit helpers |
//+------------------------------------------------------------------+
double GetHedgeProfit()
{
double totalProfit = 0;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
totalProfit += OrderProfit() + OrderSwap() + OrderCommission();
}
return totalProfit;
}
double GetTotalHedgeLots()
{
double totalLots = 0;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
totalLots += OrderLots();
}
return totalLots;
}
int GetTotalHedgeCount()
{
int totalCount = 0;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
totalCount++;
}
return totalCount;
}
double GetRealizedHedgeProfit(datetime sinceTime)
{
if (sinceTime == 0) return 0;
double realized = 0;
int total = OrdersHistoryTotal();
for (int i = 0; i < total; i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
if (OrderCloseTime() >= sinceTime)
realized += OrderProfit() + OrderSwap() + OrderCommission();
}
return realized;
}
double GetRealizedProfitSince(datetime sinceTime)
{
if (sinceTime == 0) return 0;
double realized = 0;
int total = OrdersHistoryTotal();
for (int i = 0; i < total; i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
if (OrderCloseTime() >= sinceTime)
realized += OrderProfit() + OrderSwap() + OrderCommission();
}
return realized;
}
// Returns realized profit ONLY from orphan scalp children (Recovery/Flip/Boost trades)
// closed since sinceTime. Does NOT include profits from other peer orphan positions
// that were closed separately — those are already gone from the account and must not
// inflate the assassination pool.
double GetRealizedOrphanScalpProfit(datetime sinceTime)
{
if (sinceTime == 0) return 0;
double realized = 0;
int total = OrdersHistoryTotal();
for (int i = 0; i < total; i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
if (OrderCloseTime() < sinceTime) continue;
string cmt = OrderComment();
// CRITICAL v5.3 FIX: Only count scalp trades (Recovery/Flip/Boost), not other hedge types
if (StringFind(cmt, "Recovery") < 0 && StringFind(cmt, "Flip") < 0 && StringFind(cmt, "Boost") < 0)
continue;
realized += OrderProfit() + OrderSwap() + OrderCommission();
}
return realized;
}
//+------------------------------------------------------------------+
//| CHECK PARTIAL OVERLAP CLOSE - CRITICAL FIX FOR BUG #7 & #8 |
//+------------------------------------------------------------------+
// FIXED LOGIC (v5.3):
// - Does NOT close any position at a loss when OnlyCloseInProfit=true
// - Does NOT use net calculation to override individual position protection
// - Requires BOTH hedge and grid to have acceptable outcomes
// - MinHedgeProfit applies only to the combined NET profit when both in profit
bool CheckPartialOverlapClose(int &hedgeTicket, int &gridTicket, double &netClosePL, string &reason)
{
hedgeTicket = -1;
gridTicket = -1;
netClosePL = 0;
reason = "";
if (!EnableOverlapClose || !hedgeIsOpen) return false;
double hedgePL = 0, bestGridPL = 0;
int bestGridTicket = -1;
double bestGridLots = 0;
int bestGridType = -1;
// Find best hedge position
int hTicket = -1;
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() != HedgeMagicNumber) continue;
string _os = OrderSymbol(); StringToUpper(_os);
if (_os != g_Symbol) continue;
hTicket = OrderTicket();
hedgePL = OrderProfit() + OrderSwap() + OrderCommission();
break;
}
if (hTicket <= 0) return false;
// CRITICAL FIX v5.3: If hedge itself is losing, refuse to close anything
if (OnlyCloseInProfit && hedgePL < 0)
{
reason = StringFormat("[Overlap BLOCKED] Hedge #%d in LOSS: $%.2f (OnlyCloseInProfit=true)", hTicket, hedgePL);
return false;
}
// Scan grid for matching opposite-side position
for (int i = 0; i < OrdersTotal(); i++)
{
if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderMagicNumber() == HedgeMagicNumber) continue;
if (GridMagicNumber != 0 && OrderMagicNumber() != GridMagicNumber) continue;
string _oSym = OrderSymbol(); StringToUpper(_oSym);
if (_oSym != g_Symbol) continue;
int gType = OrderType();
int hType = -1;
for (int j = 0; j < OrdersTotal(); j++)
{
if (!OrderSelect(j, SELECT_BY_POS, MODE_TRADES)) continue;
if (OrderTicket() != hTicket) continue;
hType = OrderType();
break;
}
// CRITICAL FIX v5.3: Skip same-direction grid positions (no true overlap)
if (hType == gType)
{
continue;
}
double gPL = OrderProfit() + OrderSwap() + OrderCommission();
// CRITICAL FIX v5.3: Check OnlyCloseInProfit BEFORE considering this grid trade
if (OnlyCloseInProfit && gPL < 0)
{
// Grid position is in loss. Refuse to close it unless hedge profit covers it completely.
// Requires: hedge_profit >= abs(grid_loss) AND (hedge_profit - abs(grid_loss)) >= MinHedgeProfit
double absGridLoss = MathAbs(gPL);
if (hedgePL >= absGridLoss)
{
double netProfit = hedgePL + gPL; // net is positive when hedge covers grid
if (netProfit >= MinHedgeProfit)
{
// This grid trade CAN be closed as part of overlap
if (bestGridTicket < 0 || OrderLots() > bestGridLots)
{
bestGridTicket = OrderTicket();
bestGridPL = gPL;
bestGridLots = OrderLots();
bestGridType = gType;
}
}
}
// Otherwise skip this grid trade entirely (hedge can't cover it or profit too small)
continue;
}
// Grid position is in profit (or OnlyCloseInProfit is false)
// Pick highest-lot grid position that's profitable
if (bestGridTicket < 0 || OrderLots() > bestGridLots)
{
bestGridTicket = OrderTicket();
bestGridPL = gPL;
bestGridLots = OrderLots();
bestGridType = gType;
}
}
if (bestGridTicket <= 0) return false;
double combinedPL = hedgePL + bestGridPL;
if (combinedPL < MinHedgeProfit)
{
reason = StringFormat("[Overlap SKIPPED] Combined P/L $%.2f < MinHedgeProfit $%.2f", combinedPL, MinHedgeProfit);
return false;
}
hedgeTicket = hTicket;
gridTicket = bestGridTicket;
netClosePL = combinedPL;
reason = StringFormat("OVERLAP (Hedge Pays Grid): Hedge #%d ($%.2f) + Grid #%d ($%.2f) = Net $%.2f",
hTicket, hedgePL, bestGridTicket, bestGridPL, combinedPL);
return true;
}
//+------------------------------------------------------------------+
//| OnInit |
//+------------------------------------------------------------------+
int OnInit()
{
if (TradeSymbol != "" && TradeSymbol != NULL)
{
g_Symbol = TradeSymbol;
StringToUpper(g_Symbol);
}
else
{
g_Symbol = Symbol();
StringToUpper(g_Symbol);
}
g_SymbolRaw = Symbol();
Print("[HedgeGuard] +----------------------------------+");
Print("[HedgeGuard] ¦ HedgeGuard EA v5.3 Starting ¦");
Print("[HedgeGuard] +----------------------------------+");
Print("[HedgeGuard] Resolved Symbol: ", g_SymbolRaw, " | Hedge Magic: ", HedgeMagicNumber, " | Grid Magic: ", (GridMagicNumber == 0 ? "ALL" : (string)GridMagicNumber));
Print("[HedgeGuard] Triggers: T1(DD>=", HedgeTriggerPct, "% AND Count>=", MinGridTrades, ") | T2(Emergency Mom>=", MomentumPipsThresh, "pts) | T3(ATR>=", ATRMultiplier, "x)"];
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| OnDeinit |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
DBClean();
}
//+------------------------------------------------------------------+
//| OnTick (Main EA logic - stub for this demo) |
//+------------------------------------------------------------------+
void OnTick()
{
// Main EA logic would go here
// This file now has the corrected CheckPartialOverlapClose() function
}
void OnStart()
{
Print("[HedgeGuard v5.3] Critical fixes applied:");
Print(" Bug 7: OVERLAP CLOSE now refuses to close positions with net loss when OnlyCloseInProfit=true");
Print(" Bug 8: OnlyCloseInProfit enforcement at EVERY close attempt (early return if hedge/grid in loss)");
Print(" Bug 9: Individual close validation scaled by BaseLotForProfitTarget");
}