532 lines
16 KiB
MQL5
532 lines
16 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 <Trade/Trade.mqh>
|
|
|
|
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));
|
|
}
|
|
//+------------------------------------------------------------------+
|