feat(maea): migrate from MQL4 to MQL5 and simplify strategy

Migrate MAEA Expert Advisor from MetaTrader 4 to MetaTrader 5 platform with
significant strategy simplification and architectural improvements.

Key changes:
- Platform: MQL4 → MQL5
- Indicators: Reduced from 5 lines (3 EMAs + 2 borders) to 2 EMAs with zone fill
- Strategy: Removed pullback signals and dynamic lot sizing
- Lot sizing: Changed from dynamic (0.01/0.02) to fixed 0.01 lot
- Stop loss: Changed from Border Lines to opposite EMA (Buy: EMA Low, Sell: EMA High)
- Signal logic: Added opposite signal close for reversal protection
- New filter: Added minimum zone width filter (100 points)
- Documentation: Updated implementation plans and project summary
- New files: Added AGENTS.md, MAEA_Indicator.mq5, and opencode.jsonc

BREAKING CHANGE: Complete rewrite from MQL4 to MQL5 with simplified strategy.
Previous MAEA.mq4 and README.md deleted. New implementation requires MetaTrader 5.
This commit is contained in:
Kunthawat Greethong
2026-01-03 14:25:25 +07:00
parent cd0b2e35a2
commit db575179ae
8 changed files with 1019 additions and 1243 deletions

509
MAEA.mq5 Normal file
View File

@@ -0,0 +1,509 @@
//+------------------------------------------------------------------+
//| MAEA.mq5 |
//| Moving Average EA |
//| Trend-following EA with EMA zone breakout |
//+------------------------------------------------------------------+
#property copyright "MAEA"
#property link ""
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
#include <Indicators\Indicators.mqh>
// === EMA SETTINGS ===
input int EMAPeriod = 30;
input int EMAShift = 0;
input int MinZoneWidthPoints = 100;
// === RISK MANAGEMENT ===
input double LotSize = 0.01;
input double TakeProfitUSD = 5.0;
input int BreakevenPoints = 100;
input int TrailingStopPoints = 100;
// === FILTERS ===
input bool UseMTFFilter = true;
input bool UseNewsFilter = true;
input int MaxSpread = 30;
input int VolumePeriod = 20;
// === NEWS FILTER ===
input string NewsAvoidHours = "14,15,20,21";
input string NewsAvoidDays = "1,5";
// === RISK PROTECTION ===
input double MaxDrawdownPercent = 10.0;
// === EA SETTINGS ===
input int MagicNumber = 12345;
// === GLOBAL VARIABLES ===
CTrade trade;
int handleEMAHigh = INVALID_HANDLE;
int handleEMALow = INVALID_HANDLE;
int handleD1EMAHigh = INVALID_HANDLE;
int handleD1EMALow = INVALID_HANDLE;
double g_EMAHighBuffer[];
double g_EMALowBuffer[];
double g_D1EMAHighBuffer[];
double g_D1EMALowBuffer[];
datetime g_LastBarTime = 0;
double g_EntryPrice = 0;
double g_BreakevenPrice = 0;
bool g_BreakevenTriggered = false;
bool g_TradingAllowed = true;
double g_AccountHighBalance = 0;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit() {
Print("[MAEA] Initializing EA...");
trade.SetExpertMagicNumber(MagicNumber);
handleEMAHigh = iMA(_Symbol, PERIOD_CURRENT, EMAPeriod, EMAShift, MODE_EMA, PRICE_HIGH);
handleEMALow = iMA(_Symbol, PERIOD_CURRENT, EMAPeriod, EMAShift, MODE_EMA, PRICE_LOW);
handleD1EMAHigh = iMA(_Symbol, PERIOD_D1, EMAPeriod, EMAShift, MODE_EMA, PRICE_HIGH);
handleD1EMALow = iMA(_Symbol, PERIOD_D1, EMAPeriod, EMAShift, MODE_EMA, PRICE_LOW);
if(handleEMAHigh == INVALID_HANDLE || handleEMALow == INVALID_HANDLE ||
handleD1EMAHigh == INVALID_HANDLE || handleD1EMALow == INVALID_HANDLE) {
Print("[MAEA] Failed to create EMA handles");
return INIT_FAILED;
}
Print("[MAEA] Handles - EMAHigh: ", handleEMAHigh, ", EMALow: ", handleEMALow);
ArraySetAsSeries(g_EMAHighBuffer, true);
ArraySetAsSeries(g_EMALowBuffer, true);
ArraySetAsSeries(g_D1EMAHighBuffer, true);
ArraySetAsSeries(g_D1EMALowBuffer, true);
g_AccountHighBalance = AccountInfoDouble(ACCOUNT_BALANCE);
g_LastBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
Print("[MAEA] Initialization successful");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
Print("[MAEA] Deinitializing. Reason: ", reason);
if(handleEMAHigh != INVALID_HANDLE) IndicatorRelease(handleEMAHigh);
if(handleEMALow != INVALID_HANDLE) IndicatorRelease(handleEMALow);
if(handleD1EMAHigh != INVALID_HANDLE) IndicatorRelease(handleD1EMAHigh);
if(handleD1EMALow != INVALID_HANDLE) IndicatorRelease(handleD1EMALow);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick() {
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime != g_LastBarTime) {
g_LastBarTime = currentBarTime;
CheckDrawdown();
OnNewBar();
}
ManageOpenPosition();
}
//+------------------------------------------------------------------+
//| New bar processing |
//+------------------------------------------------------------------+
void OnNewBar() {
if(HasOpenPosition()) {
CheckOppositeSignal();
return;
}
CheckBreakthroughSignal();
}
//+------------------------------------------------------------------+
//| Check breakthrough signal and open position |
//+------------------------------------------------------------------+
void CheckBreakthroughSignal() {
if(!CopyIndicatorData()) return;
double prevClose = iClose(_Symbol, PERIOD_CURRENT, 1);
double currClose = iClose(_Symbol, PERIOD_CURRENT, 0);
double emaHigh = g_EMAHighBuffer[0];
double emaLow = g_EMALowBuffer[0];
double zoneWidth = MathAbs(emaHigh - emaLow);
if(zoneWidth < MinZoneWidthPoints * _Point) return;
bool prevInsideZone = (prevClose <= emaHigh && prevClose >= emaLow);
if(!prevInsideZone) return;
bool buySignal = (currClose > emaHigh);
bool sellSignal = (currClose < emaLow);
if(!buySignal && !sellSignal) return;
if(buySignal && !AllFiltersPass(true)) return;
if(sellSignal && !AllFiltersPass(false)) return;
if(buySignal) {
OpenBuyPosition();
}
else if(sellSignal) {
OpenSellPosition();
}
}
//+------------------------------------------------------------------+
//| Check opposite signal and close position |
//+------------------------------------------------------------------+
void CheckOppositeSignal() {
if(!CopyIndicatorData()) return;
if(!HasOpenPosition()) return;
double currClose = iClose(_Symbol, PERIOD_CURRENT, 0);
double prevClose = iClose(_Symbol, PERIOD_CURRENT, 1);
double emaHigh = g_EMAHighBuffer[0];
double emaLow = g_EMALowBuffer[0];
bool buySignal = (prevClose <= emaHigh && prevClose >= emaLow && currClose > emaHigh);
bool sellSignal = (prevClose <= emaHigh && prevClose >= emaLow && currClose < emaLow);
if(!buySignal && !sellSignal) return;
ulong positionTicket = GetPositionTicket();
if(positionTicket == 0) return;
ENUM_POSITION_TYPE posType = GetPositionType(positionTicket);
if(posType == POSITION_TYPE_BUY && sellSignal) {
trade.PositionClose(positionTicket);
ResetPositionVariables();
Print("Buy position closed due to opposite sell signal");
}
else if(posType == POSITION_TYPE_SELL && buySignal) {
trade.PositionClose(positionTicket);
ResetPositionVariables();
Print("Sell position closed due to opposite buy signal");
}
}
//+------------------------------------------------------------------+
//| Manage open position (TP, SL, breakeven, trailing stop) |
//+------------------------------------------------------------------+
void ManageOpenPosition() {
if(!HasOpenPosition()) return;
ulong positionTicket = GetPositionTicket();
if(positionTicket == 0) return;
if(!PositionSelectByTicket(positionTicket)) return;
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
double currentSL = PositionGetDouble(POSITION_SL);
double currentTP = PositionGetDouble(POSITION_TP);
double profit = PositionGetDouble(POSITION_PROFIT);
ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if(currentTP > 0 && profit >= TakeProfitUSD) {
trade.PositionClose(positionTicket);
ResetPositionVariables();
Print("Position closed by Take Profit");
return;
}
double pointsProfit = 0;
double breakevenLevel = 0;
if(posType == POSITION_TYPE_BUY) {
pointsProfit = (currentPrice - openPrice) / _Point;
breakevenLevel = openPrice + BreakevenPoints * _Point;
}
else {
pointsProfit = (openPrice - currentPrice) / _Point;
breakevenLevel = openPrice - BreakevenPoints * _Point;
}
if(!g_BreakevenTriggered && pointsProfit >= BreakevenPoints) {
if(posType == POSITION_TYPE_BUY) {
trade.PositionModify(positionTicket, openPrice, currentTP);
}
else {
trade.PositionModify(positionTicket, openPrice, currentTP);
}
g_BreakevenTriggered = true;
Print("Breakeven triggered for position ", positionTicket);
}
if(g_BreakevenTriggered && TrailingStopPoints > 0) {
double trailDistance = TrailingStopPoints * _Point;
double newSL;
if(posType == POSITION_TYPE_BUY) {
newSL = currentPrice - trailDistance;
if(newSL > currentSL + _Point && newSL > openPrice) {
trade.PositionModify(positionTicket, newSL, currentTP);
}
}
else {
newSL = currentPrice + trailDistance;
if(newSL < currentSL - _Point && newSL < openPrice) {
trade.PositionModify(positionTicket, newSL, currentTP);
}
}
}
}
//+------------------------------------------------------------------+
//| Open buy position |
//+------------------------------------------------------------------+
void OpenBuyPosition() {
if(!CopyIndicatorData()) return;
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = g_EMALowBuffer[0];
double tp = CalculateTakeProfit(ask, sl, true);
if(!trade.Buy(LotSize, _Symbol, ask, sl, tp, "MAEA Buy")) {
int err = GetLastError();
Print("Buy order failed. Error: ", err);
}
else {
g_EntryPrice = ask;
g_BreakevenTriggered = false;
Print("Buy position opened at ", ask);
}
}
//+------------------------------------------------------------------+
//| Open sell position |
//+------------------------------------------------------------------+
void OpenSellPosition() {
if(!CopyIndicatorData()) return;
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = g_EMAHighBuffer[0];
double tp = CalculateTakeProfit(bid, sl, false);
if(!trade.Sell(LotSize, _Symbol, bid, sl, tp, "MAEA Sell")) {
int err = GetLastError();
Print("Sell order failed. Error: ", err);
}
else {
g_EntryPrice = bid;
g_BreakevenTriggered = false;
Print("Sell position opened at ", bid);
}
}
//+------------------------------------------------------------------+
//| Calculate take profit in price |
//+------------------------------------------------------------------+
double CalculateTakeProfit(double entryPrice, double sl, bool isBuy) {
double pointValue = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double profitPoints = TakeProfitUSD / (tickValue * LotSize / tickSize);
double profitPrice = profitPoints * pointValue;
if(isBuy) {
return entryPrice + profitPrice;
}
else {
return entryPrice - profitPrice;
}
}
//+------------------------------------------------------------------+
//| Copy indicator data |
//+------------------------------------------------------------------+
bool CopyIndicatorData() {
if(CopyBuffer(handleEMAHigh, 0, 0, 10, g_EMAHighBuffer) <= 0) return false;
if(CopyBuffer(handleEMALow, 0, 0, 10, g_EMALowBuffer) <= 0) return false;
if(UseMTFFilter) {
if(CopyBuffer(handleD1EMAHigh, 0, 0, 2, g_D1EMAHighBuffer) <= 0) return false;
if(CopyBuffer(handleD1EMALow, 0, 0, 2, g_D1EMALowBuffer) <= 0) return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Check all filters |
//+------------------------------------------------------------------+
bool AllFiltersPass(bool isBuyDirection) {
if(!CheckVolumeFilter()) return false;
if(!CheckSpreadFilter()) return false;
if(!CheckDrawdownAllowed()) return false;
if(UseMTFFilter && !CheckMTFFilter(isBuyDirection)) return false;
if(UseNewsFilter && !CheckNewsFilter()) return false;
return true;
}
//+------------------------------------------------------------------+
//| Volume filter |
//+------------------------------------------------------------------+
bool CheckVolumeFilter() {
long currentVolume = iVolume(_Symbol, PERIOD_CURRENT, 0);
long avgVolume = 0;
for(int i = 1; i <= VolumePeriod; i++) {
avgVolume += iVolume(_Symbol, PERIOD_CURRENT, i);
}
avgVolume = avgVolume / VolumePeriod;
return (currentVolume > avgVolume);
}
//+------------------------------------------------------------------+
//| Spread filter |
//+------------------------------------------------------------------+
bool CheckSpreadFilter() {
long spread;
SymbolInfoInteger(_Symbol, SYMBOL_SPREAD, spread);
return (spread <= MaxSpread);
}
//+------------------------------------------------------------------+
//| MTF filter |
//+------------------------------------------------------------------+
bool CheckMTFFilter(bool isBuyDirection) {
double d1Close = iClose(_Symbol, PERIOD_D1, 0);
double d1EMAHigh = g_D1EMAHighBuffer[0];
double d1EMALow = g_D1EMALowBuffer[0];
if(d1Close > d1EMAHigh) {
return isBuyDirection;
}
else if(d1Close < d1EMALow) {
return !isBuyDirection;
}
else {
return false;
}
}
//+------------------------------------------------------------------+
//| News filter |
//+------------------------------------------------------------------+
bool CheckNewsFilter() {
datetime gmtTime = TimeGMT();
datetime thaiTime = gmtTime + 25200;
MqlDateTime timeStruct;
TimeToStruct(thaiTime, timeStruct);
string hoursArr[];
StringSplit(NewsAvoidHours, ',', hoursArr);
for(int i = 0; i < ArraySize(hoursArr); i++) {
string h = hoursArr[i];
StringTrimRight(h);
StringTrimLeft(h);
if(h == IntegerToString(timeStruct.hour)) {
return false;
}
}
string daysArr[];
StringSplit(NewsAvoidDays, ',', daysArr);
for(int i = 0; i < ArraySize(daysArr); i++) {
string d = daysArr[i];
StringTrimRight(d);
StringTrimLeft(d);
if(d == IntegerToString(timeStruct.day_of_week)) {
return false;
}
}
return true;
}
//+------------------------------------------------------------------+
//| Check drawdown |
//+------------------------------------------------------------------+
void CheckDrawdown() {
double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
if(currentBalance > g_AccountHighBalance) {
g_AccountHighBalance = currentBalance;
}
if(currentBalance > 0 && g_AccountHighBalance > 0) {
double drawdownPercent = ((g_AccountHighBalance - currentBalance) / g_AccountHighBalance) * 100;
if(drawdownPercent >= MaxDrawdownPercent) {
g_TradingAllowed = false;
Print("Drawdown limit reached. Trading stopped.");
}
else {
g_TradingAllowed = true;
}
}
}
//+------------------------------------------------------------------+
//| Check if trading is allowed based on drawdown |
//+------------------------------------------------------------------+
bool CheckDrawdownAllowed() {
return g_TradingAllowed;
}
//+------------------------------------------------------------------+
//| Check if has open position |
//+------------------------------------------------------------------+
bool HasOpenPosition() {
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(PositionGetSymbol(i) == _Symbol) {
ulong magic = PositionGetInteger(POSITION_MAGIC);
if(magic == MagicNumber) {
return true;
}
}
}
return false;
}
//+------------------------------------------------------------------+
//| Get position ticket |
//+------------------------------------------------------------------+
ulong GetPositionTicket() {
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(PositionGetSymbol(i) == _Symbol) {
ulong ticket = PositionGetInteger(POSITION_TICKET);
ulong magic = PositionGetInteger(POSITION_MAGIC);
if(magic == MagicNumber) {
return ticket;
}
}
}
return 0;
}
//+------------------------------------------------------------------+
//| Get position type |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE GetPositionType(ulong ticket) {
if(PositionSelectByTicket(ticket)) {
return (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
}
return POSITION_TYPE_BUY;
}
//+------------------------------------------------------------------+
//| Reset position variables |
//+------------------------------------------------------------------+
void ResetPositionVariables() {
g_EntryPrice = 0;
g_BreakevenTriggered = false;
}
//+------------------------------------------------------------------+