0894d18db4
Confluence EA (v1.16 → v1.20): - Per-EA realized P&L tracking via history deals - Weekly drawdown protection - Warmup bars, pivot cache, state persistence - Point-scaled pivot thresholds, ranging ATR factor - Market filling mode helper per symbol Grid EA (v3.1 → v4.1): - Adaptive filters, adaptive entry, spread filter - Session filter, breakeven, correlation caps, range drift - Profit protection (stop-after-profit, cycle reports) - Edge cleanup v5.0 — close wrong-side positions outside grid - Master one-shot shutdown, grid state persistence Presets: - Fix GetOut=Y shutdown bug on 4 grid presets - Relax ADXMax 18→40, widen RSI 20/80 across grid presets - Standardize daily drawdown 3%→5%, add weekly 10% - Increase grid lots 0.01→0.03 - Normalize confluence ATR thresholds per pair - Add XAGUSD, EURCHF, EURGBP, AUDNZD presets Docs & DevOps: - April 23 audit files (preset mismatch, code review, checklist) - n8n workflow and validation infrastructure updates - AI agent analyses in notes/ Known issues carried forward: - Shared drawdown budget contamination (both EAs) - Confluence ranging-market threshold inversion - Older grid presets missing v4.1 safety controls
1452 lines
53 KiB
Plaintext
Executable File
1452 lines
53 KiB
Plaintext
Executable File
//+------------------------------------------------------------------+
|
|
//| OrdersEA_Smart_Grid.mq5 |
|
|
//| Copyright 2024, Garfield Heron |
|
|
//| https://fetcherpay.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2024, Garfield Heron"
|
|
#property link "https://fetcherpay.com"
|
|
#property version "4.1"
|
|
|
|
#include <Trade\Trade.mqh>
|
|
#include <Trade\PositionInfo.mqh>
|
|
|
|
#define VERSION "Version 4.1 Smart Grid MT5"
|
|
#define MAX_TRADES 600
|
|
#define MAX_LOG_TRADES 1200
|
|
|
|
//--- Input Parameters
|
|
input string Email= "garfield@fetcherpay.com";
|
|
input int MagicNum= 333;
|
|
|
|
//--- Smart Grid Settings
|
|
input string GridSettings = "=== Smart Grid Settings ===";
|
|
input bool UseAutoPivots = true;
|
|
input double InpManualHigh= 0; // Manual HIGH level (0 = use AutoPivots)
|
|
input double InpManualLow= 0; // Manual LOW level (0 = use AutoPivots)
|
|
input double Entry= 10; // Grid spacing in points
|
|
input double TP= 15; // Take-profit in points per level
|
|
input double Lots=0.01;
|
|
input int MaxLevels = 10; // Max levels per side (cap; adaptive may place fewer)
|
|
|
|
//--- Range Filter Settings
|
|
input string FilterSettings = "=== Range Filters ===";
|
|
input bool UseRSIFilter = true;
|
|
input int RSIPeriod = 14;
|
|
input int RSILower = 35;
|
|
input int RSIUpper = 65;
|
|
input bool UseADXFilter = true;
|
|
input int ADXPeriod = 14;
|
|
input double ADXMax = 30; // Widened default from 25
|
|
input bool UseATRFilter = true;
|
|
input int ATRPeriod = 14;
|
|
input double ATRMultiplier = 1.5;
|
|
|
|
//--- Adaptive Filters (4.4)
|
|
input string AdaptiveSettings = "=== Adaptive Filters ===";
|
|
input int InpRelaxFilterAfterDays = 3; // If no trade for N days, relax filters (0=disabled)
|
|
input double InpRelaxADXFactor = 1.3; // Multiply ADXMax by this when relaxed
|
|
input double InpRelaxLotFactor = 0.5; // Multiply lots by this when relaxed
|
|
|
|
//--- Adaptive Entry (3.3)
|
|
input bool InpAdaptiveEntry = false; // Auto-scale grid spacing from ATR
|
|
input double InpEntryATRFactor = 0.5; // spacing = ATR * factor / MaxLevels (per level)
|
|
|
|
//--- Spread Filter (3.4)
|
|
input string SpreadSettings = "=== Spread Filter ===";
|
|
input int InpMaxSpreadPoints = 30; // 0 = disabled
|
|
|
|
//--- Stop Loss (3.5)
|
|
input string SLSettings = "=== Stop Loss ===";
|
|
input bool InpUseStopLoss = false; // false = NO stop loss (original grid behavior)
|
|
input int StopLoss = 300; // Only applied if InpUseStopLoss=true
|
|
|
|
//--- Session Filter (4.1)
|
|
input string SessionSettings = "=== Session Filter ===";
|
|
input bool InpUseSessionFilter = false;
|
|
input int InpSessionStartHour = 1; // Broker time, inclusive
|
|
input int InpSessionEndHour = 10; // Broker time, exclusive
|
|
|
|
//--- Breakeven (4.2)
|
|
input string BreakevenSettings = "=== Breakeven ===";
|
|
input bool InpUseBreakeven = false; // Move remaining SLs to breakeven after first TP hits
|
|
input int InpBreakevenBufferPoints = 5; // Spread cushion above entry
|
|
|
|
//--- Correlation Cap (4.3)
|
|
input string CorrelationSettings = "=== Correlation Cap ===";
|
|
input int InpMaxLongSymbols = 0; // Max distinct symbols with open longs (0 = unlimited)
|
|
input int InpMaxShortSymbols = 0; // Max distinct symbols with open shorts (0 = unlimited)
|
|
|
|
//--- Range Drift (4.6)
|
|
input string DriftSettings = "=== Range Drift ===";
|
|
input bool InpRangeDriftEnable = false;
|
|
input int InpMoveRangeTrigger = 200; // Re-center grid when mid drifts by N points
|
|
|
|
//--- Risk Management
|
|
input string RiskSettings = "=== Risk Management ===";
|
|
input int TRADE_RANGE= 50;
|
|
input double LongLimit= 0;
|
|
input double ShortLimit= 0;
|
|
input string GetOut= "N";
|
|
input string OpenNewTrades="Y";
|
|
input int TakeProfitLevelPercent= 0; // % equity gain to close cycle (0=disabled)
|
|
input int TakeProfitLevelDollarAmount= 0; // $ gain to close cycle (0=disabled)
|
|
input int EquityFactorPercent= 0;
|
|
input int LotsFactorPercent= 0;
|
|
input int BaseEquity= 10000;
|
|
input bool Master= false;
|
|
input bool DiagnosticModeOn= false;
|
|
input double InpMaxDailyDrawdown = 5.0; // Max daily EA drawdown % (per-EA)
|
|
input double InpMaxWeeklyDrawdown = 10.0; // Max weekly EA drawdown % (per-EA, 0=disable)
|
|
|
|
//--- Profit Protection
|
|
input string ProfitSettings = "=== Profit Protection ===";
|
|
input bool InpStopAfterProfit = false; // Stop new grids after profitable cycle
|
|
input bool InpCycleReport = true; // Emit notification on cycle end
|
|
|
|
//--- Weekend Protection
|
|
input string WeekendSettings = "=== Weekend Protection ===";
|
|
input bool InpCloseBeforeWeekend = true;
|
|
input int InpWeekendCloseHour = 17;
|
|
input bool InpCancelPendingBeforeWeekend = true;
|
|
|
|
//--- Trade Object
|
|
CTrade trade;
|
|
CPositionInfo positionInfo;
|
|
|
|
//--- Indicator Handles
|
|
int RSIHandle = INVALID_HANDLE;
|
|
int ADXHandle = INVALID_HANDLE;
|
|
int ATRHandle = INVALID_HANDLE;
|
|
|
|
//--- Pivot Point Variables
|
|
double PivotP = 0;
|
|
double PivotR1 = 0;
|
|
double PivotR2 = 0;
|
|
double PivotS1 = 0;
|
|
double PivotS2 = 0;
|
|
double GridHigh = 0;
|
|
double GridLow = 0;
|
|
|
|
//--- Stats Variables (1.3 — now actually computed)
|
|
int longs = 0;
|
|
int shorts = 0;
|
|
double longAvgPrice = 0;
|
|
double longAvgLots = 0;
|
|
double shortAvgPrice = 0;
|
|
double shortAvgLots = 0;
|
|
double longProfit = 0;
|
|
double shortProfit = 0;
|
|
|
|
//--- State
|
|
double initEquity;
|
|
int lotDigits;
|
|
bool bEnableLongs = false;
|
|
bool bEnableShorts = false;
|
|
bool bGetOutOK = false;
|
|
bool bOpenNewTradesOK = false;
|
|
bool bGetOutHandled = false;
|
|
|
|
//--- Daily Drawdown (per-EA, 3.1B)
|
|
double dailyStartEquity = 0;
|
|
datetime lastEquityReset = 0;
|
|
double realizedPnLToday = 0;
|
|
datetime lastRealizedScan = 0;
|
|
|
|
//--- Weekly Drawdown (per-EA)
|
|
double weeklyStartEquity = 0;
|
|
datetime lastWeeklyReset = 0;
|
|
double realizedPnLWeek = 0;
|
|
datetime lastWeeklyScan = 0;
|
|
|
|
//--- Weekend Protection
|
|
bool weekendCloseExecuted = false;
|
|
|
|
//--- Master one-shot (1.4)
|
|
bool masterShutdownDone = false;
|
|
|
|
//--- Pivot recalc (3.8)
|
|
datetime lastPivotCalcDate = 0;
|
|
|
|
//--- Grid State (3.7 — now persisted)
|
|
bool gridPlaced = false;
|
|
double cycleStartEquity = 0;
|
|
datetime cycleStartTime = 0;
|
|
bool cycleProfitStop = false;
|
|
int cyclePartialTPCount = 0; // For breakeven logic
|
|
datetime lastTradePlacedTime = 0; // For adaptive filters
|
|
|
|
//--- Runtime derived
|
|
double currentEntryPts = 0; // Resolved at bar open (adaptive or fixed)
|
|
bool filtersRelaxed = false; // Set by adaptive filter check
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Log helper with symbol prefix (3.6) |
|
|
//+------------------------------------------------------------------+
|
|
void PrintS(string msg)
|
|
{
|
|
Print("[", _Symbol, ":", MagicNum, "] ", msg);
|
|
}
|
|
|
|
void Log(string st)
|
|
{
|
|
if(DiagnosticModeOn)
|
|
PrintS(st);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Determine filling mode supported by symbol (1.2) |
|
|
//+------------------------------------------------------------------+
|
|
ENUM_ORDER_TYPE_FILLING GetMarketFilling()
|
|
{
|
|
int modes = (int)SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
|
|
if((modes & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
|
|
if((modes & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
|
|
return ORDER_FILLING_RETURN;
|
|
}
|
|
|
|
// ORDER_FILLING_RETURN is always valid for pending orders per MT5 docs —
|
|
// SYMBOL_FILLING_MODE bitmask only exposes FOK/IOC flags (RETURN is implicit).
|
|
ENUM_ORDER_TYPE_FILLING GetPendingFilling()
|
|
{
|
|
return ORDER_FILLING_RETURN;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| State persistence (3.7) |
|
|
//+------------------------------------------------------------------+
|
|
string GvKey(string suffix)
|
|
{
|
|
return "OrdersEA_" + IntegerToString(MagicNum) + "_" + _Symbol + "_" + suffix;
|
|
}
|
|
|
|
void SaveGridState()
|
|
{
|
|
GlobalVariableSet(GvKey("gridPlaced"), gridPlaced ? 1.0 : 0.0);
|
|
GlobalVariableSet(GvKey("cycleStartEquity"), cycleStartEquity);
|
|
GlobalVariableSet(GvKey("cycleStartTime"), (double)cycleStartTime);
|
|
GlobalVariableSet(GvKey("cycleProfitStop"), cycleProfitStop ? 1.0 : 0.0);
|
|
GlobalVariableSet(GvKey("lastPivotCalcDate"), (double)lastPivotCalcDate);
|
|
GlobalVariableSet(GvKey("lastTradeTime"), (double)lastTradePlacedTime);
|
|
GlobalVariableSet(GvKey("dailyStartEquity"), dailyStartEquity);
|
|
GlobalVariableSet(GvKey("lastEquityReset"), (double)lastEquityReset);
|
|
GlobalVariableSet(GvKey("weeklyStartEquity"), weeklyStartEquity);
|
|
GlobalVariableSet(GvKey("lastWeeklyReset"), (double)lastWeeklyReset);
|
|
}
|
|
|
|
void LoadGridState()
|
|
{
|
|
if(GlobalVariableCheck(GvKey("gridPlaced"))) gridPlaced = GlobalVariableGet(GvKey("gridPlaced")) > 0.5;
|
|
if(GlobalVariableCheck(GvKey("cycleStartEquity"))) cycleStartEquity = GlobalVariableGet(GvKey("cycleStartEquity"));
|
|
if(GlobalVariableCheck(GvKey("cycleStartTime"))) cycleStartTime = (datetime)GlobalVariableGet(GvKey("cycleStartTime"));
|
|
if(GlobalVariableCheck(GvKey("cycleProfitStop"))) cycleProfitStop = GlobalVariableGet(GvKey("cycleProfitStop")) > 0.5;
|
|
if(GlobalVariableCheck(GvKey("lastPivotCalcDate"))) lastPivotCalcDate = (datetime)GlobalVariableGet(GvKey("lastPivotCalcDate"));
|
|
if(GlobalVariableCheck(GvKey("lastTradeTime"))) lastTradePlacedTime = (datetime)GlobalVariableGet(GvKey("lastTradeTime"));
|
|
if(GlobalVariableCheck(GvKey("dailyStartEquity"))) dailyStartEquity = GlobalVariableGet(GvKey("dailyStartEquity"));
|
|
if(GlobalVariableCheck(GvKey("lastEquityReset"))) lastEquityReset = (datetime)GlobalVariableGet(GvKey("lastEquityReset"));
|
|
if(GlobalVariableCheck(GvKey("weeklyStartEquity"))) weeklyStartEquity = GlobalVariableGet(GvKey("weeklyStartEquity"));
|
|
if(GlobalVariableCheck(GvKey("lastWeeklyReset"))) lastWeeklyReset = (datetime)GlobalVariableGet(GvKey("lastWeeklyReset"));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Today-at-broker / Monday-at-broker |
|
|
//+------------------------------------------------------------------+
|
|
datetime TodayStartBroker()
|
|
{
|
|
datetime now = TimeCurrent();
|
|
return (datetime)((now / 86400) * 86400);
|
|
}
|
|
|
|
datetime WeekStartBroker()
|
|
{
|
|
datetime today = TodayStartBroker();
|
|
MqlDateTime dt;
|
|
TimeToStruct(today, dt);
|
|
int offset = (dt.day_of_week == 0) ? 6 : (dt.day_of_week - 1);
|
|
return (datetime)(today - offset * 86400);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Sum deals for this EA in a window (3.1B) |
|
|
//+------------------------------------------------------------------+
|
|
double SumRealizedPnLSince(datetime from)
|
|
{
|
|
datetime to = TimeCurrent() + 60;
|
|
if(!HistorySelect(from, to)) return 0;
|
|
double total = 0;
|
|
int deals = HistoryDealsTotal();
|
|
for(int i = 0; i < deals; i++)
|
|
{
|
|
ulong dealTicket = HistoryDealGetTicket(i);
|
|
if(dealTicket == 0) continue;
|
|
if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != MagicNum) continue;
|
|
if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) continue;
|
|
long entry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY);
|
|
if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT) continue;
|
|
total += HistoryDealGetDouble(dealTicket, DEAL_PROFIT);
|
|
total += HistoryDealGetDouble(dealTicket, DEAL_SWAP);
|
|
total += HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
double GetRealizedPnLToday()
|
|
{
|
|
if(TimeCurrent() - lastRealizedScan < 60 && realizedPnLToday != 0)
|
|
return realizedPnLToday;
|
|
realizedPnLToday = SumRealizedPnLSince(TodayStartBroker());
|
|
lastRealizedScan = TimeCurrent();
|
|
return realizedPnLToday;
|
|
}
|
|
|
|
double GetRealizedPnLWeek()
|
|
{
|
|
if(TimeCurrent() - lastWeeklyScan < 120 && realizedPnLWeek != 0)
|
|
return realizedPnLWeek;
|
|
realizedPnLWeek = SumRealizedPnLSince(WeekStartBroker());
|
|
lastWeeklyScan = TimeCurrent();
|
|
return realizedPnLWeek;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Compute floating P&L for this EA |
|
|
//+------------------------------------------------------------------+
|
|
double GetFloatingPnL()
|
|
{
|
|
double total = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
total += PositionGetDouble(POSITION_PROFIT);
|
|
total += PositionGetDouble(POSITION_SWAP);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check Drawdown — daily + weekly, per-EA (3.1B) |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckDailyDrawdown()
|
|
{
|
|
static bool dailyWarned = false;
|
|
static bool weeklyWarned = false;
|
|
|
|
// --- Daily rollover ---
|
|
datetime today = TodayStartBroker();
|
|
if(today != lastEquityReset)
|
|
{
|
|
dailyStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
lastEquityReset = today;
|
|
realizedPnLToday = 0;
|
|
lastRealizedScan = 0;
|
|
dailyWarned = false;
|
|
PrintS("Daily equity reset: $" + DoubleToString(dailyStartEquity, 2));
|
|
SaveGridState();
|
|
}
|
|
|
|
// --- Weekly rollover (Monday 00:00 broker) ---
|
|
datetime weekStart = WeekStartBroker();
|
|
if(weekStart != lastWeeklyReset)
|
|
{
|
|
weeklyStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
lastWeeklyReset = weekStart;
|
|
realizedPnLWeek = 0;
|
|
lastWeeklyScan = 0;
|
|
weeklyWarned = false;
|
|
PrintS("Weekly equity reset: $" + DoubleToString(weeklyStartEquity, 2));
|
|
SaveGridState();
|
|
}
|
|
|
|
double floating = GetFloatingPnL();
|
|
|
|
// --- Daily check ---
|
|
if(InpMaxDailyDrawdown > 0 && dailyStartEquity > 0)
|
|
{
|
|
double dailyPnL = GetRealizedPnLToday() + floating;
|
|
double dailyLimit = dailyStartEquity * InpMaxDailyDrawdown / 100.0;
|
|
if(dailyPnL <= -dailyLimit)
|
|
{
|
|
if(!dailyWarned)
|
|
{
|
|
PrintS("⚠️ DAILY DD HIT: $" + DoubleToString(dailyPnL, 2) +
|
|
" (limit: -$" + DoubleToString(dailyLimit, 2) + ")");
|
|
SendNotification("Grid EA " + _Symbol + ": DAILY DD reached ($" +
|
|
DoubleToString(dailyPnL, 2) + ")");
|
|
dailyWarned = true;
|
|
}
|
|
return false;
|
|
}
|
|
dailyWarned = false;
|
|
}
|
|
|
|
// --- Weekly check ---
|
|
if(InpMaxWeeklyDrawdown > 0 && weeklyStartEquity > 0)
|
|
{
|
|
double weeklyPnL = GetRealizedPnLWeek() + floating;
|
|
double weeklyLimit = weeklyStartEquity * InpMaxWeeklyDrawdown / 100.0;
|
|
if(weeklyPnL <= -weeklyLimit)
|
|
{
|
|
if(!weeklyWarned)
|
|
{
|
|
PrintS("⚠️ WEEKLY DD HIT: $" + DoubleToString(weeklyPnL, 2) +
|
|
" (limit: -$" + DoubleToString(weeklyLimit, 2) + ")");
|
|
SendNotification("Grid EA " + _Symbol + ": WEEKLY DD reached ($" +
|
|
DoubleToString(weeklyPnL, 2) + ")");
|
|
weeklyWarned = true;
|
|
}
|
|
return false;
|
|
}
|
|
weeklyWarned = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check Weekend Protection |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckWeekendProtection()
|
|
{
|
|
if(!InpCloseBeforeWeekend) return true;
|
|
MqlDateTime dt;
|
|
TimeToStruct(TimeCurrent(), dt);
|
|
|
|
if(dt.day_of_week != FRIDAY)
|
|
{
|
|
if(weekendCloseExecuted) weekendCloseExecuted = false;
|
|
return true;
|
|
}
|
|
if(weekendCloseExecuted) return true;
|
|
|
|
if(dt.hour >= InpWeekendCloseHour)
|
|
{
|
|
PrintS("⚠️ WEEKEND CLOSE — closing all positions");
|
|
SendNotificationEx("WEEKEND CLOSE", "Closing positions before weekend");
|
|
CloseAllPositions("Weekend protection");
|
|
if(InpCancelPendingBeforeWeekend)
|
|
CancelAllOrders("Weekend protection");
|
|
weekendCloseExecuted = true;
|
|
gridPlaced = false;
|
|
SaveGridState();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Session Filter (4.1) |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckSessionFilter()
|
|
{
|
|
if(!InpUseSessionFilter) return true;
|
|
MqlDateTime dt;
|
|
TimeToStruct(TimeCurrent(), dt);
|
|
int h = dt.hour;
|
|
bool inSession;
|
|
if(InpSessionStartHour <= InpSessionEndHour)
|
|
inSession = (h >= InpSessionStartHour && h < InpSessionEndHour);
|
|
else
|
|
inSession = (h >= InpSessionStartHour || h < InpSessionEndHour);
|
|
if(!inSession && DiagnosticModeOn)
|
|
PrintS("Session filter: outside window (" + IntegerToString(h) + "h)");
|
|
return inSession;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Spread Filter (3.4) |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckSpreadFilter()
|
|
{
|
|
if(InpMaxSpreadPoints <= 0) return true;
|
|
int spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
|
|
if(spread > InpMaxSpreadPoints)
|
|
{
|
|
if(DiagnosticModeOn)
|
|
PrintS("Spread filter: " + IntegerToString(spread) + "pts > " + IntegerToString(InpMaxSpreadPoints));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Correlation Cap (4.3) — count distinct symbols w/ open positions |
|
|
//+------------------------------------------------------------------+
|
|
int CountSymbolsWithOpenPositions(int posType)
|
|
{
|
|
string symbols[];
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
if(PositionGetInteger(POSITION_TYPE) != posType) continue;
|
|
string sym = PositionGetString(POSITION_SYMBOL);
|
|
bool found = false;
|
|
for(int j = 0; j < count; j++)
|
|
if(symbols[j] == sym) { found = true; break; }
|
|
if(!found)
|
|
{
|
|
ArrayResize(symbols, count + 1);
|
|
symbols[count] = sym;
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool CheckCorrelationCap(bool forLong)
|
|
{
|
|
int cap = forLong ? InpMaxLongSymbols : InpMaxShortSymbols;
|
|
if(cap <= 0) return true;
|
|
int type = forLong ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
|
|
int current = CountSymbolsWithOpenPositions(type);
|
|
// If this symbol already has positions, we're not adding a new distinct symbol
|
|
bool thisSymbolHasPos = false;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_TYPE) == type) { thisSymbolHasPos = true; break; }
|
|
}
|
|
if(thisSymbolHasPos) return true;
|
|
if(current >= cap)
|
|
{
|
|
if(DiagnosticModeOn)
|
|
PrintS("Correlation cap: " + IntegerToString(current) + "/" + IntegerToString(cap) +
|
|
(forLong ? " long" : " short") + " symbols open");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Adaptive Filters (4.4) |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateFilterRelaxation()
|
|
{
|
|
filtersRelaxed = false;
|
|
if(InpRelaxFilterAfterDays <= 0) return;
|
|
if(lastTradePlacedTime == 0) return; // never traded — don't relax yet
|
|
long secsIdle = (long)TimeCurrent() - (long)lastTradePlacedTime;
|
|
if(secsIdle > (long)InpRelaxFilterAfterDays * 86400)
|
|
filtersRelaxed = true;
|
|
}
|
|
|
|
double EffectiveADXMax()
|
|
{
|
|
return filtersRelaxed ? (ADXMax * InpRelaxADXFactor) : ADXMax;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Pivot Points |
|
|
//+------------------------------------------------------------------+
|
|
void CalculatePivotPoints()
|
|
{
|
|
MqlRates rates[1];
|
|
int copied = CopyRates(_Symbol, PERIOD_D1, 1, 1, rates);
|
|
if(copied < 1)
|
|
{
|
|
PrintS("Failed to get daily rates for pivot calculation");
|
|
return;
|
|
}
|
|
double prevHigh = rates[0].high;
|
|
double prevLow = rates[0].low;
|
|
double prevClose = rates[0].close;
|
|
|
|
PivotP = (prevHigh + prevLow + prevClose) / 3.0;
|
|
PivotR1 = (2.0 * PivotP) - prevLow;
|
|
PivotS1 = (2.0 * PivotP) - prevHigh;
|
|
PivotR2 = PivotP + (prevHigh - prevLow);
|
|
PivotS2 = PivotP - (prevHigh - prevLow);
|
|
|
|
if(UseAutoPivots)
|
|
{
|
|
double atr = 0;
|
|
if(ATRHandle != INVALID_HANDLE)
|
|
{
|
|
double atrBuf[1];
|
|
if(CopyBuffer(ATRHandle, 0, 0, 1, atrBuf) > 0)
|
|
atr = atrBuf[0];
|
|
}
|
|
if(UseATRFilter && atr > 0)
|
|
{
|
|
GridHigh = NormalizeDouble(PivotP + (atr * ATRMultiplier), _Digits);
|
|
GridLow = NormalizeDouble(PivotP - (atr * ATRMultiplier), _Digits);
|
|
}
|
|
else
|
|
{
|
|
GridHigh = PivotR1;
|
|
GridLow = PivotS1;
|
|
}
|
|
PrintS("AutoPivots: HIGH=" + DoubleToString(GridHigh, _Digits) +
|
|
" LOW=" + DoubleToString(GridLow, _Digits) +
|
|
" ATR=" + DoubleToString(atr, _Digits));
|
|
}
|
|
PrintS("Pivot: P=" + DoubleToString(PivotP, _Digits) +
|
|
" R1=" + DoubleToString(PivotR1, _Digits) +
|
|
" S1=" + DoubleToString(PivotS1, _Digits));
|
|
|
|
lastPivotCalcDate = TodayStartBroker();
|
|
SaveGridState();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Resolve Entry spacing at runtime (3.3 adaptive) |
|
|
//+------------------------------------------------------------------+
|
|
double ResolveEntryPoints()
|
|
{
|
|
if(!InpAdaptiveEntry) return Entry;
|
|
if(ATRHandle == INVALID_HANDLE) return Entry;
|
|
double buf[1];
|
|
if(CopyBuffer(ATRHandle, 0, 0, 1, buf) <= 0) return Entry;
|
|
double atr = buf[0];
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
if(point <= 0 || atr <= 0) return Entry;
|
|
double maxLvl = MathMax(1, MaxLevels);
|
|
double pts = (atr * InpEntryATRFactor) / maxLvl / point;
|
|
return MathMax(5.0, pts); // floor at 5pts
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if Market is Ranging |
|
|
//+------------------------------------------------------------------+
|
|
bool IsRangingMarket()
|
|
{
|
|
bool isRanging = true;
|
|
double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
if(UseRSIFilter && RSIHandle != INVALID_HANDLE)
|
|
{
|
|
double rsiBuf[1];
|
|
if(CopyBuffer(RSIHandle, 0, 0, 1, rsiBuf) > 0)
|
|
{
|
|
double rsi = rsiBuf[0];
|
|
if(rsi < RSILower || rsi > RSIUpper)
|
|
{
|
|
if(DiagnosticModeOn) PrintS("RSI extreme: " + DoubleToString(rsi, 1));
|
|
isRanging = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(UseADXFilter && ADXHandle != INVALID_HANDLE && isRanging)
|
|
{
|
|
double adxBuf[1];
|
|
if(CopyBuffer(ADXHandle, 0, 0, 1, adxBuf) > 0)
|
|
{
|
|
double adx = adxBuf[0];
|
|
double adxCap = EffectiveADXMax();
|
|
if(adx > adxCap)
|
|
{
|
|
if(DiagnosticModeOn)
|
|
PrintS("ADX trending: " + DoubleToString(adx, 1) + " > " + DoubleToString(adxCap, 1) +
|
|
(filtersRelaxed ? " (relaxed)" : ""));
|
|
isRanging = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
double actualHigh = (InpManualHigh > 0) ? InpManualHigh : GridHigh;
|
|
double actualLow = (InpManualLow > 0) ? InpManualLow : GridLow;
|
|
if(currentPrice > actualHigh || currentPrice < actualLow)
|
|
{
|
|
if(DiagnosticModeOn) PrintS("Price outside grid range");
|
|
isRanging = false;
|
|
}
|
|
return isRanging;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Breakout check (1.1 — no longer clobbers GridHigh/GridLow) |
|
|
//+------------------------------------------------------------------+
|
|
bool IsBreakout()
|
|
{
|
|
double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
if(currentPrice < PivotS2 || currentPrice > PivotR2)
|
|
{
|
|
PrintS("BREAKOUT: price=" + DoubleToString(currentPrice, _Digits) +
|
|
" S2=" + DoubleToString(PivotS2, _Digits) +
|
|
" R2=" + DoubleToString(PivotR2, _Digits));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Lot Size |
|
|
//+------------------------------------------------------------------+
|
|
double CalcLots()
|
|
{
|
|
double tmp = (AccountInfoDouble(ACCOUNT_EQUITY) - initEquity);
|
|
double a = EquityFactorPercent;
|
|
double b = LotsFactorPercent;
|
|
double lots;
|
|
|
|
if(0 == EquityFactorPercent || 0 == LotsFactorPercent)
|
|
lots = Lots;
|
|
else
|
|
{
|
|
a = initEquity * a / 100.0;
|
|
b = b / 100.0;
|
|
if(tmp > 0) tmp = MathPow(1 + b, (tmp / a));
|
|
else if(tmp < 0) tmp = MathPow(1 - b, MathAbs(tmp / a));
|
|
else tmp = 1;
|
|
lots = NormalizeDouble(Lots * tmp, lotDigits);
|
|
}
|
|
|
|
if(filtersRelaxed) lots *= InpRelaxLotFactor;
|
|
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
|
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
if(lots < minLot) lots = minLot;
|
|
if(lots > maxLot) lots = maxLot;
|
|
if(step > 0) lots = MathFloor(lots / step + 1e-9) * step;
|
|
return NormalizeDouble(lots, lotDigits);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Send Notification |
|
|
//+------------------------------------------------------------------+
|
|
void SendNotificationEx(string title, string subject)
|
|
{
|
|
if(MQLInfoInteger(MQL_OPTIMIZATION)) return;
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
string msg = "[" + _Symbol + "] " + title + ": " + subject;
|
|
msg += " | Px: " + DoubleToString(bid, _Digits);
|
|
msg += " | L: " + IntegerToString(longs) + " @ " + DoubleToString(longAvgPrice, _Digits);
|
|
msg += " | S: " + IntegerToString(shorts) + " @ " + DoubleToString(shortAvgPrice, _Digits);
|
|
msg += " | Eq: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2);
|
|
SendNotification(msg);
|
|
PrintS(title + ": " + subject);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Compute averages for this EA (1.3) |
|
|
//+------------------------------------------------------------------+
|
|
void CalcAvgPrice()
|
|
{
|
|
longs = 0; shorts = 0;
|
|
longAvgPrice = 0; longAvgLots = 0;
|
|
shortAvgPrice = 0; shortAvgLots = 0;
|
|
longProfit = 0; shortProfit = 0;
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
|
|
long ptype = PositionGetInteger(POSITION_TYPE);
|
|
double vol = PositionGetDouble(POSITION_VOLUME);
|
|
double open = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double profit = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
|
|
|
|
if(ptype == POSITION_TYPE_BUY)
|
|
{
|
|
longAvgPrice += open * vol;
|
|
longAvgLots += vol;
|
|
longProfit += profit;
|
|
longs++;
|
|
}
|
|
else if(ptype == POSITION_TYPE_SELL)
|
|
{
|
|
shortAvgPrice += open * vol;
|
|
shortAvgLots += vol;
|
|
shortProfit += profit;
|
|
shorts++;
|
|
}
|
|
}
|
|
if(longAvgLots > 0) longAvgPrice /= longAvgLots;
|
|
if(shortAvgLots > 0) shortAvgPrice /= shortAvgLots;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Breakeven move (4.2) |
|
|
//+------------------------------------------------------------------+
|
|
void ApplyBreakeven()
|
|
{
|
|
if(!InpUseBreakeven) return;
|
|
if(cyclePartialTPCount <= 0) return; // Nothing filled profitably yet
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
double buffer = InpBreakevenBufferPoints * point;
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
|
|
long ptype = PositionGetInteger(POSITION_TYPE);
|
|
double open = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double curSL = PositionGetDouble(POSITION_SL);
|
|
double tp = PositionGetDouble(POSITION_TP);
|
|
double newSL = (ptype == POSITION_TYPE_BUY) ? (open - buffer) : (open + buffer);
|
|
newSL = NormalizeDouble(newSL, _Digits);
|
|
|
|
bool alreadyAtBE = (ptype == POSITION_TYPE_BUY)
|
|
? (curSL >= open - point)
|
|
: (curSL <= open + point && curSL > 0);
|
|
if(alreadyAtBE) continue;
|
|
|
|
if(!trade.PositionModify(ticket, newSL, tp))
|
|
PrintS("Breakeven modify failed #" + IntegerToString((int)ticket) +
|
|
" err=" + IntegerToString((int)trade.ResultRetcode()));
|
|
else
|
|
PrintS("Breakeven set #" + IntegerToString((int)ticket) + " SL=" + DoubleToString(newSL, _Digits));
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Count partial TP fills since cycle start |
|
|
//+------------------------------------------------------------------+
|
|
int CountPartialTPsSinceCycle()
|
|
{
|
|
if(cycleStartTime == 0) return 0;
|
|
if(!HistorySelect(cycleStartTime, TimeCurrent() + 60)) return 0;
|
|
int n = 0;
|
|
int total = HistoryDealsTotal();
|
|
for(int i = 0; i < total; i++)
|
|
{
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != MagicNum) continue;
|
|
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol) continue;
|
|
long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
|
|
if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT) continue;
|
|
if(HistoryDealGetDouble(ticket, DEAL_PROFIT) > 0) n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Profit target check (2.1 + 2.2) |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckProfitTarget()
|
|
{
|
|
if(cycleStartEquity <= 0) return false;
|
|
double gain = AccountInfoDouble(ACCOUNT_EQUITY) - cycleStartEquity;
|
|
double pctGain = (gain / cycleStartEquity) * 100.0;
|
|
|
|
bool pctHit = (TakeProfitLevelPercent > 0 && pctGain >= TakeProfitLevelPercent);
|
|
bool dollHit = (TakeProfitLevelDollarAmount > 0 && gain >= TakeProfitLevelDollarAmount);
|
|
|
|
if(pctHit || dollHit)
|
|
{
|
|
PrintS("PROFIT TARGET — gain=$" + DoubleToString(gain, 2) +
|
|
" (" + DoubleToString(pctGain, 2) + "%) closing cycle");
|
|
SendNotificationEx("PROFIT TARGET HIT",
|
|
"Gain $" + DoubleToString(gain, 2) +
|
|
" (" + DoubleToString(pctGain, 2) + "%)");
|
|
CloseAllPositions("Profit target");
|
|
CancelAllOrders("Profit target");
|
|
EmitCycleReport(true);
|
|
gridPlaced = false;
|
|
cycleStartEquity = 0;
|
|
cycleStartTime = 0;
|
|
cyclePartialTPCount = 0;
|
|
SaveGridState();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Cycle Report (4.5) |
|
|
//+------------------------------------------------------------------+
|
|
void EmitCycleReport(bool targetHit)
|
|
{
|
|
if(!InpCycleReport) return;
|
|
if(cycleStartEquity <= 0) return;
|
|
double gain = AccountInfoDouble(ACCOUNT_EQUITY) - cycleStartEquity;
|
|
long dur = (long)TimeCurrent() - (long)cycleStartTime;
|
|
int hrs = (int)(dur / 3600);
|
|
int mins = (int)((dur % 3600) / 60);
|
|
string tag = targetHit ? "TARGET" : "END";
|
|
SendNotificationEx("CYCLE " + tag,
|
|
"P&L: $" + DoubleToString(gain, 2) +
|
|
" | Dur: " + IntegerToString(hrs) + "h" + IntegerToString(mins) + "m" +
|
|
" | Partials: " + IntegerToString(cyclePartialTPCount));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Order helpers |
|
|
//+------------------------------------------------------------------+
|
|
int CountPendingOrders(int type)
|
|
{
|
|
int count = 0;
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue;
|
|
if(OrderGetInteger(ORDER_TYPE) == type) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
void CancelAllOrders(string reason)
|
|
{
|
|
int cancelled = 0;
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue;
|
|
if(trade.OrderDelete(ticket)) cancelled++;
|
|
}
|
|
if(cancelled > 0) PrintS("Cancelled " + IntegerToString(cancelled) + " orders: " + reason);
|
|
}
|
|
|
|
void CloseAllPositions(string reason)
|
|
{
|
|
int closed = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
if(trade.PositionClose(ticket)) closed++;
|
|
}
|
|
if(closed > 0) PrintS("Closed " + IntegerToString(closed) + " positions: " + reason);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Close positions by direction (edge cleanup 5.0) |
|
|
//+------------------------------------------------------------------+
|
|
void ClosePositionsBySide(long posType, string reason)
|
|
{
|
|
int closed = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
if(PositionGetInteger(POSITION_TYPE) != posType) continue;
|
|
if(trade.PositionClose(ticket)) closed++;
|
|
}
|
|
if(closed > 0) PrintS("Closed " + IntegerToString(closed) + " " +
|
|
(posType == POSITION_TYPE_BUY ? "BUY" : "SELL") +
|
|
" positions: " + reason);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Place Buy/Sell Limit Orders |
|
|
//+------------------------------------------------------------------+
|
|
bool PlaceBuyLimit(double priceLevel, int level)
|
|
{
|
|
if(level >= MaxLevels) return false;
|
|
|
|
double lots = CalcLots();
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
|
|
|
|
double minDistance = currentAsk - stopLevel - (currentEntryPts * point);
|
|
if(priceLevel > minDistance)
|
|
{
|
|
if(DiagnosticModeOn) PrintS("BuyLimit " + IntegerToString(level) + " too close — skip");
|
|
return false;
|
|
}
|
|
|
|
double sl = 0;
|
|
if(InpUseStopLoss && StopLoss > 0)
|
|
sl = NormalizeDouble(priceLevel - (StopLoss * point), _Digits);
|
|
double tp = NormalizeDouble(priceLevel + (TP * point), _Digits);
|
|
if(tp <= priceLevel + stopLevel)
|
|
tp = NormalizeDouble(priceLevel + stopLevel + (TP * point), _Digits);
|
|
|
|
MqlTradeRequest req = {};
|
|
MqlTradeResult res = {};
|
|
req.action = TRADE_ACTION_PENDING;
|
|
req.symbol = _Symbol;
|
|
req.volume = lots;
|
|
req.price = NormalizeDouble(priceLevel, _Digits);
|
|
req.sl = sl;
|
|
req.tp = tp;
|
|
req.deviation = 10;
|
|
req.magic = MagicNum;
|
|
req.comment = "SG Buy " + IntegerToString(level);
|
|
req.type = ORDER_TYPE_BUY_LIMIT;
|
|
req.type_filling = GetPendingFilling();
|
|
|
|
if(!trade.OrderSend(req, res))
|
|
{
|
|
PrintS("BuyLimit error: " + trade.ResultRetcodeDescription() + " (" + IntegerToString((int)res.retcode) + ")");
|
|
return false;
|
|
}
|
|
if(res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED)
|
|
{
|
|
PrintS("BuyLimit L" + IntegerToString(level) + " @ " + DoubleToString(req.price, _Digits) +
|
|
" TP=" + DoubleToString(tp, _Digits) + " #" + IntegerToString((int)res.order));
|
|
lastTradePlacedTime = TimeCurrent();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PlaceSellLimit(double priceLevel, int level)
|
|
{
|
|
if(level >= MaxLevels) return false;
|
|
|
|
double lots = CalcLots();
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point;
|
|
|
|
double minDistance = currentBid + stopLevel + (currentEntryPts * point);
|
|
if(priceLevel < minDistance)
|
|
{
|
|
if(DiagnosticModeOn) PrintS("SellLimit " + IntegerToString(level) + " too close — skip");
|
|
return false;
|
|
}
|
|
|
|
double sl = 0;
|
|
if(InpUseStopLoss && StopLoss > 0)
|
|
sl = NormalizeDouble(priceLevel + (StopLoss * point), _Digits);
|
|
double tp = NormalizeDouble(priceLevel - (TP * point), _Digits);
|
|
if(tp >= priceLevel - stopLevel)
|
|
tp = NormalizeDouble(priceLevel - stopLevel - (TP * point), _Digits);
|
|
|
|
MqlTradeRequest req = {};
|
|
MqlTradeResult res = {};
|
|
req.action = TRADE_ACTION_PENDING;
|
|
req.symbol = _Symbol;
|
|
req.volume = lots;
|
|
req.price = NormalizeDouble(priceLevel, _Digits);
|
|
req.sl = sl;
|
|
req.tp = tp;
|
|
req.deviation = 10;
|
|
req.magic = MagicNum;
|
|
req.comment = "SG Sell " + IntegerToString(level);
|
|
req.type = ORDER_TYPE_SELL_LIMIT;
|
|
req.type_filling = GetPendingFilling();
|
|
|
|
if(!trade.OrderSend(req, res))
|
|
{
|
|
PrintS("SellLimit error: " + trade.ResultRetcodeDescription() + " (" + IntegerToString((int)res.retcode) + ")");
|
|
return false;
|
|
}
|
|
if(res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED)
|
|
{
|
|
PrintS("SellLimit L" + IntegerToString(level) + " @ " + DoubleToString(req.price, _Digits) +
|
|
" TP=" + DoubleToString(tp, _Digits) + " #" + IntegerToString((int)res.order));
|
|
lastTradePlacedTime = TimeCurrent();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Handle GetOut mode |
|
|
//+------------------------------------------------------------------+
|
|
void HandleGetOut()
|
|
{
|
|
bool getOutLongs = (GetOut=="L"||GetOut=="l"||GetOut=="A"||GetOut=="a"||GetOut=="X"||GetOut=="x");
|
|
bool getOutShorts = (GetOut=="S"||GetOut=="s"||GetOut=="A"||GetOut=="a"||GetOut=="X"||GetOut=="x");
|
|
|
|
if(!bGetOutHandled)
|
|
{
|
|
PrintS("GET OUT (" + GetOut + ") — closing " +
|
|
(getOutLongs ? "L " : "") + (getOutShorts ? "S" : ""));
|
|
bGetOutHandled = true;
|
|
}
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
long ptype = PositionGetInteger(POSITION_TYPE);
|
|
if(ptype == POSITION_TYPE_BUY && getOutLongs) trade.PositionClose(ticket);
|
|
if(ptype == POSITION_TYPE_SELL && getOutShorts) trade.PositionClose(ticket);
|
|
}
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue;
|
|
trade.OrderDelete(ticket);
|
|
}
|
|
gridPlaced = false;
|
|
SaveGridState();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Master global shutdown — one-shot (1.4) |
|
|
//+------------------------------------------------------------------+
|
|
void GlobalShutdown()
|
|
{
|
|
int posCount = 0, ordCount = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
posCount++;
|
|
trade.PositionClose(ticket);
|
|
}
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue;
|
|
ordCount++;
|
|
trade.OrderDelete(ticket);
|
|
}
|
|
PrintS("MASTER SHUTDOWN — closed " + IntegerToString(posCount) + " pos, " +
|
|
IntegerToString(ordCount) + " orders");
|
|
gridPlaced = false;
|
|
SaveGridState();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Range Drift (4.6) |
|
|
//+------------------------------------------------------------------+
|
|
bool CheckRangeDrift()
|
|
{
|
|
if(!InpRangeDriftEnable) return false;
|
|
if(InpManualHigh > 0 || InpManualLow > 0) return false; // manual mode — don't drift
|
|
if(GridHigh <= 0 || GridLow <= 0) return false;
|
|
|
|
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
double mid = (GridHigh + GridLow) / 2.0;
|
|
double drift = MathAbs(price - mid) / point;
|
|
|
|
if(drift > InpMoveRangeTrigger)
|
|
{
|
|
double shift = price - mid;
|
|
GridHigh = NormalizeDouble(GridHigh + shift, _Digits);
|
|
GridLow = NormalizeDouble(GridLow + shift, _Digits);
|
|
PrintS("Range drift — shifted by " + DoubleToString(shift, _Digits) +
|
|
" new HIGH=" + DoubleToString(GridHigh, _Digits) +
|
|
" LOW=" + DoubleToString(GridLow, _Digits));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert initialization |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
trade.SetExpertMagicNumber(MagicNum);
|
|
trade.SetDeviationInPoints(10);
|
|
trade.SetTypeFilling(GetMarketFilling());
|
|
|
|
initEquity = (double)BaseEquity;
|
|
lotDigits = 2;
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
if(minLot > 0)
|
|
{
|
|
double l = minLot; lotDigits = 0;
|
|
while(l < 1 && lotDigits < 6) { l *= 10; lotDigits++; }
|
|
}
|
|
|
|
if(UseRSIFilter) RSIHandle = iRSI(_Symbol, PERIOD_CURRENT, RSIPeriod, PRICE_CLOSE);
|
|
if(UseADXFilter) ADXHandle = iADX(_Symbol, PERIOD_CURRENT, ADXPeriod);
|
|
if(UseATRFilter || InpAdaptiveEntry || UseAutoPivots) ATRHandle = iATR(_Symbol, PERIOD_CURRENT, ATRPeriod);
|
|
|
|
LoadGridState();
|
|
|
|
if(lastPivotCalcDate != TodayStartBroker())
|
|
CalculatePivotPoints();
|
|
|
|
bGetOutOK = (GetOut=="L"||GetOut=="l"||GetOut=="A"||GetOut=="a"||
|
|
GetOut=="S"||GetOut=="s"||GetOut=="N"||GetOut=="n"||
|
|
GetOut=="X"||GetOut=="x");
|
|
bOpenNewTradesOK = (OpenNewTrades=="Y"||OpenNewTrades=="y"||OpenNewTrades=="L"||OpenNewTrades=="l"||
|
|
OpenNewTrades=="N"||OpenNewTrades=="n"||OpenNewTrades=="S"||OpenNewTrades=="s");
|
|
bEnableLongs = (OpenNewTrades=="Y"||OpenNewTrades=="y"||OpenNewTrades=="L"||OpenNewTrades=="l");
|
|
bEnableShorts = (OpenNewTrades=="Y"||OpenNewTrades=="y"||OpenNewTrades=="S"||OpenNewTrades=="s");
|
|
|
|
if(!bGetOutOK) PrintS("WARNING: invalid GetOut value: " + GetOut);
|
|
if(!bOpenNewTradesOK) PrintS("WARNING: invalid OpenNewTrades value: " + OpenNewTrades);
|
|
|
|
PrintS(VERSION + " initialized — Magic=" + IntegerToString(MagicNum) +
|
|
" SL=" + (InpUseStopLoss ? "ON(" + IntegerToString(StopLoss) + ")" : "OFF") +
|
|
" AdaptiveEntry=" + (InpAdaptiveEntry ? "ON" : "OFF"));
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert deinitialization |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
SaveGridState();
|
|
if(RSIHandle != INVALID_HANDLE) IndicatorRelease(RSIHandle);
|
|
if(ADXHandle != INVALID_HANDLE) IndicatorRelease(ADXHandle);
|
|
if(ATRHandle != INVALID_HANDLE) IndicatorRelease(ATRHandle);
|
|
PrintS("Deinitialized (reason=" + IntegerToString(reason) + ")");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert tick function |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
// --- Master one-shot (1.4) ---
|
|
if(Master)
|
|
{
|
|
if(!masterShutdownDone)
|
|
{
|
|
GlobalShutdown();
|
|
masterShutdownDone = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// --- Per-EA daily drawdown (3.1B) ---
|
|
if(!CheckDailyDrawdown())
|
|
{
|
|
if(gridPlaced)
|
|
{
|
|
CancelAllOrders("Daily drawdown");
|
|
gridPlaced = false;
|
|
SaveGridState();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// --- Weekend protection ---
|
|
if(!CheckWeekendProtection())
|
|
return;
|
|
|
|
// --- GetOut mode ---
|
|
if(GetOut != "N" && GetOut != "n")
|
|
{
|
|
HandleGetOut();
|
|
return;
|
|
}
|
|
else if(bGetOutHandled)
|
|
bGetOutHandled = false;
|
|
|
|
// --- Pivot recalc (3.8 — reliable, runs once per broker day whenever tick arrives) ---
|
|
datetime today = TodayStartBroker();
|
|
if(today != lastPivotCalcDate)
|
|
{
|
|
PrintS("New day — recalc pivots, reset cycle flags");
|
|
if(gridPlaced)
|
|
{
|
|
CancelAllOrders("End of day");
|
|
gridPlaced = false;
|
|
}
|
|
if(cycleProfitStop)
|
|
cycleProfitStop = false;
|
|
CalculatePivotPoints(); // also updates lastPivotCalcDate & saves
|
|
}
|
|
|
|
// --- Profit target check runs every tick (cheap) ---
|
|
if(CheckProfitTarget())
|
|
return;
|
|
|
|
// --- Stats + breakeven throttled to every 5s (history scans are expensive) ---
|
|
static datetime lastStatsTime = 0;
|
|
if(TimeCurrent() - lastStatsTime >= 5)
|
|
{
|
|
CalcAvgPrice();
|
|
cyclePartialTPCount = CountPartialTPsSinceCycle();
|
|
ApplyBreakeven();
|
|
lastStatsTime = TimeCurrent();
|
|
}
|
|
|
|
// --- Bar-close gate for expensive checks ---
|
|
static datetime lastBarTime = 0;
|
|
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
|
|
if(currentBarTime == lastBarTime) return;
|
|
lastBarTime = currentBarTime;
|
|
|
|
// --- Update adaptive-filter state each bar ---
|
|
UpdateFilterRelaxation();
|
|
|
|
// --- Session + spread filters ---
|
|
if(!CheckSessionFilter()) return;
|
|
if(!CheckSpreadFilter()) return;
|
|
|
|
// --- Range filter ---
|
|
if(!IsRangingMarket())
|
|
{
|
|
if(gridPlaced)
|
|
{
|
|
PrintS("No longer ranging — cancelling grid");
|
|
CancelAllOrders("Range filter tripped");
|
|
gridPlaced = false;
|
|
SaveGridState();
|
|
}
|
|
// --- Edge cleanup (5.0): close wrong-side positions stranded outside grid ---
|
|
double actualHigh = (InpManualHigh > 0) ? InpManualHigh : GridHigh;
|
|
double actualLow = (InpManualLow > 0) ? InpManualLow : GridLow;
|
|
double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
if(currentPrice > actualHigh && actualHigh > 0)
|
|
ClosePositionsBySide(POSITION_TYPE_SELL, "Edge cleanup — above GridHigh");
|
|
else if(currentPrice < actualLow && actualLow > 0)
|
|
ClosePositionsBySide(POSITION_TYPE_BUY, "Edge cleanup — below GridLow");
|
|
return;
|
|
}
|
|
|
|
// --- Breakout check ---
|
|
if(IsBreakout())
|
|
{
|
|
CancelAllOrders("Breakout");
|
|
CloseAllPositions("Breakout");
|
|
EmitCycleReport(false);
|
|
gridPlaced = false;
|
|
cycleStartEquity = 0;
|
|
cycleStartTime = 0;
|
|
cyclePartialTPCount = 0;
|
|
SaveGridState();
|
|
return;
|
|
}
|
|
|
|
// --- Range drift (4.6) — re-center if price drifted ---
|
|
if(gridPlaced && CheckRangeDrift())
|
|
{
|
|
CancelAllOrders("Range drift");
|
|
gridPlaced = false; // will replace below
|
|
}
|
|
|
|
// --- Grid already placed: monitor fills & decide when to reset ---
|
|
if(gridPlaced)
|
|
{
|
|
int buyLimits = CountPendingOrders(ORDER_TYPE_BUY_LIMIT);
|
|
int sellLimits = CountPendingOrders(ORDER_TYPE_SELL_LIMIT);
|
|
int buyPos = 0, sellPos = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue;
|
|
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) buyPos++;
|
|
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) sellPos++;
|
|
}
|
|
|
|
// Cancel opposite-side limits once a direction fills (avoid overexposure)
|
|
if(buyPos > 0 && buyLimits > 0)
|
|
{
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue;
|
|
if(OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_BUY_LIMIT)
|
|
trade.OrderDelete(ticket);
|
|
}
|
|
}
|
|
if(sellPos > 0 && sellLimits > 0)
|
|
{
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = OrderGetTicket(i);
|
|
if(ticket == 0) continue;
|
|
if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
|
|
if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue;
|
|
if(OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_SELL_LIMIT)
|
|
trade.OrderDelete(ticket);
|
|
}
|
|
}
|
|
|
|
// Cycle closed?
|
|
if(buyLimits == 0 && sellLimits == 0 && buyPos == 0 && sellPos == 0)
|
|
{
|
|
if(InpStopAfterProfit && AccountInfoDouble(ACCOUNT_EQUITY) > cycleStartEquity)
|
|
{
|
|
PrintS("Profitable cycle — stop for today");
|
|
cycleProfitStop = true;
|
|
}
|
|
EmitCycleReport(false);
|
|
gridPlaced = false;
|
|
cycleStartEquity = 0;
|
|
cycleStartTime = 0;
|
|
cyclePartialTPCount = 0;
|
|
SaveGridState();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// --- Block new grids if profit-stop active ---
|
|
if(cycleProfitStop)
|
|
{
|
|
if(DiagnosticModeOn) PrintS("Profit-stop active — no new grid today");
|
|
return;
|
|
}
|
|
|
|
// --- Trade enable checks ---
|
|
if(OpenNewTrades == "N" || OpenNewTrades == "n") return;
|
|
if(!bEnableLongs && !bEnableShorts) return;
|
|
|
|
// --- Correlation cap (4.3) ---
|
|
bool canLong = bEnableLongs && CheckCorrelationCap(true);
|
|
bool canShort = bEnableShorts && CheckCorrelationCap(false);
|
|
if(!canLong && !canShort) return;
|
|
|
|
// --- Resolve grid geometry ---
|
|
double actualHigh = (InpManualHigh > 0) ? InpManualHigh : GridHigh;
|
|
double actualLow = (InpManualLow > 0) ? InpManualLow : GridLow;
|
|
if(actualHigh <= actualLow)
|
|
{
|
|
PrintS("Invalid grid bounds — skip");
|
|
return;
|
|
}
|
|
|
|
currentEntryPts = ResolveEntryPoints();
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
double entryPrice = currentEntryPts * point;
|
|
double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
// --- Derive effective levels from actual range (3.2) ---
|
|
double span = actualHigh - actualLow;
|
|
int derivedLevels = (entryPrice > 0) ? (int)(span / entryPrice) : MaxLevels;
|
|
int effectiveLevels = MathMin(MaxLevels, MathMax(1, derivedLevels));
|
|
|
|
PrintS("Placing grid — px=" + DoubleToString(currentPrice, _Digits) +
|
|
" [" + DoubleToString(actualLow, _Digits) + "-" + DoubleToString(actualHigh, _Digits) + "]" +
|
|
" entry=" + DoubleToString(currentEntryPts, 0) + "pts" +
|
|
" levels=" + IntegerToString(effectiveLevels) +
|
|
(filtersRelaxed ? " (relaxed)" : ""));
|
|
|
|
int buyCount = 0, sellCount = 0;
|
|
double noTradeZone = entryPrice * 2;
|
|
|
|
for(int i = 0; i < effectiveLevels; i++)
|
|
{
|
|
double buyLevel = actualLow + (i * entryPrice);
|
|
double sellLevel = actualHigh - (i * entryPrice);
|
|
|
|
if(canLong && buyLevel < currentPrice - noTradeZone)
|
|
if(PlaceBuyLimit(buyLevel, i)) buyCount++;
|
|
if(canShort && sellLevel > currentPrice + noTradeZone)
|
|
if(PlaceSellLimit(sellLevel, i)) sellCount++;
|
|
}
|
|
|
|
PrintS("Grid placed: " + IntegerToString(buyCount) + " buy, " +
|
|
IntegerToString(sellCount) + " sell limits");
|
|
|
|
if(buyCount > 0 || sellCount > 0)
|
|
{
|
|
gridPlaced = true;
|
|
cycleStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
cycleStartTime = TimeCurrent();
|
|
cyclePartialTPCount = 0;
|
|
SaveGridState();
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|