//+------------------------------------------------------------------+ //| StrategyFrameworkEA.mq5 | //| Reusable MT5 Expert Advisor framework | //| Strategy logic is intentionally separated from trade management | //+------------------------------------------------------------------+ #property strict #property version "1.00" #property description "Reusable EA framework. Add your strategy in CheckBuySetup/CheckSellSetup." #include CTrade trade; //=========================== INPUTS ================================= //--- General input ulong InpMagicNumber = 26081001; input bool InpAllowBuy = true; input bool InpAllowSell = true; input bool InpOneTradePerBar = true; input int InpMaxOpenTrades = 1; //--- Risk input double InpRiskPercent = 1.0; // % of equity risked per trade input double InpMaxDailyLossPercent = 3.0; // EA stops opening new trades input double InpMaxSpreadPoints = 30.0; //--- Stop/Target input double InpStopLossPoints = 300.0; input double InpTakeProfitPoints = 600.0; //--- Trading session (server time) input bool InpUseSessionFilter = true; input int InpSessionStartHour = 8; input int InpSessionStartMinute = 0; input int InpSessionEndHour = 18; input int InpSessionEndMinute = 0; //--- Break-even input bool InpUseBreakEven = false; input double InpBreakEvenTriggerPts = 300.0; input double InpBreakEvenLockPts = 20.0; //--- Trailing stop input bool InpUseTrailingStop = false; input double InpTrailingStartPts = 400.0; input double InpTrailingDistancePts = 250.0; //=========================== GLOBALS ================================ datetime g_lastBarTime = 0; string g_dayGVName = ""; //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(InpMagicNumber); trade.SetTypeFillingBySymbol(_Symbol); g_dayGVName = "EA_DailyStart_" + IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN)) + "_" + IntegerToString((long)InpMagicNumber); ResetDailyStartIfNeeded(); Print("StrategyFrameworkEA initialized on ", _Symbol); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick | //+------------------------------------------------------------------+ void OnTick() { ResetDailyStartIfNeeded(); // Existing trades are managed even when new entries are blocked. ManageOpenPositions(); if(!CanOpenNewTrade()) return; if(InpOneTradePerBar && !IsNewBar()) return; // ============================================================== // STRATEGY MODULE // Put your strategy rules inside these two functions. // ============================================================== bool buySignal = InpAllowBuy && CheckBuySetup(); bool sellSignal = InpAllowSell && CheckSellSetup(); // Prevent both directions from firing on the same tick. if(buySignal && !sellSignal) OpenBuy(); if(sellSignal && !buySignal) OpenSell(); } //+------------------------------------------------------------------+ //| BUY strategy placeholder | //| Return TRUE only when ALL your BUY conditions are satisfied. | //+------------------------------------------------------------------+ bool CheckBuySetup() { // EXAMPLE: // if(condition1 && condition2 && condition3) // return true; return(false); } //+------------------------------------------------------------------+ //| SELL strategy placeholder | //| Return TRUE only when ALL your SELL conditions are satisfied. | //+------------------------------------------------------------------+ bool CheckSellSetup() { // EXAMPLE: // if(condition1 && condition2 && condition3) // return true; return(false); } //+------------------------------------------------------------------+ //| Can a new trade be opened? | //+------------------------------------------------------------------+ bool CanOpenNewTrade() { if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) return(false); if(!MQLInfoInteger(MQL_TRADE_ALLOWED)) return(false); if(!IsWithinTradingSession()) return(false); if(CurrentSpreadPoints() > InpMaxSpreadPoints) return(false); if(DailyLossLimitReached()) return(false); if(CountOurOpenPositions() >= InpMaxOpenTrades) return(false); return(true); } //+------------------------------------------------------------------+ //| Open BUY | //+------------------------------------------------------------------+ void OpenBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(ask <= 0.0) return; double sl = 0.0; double tp = 0.0; if(InpStopLossPoints > 0) sl = NormalizePrice(ask - InpStopLossPoints * _Point); if(InpTakeProfitPoints > 0) tp = NormalizePrice(ask + InpTakeProfitPoints * _Point); double volume = CalculateRiskVolume(InpStopLossPoints); if(volume <= 0.0) return; if(!trade.Buy(volume, _Symbol, 0.0, sl, tp, "Strategy BUY")) Print("BUY failed. Retcode=", trade.ResultRetcode(), " ", trade.ResultRetcodeDescription()); } //+------------------------------------------------------------------+ //| Open SELL | //+------------------------------------------------------------------+ void OpenSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(bid <= 0.0) return; double sl = 0.0; double tp = 0.0; if(InpStopLossPoints > 0) sl = NormalizePrice(bid + InpStopLossPoints * _Point); if(InpTakeProfitPoints > 0) tp = NormalizePrice(bid - InpTakeProfitPoints * _Point); double volume = CalculateRiskVolume(InpStopLossPoints); if(volume <= 0.0) return; if(!trade.Sell(volume, _Symbol, 0.0, sl, tp, "Strategy SELL")) Print("SELL failed. Retcode=", trade.ResultRetcode(), " ", trade.ResultRetcodeDescription()); } //+------------------------------------------------------------------+ //| Risk-based position sizing | //+------------------------------------------------------------------+ double CalculateRiskVolume(double stopPoints) { if(stopPoints <= 0.0) { // If no SL is configured, use minimum volume rather than // pretending a risk-based calculation is possible. return(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); } double equity = AccountInfoDouble(ACCOUNT_EQUITY); double riskMoney = equity * (InpRiskPercent / 100.0); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); if(tickSize <= 0.0 || tickValue <= 0.0) return(0.0); double priceDistance = stopPoints * _Point; double moneyPerLot = (priceDistance / tickSize) * tickValue; if(moneyPerLot <= 0.0) return(0.0); double volume = riskMoney / moneyPerLot; return(NormalizeVolume(volume)); } //+------------------------------------------------------------------+ //| Manage open positions | //+------------------------------------------------------------------+ void ManageOpenPositions() { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; if(InpUseBreakEven) ApplyBreakEven(ticket); if(InpUseTrailingStop) ApplyTrailingStop(ticket); } } //+------------------------------------------------------------------+ //| Break-even | //+------------------------------------------------------------------+ void ApplyBreakEven(ulong ticket) { if(!PositionSelectByTicket(ticket)) return; long type = PositionGetInteger(POSITION_TYPE); double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); double sl = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); double currentPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); double profitPoints = (type == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / _Point : (openPrice - currentPrice) / _Point; if(profitPoints < InpBreakEvenTriggerPts) return; double newSL = (type == POSITION_TYPE_BUY) ? openPrice + InpBreakEvenLockPts * _Point : openPrice - InpBreakEvenLockPts * _Point; newSL = NormalizePrice(newSL); bool improve = false; if(type == POSITION_TYPE_BUY) improve = (sl == 0.0 || newSL > sl); else improve = (sl == 0.0 || newSL < sl); if(improve) trade.PositionModify(ticket, newSL, tp); } //+------------------------------------------------------------------+ //| Trailing stop | //+------------------------------------------------------------------+ void ApplyTrailingStop(ulong ticket) { if(!PositionSelectByTicket(ticket)) return; long type = PositionGetInteger(POSITION_TYPE); double currentSL = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); double currentPrice = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK); double profitPoints = (type == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / _Point : (openPrice - currentPrice) / _Point; if(profitPoints < InpTrailingStartPts) return; double newSL = (type == POSITION_TYPE_BUY) ? currentPrice - InpTrailingDistancePts * _Point : currentPrice + InpTrailingDistancePts * _Point; newSL = NormalizePrice(newSL); bool improve = false; if(type == POSITION_TYPE_BUY) improve = (currentSL == 0.0 || newSL > currentSL); else improve = (currentSL == 0.0 || newSL < currentSL); if(improve) trade.PositionModify(ticket, newSL, tp); } //+------------------------------------------------------------------+ //| Count this EA's open positions on this symbol | //+------------------------------------------------------------------+ int CountOurOpenPositions() { int count = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(!PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) == _Symbol && (ulong)PositionGetInteger(POSITION_MAGIC) == InpMagicNumber) { count++; } } return(count); } //+------------------------------------------------------------------+ //| Current spread in points | //+------------------------------------------------------------------+ double CurrentSpreadPoints() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(ask <= 0.0 || bid <= 0.0) return(999999.0); return((ask - bid) / _Point); } //+------------------------------------------------------------------+ //| Trading session | //+------------------------------------------------------------------+ bool IsWithinTradingSession() { if(!InpUseSessionFilter) return(true); MqlDateTime tm; TimeToStruct(TimeCurrent(), tm); int nowMinutes = tm.hour * 60 + tm.min; int startMinutes = InpSessionStartHour * 60 + InpSessionStartMinute; int endMinutes = InpSessionEndHour * 60 + InpSessionEndMinute; // Same start/end means session is treated as 24 hours. if(startMinutes == endMinutes) return(true); // Normal session. if(startMinutes < endMinutes) return(nowMinutes >= startMinutes && nowMinutes < endMinutes); // Overnight session, e.g. 22:00 -> 06:00. return(nowMinutes >= startMinutes || nowMinutes < endMinutes); } //+------------------------------------------------------------------+ //| Daily loss protection | //+------------------------------------------------------------------+ bool DailyLossLimitReached() { if(InpMaxDailyLossPercent <= 0.0) return(false); double startBalance = GetDailyStartBalance(); if(startBalance <= 0.0) return(false); double equity = AccountInfoDouble(ACCOUNT_EQUITY); double lossPercent = ((startBalance - equity) / startBalance) * 100.0; return(lossPercent >= InpMaxDailyLossPercent); } //+------------------------------------------------------------------+ //| Get/create today's starting balance | //+------------------------------------------------------------------+ double GetDailyStartBalance() { if(g_dayGVName == "") return(0.0); if(!GlobalVariableCheck(g_dayGVName)) GlobalVariableSet(g_dayGVName, AccountInfoDouble(ACCOUNT_BALANCE)); return(GlobalVariableGet(g_dayGVName)); } //+------------------------------------------------------------------+ //| Reset daily balance when server date changes | //+------------------------------------------------------------------+ void ResetDailyStartIfNeeded() { if(g_dayGVName == "") return; string dateName = g_dayGVName + "_DATE"; string today = TimeToString(TimeCurrent(), TIME_DATE); if(!GlobalVariableCheck(g_dayGVName) || !GlobalVariableCheck(dateName)) { GlobalVariableSet(g_dayGVName, AccountInfoDouble(ACCOUNT_BALANCE)); GlobalVariableSet(dateName, (double)StringToTime(today)); return; } datetime storedDate = (datetime)GlobalVariableGet(dateName); datetime todayDate = StringToTime(today); if(storedDate != todayDate) { GlobalVariableSet(g_dayGVName, AccountInfoDouble(ACCOUNT_BALANCE)); GlobalVariableSet(dateName, (double)todayDate); } } //+------------------------------------------------------------------+ //| Detect a new bar | //+------------------------------------------------------------------+ bool IsNewBar() { datetime currentBar = iTime(_Symbol, PERIOD_CURRENT, 0); if(currentBar == 0) return(false); if(currentBar != g_lastBarTime) { g_lastBarTime = currentBar; return(true); } return(false); } //+------------------------------------------------------------------+ //| Normalize price | //+------------------------------------------------------------------+ double NormalizePrice(double price) { int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); return(NormalizeDouble(price, digits)); } //+------------------------------------------------------------------+ //| Normalize volume to broker constraints | //+------------------------------------------------------------------+ double NormalizeVolume(double volume) { double minVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double stepVol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); if(stepVol <= 0.0) return(0.0); volume = MathMax(minVol, MathMin(maxVol, volume)); volume = MathFloor(volume / stepVol) * stepVol; if(volume < minVol) volume = minVol; int volumeDigits = 2; if(stepVol == 1.0) volumeDigits = 0; else if(stepVol == 0.1) volumeDigits = 1; else if(stepVol == 0.01) volumeDigits = 2; else if(stepVol == 0.001)volumeDigits = 3; return(NormalizeDouble(volume, volumeDigits)); } //+------------------------------------------------------------------+