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));
| |||
}
| |||
//+------------------------------------------------------------------+
|