Merge pull request #1 from n30dyn4m1c/feature/gemini-suggestions

Feature/gemini suggestions
This commit is contained in:
$N30
2026-03-27 14:45:22 +10:00
committed by GitHub
4 changed files with 2082 additions and 500 deletions
+7
View File
@@ -61,3 +61,10 @@ Uses separate magic numbers (777333 for MR, 777444 for TB) to manage positions i
- Order execution via `OrderSend()` with `MqlTradeRequest`/`MqlTradeResult`
- News data from `CalendarValueHistory()` - only `CALENDAR_IMPORTANCE_HIGH` (red folder) is filtered
- Fill mode detection: check `SYMBOL_FILLING_MODE` for FOK/IOC support
## After Every EA Update
After every update to any `.mq5` EA file, ensure this comment is the very last line of the file:
```
// This work is my worship unto GOD
```
File diff suppressed because it is too large Load Diff
+566
View File
@@ -0,0 +1,566 @@
//+------------------------------------------------------------------+
//| XAU_Quant_Reversion.mq5 |
//| Copyright 2026, n30dyn4m1c |
//+------------------------------------------------------------------+
#property strict
#property copyright "Copyright 2026, n30dyn4m1c"
#property link ""
#property version "5.00"
#property description "N30 Gold Reversion - Mean Reversion Z-Score EA"
//--- Inputs: Strategy
input string InpTradeSymbol = "GOLD"; // Trade symbol (GOLD, XAUUSD, etc.)
string TradeSymbol;
input double InpEntryZ = 2.0; // Z-Score entry threshold
input bool InpUseDynamicRisk = true; // Enable equity-based risk tiers (overrides InpRiskPct)
input double InpRiskPct = 10.0; // Risk % per trade (used when dynamic risk is off)
input double InpSLPoints = 800; // Fixed SL in points (survives gold spikes)
input double InpHardTPPoints = 1500; // Hard TP in points (server-side safety net, wide)
input double InpExitZ = 0.5; // Z-Score exit threshold (close when Z returns near 0)
input double InpTrailingATR = 2.0; // ATR multiplier for trailing
input int InpStartHour = 10; // Trade window start hour
input int InpEndHour = 20; // Trade window end hour (exclusive)
input int InpFridayCloseHour = 20; // Friday hour to close and stop
input int InpMaxHoldMinutes = 30; // Max trade duration in minutes
input int InpMagic = 777333; // Magic number
//--- Inputs: Indicators
input int InpMAPeriod = 20; // MA / StdDev period
input int InpATRPeriod = 14; // ATR period
input int InpFilterPeriodH1 = 50; // H1 SMA period for trend filter (0 = disabled)
//--- Inputs: Execution
input int InpSlippage = 30; // Max slippage in points
input double InpMaxSpreadPts = 50.0; // Max allowed spread in points
input int InpMaxPositions = 1; // Max open positions allowed
//--- Inputs: News Filter (red folder / CALENDAR_IMPORTANCE_HIGH only)
input bool InpUseNewsFilter = true; // Enable news time filter
input int InpNewsMinsBefore = 60; // Minutes to pause BEFORE red-folder news
input int InpNewsMinsAfter = 60; // Minutes to pause AFTER red-folder news
input bool InpCloseBeforeNews = true; // Close open trades before red-folder news
//--- Inputs: Daily Loss Limit
input bool InpUseDailyLossLimit = true; // Enable max daily loss stop
input double InpMaxDailyLossPct = 20.0; // Max daily loss % of balance (stops trading)
//--- Global Handles & State
int handleMA, handleSD, handleATR, handleMA_H1;
//--- News schedule: red folder (CALENDAR_IMPORTANCE_HIGH) only
#define MAX_NEWS 40
datetime newsRed[MAX_NEWS];
int newsRedCount = 0;
datetime lastNewsLoad = 0;
//--- Daily loss tracking
double dailyStartBalance = 0;
int dailyStartDay = -1;
bool dailyLossHit = false;
//--- New-bar tracking for trailing
datetime lastTrailBar = 0;
//+------------------------------------------------------------------+
int OnInit() {
// Initialize and validate symbol
TradeSymbol = InpTradeSymbol;
if(!SymbolInfoInteger(TradeSymbol, SYMBOL_EXIST)) {
Print("Symbol ", TradeSymbol, " not found - trying XAUUSD");
TradeSymbol = "XAUUSD";
if(!SymbolInfoInteger(TradeSymbol, SYMBOL_EXIST)) {
Print("Neither GOLD nor XAUUSD found. Please set TradeSymbol manually.");
return(INIT_FAILED);
}
}
handleMA = iMA(TradeSymbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
handleSD = iStdDev(TradeSymbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
handleATR = iATR(TradeSymbol, _Period, InpATRPeriod);
if(InpFilterPeriodH1 > 0)
handleMA_H1 = iMA(TradeSymbol, PERIOD_H1, InpFilterPeriodH1, 0, MODE_SMA, PRICE_CLOSE);
else
handleMA_H1 = INVALID_HANDLE;
if(handleMA == INVALID_HANDLE || handleSD == INVALID_HANDLE ||
handleATR == INVALID_HANDLE) {
Print("Failed to create indicator handles");
return(INIT_FAILED);
}
if(InpFilterPeriodH1 > 0 && handleMA_H1 == INVALID_HANDLE) {
Print("Failed to create H1 MA handle");
return(INIT_FAILED);
}
if(InpUseNewsFilter) LoadNewsEvents();
dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
dailyStartDay = dt.day_of_year;
dailyLossHit = false;
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
if(handleMA != INVALID_HANDLE) IndicatorRelease(handleMA);
if(handleSD != INVALID_HANDLE) IndicatorRelease(handleSD);
if(handleATR != INVALID_HANDLE) IndicatorRelease(handleATR);
if(handleMA_H1 != INVALID_HANDLE) IndicatorRelease(handleMA_H1);
}
//+------------------------------------------------------------------+
// Daily Loss Limit
//+------------------------------------------------------------------+
void CheckDailyReset() {
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_year != dailyStartDay) {
dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
dailyStartDay = dt.day_of_year;
dailyLossHit = false;
Print("Daily loss tracker reset. Starting balance: ", dailyStartBalance);
}
}
bool IsDailyLossLimitHit() {
if(!InpUseDailyLossLimit) return false;
if(dailyLossHit) return true;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double lossPercent = ((dailyStartBalance - equity) / dailyStartBalance) * 100.0;
double dailyLimit = GetDailyLossLimitPct();
if(lossPercent >= dailyLimit) {
dailyLossHit = true;
Print("DAILY LOSS LIMIT HIT: ", DoubleToString(lossPercent, 2), "% lost (limit ", DoubleToString(dailyLimit, 1), "%). Trading stopped for today.");
return true;
}
return false;
}
//+------------------------------------------------------------------+
// News Filter — uses MQL5 economic calendar
//+------------------------------------------------------------------+
void LoadNewsEvents() {
newsRedCount = 0;
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
datetime dayStart = TimeCurrent() - (dt.hour * 3600 + dt.min * 60 + dt.sec);
datetime dayEnd = dayStart + 86400;
MqlCalendarValue values[];
if(!CalendarValueHistory(values, dayStart, dayEnd)) return;
int total = ArraySize(values);
for(int i = 0; i < total; i++) {
MqlCalendarEvent event;
if(!CalendarEventById(values[i].event_id, event)) continue;
if(event.importance != CALENDAR_IMPORTANCE_HIGH) continue;
MqlCalendarCountry country;
if(!CalendarCountryById(event.country_id, country)) continue;
if(country.currency != "USD") continue;
if(newsRedCount < MAX_NEWS) {
newsRed[newsRedCount] = values[i].time;
newsRedCount++;
}
}
lastNewsLoad = TimeCurrent();
Print("News loaded: ", newsRedCount, " (red folder) high-impact USD events today");
}
//+------------------------------------------------------------------+
bool IsNearNews() {
if(!InpUseNewsFilter) return false;
MqlDateTime dtNow, dtLast;
TimeToStruct(TimeCurrent(), dtNow);
TimeToStruct(lastNewsLoad, dtLast);
if(dtNow.day_of_year != dtLast.day_of_year) LoadNewsEvents();
datetime now = TimeCurrent();
for(int i = 0; i < newsRedCount; i++) {
long diff = (long)(newsRed[i] - now);
if(diff > -(InpNewsMinsAfter * 60) && diff < (InpNewsMinsBefore * 60))
return true;
}
return false;
}
//+------------------------------------------------------------------+
bool IsRedNewsImminent() {
if(!InpUseNewsFilter || !InpCloseBeforeNews) return false;
datetime now = TimeCurrent();
for(int i = 0; i < newsRedCount; i++) {
long diff = (long)(newsRed[i] - now);
if(diff > 0 && diff < (InpNewsMinsBefore * 60))
return true;
}
return false;
}
//+------------------------------------------------------------------+
// Dynamic Risk Tiers — scales risk down as equity grows
//+------------------------------------------------------------------+
double GetRiskPct() {
if(!InpUseDynamicRisk) return InpRiskPct;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity < 500) return 10.0;
if(equity < 2000) return 7.0;
if(equity < 5000) return 5.0;
if(equity < 20000) return 3.0;
return 1.5;
}
double GetDailyLossLimitPct() {
if(!InpUseDynamicRisk) return InpMaxDailyLossPct;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity < 500) return 25.0;
if(equity < 2000) return 20.0;
if(equity < 5000) return 15.0;
if(equity < 20000) return 10.0;
return 7.0;
}
//+------------------------------------------------------------------+
bool SelectOwnPosition() {
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) == TradeSymbol &&
PositionGetInteger(POSITION_MAGIC) == (long)InpMagic) {
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
int CountOwnPositions() {
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) == TradeSymbol &&
PositionGetInteger(POSITION_MAGIC) == (long)InpMagic) {
count++;
}
}
return count;
}
//+------------------------------------------------------------------+
string TruncateComment(string comment, int maxLen = 31) {
if(StringLen(comment) <= maxLen) return comment;
return StringSubstr(comment, 0, maxLen);
}
//+------------------------------------------------------------------+
double NormalizeLot(double lot) {
double minLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_STEP);
lot = MathMax(minLot, lot);
lot = MathMin(maxLot, lot);
lot = MathFloor(lot / stepLot) * stepLot;
lot = NormalizeDouble(lot, 2);
return lot;
}
//+------------------------------------------------------------------+
void CloseAllOwnPositions(string reason) {
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != TradeSymbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue;
MqlTradeRequest req = {}; MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.position = ticket;
req.symbol = TradeSymbol;
req.volume = PositionGetDouble(POSITION_VOLUME);
long posType = PositionGetInteger(POSITION_TYPE);
req.type = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
req.price = (req.type == ORDER_TYPE_SELL) ? SymbolInfoDouble(TradeSymbol, SYMBOL_BID)
: SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
req.deviation = InpSlippage;
req.comment = "N30 " + reason;
uint fill = (uint)SymbolInfoInteger(TradeSymbol, SYMBOL_FILLING_MODE);
if(fill & SYMBOL_FILLING_FOK) req.type_filling = ORDER_FILLING_FOK;
else if(fill & SYMBOL_FILLING_IOC) req.type_filling = ORDER_FILLING_IOC;
else req.type_filling = ORDER_FILLING_RETURN;
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE) {
Print("Close position failed (", reason, "): ticket=", ticket, " retcode=", res.retcode);
} else {
Print("Position closed (", reason, "): ticket=", ticket);
}
}
}
//+------------------------------------------------------------------+
// IsWeekendRisk — block Friday late sessions and weekends
//+------------------------------------------------------------------+
bool IsWeekendRisk() {
MqlDateTime dt;
TimeCurrent(dt);
// Friday late (>= InpFridayCloseHour) or Saturday (6) or Sunday (0)
if((dt.day_of_week == 5 && dt.hour >= InpFridayCloseHour) || dt.day_of_week == 6 || dt.day_of_week == 0)
return true;
return false;
}
//+------------------------------------------------------------------+
// H1 Trend Filter — skip M1 entries that fight the H1 structural trend
//+------------------------------------------------------------------+
bool IsAlignedWithH1Trend(bool isBuy) {
if(InpFilterPeriodH1 <= 0 || handleMA_H1 == INVALID_HANDLE) return true;
double h1ma[1];
if(CopyBuffer(handleMA_H1, 0, 0, 1, h1ma) < 1) return true; // fail open
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
if(isBuy) return bid > h1ma[0]; // only buy when price is above H1 SMA
return bid < h1ma[0]; // only sell when price is below H1 SMA
}
//+------------------------------------------------------------------+
//| New bar detection (persistent across ticks) |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(TradeSymbol, _Period, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
//+------------------------------------------------------------------+
void OnTick()
{
CheckDailyReset();
double ma[1], sd[1], atr[1];
if(CopyBuffer(handleMA,0,0,1,ma)<1 || CopyBuffer(handleSD,0,0,1,sd)<1 ||
CopyBuffer(handleATR,0,0,1,atr)<1) return;
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
double ask = SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
double point = SymbolInfoDouble(TradeSymbol, SYMBOL_POINT);
if(sd[0] <= 0.0) return;
double zScore = (bid - ma[0]) / sd[0];
bool nearNews = IsNearNews();
bool lossLimitHit = IsDailyLossLimitHit();
bool redNewsImminent = IsRedNewsImminent();
bool weekendRisk = IsWeekendRisk();
double spreadPts = (ask - bid) / point;
double atrPts = atr[0] / point;
bool spreadOk = (spreadPts <= InpMaxSpreadPts) && (spreadPts <= 1.0 * atrPts);
bool inWindow = (dt.hour >= InpStartHour && dt.hour < InpEndHour);
string h1Status = "OFF";
if(InpFilterPeriodH1 > 0 && handleMA_H1 != INVALID_HANDLE) {
double h1ma[1];
if(CopyBuffer(handleMA_H1, 0, 0, 1, h1ma) >= 1)
h1Status = (bid > h1ma[0]) ? "BULL" : "BEAR";
}
if(lossLimitHit) {
if(SelectOwnPosition()) CloseAllOwnPositions("daily loss limit");
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"DAILY LOSS LIMIT REACHED - TRADING STOPPED\n",
"Loss: ", DoubleToString(((dailyStartBalance - AccountInfoDouble(ACCOUNT_EQUITY)) / dailyStartBalance) * 100.0, 2), "%");
return;
}
if(weekendRisk) {
if(SelectOwnPosition()) CloseAllOwnPositions("Friday Exit");
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"WEEKEND PAUSE - NO NEW TRADES\n",
"H1 Trend: ", h1Status);
return;
}
if(redNewsImminent && SelectOwnPosition()) {
CloseAllOwnPositions("(red folder) high-impact news imminent");
}
// POSITION MANAGEMENT (still every tick - unchanged)
string tradeDurStr = "None";
if(SelectOwnPosition()) {
long openTime = PositionGetInteger(POSITION_TIME);
long durationSec = TimeCurrent() - openTime;
int durMins = (int)(durationSec / 60);
int durSecs = (int)(durationSec % 60);
tradeDurStr = IntegerToString(durMins) + "m " + IntegerToString(durSecs) + "s";
if(durMins >= InpMaxHoldMinutes) {
CloseAllOwnPositions("Time Exit (" + tradeDurStr + ")");
} else {
long posType = PositionGetInteger(POSITION_TYPE);
bool zRevert = false;
if(posType == POSITION_TYPE_BUY && zScore >= -InpExitZ) zRevert = true;
if(posType == POSITION_TYPE_SELL && zScore <= InpExitZ) zRevert = true;
if(zRevert) {
CloseAllOwnPositions("Z-Score TP (Z=" + DoubleToString(zScore, 2) + ")");
} else {
datetime curBar = iTime(TradeSymbol, _Period, 0);
if(curBar != lastTrailBar) {
lastTrailBar = curBar;
HandleTrailingStop(atr[0]);
}
}
}
}
else
{
// === ENTRY LOGIC - NOW ONLY ON NEW BAR ===
if(CountOwnPositions() >= InpMaxPositions) return;
bool baseFilters = (inWindow && spreadOk && !nearNews);
if(baseFilters && IsNewBar())
{
// Use PREVIOUS closed bar's MA/SD for entry decision
double maPrev[1], sdPrev[1];
if(CopyBuffer(handleMA,0,1,1,maPrev)<1 || CopyBuffer(handleSD,0,1,1,sdPrev)<1) return;
if(sdPrev[0] <= 0) return;
double closedZ = (bid - maPrev[0]) / sdPrev[0];
if(closedZ < -InpEntryZ && IsAlignedWithH1Trend(true))
ExecuteTrade(ORDER_TYPE_BUY, ask, atr[0], closedZ);
else if(closedZ > InpEntryZ && IsAlignedWithH1Trend(false))
ExecuteTrade(ORDER_TYPE_SELL, bid, atr[0], closedZ);
}
static double lastZ = 0;
if(MathAbs(zScore) > InpEntryZ && MathAbs(lastZ) > InpEntryZ * 0.8) // required two ticks confirmation
{
// then do the entry checks
}
lastZ = zScore;
}
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"Z-Score: ", DoubleToString(zScore, 2), "\n",
"H1 Trend: ", h1Status, "\n",
"News: ", (nearNews ? "BLOCKED" : "clear"),
(redNewsImminent ? " [CLOSING NOW]" : ""), "\n",
"Trade Duration: ", tradeDurStr);
}
//+------------------------------------------------------------------+
void HandleTrailingStop(double atrVal) {
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
double ask = SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
long type = PositionGetInteger(POSITION_TYPE);
double trailDist = atrVal * InpTrailingATR;
int digits = (int)SymbolInfoInteger(TradeSymbol, SYMBOL_DIGITS);
if(type == POSITION_TYPE_BUY) {
double newSL = NormalizeDouble(bid - trailDist, digits);
if(newSL > currentSL + (atrVal * 0.2)) ModifySL(newSL, currentTP);
} else {
double newSL = NormalizeDouble(ask + trailDist, digits);
if(newSL < currentSL - (atrVal * 0.2) || currentSL == 0) ModifySL(newSL, currentTP);
}
}
//+------------------------------------------------------------------+
void ModifySL(double nSL, double currentTP) {
MqlTradeRequest r = {}; MqlTradeResult rs = {};
r.action = TRADE_ACTION_SLTP;
r.position = (ulong)PositionGetInteger(POSITION_TICKET);
r.symbol = TradeSymbol;
r.sl = nSL;
r.tp = currentTP;
if(!OrderSend(r, rs) || rs.retcode != TRADE_RETCODE_DONE) {
Print("TrailSL modify failed: retcode=", rs.retcode);
}
}
//+------------------------------------------------------------------+
void ExecuteTrade(ENUM_ORDER_TYPE type, double p, double a, double zScore) {
MqlTradeRequest req = {}; MqlTradeResult res = {};
double point = SymbolInfoDouble(TradeSymbol, SYMBOL_POINT);
double slD = InpSLPoints * point;
double tpD = InpHardTPPoints * point;
double sl = (type == ORDER_TYPE_BUY) ? (p - slD) : (p + slD);
double tp = (type == ORDER_TYPE_BUY) ? (p + tpD) : (p - tpD);
double riskPct = GetRiskPct();
double risk = AccountInfoDouble(ACCOUNT_BALANCE) * (riskPct / 100.0);
double tickV = SymbolInfoDouble(TradeSymbol, SYMBOL_TRADE_TICK_VALUE);
double tickS = SymbolInfoDouble(TradeSymbol, SYMBOL_TRADE_TICK_SIZE);
if(tickV <= 0 || tickS <= 0) {
Print("Invalid tick value/size, skipping trade");
return;
}
double lot = risk / (slD * (1.0 / tickS) * tickV);
lot = NormalizeLot(lot);
int digits = (int)SymbolInfoInteger(TradeSymbol, SYMBOL_DIGITS);
// Check sufficient margin before opening trade
double marginRequired;
if(!OrderCalcMargin(type, TradeSymbol, lot, p, marginRequired)) {
Print("Failed to calculate margin, skipping trade");
return;
}
if(marginRequired > AccountInfoDouble(ACCOUNT_MARGIN_FREE)) {
Print("Insufficient margin: required=", marginRequired, " free=", AccountInfoDouble(ACCOUNT_MARGIN_FREE));
return;
}
string dir = (type == ORDER_TYPE_BUY) ? "B" : "S";
string comment = TruncateComment("N30 " + dir
+ "|Z" + DoubleToString(zScore, 2)
+ "|R" + DoubleToString(a, 2));
req.action = TRADE_ACTION_DEAL;
req.symbol = TradeSymbol;
req.volume = lot;
req.type = type;
req.price = p;
req.magic = InpMagic;
req.sl = NormalizeDouble(sl, digits);
req.tp = NormalizeDouble(tp, digits);
req.deviation = InpSlippage;
req.comment = comment;
uint fill = (uint)SymbolInfoInteger(TradeSymbol, SYMBOL_FILLING_MODE);
if(fill & SYMBOL_FILLING_FOK) req.type_filling = ORDER_FILLING_FOK;
else if(fill & SYMBOL_FILLING_IOC) req.type_filling = ORDER_FILLING_IOC;
else req.type_filling = ORDER_FILLING_RETURN;
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE) {
Print("Entry failed: retcode=", res.retcode, " comment=", res.comment);
} else {
Print("Trade opened: ", EnumToString(type), " ", lot, " lots, SL=", NormalizeDouble(sl, digits), " TP(hard)=", NormalizeDouble(tp, digits), " exitZ=", InpExitZ);
}
}
//+------------------------------------------------------------------+
// This work is my worship unto GOD
+970
View File
@@ -0,0 +1,970 @@
//+------------------------------------------------------------------+
//| XAU_Quant_Reversion.mq5 |
//| Copyright 2026, n30dyn4m1c |
//+------------------------------------------------------------------+
#property strict
#property copyright "Copyright 2026, n30dyn4m1c"
#property link ""
#property version "5.00"
#property description "N30 Gold Reversion - Mean Reversion Z-Score EA"
//--- Inputs: Strategy
input string InpTradeSymbol = "GOLD"; // Trade symbol (GOLD, XAUUSD, etc.)
string TradeSymbol;
input double InpEntryZ = 2.0; // Z-Score entry threshold
input bool InpUseDynamicRisk = true; // Enable equity-based risk tiers (overrides InpRiskPct)
input double InpRiskPct = 10.0; // Risk % per trade (used when dynamic risk is off)
input double InpSLPoints = 800; // Fixed SL in points (survives gold spikes)
input double InpHardTPPoints = 1500; // Hard TP in points (server-side safety net, wide)
input double InpExitZ = 0.5; // Z-Score exit threshold (close when Z returns near 0)
input double InpTrailingATR = 2.0; // ATR multiplier for trailing
input double InpMinProfitBuffer = 50.0; // Minimum profit buffer in points before breakeven triggers
input int InpStartHour = 10; // Trade window start hour
input int InpEndHour = 20; // Trade window end hour (exclusive)
input int InpFridayCloseHour = 20; // Friday hour to close and stop
input int InpMaxHoldMinutes = 30; // Max trade duration in minutes
input int InpMagic = 777333; // Magic number
//--- Inputs: Indicators
input int InpMAPeriod = 20; // MA / StdDev period
input int InpATRPeriod = 14; // ATR period
input int InpFilterPeriodH1 = 50; // H1 SMA period for trend filter (0 = disabled)
//--- Inputs: Execution
input int InpSlippage = 30; // Max slippage in points
input double InpMaxSpreadPts = 50.0; // Max allowed spread in points
input int InpMaxPositions = 1; // Max open positions allowed
//--- Inputs: News Filter (red folder / CALENDAR_IMPORTANCE_HIGH only)
input bool InpUseNewsFilter = true; // Enable news time filter
input int InpNewsMinsBefore = 60; // Minutes to pause BEFORE red-folder news
input int InpNewsMinsAfter = 120; // Minutes to pause AFTER red-folder news (washout buffer)
input bool InpCloseBeforeNews = true; // Close open trades before red-folder news
//--- Inputs: Daily Loss Limit
input bool InpUseDailyLossLimit = true; // Enable max daily loss stop
input double InpMaxDailyLossPct = 20.0; // Max daily loss % of balance (stops trading)
//--- Trade log: open-trade snapshot captured at entry
struct TradeRecord {
ulong ticket;
datetime openTime;
double openPrice;
double lots;
int type; // POSITION_TYPE_BUY or POSITION_TYPE_SELL
double openBalance;
double openEquity;
double zScore;
double atr;
double spreadAtEntry;
double riskPct;
double sl;
double tp;
string h1Status;
string comment;
};
//--- Global Handles & State
int handleMA, handleSD, handleATR, handleMA_H1;
ulong glTicket = 0; // active position ticket (0 = no open trade)
string glPendingCloseReason = ""; // reason set by CloseAllOwnPositions, read on next tick
TradeRecord glTradeRecord; // snapshot of the open trade
int glTradeCount = 0; // cumulative trade counter (persists across sessions via file)
double spreadBuffer[20];
int spreadIdx = 0;
bool spreadBufferFull = false;
//--- News schedule: red folder (CALENDAR_IMPORTANCE_HIGH) only
#define MAX_NEWS 40
datetime newsRed[MAX_NEWS];
int newsRedCount = 0;
datetime lastNewsLoad = 0;
//--- Daily loss tracking
double dailyStartBalance = 0;
int dailyStartDay = -1;
bool dailyLossHit = false;
//--- New-bar tracking
datetime lastTrailBar = 0;
datetime lastBarTime = 0;
//--- Entry confirmation gate: filter out flash spikes
// ExecuteTrade fires only after signal persists 3+ consecutive ticks OR 2+ seconds
int glEntryConfirmTicks = 0; // ticks that have confirmed the current signal
datetime glEntryConfirmStart = 0; // wall-clock time of the first confirming tick
int glEntryConfirmDir = 0; // 1 = buy signal, -1 = sell signal, 0 = no signal
//--- Price Velocity filter: block entry when price moves > 25% of M1 ATR within 10 ticks
#define VELOCITY_TICKS 10
double glVelBuf[VELOCITY_TICKS]; // circular buffer of last 10 bid prices
int glVelIdx = 0; // write index
bool glVelBufFull = false; // true once buffer has been filled once
datetime glVelBlockUntil = 0; // entry blocked until this server time
//+------------------------------------------------------------------+
int OnInit() {
// Initialize and validate symbol
TradeSymbol = InpTradeSymbol;
if(!SymbolInfoInteger(TradeSymbol, SYMBOL_EXIST)) {
Print("Symbol ", TradeSymbol, " not found - trying XAUUSD");
TradeSymbol = "XAUUSD";
if(!SymbolInfoInteger(TradeSymbol, SYMBOL_EXIST)) {
Print("Neither GOLD nor XAUUSD found. Please set TradeSymbol manually.");
return(INIT_FAILED);
}
}
// Ensure symbol is in Market Watch so indicator handles and quotes work
if(!SymbolSelect(TradeSymbol, true)) {
Print("Failed to add ", TradeSymbol, " to Market Watch");
return(INIT_FAILED);
}
handleMA = iMA(TradeSymbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
handleSD = iStdDev(TradeSymbol, _Period, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
handleATR = iATR(TradeSymbol, _Period, InpATRPeriod);
if(InpFilterPeriodH1 > 0)
handleMA_H1 = iMA(TradeSymbol, PERIOD_H1, InpFilterPeriodH1, 0, MODE_SMA, PRICE_CLOSE);
else
handleMA_H1 = INVALID_HANDLE;
if(handleMA == INVALID_HANDLE || handleSD == INVALID_HANDLE ||
handleATR == INVALID_HANDLE) {
Print("Failed to create indicator handles");
return(INIT_FAILED);
}
if(InpFilterPeriodH1 > 0 && handleMA_H1 == INVALID_HANDLE) {
Print("Failed to create H1 MA handle");
return(INIT_FAILED);
}
if(InpUseNewsFilter) LoadNewsEvents();
InitTradeLog();
dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
dailyStartDay = dt.day_of_year;
dailyLossHit = false;
// Initialize bar trackers to current time to avoid immediate action on stale bars
datetime currentBarTime = iTime(TradeSymbol, _Period, 0);
lastBarTime = currentBarTime;
lastTrailBar = currentBarTime;
// Reset entry confirmation gate on (re)init
glEntryConfirmDir = 0;
glEntryConfirmTicks = 0;
glEntryConfirmStart = 0;
// Reset price velocity filter on (re)init
ArrayInitialize(glVelBuf, 0.0);
glVelIdx = 0;
glVelBufFull = false;
glVelBlockUntil = 0;
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
if(handleMA != INVALID_HANDLE) IndicatorRelease(handleMA);
if(handleSD != INVALID_HANDLE) IndicatorRelease(handleSD);
if(handleATR != INVALID_HANDLE) IndicatorRelease(handleATR);
if(handleMA_H1 != INVALID_HANDLE) IndicatorRelease(handleMA_H1);
}
//+------------------------------------------------------------------+
// Daily Loss Limit
//+------------------------------------------------------------------+
void CheckDailyReset() {
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_year != dailyStartDay) {
dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
dailyStartDay = dt.day_of_year;
dailyLossHit = false;
Print("Daily loss tracker reset. Starting balance: ", dailyStartBalance);
}
}
bool IsDailyLossLimitHit() {
if(!InpUseDailyLossLimit) return false;
if(dailyLossHit) return true;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double lossPercent = ((dailyStartBalance - equity) / dailyStartBalance) * 100.0;
double dailyLimit = GetDailyLossLimitPct();
if(lossPercent >= dailyLimit) {
dailyLossHit = true;
Print("DAILY LOSS LIMIT HIT: ", DoubleToString(lossPercent, 2), "% lost (limit ", DoubleToString(dailyLimit, 1), "%). Trading stopped for today.");
return true;
}
return false;
}
//+------------------------------------------------------------------+
// News Filter — uses MQL5 economic calendar
//+------------------------------------------------------------------+
void LoadNewsEvents() {
newsRedCount = 0;
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
datetime dayStart = TimeCurrent() - (dt.hour * 3600 + dt.min * 60 + dt.sec);
datetime dayEnd = dayStart + 86400;
MqlCalendarValue values[];
if(!CalendarValueHistory(values, dayStart, dayEnd)) return;
int total = ArraySize(values);
for(int i = 0; i < total; i++) {
MqlCalendarEvent event;
if(!CalendarEventById(values[i].event_id, event)) continue;
if(event.importance != CALENDAR_IMPORTANCE_HIGH) continue;
MqlCalendarCountry country;
if(!CalendarCountryById(event.country_id, country)) continue;
if(country.currency != "USD") continue;
if(newsRedCount < MAX_NEWS) {
newsRed[newsRedCount] = values[i].time;
newsRedCount++;
}
}
lastNewsLoad = TimeCurrent();
Print("News loaded: ", newsRedCount, " (red folder) high-impact USD events today");
}
//+------------------------------------------------------------------+
bool IsNearNews() {
if(!InpUseNewsFilter) return false;
MqlDateTime dtNow, dtLast;
TimeToStruct(TimeCurrent(), dtNow);
TimeToStruct(lastNewsLoad, dtLast);
if(dtNow.day_of_year != dtLast.day_of_year) LoadNewsEvents();
datetime now = TimeCurrent();
for(int i = 0; i < newsRedCount; i++) {
long diff = (long)(newsRed[i] - now);
if(diff > -(InpNewsMinsAfter * 60) && diff < (InpNewsMinsBefore * 60))
return true;
}
return false;
}
//+------------------------------------------------------------------+
bool IsRedNewsImminent() {
if(!InpUseNewsFilter || !InpCloseBeforeNews) return false;
datetime now = TimeCurrent();
for(int i = 0; i < newsRedCount; i++) {
long diff = (long)(newsRed[i] - now);
if(diff > 0 && diff < (InpNewsMinsBefore * 60))
return true;
}
return false;
}
//+------------------------------------------------------------------+
// Dynamic Risk Tiers — scales risk down as equity grows
//+------------------------------------------------------------------+
double GetRiskPct() {
if(!InpUseDynamicRisk) return InpRiskPct;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity < 500) return 10.0;
if(equity < 2000) return 7.0;
if(equity < 5000) return 5.0;
if(equity < 20000) return 3.0;
return 1.5;
}
double GetDailyLossLimitPct() {
if(!InpUseDynamicRisk) return InpMaxDailyLossPct;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity < 500) return 25.0;
if(equity < 2000) return 20.0;
if(equity < 5000) return 15.0;
if(equity < 20000) return 10.0;
return 7.0;
}
//+------------------------------------------------------------------+
bool SelectOwnPosition() {
if(glTicket == 0) return false;
return PositionSelectByTicket(glTicket);
}
//+------------------------------------------------------------------+
int CountOwnPositions() {
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) == TradeSymbol &&
PositionGetInteger(POSITION_MAGIC) == (long)InpMagic) {
count++;
}
}
return count;
}
//+------------------------------------------------------------------+
string TruncateComment(string comment, int maxLen = 31) {
if(StringLen(comment) <= maxLen) return comment;
return StringSubstr(comment, 0, maxLen);
}
//+------------------------------------------------------------------+
double NormalizeLot(double lot) {
double minLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_STEP);
lot = MathMax(minLot, lot);
lot = MathMin(maxLot, lot);
// Strictly follow volume step using MathFloor to avoid 'Invalid Volume' errors
lot = MathFloor(lot / stepLot) * stepLot;
// Derive decimal places from step size without fragile float == comparison
int digits = (int)MathRound(-MathLog10(stepLot + 1e-10));
if(digits < 0) digits = 0;
return NormalizeDouble(lot, digits);
}
//+------------------------------------------------------------------+
void CloseAllOwnPositions(string reason) {
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != TradeSymbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue;
MqlTradeRequest req = {}; MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.position = ticket;
req.symbol = TradeSymbol;
req.volume = PositionGetDouble(POSITION_VOLUME);
long posType = PositionGetInteger(POSITION_TYPE);
req.type = (posType == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
double price = (req.type == ORDER_TYPE_SELL) ? SymbolInfoDouble(TradeSymbol, SYMBOL_BID)
: SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
req.price = NormalizeDouble(price, _Digits);
req.deviation = InpSlippage;
req.comment = "N30 " + reason;
uint fill = (uint)SymbolInfoInteger(TradeSymbol, SYMBOL_FILLING_MODE);
if(fill & SYMBOL_FILLING_FOK) req.type_filling = ORDER_FILLING_FOK;
else if(fill & SYMBOL_FILLING_IOC) req.type_filling = ORDER_FILLING_IOC;
else req.type_filling = ORDER_FILLING_RETURN;
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE) {
Print("Close position failed (", reason, "): ticket=", ticket, " retcode=", res.retcode);
} else {
Print("Position closed (", reason, "): ticket=", ticket);
if(ticket == glTicket) {
glPendingCloseReason = reason; // OnTick will detect close and log with this reason
}
}
}
}
//+------------------------------------------------------------------+
// IsWeekendRisk — block Friday late sessions and weekends
//+------------------------------------------------------------------+
bool IsWeekendRisk() {
MqlDateTime dt;
TimeCurrent(dt);
// Friday late (>= InpFridayCloseHour) or Saturday (6) or Sunday (0)
if((dt.day_of_week == 5 && dt.hour >= InpFridayCloseHour) || dt.day_of_week == 6 || dt.day_of_week == 0)
return true;
return false;
}
//+------------------------------------------------------------------+
// H1 Trend Filter — skip M1 entries that fight the H1 structural trend
//+------------------------------------------------------------------+
bool IsAlignedWithH1Trend(bool isBuy) {
if(InpFilterPeriodH1 <= 0 || handleMA_H1 == INVALID_HANDLE) return true;
double h1ma[1];
if(CopyBuffer(handleMA_H1, 0, 0, 1, h1ma) < 1) return true; // fail open
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
if(isBuy) return bid > h1ma[0]; // only buy when price is above H1 SMA
return bid < h1ma[0]; // only sell when price is below H1 SMA
}
//+------------------------------------------------------------------+
void OnTick() {
CheckDailyReset();
// Detect position close (works in both live and Strategy Tester)
if(glTicket != 0 && !PositionSelectByTicket(glTicket)) {
LogPositionClose(glPendingCloseReason);
glTicket = 0;
glPendingCloseReason = "";
}
double ma[1], sd[1], atr[1];
// We always need current data for spread and ATR (trailing)
if(CopyBuffer(handleMA,0,0,1,ma)<1 || CopyBuffer(handleSD,0,0,1,sd)<1 ||
CopyBuffer(handleATR,0,0,1,atr)<1) return;
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
double ask = SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
double point = SymbolInfoDouble(TradeSymbol, SYMBOL_POINT);
// --- Spread Calculation & Buffer Update ---
double currentSpread = (ask - bid) / point;
spreadBuffer[spreadIdx] = currentSpread;
spreadIdx++;
if(spreadIdx >= 20) {
spreadIdx = 0;
spreadBufferFull = true;
}
double spreadSum = 0;
int count = spreadBufferFull ? 20 : spreadIdx;
for(int i = 0; i < count; i++) spreadSum += spreadBuffer[i];
double spreadMA = (count > 0) ? (spreadSum / count) : currentSpread;
// --- PRICE VELOCITY FILTER: update 10-tick bid buffer ---
glVelBuf[glVelIdx] = bid;
glVelIdx++;
if(glVelIdx >= VELOCITY_TICKS) { glVelIdx = 0; glVelBufFull = true; }
if(glVelBufFull) {
// oldest value is at current write position (about to be overwritten next tick)
double oldest = glVelBuf[glVelIdx % VELOCITY_TICKS];
double newest = glVelBuf[(glVelIdx + VELOCITY_TICKS - 1) % VELOCITY_TICKS];
double velMove = MathAbs(newest - oldest);
if(velMove > 0.25 * atr[0])
glVelBlockUntil = TimeCurrent() + 3; // block entries for 3 seconds
}
// --- NEW BAR DETECTION ---
datetime currentTime = iTime(TradeSymbol, _Period, 0);
bool isNewBar = (currentTime != lastBarTime);
if(isNewBar) lastBarTime = currentTime; // STRICT OHLC: Update immediately so entry only runs once per bar
// --- Z-Score Calculation (Shift 1 for OHLC alignment) ---
// We only update Z-Score once per bar to match OHLC backtest signals
static double zScore = 0;
if(isNewBar) {
double ma1[1], sd1[1];
if(CopyBuffer(handleMA,0,1,1,ma1)>=1 && CopyBuffer(handleSD,0,1,1,sd1)>=1) {
double close1 = iClose(TradeSymbol, _Period, 1);
zScore = (sd1[0] > 0.0) ? (close1 - ma1[0]) / sd1[0] : 0.0;
}
}
// --- Volatility Ratio & Dynamic Z-Score Threshold ---
double atrBuf100[100];
double avgATR100 = 0;
if(CopyBuffer(handleATR, 0, 0, 100, atrBuf100) >= 100) {
double atrSum = 0;
for(int k = 0; k < 100; k++) atrSum += atrBuf100[k];
avgATR100 = atrSum / 100.0;
} else {
avgATR100 = atr[0]; // fallback: ratio = 1.0 (no adjustment)
}
double volRatio = (avgATR100 > 0) ? (atr[0] / avgATR100) : 1.0;
double dynamicZ = InpEntryZ + (volRatio > 1.5 ? 0.5 : 0.0);
double softZ = InpEntryZ * 0.75;
// Stable market (volRatio 0.7–1.1): lower soft threshold captures more standard reversions
// Elevated volatility (volRatio > 1.2): demand full dynamicZ to avoid noise spikes
double entryZ = (volRatio >= 0.7 && volRatio <= 1.1) ? softZ : dynamicZ;
bool nearNews = IsNearNews();
bool lossLimitHit = IsDailyLossLimitHit();
bool redNewsImminent = IsRedNewsImminent();
bool weekendRisk = IsWeekendRisk();
// --- Spread Check (ATR-relative only) ---
double atrPts = atr[0] / point;
double maxAllowedSpread = atrPts * 0.12; // block entry when spread > 12% of current M1 ATR
bool spreadOk = (currentSpread <= maxAllowedSpread) && // ATR-relative gate
(currentSpread <= spreadMA * 1.5); // liquidity-gap guard: no entry if spread > 1.5x 20-tick average
// --- Pre-compute filter states ---
bool inWindow = (dt.hour >= InpStartHour && dt.hour < InpEndHour);
string h1Status = "OFF";
if(InpFilterPeriodH1 > 0 && handleMA_H1 != INVALID_HANDLE) {
double h1ma[1];
if(CopyBuffer(handleMA_H1, 0, 0, 1, h1ma) >= 1)
h1Status = (bid > h1ma[0]) ? "BULL" : "BEAR";
}
// --- DAILY LOSS: close everything and stop ---
if(lossLimitHit) {
if(SelectOwnPosition()) CloseAllOwnPositions("daily loss limit");
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"DAILY LOSS LIMIT REACHED - TRADING STOPPED\n",
"Loss: ", DoubleToString(((dailyStartBalance - AccountInfoDouble(ACCOUNT_EQUITY)) / dailyStartBalance) * 100.0, 2), "%");
return;
}
// --- FRIDAY EXIT / WEEKEND SAFETY ---
if(weekendRisk) {
if(SelectOwnPosition()) CloseAllOwnPositions("Friday Exit");
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"WEEKEND PAUSE - NO NEW TRADES\n",
"H1 Trend: ", h1Status);
return;
}
// --- CLOSE BEFORE (RED FOLDER) HIGH-IMPACT NEWS ---
if(redNewsImminent && SelectOwnPosition()) {
CloseAllOwnPositions("(red folder) high-impact news imminent");
}
// --- POSITION MANAGEMENT (Every Tick) ---
string tradeDurStr = "None";
bool positionOpen = SelectOwnPosition();
if(positionOpen) {
long openTime = PositionGetInteger(POSITION_TIME);
long durationSec = TimeCurrent() - openTime;
int durMins = (int)(durationSec / 60);
int durSecs = (int)(durationSec % 60);
// Gravity Exit: exit Z decays 0.1 per every 5 mins open, floors at 0.0 after 15 mins
double gravityExitZ = (durMins >= 15) ? 0.0 : MathMax(0.0, InpExitZ - (durMins / 5) * 0.1);
tradeDurStr = IntegerToString(durMins) + "m " + IntegerToString(durSecs) + "s"
+ " [ExitZ=" + DoubleToString(gravityExitZ, 2) + "]";
// Time-Based Exit
if(durMins >= InpMaxHoldMinutes) {
CloseAllOwnPositions("Time Exit (" + tradeDurStr + ")");
} else {
// Z-Score TP: Gravity Exit — exit threshold decays toward 0 as trade ages
long posType = PositionGetInteger(POSITION_TYPE);
bool zRevert = false;
if(posType == POSITION_TYPE_BUY && zScore >= -gravityExitZ) zRevert = true;
if(posType == POSITION_TYPE_SELL && zScore <= gravityExitZ) zRevert = true;
if(zRevert) {
CloseAllOwnPositions("Gravity Exit (Z=" + DoubleToString(zScore, 2) + " tgt=" + DoubleToString(gravityExitZ, 2) + ")");
} else {
// Breakeven: every tick — lock in no-loss once 1.5*ATR in profit
CheckBreakeven(atr[0]);
// Trail on new bar only
if(currentTime != lastTrailBar) {
lastTrailBar = currentTime;
HandleTrailingStop(atr[0]);
}
}
}
} else { // --- ENTRY LOGIC (every tick, gated by flash-spike filter) ---
if(CountOwnPositions() < InpMaxPositions) {
bool baseFilters = (inWindow && spreadOk && !nearNews);
// Determine current signal direction (0 = no signal)
int signalDir = 0;
if(baseFilters) {
if(zScore < -entryZ && IsAlignedWithH1Trend(true)) signalDir = 1;
else if(zScore > entryZ && IsAlignedWithH1Trend(false)) signalDir = -1;
}
// --- Confirmation gate: accumulate or reset ---
if(signalDir != 0 && signalDir == glEntryConfirmDir) {
glEntryConfirmTicks++; // same direction: count up
} else if(signalDir != 0) {
glEntryConfirmDir = signalDir; // new direction: start fresh
glEntryConfirmTicks = 1;
glEntryConfirmStart = TimeCurrent();
} else {
glEntryConfirmDir = 0; // signal gone: reset
glEntryConfirmTicks = 0;
glEntryConfirmStart = 0;
}
// Fire only after 3+ consecutive ticks OR 2+ seconds of sustained signal,
// and only when the price-velocity block has expired (no knife-catching)
bool ticksOk = (glEntryConfirmTicks >= 3);
bool timeOk = (glEntryConfirmStart > 0 && (TimeCurrent() - glEntryConfirmStart) >= 2);
bool velOk = (TimeCurrent() >= glVelBlockUntil);
if(glEntryConfirmDir != 0 && (ticksOk || timeOk) && velOk) {
if(glEntryConfirmDir == 1)
ExecuteTrade(ORDER_TYPE_BUY, ask, atr[0], zScore);
else
ExecuteTrade(ORDER_TYPE_SELL, bid, atr[0], zScore);
// Reset gate after firing to prevent double-entry
glEntryConfirmDir = 0;
glEntryConfirmTicks = 0;
glEntryConfirmStart = 0;
}
}
}
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"Z-Score(1): ", DoubleToString(zScore, 2),
" | Z-Target: ", DoubleToString(entryZ, 2),
" (", (volRatio >= 0.7 && volRatio <= 1.1 ? "SOFT" : "FULL"), ")\n",
"Volatility Ratio: ", DoubleToString(volRatio, 2),
(volRatio > 1.5 ? " [HIGH - Z raised +0.5]" : (volRatio >= 0.7 && volRatio <= 1.1 ? " [stable - soft threshold]" : " [normal]")), "\n",
"H1 Trend: ", h1Status, "\n",
"Spread: ", DoubleToString(currentSpread, 1),
" / MaxAllowed: ", DoubleToString(maxAllowedSpread, 1),
" (Avg20: ", DoubleToString(spreadMA, 1), ") ", (spreadOk ? "OK" : "BLOCKED"), "\n",
"News: ", (nearNews ? "BLOCKED" : "clear"),
(redNewsImminent ? " [CLOSING NOW]" : ""), "\n",
"Velocity: ", (TimeCurrent() < glVelBlockUntil ? "BLOCKED (" + IntegerToString((int)(glVelBlockUntil - TimeCurrent())) + "s)" : "ok"), "\n",
"Trade Duration: ", tradeDurStr);
}
//+------------------------------------------------------------------+
// CheckBreakeven — move SL to entry + 10 pts once (1.5*ATR + Buffer) in profit
//+------------------------------------------------------------------+
void CheckBreakeven(double atrVal) {
if(glTicket == 0 || !PositionSelectByTicket(glTicket)) return;
double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
long posType = PositionGetInteger(POSITION_TYPE);
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
double ask = SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
double point = SymbolInfoDouble(TradeSymbol, SYMBOL_POINT);
int stopLevel = (int)SymbolInfoInteger(TradeSymbol, SYMBOL_TRADE_STOPS_LEVEL);
double bePts = 10.0 * point;
double triggerDist = 1.5 * atrVal + InpMinProfitBuffer * point; // ATR threshold + buffer
if(posType == POSITION_TYPE_BUY) {
if(bid < entryPrice + triggerDist) return; // not (1.5*ATR + buffer) in profit yet
double beSL = NormalizeDouble(entryPrice + bePts, _Digits);
if(beSL <= currentSL) return; // already at or beyond breakeven
if(bid - beSL < stopLevel * point)
beSL = NormalizeDouble(bid - stopLevel * point, _Digits);
if(beSL > currentSL) {
Print("Breakeven triggered (BUY): SL -> ", DoubleToString(beSL, _Digits));
ModifySL(beSL, currentTP);
}
} else {
if(ask > entryPrice - triggerDist) return; // not (1.5*ATR + buffer) in profit yet
double beSL = NormalizeDouble(entryPrice - bePts, _Digits);
if(currentSL != 0 && beSL >= currentSL) return; // already at or beyond breakeven
if(beSL - ask < stopLevel * point)
beSL = NormalizeDouble(ask + stopLevel * point, _Digits);
if(currentSL == 0 || beSL < currentSL) {
Print("Breakeven triggered (SELL): SL -> ", DoubleToString(beSL, _Digits));
ModifySL(beSL, currentTP);
}
}
}
//+------------------------------------------------------------------+
void HandleTrailingStop(double atrVal) {
if(glTicket == 0 || !PositionSelectByTicket(glTicket)) return;
// Time-decay: tighten trail by 50% after 50% of InpMaxHoldMinutes has elapsed
long openTimeSec = PositionGetInteger(POSITION_TIME);
long durationSec = TimeCurrent() - openTimeSec;
long halfMaxSec = (long)InpMaxHoldMinutes * 30; // 50% of max hold in seconds
double trailMult = (durationSec > halfMaxSec) ? InpTrailingATR * 0.5 : InpTrailingATR;
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
double ask = SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
long type = PositionGetInteger(POSITION_TYPE);
double trailDist = atrVal * trailMult;
double point = SymbolInfoDouble(TradeSymbol, SYMBOL_POINT);
int stopLevel = (int)SymbolInfoInteger(TradeSymbol, SYMBOL_TRADE_STOPS_LEVEL);
double minMove = atrVal * 0.3; // only modify if new SL is at least 0.3*ATR from current
if(type == POSITION_TYPE_BUY) {
double newSL = NormalizeDouble(bid - trailDist, _Digits);
if(bid - newSL < stopLevel * point) newSL = NormalizeDouble(bid - stopLevel * point, _Digits);
if(newSL > currentSL + minMove) ModifySL(newSL, currentTP);
} else {
double newSL = NormalizeDouble(ask + trailDist, _Digits);
if(newSL - ask < stopLevel * point) newSL = NormalizeDouble(ask + stopLevel * point, _Digits);
if(newSL < currentSL - minMove || currentSL == 0) ModifySL(newSL, currentTP);
}
}
//+------------------------------------------------------------------+
void ModifySL(double nSL, double currentTP) {
MqlTradeRequest r = {}; MqlTradeResult rs = {};
r.action = TRADE_ACTION_SLTP;
r.position = glTicket;
r.symbol = TradeSymbol;
r.sl = NormalizeDouble(nSL, _Digits);
r.tp = NormalizeDouble(currentTP, _Digits);
if(!OrderSend(r, rs) || rs.retcode != TRADE_RETCODE_DONE) {
Print("TrailSL modify failed: retcode=", rs.retcode);
}
}
//+------------------------------------------------------------------+
void ExecuteTrade(ENUM_ORDER_TYPE type, double p, double a, double zScore) {
MqlTradeRequest req = {}; MqlTradeResult res = {};
double point = SymbolInfoDouble(TradeSymbol, SYMBOL_POINT);
double bid = SymbolInfoDouble(TradeSymbol, SYMBOL_BID);
double ask = SymbolInfoDouble(TradeSymbol, SYMBOL_ASK);
int stopLevel = (int)SymbolInfoInteger(TradeSymbol, SYMBOL_TRADE_STOPS_LEVEL);
double slD = InpSLPoints * point;
double tpD = InpHardTPPoints * point;
// Apply SYMBOL_TRADE_STOPS_LEVEL safety
if(slD < stopLevel * point) slD = stopLevel * point + 5 * point;
if(tpD < stopLevel * point) tpD = stopLevel * point + 5 * point;
double sl = (type == ORDER_TYPE_BUY) ? (p - slD) : (p + slD);
double tp = (type == ORDER_TYPE_BUY) ? (p + tpD) : (p - tpD);
double riskPct = GetRiskPct();
double risk = AccountInfoDouble(ACCOUNT_BALANCE) * (riskPct / 100.0);
double tickV = SymbolInfoDouble(TradeSymbol, SYMBOL_TRADE_TICK_VALUE);
double tickS = SymbolInfoDouble(TradeSymbol, SYMBOL_TRADE_TICK_SIZE);
if(tickV <= 0 || tickS <= 0) {
Print("Invalid tick value/size, skipping trade");
return;
}
double lot = risk / (slD * (1.0 / tickS) * tickV);
lot = NormalizeLot(lot);
double minLot = SymbolInfoDouble(TradeSymbol, SYMBOL_VOLUME_MIN);
if(lot < minLot) {
Print("Calculated lot (", DoubleToString(lot, 3), ") below broker minimum (", DoubleToString(minLot, 3), "). Insufficient balance for this risk level.");
return;
}
// Check sufficient margin before opening trade
double marginRequired;
if(!OrderCalcMargin(type, TradeSymbol, lot, p, marginRequired)) {
Print("Failed to calculate margin, skipping trade");
return;
}
if(marginRequired > AccountInfoDouble(ACCOUNT_MARGIN_FREE)) {
Print("Insufficient margin: required=", marginRequired, " free=", AccountInfoDouble(ACCOUNT_MARGIN_FREE));
return;
}
string dir = (type == ORDER_TYPE_BUY) ? "B" : "S";
string comment = TruncateComment("N30 " + dir
+ "|Z" + DoubleToString(zScore, 2)
+ "|R" + DoubleToString(a, 2));
req.action = TRADE_ACTION_DEAL;
req.symbol = TradeSymbol;
req.volume = lot;
req.type = type;
req.price = NormalizeDouble(p, _Digits);
req.magic = InpMagic;
req.sl = NormalizeDouble(sl, _Digits);
req.tp = NormalizeDouble(tp, _Digits);
req.deviation = InpSlippage;
req.comment = comment;
uint fill = (uint)SymbolInfoInteger(TradeSymbol, SYMBOL_FILLING_MODE);
if(fill & SYMBOL_FILLING_FOK) req.type_filling = ORDER_FILLING_FOK;
else if(fill & SYMBOL_FILLING_IOC) req.type_filling = ORDER_FILLING_IOC;
else req.type_filling = ORDER_FILLING_RETURN;
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE) {
Print("Entry failed: retcode=", res.retcode, " comment=", res.comment);
} else {
// Capture position ticket: res.deal is the position ticket on netting accounts;
// fall back to res.order for hedging accounts.
glTicket = res.deal;
bool posSelected = PositionSelectByTicket(glTicket);
if(!posSelected) {
glTicket = res.order;
posSelected = PositionSelectByTicket(glTicket);
}
// Snapshot open-trade details for the trade log
glTradeRecord.ticket = glTicket;
glTradeRecord.openTime = posSelected ? (datetime)PositionGetInteger(POSITION_TIME) : TimeCurrent();
glTradeRecord.openPrice = p;
glTradeRecord.lots = lot;
glTradeRecord.type = (type == ORDER_TYPE_BUY) ? (int)POSITION_TYPE_BUY : (int)POSITION_TYPE_SELL;
glTradeRecord.openBalance = AccountInfoDouble(ACCOUNT_BALANCE);
glTradeRecord.openEquity = AccountInfoDouble(ACCOUNT_EQUITY);
glTradeRecord.zScore = zScore;
glTradeRecord.atr = a;
glTradeRecord.spreadAtEntry = (ask - bid) / point;
glTradeRecord.riskPct = riskPct;
glTradeRecord.sl = NormalizeDouble(sl, _Digits);
glTradeRecord.tp = NormalizeDouble(tp, _Digits);
string h1Stat = "OFF";
if(InpFilterPeriodH1 > 0 && handleMA_H1 != INVALID_HANDLE) {
double h1buf[1];
if(CopyBuffer(handleMA_H1, 0, 0, 1, h1buf) >= 1)
h1Stat = (bid > h1buf[0]) ? "BULL" : "BEAR";
}
glTradeRecord.h1Status = h1Stat;
glTradeRecord.comment = comment;
Print("Trade opened: ticket=", glTicket, " ", EnumToString(type), " ", lot,
" lots, SL=", NormalizeDouble(sl, _Digits),
" TP(hard)=", NormalizeDouble(tp, _Digits), " exitZ=", InpExitZ);
}
}
//+------------------------------------------------------------------+
// OnTradeTransaction — kept for broker-side state sync only.
// All trade logging is handled in OnTick via LogPositionClose().
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result) {
}
//+------------------------------------------------------------------+
// LogPositionClose — called from OnTick when position disappears.
// Scans deal history for the closing deal so it works in both live
// trading and the Strategy Tester (where OnTradeTransaction is unreliable).
//+------------------------------------------------------------------+
void LogPositionClose(string reason) {
datetime from = (glTradeRecord.openTime > 0) ? glTradeRecord.openTime : TimeCurrent() - 86400;
if(!HistorySelect(from, TimeCurrent() + 1)) {
Print("LogPositionClose: HistorySelect failed");
return;
}
int total = HistoryDealsTotal();
for(int i = total - 1; i >= 0; i--) {
ulong deal = HistoryDealGetTicket(i);
if(deal == 0) continue;
if(HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
// Match by position ID (= glTradeRecord.ticket on both netting and hedging accounts)
if((ulong)HistoryDealGetInteger(deal, DEAL_POSITION_ID) != glTradeRecord.ticket) continue;
datetime closeTime = (datetime)HistoryDealGetInteger(deal, DEAL_TIME);
double closePrice = HistoryDealGetDouble(deal, DEAL_PRICE);
double grossProfit = HistoryDealGetDouble(deal, DEAL_PROFIT);
double swap = HistoryDealGetDouble(deal, DEAL_SWAP);
double commission = HistoryDealGetDouble(deal, DEAL_COMMISSION);
string closeReason = (reason != "") ? reason : HistoryDealGetString(deal, DEAL_COMMENT);
WriteTradeResult(closeTime, closePrice, grossProfit, swap, commission, closeReason);
return;
}
Print("LogPositionClose: no closing deal found in history for ticket=", glTradeRecord.ticket);
}
//+------------------------------------------------------------------+
// InitTradeLog — count existing trades and write CSV header if new
//+------------------------------------------------------------------+
void InitTradeLog() {
string filename = "N30_TradeLog_" + TradeSymbol + "_M" + IntegerToString(InpMagic) + ".csv";
int handle = FileOpen(filename, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_SHARE_READ);
if(handle == INVALID_HANDLE) {
Print("Trade log ERROR: cannot open '", filename, "' error=", GetLastError());
return;
}
if(FileSize(handle) == 0) {
// Brand-new file — write CSV header immediately so it's visible on attach
FileWriteString(handle,
"TradeNum,Ticket,OpenTime,CloseTime,Duration_mins,Type,Lots,"
"OpenPrice,ClosePrice,SL,TP,"
"ZScore,ATR,SpreadPts,RiskPct,"
"EquityAtOpen,BalAtOpen,GrossProfit,Swap,Commission,NetPnL,BalAfter,"
"H1Trend,CloseReason\n");
FileClose(handle);
Print("Trade log: new CSV created — '", filename,
"' (Live: MQL5/Files/ | Tester: MQL5/Tester/Files/)");
return;
}
// Count existing trade rows (data lines start with a digit, header starts with 'T')
glTradeCount = 0;
while(!FileIsEnding(handle)) {
string line = FileReadString(handle);
if(StringLen(line) > 0 && line[0] >= '0' && line[0] <= '9') glTradeCount++;
}
FileClose(handle);
Print("Trade log: '", filename, "' — ", glTradeCount, " existing trades");
}
//+------------------------------------------------------------------+
// WriteTradeResult — append one CSV row to the trade log
//+------------------------------------------------------------------+
void WriteTradeResult(datetime closeTime, double closePrice,
double grossProfit, double swap, double commission,
string closeReason) {
glTradeCount++;
double netPnL = grossProfit + swap + commission;
double closingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
long durationSec = (long)closeTime - (long)glTradeRecord.openTime;
int durationMins = (int)(durationSec / 60);
string filename = "N30_TradeLog_" + TradeSymbol + "_M" + IntegerToString(InpMagic) + ".csv";
int handle = FileOpen(filename, FILE_READ|FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_SHARE_READ);
if(handle == INVALID_HANDLE) {
Print("WriteTradeResult: cannot open '", filename, "' error=", GetLastError());
glTradeCount--;
return;
}
if(FileSize(handle) == 0) {
// Write CSV header on a brand-new file
FileWriteString(handle,
"TradeNum,Ticket,OpenTime,CloseTime,Duration_mins,Type,Lots,"
"OpenPrice,ClosePrice,SL,TP,"
"ZScore,ATR,SpreadPts,RiskPct,"
"EquityAtOpen,BalAtOpen,GrossProfit,Swap,Commission,NetPnL,BalAfter,"
"H1Trend,CloseReason\n");
} else {
FileSeek(handle, 0, SEEK_END);
}
string typeStr = (glTradeRecord.type == (int)POSITION_TYPE_BUY) ? "BUY" : "SELL";
// Escape close reason for CSV (replace commas and quotes)
string reasonEsc = closeReason;
StringReplace(reasonEsc, "\"", "\"\"");
if(StringFind(reasonEsc, ",") >= 0) reasonEsc = "\"" + reasonEsc + "\"";
string line = StringFormat(
"%d,%I64u,%s,%s,%d,%s,%.2f,"
"%.5f,%.5f,%.5f,%.5f,"
"%.4f,%.5f,%.1f,%.2f,"
"%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,"
"%s,%s\n",
glTradeCount,
glTradeRecord.ticket,
TimeToString(glTradeRecord.openTime, TIME_DATE|TIME_SECONDS),
TimeToString(closeTime, TIME_DATE|TIME_SECONDS),
durationMins,
typeStr,
glTradeRecord.lots,
glTradeRecord.openPrice,
closePrice,
glTradeRecord.sl,
glTradeRecord.tp,
glTradeRecord.zScore,
glTradeRecord.atr,
glTradeRecord.spreadAtEntry,
glTradeRecord.riskPct,
glTradeRecord.openEquity,
glTradeRecord.openBalance,
grossProfit,
swap,
commission,
netPnL,
closingBalance,
glTradeRecord.h1Status,
reasonEsc);
FileWriteString(handle, line);
FileClose(handle);
Print("Trade #", glTradeCount, " logged | ticket=", glTradeRecord.ticket,
" | ", typeStr, " | NetPnL=", StringFormat("%+.2f", netPnL),
" | Balance=", DoubleToString(closingBalance, 2),
" | Reason: ", closeReason);
}
//+------------------------------------------------------------------+
// This work is my worship unto GOD