Files
opencode-skill/skills/mql-developer/references/architecture-patterns.md
Kunthawat Greethong b26c8199a5 Update skills: add website-creator, mql-developer, ecommerce-astro
Changes:
- Add FAL_KEY and GEMINI_API_KEY to .env.example
- Update picture-it to use ~/.config/opencode/.env (unified creds)
- Remove shodh-memory skill (no longer used)
- Remove alphaear-* skills (deprecated)
- Remove thai-frontend-dev skill (replaced by website-creator)
- Remove theme-factory skill
- Add mql-developer skill (MQL5 trading)
- Add ecommerce-astro skill (Astro e-commerce)
- Add website-creator skill (Next.js + Payload CMS)
- Update install script for new skills
2026-04-16 17:40:27 +07:00

2427 lines
76 KiB
Markdown

# Architecture Patterns
Reference for EA project structure, architecture patterns, and design patterns in MQL4/MQL5.
## Table of Contents
- [Project Structure](#project-structure)
- [EA Architecture Patterns](#ea-architecture-patterns)
- [Design Patterns for MQL](#design-patterns-for-mql)
- [Include File Design (.mqh)](#include-file-design-mqh)
- [Complete Templates](#complete-templates)
- [Quick Reference: When to Use Each Pattern](#quick-reference-when-to-use-each-pattern)
---
## Project Structure
### Full Modular Structure (Recommended for Professional EAs)
```
MQL5/ (or MQL4/)
├── Experts/
│ └── MyEA/
│ └── MyEA.mq5 // EA entry point (orchestrator)
├── Indicators/
│ └── MyIndicator.mq5
├── Scripts/
│ └── MyScript.mq5
├── Include/
│ ├── Core/
│ │ ├── CSignalBase.mqh // Abstract signal interface
│ │ ├── CSignalMA.mqh // MA crossover signal
│ │ ├── CSignalRSI.mqh // RSI signal
│ │ ├── CTradeManager.mqh // Order execution + retries
│ │ └── CRiskManager.mqh // Position sizing + drawdown
│ ├── Filters/
│ │ ├── CFilterBase.mqh // Abstract filter interface
│ │ ├── CTimeFilter.mqh // Session/day-of-week filter
│ │ ├── CSpreadFilter.mqh // Max spread filter
│ │ └── CVolatilityFilter.mqh // ATR-based volatility filter
│ ├── Communication/
│ │ ├── CHttpClient.mqh // WebRequest wrapper
│ │ └── CJsonHelper.mqh // JSON build/parse
│ ├── UI/
│ │ └── CPanel.mqh // On-chart trading panel
│ └── Utils/
│ ├── CLogger.mqh // Logging utility
│ └── CSymbolHelper.mqh // Multi-symbol helpers
└── Libraries/
└── MyLibrary.mq5 // Compiled library (ex4/ex5)
```
### Simplified Single-File Structure (Small Projects)
For simple strategies, a single `.mq5` or `.mq4` file is acceptable:
```
MQL5/
├── Experts/
│ └── SimpleMA_EA.mq5 // Everything in one file
```
Use single-file when:
- Strategy has one signal, one entry logic, basic risk management
- No need for code reuse across EAs
- Quick prototyping or proof of concept
Move to modular when:
- Multiple signal types or strategies
- Shared code between EAs
- Complex risk or filter logic
- Team collaboration
---
## EA Architecture Patterns
### 1. Simple Single-File EA
Basic template with the three core event handlers. Suitable for simple strategies.
#### MQL4 Simple Template
```mql4
//+------------------------------------------------------------------+
//| SimpleMA_EA.mq4 |
//+------------------------------------------------------------------+
#property copyright "Developer"
#property version "1.00"
#property strict
//--- Input parameters
input int InpMagicNumber = 12345; // Magic Number
input double InpLots = 0.1; // Lot Size
input int InpStopLoss = 50; // Stop Loss (pips)
input int InpTakeProfit = 100; // Take Profit (pips)
input int InpFastMA = 10; // Fast MA Period
input int InpSlowMA = 20; // Slow MA Period
//--- Global variables
double g_pipSize;
int g_slippage = 3;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
// Detect 4/5-digit broker
if(Digits == 3 || Digits == 5)
{
g_pipSize = Point * 10;
g_slippage = 30;
}
else
{
g_pipSize = Point;
g_slippage = 3;
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// Cleanup: remove objects, release handles
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Only trade on new bar
if(!IsNewBar())
return;
// Check if already in a position
if(CountOpenOrders() > 0)
return;
// Generate signal
int signal = GetSignal();
// Execute trade
if(signal == 1) // Buy signal
OpenBuy();
else if(signal == -1) // Sell signal
OpenSell();
}
//+------------------------------------------------------------------+
//| Signal generation: MA crossover |
//+------------------------------------------------------------------+
int GetSignal()
{
double fastMA_1 = iMA(NULL, 0, InpFastMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double fastMA_2 = iMA(NULL, 0, InpFastMA, 0, MODE_EMA, PRICE_CLOSE, 2);
double slowMA_1 = iMA(NULL, 0, InpSlowMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double slowMA_2 = iMA(NULL, 0, InpSlowMA, 0, MODE_EMA, PRICE_CLOSE, 2);
// Bullish crossover
if(fastMA_2 <= slowMA_2 && fastMA_1 > slowMA_1)
return 1;
// Bearish crossover
if(fastMA_2 >= slowMA_2 && fastMA_1 < slowMA_1)
return -1;
return 0;
}
//+------------------------------------------------------------------+
//| Open Buy order |
//+------------------------------------------------------------------+
void OpenBuy()
{
double sl = (InpStopLoss > 0) ? NormalizeDouble(Ask - InpStopLoss * g_pipSize, Digits) : 0;
double tp = (InpTakeProfit > 0) ? NormalizeDouble(Ask + InpTakeProfit * g_pipSize, Digits) : 0;
int ticket = OrderSend(Symbol(), OP_BUY, InpLots, Ask, g_slippage, sl, tp,
"SimpleMA Buy", InpMagicNumber, 0, clrGreen);
if(ticket < 0)
PrintFormat("OrderSend BUY failed: error %d", GetLastError());
else
PrintFormat("BUY opened: ticket=%d price=%.5f", ticket, Ask);
}
//+------------------------------------------------------------------+
//| Open Sell order |
//+------------------------------------------------------------------+
void OpenSell()
{
double sl = (InpStopLoss > 0) ? NormalizeDouble(Bid + InpStopLoss * g_pipSize, Digits) : 0;
double tp = (InpTakeProfit > 0) ? NormalizeDouble(Bid - InpTakeProfit * g_pipSize, Digits) : 0;
int ticket = OrderSend(Symbol(), OP_SELL, InpLots, Bid, g_slippage, sl, tp,
"SimpleMA Sell", InpMagicNumber, 0, clrRed);
if(ticket < 0)
PrintFormat("OrderSend SELL failed: error %d", GetLastError());
else
PrintFormat("SELL opened: ticket=%d price=%.5f", ticket, Bid);
}
//+------------------------------------------------------------------+
//| Count open orders for this EA |
//+------------------------------------------------------------------+
int CountOpenOrders()
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
if(OrderSymbol() == Symbol() && OrderMagicNumber() == InpMagicNumber)
count++;
}
}
return count;
}
//+------------------------------------------------------------------+
//| Detect new bar |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(NULL, 0, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
```
#### MQL5 Simple Template
```mql5
//+------------------------------------------------------------------+
//| SimpleMA_EA.mq5 |
//+------------------------------------------------------------------+
#property copyright "Developer"
#property version "1.00"
#include <Trade\Trade.mqh>
//--- Input parameters
input int InpMagicNumber = 12345; // Magic Number
input double InpLots = 0.1; // Lot Size
input int InpStopLoss = 50; // Stop Loss (pips)
input int InpTakeProfit = 100; // Take Profit (pips)
input int InpFastMA = 10; // Fast MA Period
input int InpSlowMA = 20; // Slow MA Period
//--- Global variables
CTrade g_trade;
double g_pipSize;
int g_handleFastMA;
int g_handleSlowMA;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
// Detect pip size
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
g_pipSize = (digits == 3 || digits == 5) ? _Point * 10 : _Point;
// Configure CTrade
g_trade.SetExpertMagicNumber(InpMagicNumber);
g_trade.SetDeviationInPoints(10);
g_trade.SetTypeFilling(DetectFillingPolicy());
// Create indicator handles
g_handleFastMA = iMA(_Symbol, PERIOD_CURRENT, InpFastMA, 0, MODE_EMA, PRICE_CLOSE);
g_handleSlowMA = iMA(_Symbol, PERIOD_CURRENT, InpSlowMA, 0, MODE_EMA, PRICE_CLOSE);
if(g_handleFastMA == INVALID_HANDLE || g_handleSlowMA == INVALID_HANDLE)
{
PrintFormat("Failed to create indicator handles");
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_handleFastMA != INVALID_HANDLE) IndicatorRelease(g_handleFastMA);
if(g_handleSlowMA != INVALID_HANDLE) IndicatorRelease(g_handleSlowMA);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Only trade on new bar
if(!IsNewBar())
return;
// Check if already in position
if(PositionSelect(_Symbol))
return;
// Get signal
int signal = GetSignal();
// Execute
if(signal == 1)
OpenBuy();
else if(signal == -1)
OpenSell();
}
//+------------------------------------------------------------------+
//| Signal generation: MA crossover |
//+------------------------------------------------------------------+
int GetSignal()
{
double fastMA[], slowMA[];
ArraySetAsSeries(fastMA, true);
ArraySetAsSeries(slowMA, true);
if(CopyBuffer(g_handleFastMA, 0, 0, 3, fastMA) < 3) return 0;
if(CopyBuffer(g_handleSlowMA, 0, 0, 3, slowMA) < 3) return 0;
// Bullish crossover (bar[2] -> bar[1])
if(fastMA[2] <= slowMA[2] && fastMA[1] > slowMA[1])
return 1;
// Bearish crossover
if(fastMA[2] >= slowMA[2] && fastMA[1] < slowMA[1])
return -1;
return 0;
}
//+------------------------------------------------------------------+
//| Open Buy |
//+------------------------------------------------------------------+
void OpenBuy()
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = (InpStopLoss > 0) ? NormalizeDouble(ask - InpStopLoss * g_pipSize, _Digits) : 0;
double tp = (InpTakeProfit > 0) ? NormalizeDouble(ask + InpTakeProfit * g_pipSize, _Digits) : 0;
if(!g_trade.Buy(InpLots, _Symbol, ask, sl, tp, "SimpleMA Buy"))
PrintFormat("Buy failed: %d - %s", g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
}
//+------------------------------------------------------------------+
//| Open Sell |
//+------------------------------------------------------------------+
void OpenSell()
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = (InpStopLoss > 0) ? NormalizeDouble(bid + InpStopLoss * g_pipSize, _Digits) : 0;
double tp = (InpTakeProfit > 0) ? NormalizeDouble(bid - InpTakeProfit * g_pipSize, _Digits) : 0;
if(!g_trade.Sell(InpLots, _Symbol, bid, sl, tp, "SimpleMA Sell"))
PrintFormat("Sell failed: %d - %s", g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription());
}
//+------------------------------------------------------------------+
//| Detect filling policy |
//+------------------------------------------------------------------+
ENUM_ORDER_TYPE_FILLING DetectFillingPolicy()
{
long filling = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
if((filling & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK)
return ORDER_FILLING_FOK;
if((filling & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC)
return ORDER_FILLING_IOC;
return ORDER_FILLING_RETURN;
}
//+------------------------------------------------------------------+
//| Detect new bar |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
```
---
### 2. Modular EA (Signal + Trade + Risk + Filter)
The recommended architecture for professional EAs. Each responsibility is isolated in its own class.
#### Signal Enum (shared definition)
```mql5
// File: Include/Core/CSignalBase.mqh
#ifndef CSIGNAL_BASE_MQH
#define CSIGNAL_BASE_MQH
//--- Signal types
enum ENUM_SIGNAL
{
SIGNAL_NO = 0, // No signal
SIGNAL_BUY = 1, // Buy signal
SIGNAL_SELL = -1 // Sell signal
};
//+------------------------------------------------------------------+
//| Abstract base class for signal generators |
//+------------------------------------------------------------------+
class CSignalBase
{
protected:
string m_symbol;
ENUM_TIMEFRAMES m_timeframe;
string m_name;
public:
CSignalBase(void) : m_symbol(_Symbol), m_timeframe(PERIOD_CURRENT), m_name("BaseSignal") {}
virtual ~CSignalBase(void) {}
//--- Initialization
virtual bool Init(string symbol, ENUM_TIMEFRAMES timeframe)
{
m_symbol = symbol;
m_timeframe = timeframe;
return true;
}
//--- Core method: must be overridden
virtual ENUM_SIGNAL GenerateSignal(void) = 0;
//--- Release indicator handles
virtual void Release(void) {}
//--- Accessors
string Name(void) const { return m_name; }
};
#endif
```
#### MA Crossover Signal Implementation
```mql5
// File: Include/Core/CSignalMA.mqh
#ifndef CSIGNAL_MA_MQH
#define CSIGNAL_MA_MQH
#include "CSignalBase.mqh"
//+------------------------------------------------------------------+
//| MA Crossover signal generator |
//+------------------------------------------------------------------+
class CSignalMA : public CSignalBase
{
private:
int m_fastPeriod;
int m_slowPeriod;
ENUM_MA_METHOD m_method;
int m_handleFast;
int m_handleSlow;
public:
CSignalMA(void) : m_fastPeriod(10), m_slowPeriod(20),
m_method(MODE_EMA),
m_handleFast(INVALID_HANDLE),
m_handleSlow(INVALID_HANDLE)
{
m_name = "MA_Crossover";
}
~CSignalMA(void) { Release(); }
//--- Configuration
void SetParameters(int fastPeriod, int slowPeriod, ENUM_MA_METHOD method)
{
m_fastPeriod = fastPeriod;
m_slowPeriod = slowPeriod;
m_method = method;
}
//--- Initialization
virtual bool Init(string symbol, ENUM_TIMEFRAMES timeframe) override
{
if(!CSignalBase::Init(symbol, timeframe))
return false;
m_handleFast = iMA(m_symbol, m_timeframe, m_fastPeriod, 0, m_method, PRICE_CLOSE);
m_handleSlow = iMA(m_symbol, m_timeframe, m_slowPeriod, 0, m_method, PRICE_CLOSE);
if(m_handleFast == INVALID_HANDLE || m_handleSlow == INVALID_HANDLE)
{
PrintFormat("[%s] Failed to create MA handles", m_name);
return false;
}
return true;
}
//--- Generate signal
virtual ENUM_SIGNAL GenerateSignal(void) override
{
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(m_handleFast, 0, 0, 3, fast) < 3) return SIGNAL_NO;
if(CopyBuffer(m_handleSlow, 0, 0, 3, slow) < 3) return SIGNAL_NO;
// Bullish crossover on completed bars [2] -> [1]
if(fast[2] <= slow[2] && fast[1] > slow[1])
return SIGNAL_BUY;
// Bearish crossover
if(fast[2] >= slow[2] && fast[1] < slow[1])
return SIGNAL_SELL;
return SIGNAL_NO;
}
//--- Release handles
virtual void Release(void) override
{
if(m_handleFast != INVALID_HANDLE) { IndicatorRelease(m_handleFast); m_handleFast = INVALID_HANDLE; }
if(m_handleSlow != INVALID_HANDLE) { IndicatorRelease(m_handleSlow); m_handleSlow = INVALID_HANDLE; }
}
};
#endif
```
#### Trade Manager
```mql5
// File: Include/Core/CTradeManager.mqh
#ifndef CTRADE_MANAGER_MQH
#define CTRADE_MANAGER_MQH
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
//+------------------------------------------------------------------+
//| Trade Manager: order execution with retry logic |
//+------------------------------------------------------------------+
class CTradeManager
{
private:
CTrade m_trade;
CPositionInfo m_position;
int m_magicNumber;
int m_maxRetries;
int m_retryDelayMs;
double m_pipSize;
//--- Detect filling policy
ENUM_ORDER_TYPE_FILLING DetectFillingPolicy(string symbol)
{
long filling = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((filling & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK)
return ORDER_FILLING_FOK;
if((filling & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC)
return ORDER_FILLING_IOC;
return ORDER_FILLING_RETURN;
}
public:
CTradeManager(void) : m_magicNumber(0), m_maxRetries(3), m_retryDelayMs(500), m_pipSize(0) {}
~CTradeManager(void) {}
//--- Initialization
bool Init(int magicNumber, string symbol, int maxRetries = 3)
{
m_magicNumber = magicNumber;
m_maxRetries = maxRetries;
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
m_pipSize = (digits == 3 || digits == 5) ? SymbolInfoDouble(symbol, SYMBOL_POINT) * 10
: SymbolInfoDouble(symbol, SYMBOL_POINT);
m_trade.SetExpertMagicNumber(m_magicNumber);
m_trade.SetDeviationInPoints(10);
m_trade.SetTypeFilling(DetectFillingPolicy(symbol));
return true;
}
//--- Open Buy with retry logic
bool OpenBuy(string symbol, double lots, double slPips, double tpPips, string comment = "")
{
for(int attempt = 0; attempt < m_maxRetries; attempt++)
{
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
double sl = (slPips > 0) ? NormalizeDouble(ask - slPips * m_pipSize, digits) : 0;
double tp = (tpPips > 0) ? NormalizeDouble(ask + tpPips * m_pipSize, digits) : 0;
if(m_trade.Buy(lots, symbol, ask, sl, tp, comment))
{
if(m_trade.ResultRetcode() == TRADE_RETCODE_DONE ||
m_trade.ResultRetcode() == TRADE_RETCODE_PLACED)
{
PrintFormat("[TradeManager] BUY OK: ticket=%d price=%.5f",
m_trade.ResultDeal(), m_trade.ResultPrice());
return true;
}
}
PrintFormat("[TradeManager] BUY attempt %d failed: %d - %s",
attempt + 1, m_trade.ResultRetcode(), m_trade.ResultRetcodeDescription());
Sleep(m_retryDelayMs);
}
return false;
}
//--- Open Sell with retry logic
bool OpenSell(string symbol, double lots, double slPips, double tpPips, string comment = "")
{
for(int attempt = 0; attempt < m_maxRetries; attempt++)
{
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
double sl = (slPips > 0) ? NormalizeDouble(bid + slPips * m_pipSize, digits) : 0;
double tp = (tpPips > 0) ? NormalizeDouble(bid - tpPips * m_pipSize, digits) : 0;
if(m_trade.Sell(lots, symbol, bid, sl, tp, comment))
{
if(m_trade.ResultRetcode() == TRADE_RETCODE_DONE ||
m_trade.ResultRetcode() == TRADE_RETCODE_PLACED)
{
PrintFormat("[TradeManager] SELL OK: ticket=%d price=%.5f",
m_trade.ResultDeal(), m_trade.ResultPrice());
return true;
}
}
PrintFormat("[TradeManager] SELL attempt %d failed: %d - %s",
attempt + 1, m_trade.ResultRetcode(), m_trade.ResultRetcodeDescription());
Sleep(m_retryDelayMs);
}
return false;
}
//--- Close all positions for a symbol
bool CloseAll(string symbol)
{
bool allClosed = true;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i))
{
if(m_position.Symbol() == symbol && m_position.Magic() == m_magicNumber)
{
if(!m_trade.PositionClose(m_position.Ticket()))
{
PrintFormat("[TradeManager] Close failed: ticket=%d error=%d",
m_position.Ticket(), m_trade.ResultRetcode());
allClosed = false;
}
}
}
}
return allClosed;
}
//--- Check if position exists for symbol
bool HasPosition(string symbol)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(m_position.SelectByIndex(i))
{
if(m_position.Symbol() == symbol && m_position.Magic() == m_magicNumber)
return true;
}
}
return false;
}
//--- Accessors
double PipSize(void) const { return m_pipSize; }
CTrade* Trade(void) { return &m_trade; }
};
#endif
```
#### Risk Manager
```mql5
// File: Include/Core/CRiskManager.mqh
#ifndef CRISK_MANAGER_MQH
#define CRISK_MANAGER_MQH
//+------------------------------------------------------------------+
//| Risk Manager: position sizing and drawdown control |
//+------------------------------------------------------------------+
class CRiskManager
{
private:
double m_riskPercent; // Risk per trade (%)
double m_maxDrawdownPercent; // Max drawdown allowed (%)
double m_maxLots; // Maximum lot size
double m_minLots; // Minimum lot size
double m_initialBalance; // Balance at EA start
public:
CRiskManager(void) : m_riskPercent(1.0), m_maxDrawdownPercent(20.0),
m_maxLots(10.0), m_minLots(0.01),
m_initialBalance(0) {}
~CRiskManager(void) {}
//--- Initialization
bool Init(double riskPercent, double maxDrawdownPercent,
double maxLots = 10.0, double minLots = 0.01)
{
m_riskPercent = riskPercent;
m_maxDrawdownPercent = maxDrawdownPercent;
m_maxLots = maxLots;
m_minLots = minLots;
m_initialBalance = AccountInfoDouble(ACCOUNT_BALANCE);
if(m_initialBalance <= 0)
{
Print("[RiskManager] Invalid initial balance");
return false;
}
return true;
}
//--- Calculate lot size based on risk percent and stop loss
double CalculateLots(string symbol, double slPips)
{
if(slPips <= 0)
return m_minLots;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskMoney = balance * m_riskPercent / 100.0;
// Get tick value for 1 lot
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
// Pip value calculation
double pipSize = (digits == 3 || digits == 5) ? point * 10 : point;
double pipValue = tickValue * (pipSize / tickSize);
if(pipValue <= 0)
return m_minLots;
double lots = riskMoney / (slPips * pipValue);
// Normalize to lot step
double lotStep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
lots = MathFloor(lots / lotStep) * lotStep;
// Clamp to limits
lots = MathMax(lots, m_minLots);
lots = MathMin(lots, m_maxLots);
double symbolMaxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double symbolMinLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
lots = MathMax(lots, symbolMinLot);
lots = MathMin(lots, symbolMaxLot);
return NormalizeDouble(lots, 2);
}
//--- Check if drawdown limit is exceeded
bool IsDrawdownExceeded(void)
{
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
// Use the higher of initial or current balance as reference
double reference = MathMax(m_initialBalance, balance);
if(reference <= 0)
return false;
double drawdownPercent = ((reference - equity) / reference) * 100.0;
return (drawdownPercent >= m_maxDrawdownPercent);
}
//--- Check if trading is allowed from risk perspective
bool CanTrade(void)
{
if(IsDrawdownExceeded())
{
PrintFormat("[RiskManager] Drawdown limit reached (%.1f%%)", m_maxDrawdownPercent);
return false;
}
// Check free margin
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
if(freeMargin < 100) // Minimum $100 free margin
{
Print("[RiskManager] Insufficient free margin");
return false;
}
return true;
}
//--- Accessors
double RiskPercent(void) const { return m_riskPercent; }
double MaxDrawdownPercent(void) const { return m_maxDrawdownPercent; }
};
#endif
```
#### Filter Base and Implementations
```mql5
// File: Include/Filters/CFilterBase.mqh
#ifndef CFILTER_BASE_MQH
#define CFILTER_BASE_MQH
//+------------------------------------------------------------------+
//| Abstract filter base |
//+------------------------------------------------------------------+
class CFilterBase
{
protected:
string m_name;
bool m_enabled;
public:
CFilterBase(void) : m_name("BaseFilter"), m_enabled(true) {}
virtual ~CFilterBase(void) {}
virtual bool IsAllowed(void) = 0;
void SetEnabled(bool enabled) { m_enabled = enabled; }
bool IsEnabled(void) const { return m_enabled; }
string Name(void) const { return m_name; }
};
#endif
```
```mql5
// File: Include/Filters/CTimeFilter.mqh
#ifndef CTIME_FILTER_MQH
#define CTIME_FILTER_MQH
#include "CFilterBase.mqh"
//+------------------------------------------------------------------+
//| Time/session filter |
//+------------------------------------------------------------------+
class CTimeFilter : public CFilterBase
{
private:
int m_startHour;
int m_endHour;
bool m_tradeFriday;
bool m_tradeSunday;
public:
CTimeFilter(void) : m_startHour(8), m_endHour(20),
m_tradeFriday(true), m_tradeSunday(false)
{
m_name = "TimeFilter";
}
void SetHours(int startHour, int endHour)
{
m_startHour = startHour;
m_endHour = endHour;
}
void SetDays(bool tradeFriday, bool tradeSunday)
{
m_tradeFriday = tradeFriday;
m_tradeSunday = tradeSunday;
}
virtual bool IsAllowed(void) override
{
if(!m_enabled)
return true;
MqlDateTime dt;
TimeCurrent(dt);
// Day-of-week filter (0=Sunday, 5=Friday)
if(dt.day_of_week == 0 && !m_tradeSunday) return false;
if(dt.day_of_week == 5 && !m_tradeFriday) return false;
if(dt.day_of_week == 6) return false; // Saturday
// Hour filter
if(m_startHour < m_endHour)
{
// Normal range: e.g. 8-20
return (dt.hour >= m_startHour && dt.hour < m_endHour);
}
else
{
// Overnight range: e.g. 22-6
return (dt.hour >= m_startHour || dt.hour < m_endHour);
}
}
};
#endif
```
```mql5
// File: Include/Filters/CSpreadFilter.mqh
#ifndef CSPREAD_FILTER_MQH
#define CSPREAD_FILTER_MQH
#include "CFilterBase.mqh"
//+------------------------------------------------------------------+
//| Spread filter |
//+------------------------------------------------------------------+
class CSpreadFilter : public CFilterBase
{
private:
string m_symbol;
double m_maxSpreadPips;
public:
CSpreadFilter(void) : m_symbol(_Symbol), m_maxSpreadPips(3.0)
{
m_name = "SpreadFilter";
}
void SetParameters(string symbol, double maxSpreadPips)
{
m_symbol = symbol;
m_maxSpreadPips = maxSpreadPips;
}
virtual bool IsAllowed(void) override
{
if(!m_enabled)
return true;
long spreadPoints = SymbolInfoInteger(m_symbol, SYMBOL_SPREAD);
double point = SymbolInfoDouble(m_symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(m_symbol, SYMBOL_DIGITS);
double pipSize = (digits == 3 || digits == 5) ? point * 10 : point;
double spreadPips = spreadPoints * point / pipSize;
return (spreadPips <= m_maxSpreadPips);
}
};
#endif
```
```mql5
// File: Include/Filters/CVolatilityFilter.mqh
#ifndef CVOLATILITY_FILTER_MQH
#define CVOLATILITY_FILTER_MQH
#include "CFilterBase.mqh"
//+------------------------------------------------------------------+
//| ATR-based volatility filter |
//+------------------------------------------------------------------+
class CVolatilityFilter : public CFilterBase
{
private:
string m_symbol;
ENUM_TIMEFRAMES m_timeframe;
int m_atrPeriod;
double m_minATRPips;
double m_maxATRPips;
int m_handleATR;
public:
CVolatilityFilter(void) : m_symbol(_Symbol), m_timeframe(PERIOD_CURRENT),
m_atrPeriod(14),
m_minATRPips(5.0), m_maxATRPips(50.0),
m_handleATR(INVALID_HANDLE)
{
m_name = "VolatilityFilter";
}
~CVolatilityFilter(void)
{
if(m_handleATR != INVALID_HANDLE)
IndicatorRelease(m_handleATR);
}
bool Init(string symbol, ENUM_TIMEFRAMES timeframe, int atrPeriod,
double minATRPips, double maxATRPips)
{
m_symbol = symbol;
m_timeframe = timeframe;
m_atrPeriod = atrPeriod;
m_minATRPips = minATRPips;
m_maxATRPips = maxATRPips;
m_handleATR = iATR(m_symbol, m_timeframe, m_atrPeriod);
return (m_handleATR != INVALID_HANDLE);
}
virtual bool IsAllowed(void) override
{
if(!m_enabled)
return true;
double atr[];
ArraySetAsSeries(atr, true);
if(CopyBuffer(m_handleATR, 0, 1, 1, atr) < 1)
return false;
double point = SymbolInfoDouble(m_symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(m_symbol, SYMBOL_DIGITS);
double pipSize = (digits == 3 || digits == 5) ? point * 10 : point;
double atrPips = atr[0] / pipSize;
return (atrPips >= m_minATRPips && atrPips <= m_maxATRPips);
}
};
#endif
```
#### Main EA: Orchestrator
```mql5
// File: Experts/ModularEA/ModularEA.mq5
#property copyright "Developer"
#property version "1.00"
#include <Core/CSignalMA.mqh>
#include <Core/CTradeManager.mqh>
#include <Core/CRiskManager.mqh>
#include <Filters/CTimeFilter.mqh>
#include <Filters/CSpreadFilter.mqh>
#include <Filters/CVolatilityFilter.mqh>
//--- Input parameters
input int InpMagicNumber = 12345; // Magic Number
input double InpRiskPercent = 1.0; // Risk per Trade (%)
input double InpMaxDrawdown = 20.0; // Max Drawdown (%)
input int InpStopLoss = 50; // Stop Loss (pips)
input int InpTakeProfit = 100; // Take Profit (pips)
input int InpFastMA = 10; // Fast MA Period
input int InpSlowMA = 20; // Slow MA Period
input int InpStartHour = 8; // Trading Start Hour
input int InpEndHour = 20; // Trading End Hour
input double InpMaxSpread = 3.0; // Max Spread (pips)
//--- Module instances
CSignalMA *g_signal;
CTradeManager *g_trade;
CRiskManager *g_risk;
CTimeFilter *g_timeFilter;
CSpreadFilter *g_spreadFilter;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Create modules
g_signal = new CSignalMA();
g_trade = new CTradeManager();
g_risk = new CRiskManager();
g_timeFilter = new CTimeFilter();
g_spreadFilter = new CSpreadFilter();
//--- Initialize signal
g_signal.SetParameters(InpFastMA, InpSlowMA, MODE_EMA);
if(!g_signal.Init(_Symbol, PERIOD_CURRENT))
{
Print("Signal initialization failed");
return(INIT_FAILED);
}
//--- Initialize trade manager
if(!g_trade.Init(InpMagicNumber, _Symbol))
{
Print("TradeManager initialization failed");
return(INIT_FAILED);
}
//--- Initialize risk manager
if(!g_risk.Init(InpRiskPercent, InpMaxDrawdown))
{
Print("RiskManager initialization failed");
return(INIT_FAILED);
}
//--- Initialize filters
g_timeFilter.SetHours(InpStartHour, InpEndHour);
g_spreadFilter.SetParameters(_Symbol, InpMaxSpread);
PrintFormat("ModularEA initialized on %s", _Symbol);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_signal != NULL) { g_signal.Release(); delete g_signal; }
if(g_trade != NULL) { delete g_trade; }
if(g_risk != NULL) { delete g_risk; }
if(g_timeFilter != NULL) { delete g_timeFilter; }
if(g_spreadFilter != NULL) { delete g_spreadFilter; }
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Only process on new bar
if(!IsNewBar())
return;
// Check filters
if(!g_timeFilter.IsAllowed()) return;
if(!g_spreadFilter.IsAllowed()) return;
// Check risk
if(!g_risk.CanTrade()) return;
// Skip if already in position
if(g_trade.HasPosition(_Symbol))
return;
// Generate signal
ENUM_SIGNAL signal = g_signal.GenerateSignal();
// Execute trade
if(signal == SIGNAL_BUY)
{
double lots = g_risk.CalculateLots(_Symbol, InpStopLoss);
g_trade.OpenBuy(_Symbol, lots, InpStopLoss, InpTakeProfit, "ModularEA Buy");
}
else if(signal == SIGNAL_SELL)
{
double lots = g_risk.CalculateLots(_Symbol, InpStopLoss);
g_trade.OpenSell(_Symbol, lots, InpStopLoss, InpTakeProfit, "ModularEA Sell");
}
}
//+------------------------------------------------------------------+
//| Detect new bar |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
```
---
### 3. State Machine Pattern
For EAs with complex lifecycle management. Each state has clear entry/exit conditions and transitions.
#### State Transition Diagram
```
┌─────────┐
│ IDLE │
└────┬────┘
│ OnInit success
v
┌─────────────────────┐
┌──────│ WAITING_FOR_SIGNAL │<──────────────┐
│ └─────────┬───────────┘ │
│ │ Signal detected │
│ v │
│ ┌─────────────────────┐ │
│ │ OPENING_POSITION │───────────┐ │
│ └─────────┬───────────┘ Failed │ │
│ │ Success │ │
│ v v │
│ ┌─────────────────────┐ ┌─────────┐
│ │ MANAGING_POSITION │ │ ERROR │
│ └─────────┬───────────┘ └────┬────┘
│ │ Exit condition │ Recovery
│ v │
│ ┌─────────────────────┐ │
│ │ CLOSING_POSITION │──────────┘
│ └─────────┬───────────┘
│ │ Closed
└────────────────┘
```
#### State Machine Code
```mql5
//+------------------------------------------------------------------+
//| State Machine EA |
//+------------------------------------------------------------------+
#property copyright "Developer"
#property version "1.00"
#include <Trade\Trade.mqh>
//--- EA States
enum ENUM_EA_STATE
{
STATE_IDLE = 0,
STATE_WAITING_SIGNAL = 1,
STATE_OPENING_POSITION = 2,
STATE_MANAGING_POSITION = 3,
STATE_CLOSING_POSITION = 4,
STATE_ERROR = 5
};
//--- Input parameters
input int InpMagicNumber = 12345;
input double InpLots = 0.1;
input int InpStopLoss = 50;
input int InpTakeProfit = 100;
//--- Globals
CTrade g_trade;
ENUM_EA_STATE g_state = STATE_IDLE;
int g_pendingSignal = 0;
int g_errorCount = 0;
datetime g_errorTime = 0;
ulong g_positionTicket = 0;
//+------------------------------------------------------------------+
int OnInit()
{
g_trade.SetExpertMagicNumber(InpMagicNumber);
g_state = STATE_WAITING_SIGNAL;
PrintFormat("State -> WAITING_FOR_SIGNAL");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnTick()
{
switch(g_state)
{
case STATE_WAITING_SIGNAL:
HandleWaitingSignal();
break;
case STATE_OPENING_POSITION:
HandleOpeningPosition();
break;
case STATE_MANAGING_POSITION:
HandleManagingPosition();
break;
case STATE_CLOSING_POSITION:
HandleClosingPosition();
break;
case STATE_ERROR:
HandleError();
break;
default:
g_state = STATE_WAITING_SIGNAL;
break;
}
}
//+------------------------------------------------------------------+
//| Wait for entry signal |
//+------------------------------------------------------------------+
void HandleWaitingSignal()
{
if(!IsNewBar())
return;
// Check for signal (example: price above/below MA)
double ma[];
ArraySetAsSeries(ma, true);
int handle = iMA(_Symbol, PERIOD_CURRENT, 20, 0, MODE_SMA, PRICE_CLOSE);
if(CopyBuffer(handle, 0, 1, 1, ma) < 1) return;
IndicatorRelease(handle);
double close = iClose(_Symbol, PERIOD_CURRENT, 1);
if(close > ma[0])
g_pendingSignal = 1; // Buy
else if(close < ma[0])
g_pendingSignal = -1; // Sell
else
return;
// Transition
g_state = STATE_OPENING_POSITION;
PrintFormat("State -> OPENING_POSITION (signal=%d)", g_pendingSignal);
}
//+------------------------------------------------------------------+
//| Execute the trade |
//+------------------------------------------------------------------+
void HandleOpeningPosition()
{
double pipSize = (_Digits == 3 || _Digits == 5) ? _Point * 10 : _Point;
bool result = false;
if(g_pendingSignal == 1)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = NormalizeDouble(ask - InpStopLoss * pipSize, _Digits);
double tp = NormalizeDouble(ask + InpTakeProfit * pipSize, _Digits);
result = g_trade.Buy(InpLots, _Symbol, ask, sl, tp);
}
else if(g_pendingSignal == -1)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = NormalizeDouble(bid + InpStopLoss * pipSize, _Digits);
double tp = NormalizeDouble(bid - InpTakeProfit * pipSize, _Digits);
result = g_trade.Sell(InpLots, _Symbol, bid, sl, tp);
}
if(result && (g_trade.ResultRetcode() == TRADE_RETCODE_DONE ||
g_trade.ResultRetcode() == TRADE_RETCODE_PLACED))
{
g_positionTicket = g_trade.ResultDeal();
g_state = STATE_MANAGING_POSITION;
PrintFormat("State -> MANAGING_POSITION (ticket=%d)", g_positionTicket);
}
else
{
g_errorCount++;
g_errorTime = TimeCurrent();
g_state = STATE_ERROR;
PrintFormat("State -> ERROR (retcode=%d)", g_trade.ResultRetcode());
}
}
//+------------------------------------------------------------------+
//| Manage open position (trailing, partial close, etc.) |
//+------------------------------------------------------------------+
void HandleManagingPosition()
{
// Check if position still exists
bool positionExists = false;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionGetInteger(POSITION_MAGIC) == InpMagicNumber &&
PositionGetString(POSITION_SYMBOL) == _Symbol)
{
positionExists = true;
break;
}
}
if(!positionExists)
{
// Position was closed (SL/TP hit)
g_state = STATE_WAITING_SIGNAL;
PrintFormat("State -> WAITING_FOR_SIGNAL (position closed by SL/TP)");
return;
}
// Add trailing stop, break-even, partial close logic here
}
//+------------------------------------------------------------------+
//| Close position |
//+------------------------------------------------------------------+
void HandleClosingPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionGetInteger(POSITION_MAGIC) == InpMagicNumber &&
PositionGetString(POSITION_SYMBOL) == _Symbol)
{
if(g_trade.PositionClose(ticket))
{
g_state = STATE_WAITING_SIGNAL;
PrintFormat("State -> WAITING_FOR_SIGNAL (position closed)");
return;
}
else
{
g_state = STATE_ERROR;
PrintFormat("State -> ERROR (close failed)");
return;
}
}
}
// No position found to close
g_state = STATE_WAITING_SIGNAL;
}
//+------------------------------------------------------------------+
//| Error recovery |
//+------------------------------------------------------------------+
void HandleError()
{
// Wait 30 seconds before retry
if(TimeCurrent() - g_errorTime < 30)
return;
if(g_errorCount >= 5)
{
PrintFormat("[ERROR] Max retries reached. EA stopped.");
ExpertRemove();
return;
}
// Reset to waiting state
g_state = STATE_WAITING_SIGNAL;
PrintFormat("State -> WAITING_FOR_SIGNAL (error recovery, count=%d)", g_errorCount);
}
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
```
---
### 4. Multi-Timeframe Analysis
Check signals on higher timeframes and execute on lower timeframes for better precision.
#### MQL4 Multi-Timeframe
```mql4
//+------------------------------------------------------------------+
//| Multi-Timeframe Signal - MQL4 |
//| Chart: M15, Signal confirmation: H4 |
//+------------------------------------------------------------------+
#property strict
input int InpMAPeriod = 20;
int GetMultiTFSignal()
{
//--- Higher timeframe trend (H4)
double h4_ma_1 = iMA(NULL, PERIOD_H4, InpMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);
double h4_ma_2 = iMA(NULL, PERIOD_H4, InpMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 2);
double h4_close = iClose(NULL, PERIOD_H4, 1);
int h4Trend = 0;
if(h4_close > h4_ma_1 && h4_ma_1 > h4_ma_2)
h4Trend = 1; // Bullish trend on H4
else if(h4_close < h4_ma_1 && h4_ma_1 < h4_ma_2)
h4Trend = -1; // Bearish trend on H4
//--- Entry timeframe signal (M15 - current chart)
double m15_fast_1 = iMA(NULL, PERIOD_M15, 10, 0, MODE_EMA, PRICE_CLOSE, 1);
double m15_fast_2 = iMA(NULL, PERIOD_M15, 10, 0, MODE_EMA, PRICE_CLOSE, 2);
double m15_slow_1 = iMA(NULL, PERIOD_M15, 20, 0, MODE_EMA, PRICE_CLOSE, 1);
double m15_slow_2 = iMA(NULL, PERIOD_M15, 20, 0, MODE_EMA, PRICE_CLOSE, 2);
// Buy: H4 bullish + M15 bullish crossover
if(h4Trend == 1 && m15_fast_2 <= m15_slow_2 && m15_fast_1 > m15_slow_1)
return 1;
// Sell: H4 bearish + M15 bearish crossover
if(h4Trend == -1 && m15_fast_2 >= m15_slow_2 && m15_fast_1 < m15_slow_1)
return -1;
return 0;
}
```
#### MQL5 Multi-Timeframe
```mql5
//+------------------------------------------------------------------+
//| Multi-Timeframe Signal - MQL5 |
//| Uses indicator handles per timeframe |
//+------------------------------------------------------------------+
class CSignalMultiTF
{
private:
int m_handleH4_MA; // Trend MA on H4
int m_handleM15_Fast; // Entry fast MA on M15
int m_handleM15_Slow; // Entry slow MA on M15
string m_symbol;
public:
CSignalMultiTF(void) : m_handleH4_MA(INVALID_HANDLE),
m_handleM15_Fast(INVALID_HANDLE),
m_handleM15_Slow(INVALID_HANDLE) {}
~CSignalMultiTF(void) { Release(); }
bool Init(string symbol)
{
m_symbol = symbol;
// Create handles for different timeframes
m_handleH4_MA = iMA(m_symbol, PERIOD_H4, 20, 0, MODE_EMA, PRICE_CLOSE);
m_handleM15_Fast = iMA(m_symbol, PERIOD_M15, 10, 0, MODE_EMA, PRICE_CLOSE);
m_handleM15_Slow = iMA(m_symbol, PERIOD_M15, 20, 0, MODE_EMA, PRICE_CLOSE);
if(m_handleH4_MA == INVALID_HANDLE ||
m_handleM15_Fast == INVALID_HANDLE ||
m_handleM15_Slow == INVALID_HANDLE)
{
Print("Failed to create MTF indicator handles");
return false;
}
return true;
}
int GetSignal(void)
{
//--- Get H4 trend data
double h4MA[];
ArraySetAsSeries(h4MA, true);
if(CopyBuffer(m_handleH4_MA, 0, 0, 3, h4MA) < 3) return 0;
// Also need H4 close
double h4Close[];
ArraySetAsSeries(h4Close, true);
if(CopyClose(m_symbol, PERIOD_H4, 0, 2, h4Close) < 2) return 0;
int h4Trend = 0;
if(h4Close[1] > h4MA[1] && h4MA[1] > h4MA[2])
h4Trend = 1;
else if(h4Close[1] < h4MA[1] && h4MA[1] < h4MA[2])
h4Trend = -1;
if(h4Trend == 0)
return 0;
//--- Get M15 crossover data
double fast[], slow[];
ArraySetAsSeries(fast, true);
ArraySetAsSeries(slow, true);
if(CopyBuffer(m_handleM15_Fast, 0, 0, 3, fast) < 3) return 0;
if(CopyBuffer(m_handleM15_Slow, 0, 0, 3, slow) < 3) return 0;
// Buy: H4 up + M15 bullish cross
if(h4Trend == 1 && fast[2] <= slow[2] && fast[1] > slow[1])
return 1;
// Sell: H4 down + M15 bearish cross
if(h4Trend == -1 && fast[2] >= slow[2] && fast[1] < slow[1])
return -1;
return 0;
}
void Release(void)
{
if(m_handleH4_MA != INVALID_HANDLE) { IndicatorRelease(m_handleH4_MA); m_handleH4_MA = INVALID_HANDLE; }
if(m_handleM15_Fast != INVALID_HANDLE) { IndicatorRelease(m_handleM15_Fast); m_handleM15_Fast = INVALID_HANDLE; }
if(m_handleM15_Slow != INVALID_HANDLE) { IndicatorRelease(m_handleM15_Slow); m_handleM15_Slow = INVALID_HANDLE; }
}
};
```
---
### 5. Multi-Symbol EA
Trade multiple symbols from a single EA instance. Uses `OnTimer` instead of `OnTick` because `OnTick` only fires for the chart symbol.
```mql5
//+------------------------------------------------------------------+
//| Multi-Symbol EA |
//| Uses OnTimer for symbol-independent processing |
//+------------------------------------------------------------------+
#property copyright "Developer"
#property version "1.00"
#include <Trade\Trade.mqh>
//--- Input parameters
input string InpSymbols = "EURUSD,GBPUSD,USDJPY,AUDUSD"; // Symbols (comma-separated)
input int InpMagicNumber = 55555;
input double InpLots = 0.1;
input int InpTimerSeconds = 5; // Check interval (seconds)
//--- Symbol management
string g_symbols[];
int g_symbolCount;
CTrade g_trade;
//--- Per-symbol indicator handles
int g_handleMA[]; // One handle per symbol
//+------------------------------------------------------------------+
int OnInit()
{
// Parse symbol list
g_symbolCount = StringSplit(InpSymbols, ',', g_symbols);
if(g_symbolCount <= 0)
{
Print("No symbols specified");
return(INIT_FAILED);
}
// Trim whitespace from symbol names
for(int i = 0; i < g_symbolCount; i++)
StringTrimLeft(StringTrimRight(g_symbols[i]));
// Validate symbols and select them in Market Watch
for(int i = 0; i < g_symbolCount; i++)
{
if(!SymbolSelect(g_symbols[i], true))
{
PrintFormat("Symbol %s not available", g_symbols[i]);
return(INIT_FAILED);
}
}
// Create indicator handles for each symbol
ArrayResize(g_handleMA, g_symbolCount);
for(int i = 0; i < g_symbolCount; i++)
{
g_handleMA[i] = iMA(g_symbols[i], PERIOD_H1, 20, 0, MODE_EMA, PRICE_CLOSE);
if(g_handleMA[i] == INVALID_HANDLE)
{
PrintFormat("Failed to create MA handle for %s", g_symbols[i]);
return(INIT_FAILED);
}
}
// Configure trade
g_trade.SetExpertMagicNumber(InpMagicNumber);
g_trade.SetDeviationInPoints(10);
// Start timer
if(!EventSetTimer(InpTimerSeconds))
{
Print("Failed to set timer");
return(INIT_FAILED);
}
PrintFormat("Multi-Symbol EA started with %d symbols", g_symbolCount);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
EventKillTimer();
for(int i = 0; i < g_symbolCount; i++)
{
if(g_handleMA[i] != INVALID_HANDLE)
IndicatorRelease(g_handleMA[i]);
}
}
//+------------------------------------------------------------------+
//| Timer event: process all symbols |
//+------------------------------------------------------------------+
void OnTimer()
{
for(int i = 0; i < g_symbolCount; i++)
{
ProcessSymbol(g_symbols[i], g_handleMA[i]);
}
}
//+------------------------------------------------------------------+
//| Process one symbol |
//+------------------------------------------------------------------+
void ProcessSymbol(string symbol, int maHandle)
{
// Check for new bar on this symbol
if(!IsNewBarForSymbol(symbol))
return;
// Skip if already in position for this symbol
if(HasPositionForSymbol(symbol))
return;
// Generate signal
double ma[], close[];
ArraySetAsSeries(ma, true);
ArraySetAsSeries(close, true);
if(CopyBuffer(maHandle, 0, 0, 3, ma) < 3) return;
if(CopyClose(symbol, PERIOD_H1, 0, 3, close) < 3) return;
// Set filling policy per symbol
long filling = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((filling & SYMBOL_FILLING_FOK) == SYMBOL_FILLING_FOK)
g_trade.SetTypeFilling(ORDER_FILLING_FOK);
else if((filling & SYMBOL_FILLING_IOC) == SYMBOL_FILLING_IOC)
g_trade.SetTypeFilling(ORDER_FILLING_IOC);
else
g_trade.SetTypeFilling(ORDER_FILLING_RETURN);
// Simple signal: price crosses above/below MA
if(close[2] <= ma[2] && close[1] > ma[1])
{
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
g_trade.Buy(InpLots, symbol, ask, 0, 0, "MultiSym Buy");
PrintFormat("[%s] BUY signal executed", symbol);
}
else if(close[2] >= ma[2] && close[1] < ma[1])
{
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
g_trade.Sell(InpLots, symbol, bid, 0, 0, "MultiSym Sell");
PrintFormat("[%s] SELL signal executed", symbol);
}
}
//+------------------------------------------------------------------+
//| New bar detection per symbol |
//+------------------------------------------------------------------+
bool IsNewBarForSymbol(string symbol)
{
// Use a static array to track bar times per symbol
static datetime barTimes[];
static string barSymbols[];
static int barCount = 0;
datetime currentBar = iTime(symbol, PERIOD_H1, 0);
// Find existing entry
for(int i = 0; i < barCount; i++)
{
if(barSymbols[i] == symbol)
{
if(barTimes[i] != currentBar)
{
barTimes[i] = currentBar;
return true;
}
return false;
}
}
// New symbol: add entry
barCount++;
ArrayResize(barTimes, barCount);
ArrayResize(barSymbols, barCount);
barTimes[barCount - 1] = currentBar;
barSymbols[barCount - 1] = symbol;
return true;
}
//+------------------------------------------------------------------+
//| Check for existing position on a symbol |
//+------------------------------------------------------------------+
bool HasPositionForSymbol(string symbol)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionGetString(POSITION_SYMBOL) == symbol &&
PositionGetInteger(POSITION_MAGIC) == InpMagicNumber)
return true;
}
return false;
}
```
---
## Design Patterns for MQL
### Singleton Pattern (for Managers)
Ensure only one instance of a manager class exists. Useful for trade managers, loggers, or config objects.
```mql5
//+------------------------------------------------------------------+
//| Singleton Trade Manager |
//+------------------------------------------------------------------+
class CTradeManagerSingleton
{
private:
static CTradeManagerSingleton *m_instance;
CTrade m_trade;
int m_magicNumber;
// Private constructor prevents direct instantiation
CTradeManagerSingleton(void) : m_magicNumber(0) {}
~CTradeManagerSingleton(void) {}
public:
//--- Get the single instance
static CTradeManagerSingleton* GetInstance(void)
{
if(m_instance == NULL)
m_instance = new CTradeManagerSingleton();
return m_instance;
}
//--- Destroy the instance (call in OnDeinit)
static void DestroyInstance(void)
{
if(m_instance != NULL)
{
delete m_instance;
m_instance = NULL;
}
}
//--- Public interface
void Init(int magicNumber)
{
m_magicNumber = magicNumber;
m_trade.SetExpertMagicNumber(m_magicNumber);
}
bool Buy(string symbol, double lots, double sl, double tp)
{
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
return m_trade.Buy(lots, symbol, ask, sl, tp);
}
bool Sell(string symbol, double lots, double sl, double tp)
{
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
return m_trade.Sell(lots, symbol, bid, sl, tp);
}
};
// Static member initialization (must be at file scope)
CTradeManagerSingleton* CTradeManagerSingleton::m_instance = NULL;
// Usage:
// OnInit: CTradeManagerSingleton::GetInstance().Init(12345);
// OnTick: CTradeManagerSingleton::GetInstance().Buy("EURUSD", 0.1, sl, tp);
// OnDeinit: CTradeManagerSingleton::DestroyInstance();
```
### Strategy Pattern (Interchangeable Signals)
Swap signal algorithms at runtime or configuration time without modifying the EA.
```mql5
//+------------------------------------------------------------------+
//| Strategy Pattern: interchangeable signal implementations |
//+------------------------------------------------------------------+
// Base (already defined in CSignalBase.mqh above)
// class CSignalBase { virtual ENUM_SIGNAL GenerateSignal() = 0; };
// Implementation 1: MA Crossover (see CSignalMA above)
// Implementation 2: RSI Overbought/Oversold
class CSignalRSI : public CSignalBase
{
private:
int m_period;
double m_overbought;
double m_oversold;
int m_handleRSI;
public:
CSignalRSI(void) : m_period(14), m_overbought(70), m_oversold(30),
m_handleRSI(INVALID_HANDLE)
{
m_name = "RSI_Signal";
}
~CSignalRSI(void) { Release(); }
void SetParameters(int period, double overbought, double oversold)
{
m_period = period;
m_overbought = overbought;
m_oversold = oversold;
}
virtual bool Init(string symbol, ENUM_TIMEFRAMES timeframe) override
{
if(!CSignalBase::Init(symbol, timeframe))
return false;
m_handleRSI = iRSI(m_symbol, m_timeframe, m_period, PRICE_CLOSE);
return (m_handleRSI != INVALID_HANDLE);
}
virtual ENUM_SIGNAL GenerateSignal(void) override
{
double rsi[];
ArraySetAsSeries(rsi, true);
if(CopyBuffer(m_handleRSI, 0, 0, 3, rsi) < 3) return SIGNAL_NO;
// RSI crosses above oversold -> Buy
if(rsi[2] <= m_oversold && rsi[1] > m_oversold)
return SIGNAL_BUY;
// RSI crosses below overbought -> Sell
if(rsi[2] >= m_overbought && rsi[1] < m_overbought)
return SIGNAL_SELL;
return SIGNAL_NO;
}
virtual void Release(void) override
{
if(m_handleRSI != INVALID_HANDLE) { IndicatorRelease(m_handleRSI); m_handleRSI = INVALID_HANDLE; }
}
};
// --- EA usage: select strategy via input parameter ---
// input int InpSignalType = 0; // 0=MA, 1=RSI
//
// CSignalBase *g_signal;
//
// int OnInit() {
// if(InpSignalType == 0) {
// CSignalMA *sig = new CSignalMA();
// sig.SetParameters(10, 20, MODE_EMA);
// g_signal = sig;
// } else {
// CSignalRSI *sig = new CSignalRSI();
// sig.SetParameters(14, 70, 30);
// g_signal = sig;
// }
// g_signal.Init(_Symbol, PERIOD_CURRENT);
// }
```
### Observer Pattern (Event Notification)
Useful for inter-module communication: when a trade is opened, notify the logger, the panel, the risk manager, etc.
```mql5
//+------------------------------------------------------------------+
//| Observer Pattern for trade events |
//+------------------------------------------------------------------+
//--- Event types
enum ENUM_TRADE_EVENT
{
EVENT_POSITION_OPENED = 0,
EVENT_POSITION_CLOSED = 1,
EVENT_ORDER_FAILED = 2,
EVENT_SL_HIT = 3,
EVENT_TP_HIT = 4
};
//--- Observer interface
class ITradeObserver
{
public:
virtual ~ITradeObserver(void) {}
virtual void OnTradeEvent(ENUM_TRADE_EVENT event, string symbol,
double price, double lots) = 0;
};
//--- Subject: manages observers and notifies them
class CTradeEventPublisher
{
private:
ITradeObserver *m_observers[];
int m_count;
public:
CTradeEventPublisher(void) : m_count(0) {}
~CTradeEventPublisher(void) {}
void Subscribe(ITradeObserver *observer)
{
m_count++;
ArrayResize(m_observers, m_count);
m_observers[m_count - 1] = observer;
}
void Notify(ENUM_TRADE_EVENT event, string symbol,
double price, double lots)
{
for(int i = 0; i < m_count; i++)
{
if(m_observers[i] != NULL)
m_observers[i].OnTradeEvent(event, symbol, price, lots);
}
}
};
//--- Concrete observer: Logger
class CTradeLogger : public ITradeObserver
{
public:
virtual void OnTradeEvent(ENUM_TRADE_EVENT event, string symbol,
double price, double lots) override
{
string eventName;
switch(event)
{
case EVENT_POSITION_OPENED: eventName = "OPENED"; break;
case EVENT_POSITION_CLOSED: eventName = "CLOSED"; break;
case EVENT_ORDER_FAILED: eventName = "FAILED"; break;
case EVENT_SL_HIT: eventName = "SL_HIT"; break;
case EVENT_TP_HIT: eventName = "TP_HIT"; break;
default: eventName = "UNKNOWN"; break;
}
PrintFormat("[TradeLog] %s %s price=%.5f lots=%.2f", eventName, symbol, price, lots);
}
};
//--- Concrete observer: Stats tracker
class CTradeStats : public ITradeObserver
{
private:
int m_totalTrades;
int m_wins;
int m_losses;
public:
CTradeStats(void) : m_totalTrades(0), m_wins(0), m_losses(0) {}
virtual void OnTradeEvent(ENUM_TRADE_EVENT event, string symbol,
double price, double lots) override
{
if(event == EVENT_POSITION_OPENED) m_totalTrades++;
if(event == EVENT_TP_HIT) m_wins++;
if(event == EVENT_SL_HIT) m_losses++;
}
double WinRate(void) const
{
int completed = m_wins + m_losses;
return (completed > 0) ? (double)m_wins / completed * 100.0 : 0;
}
};
// --- Usage ---
// CTradeEventPublisher g_publisher;
// CTradeLogger g_logger;
// CTradeStats g_stats;
//
// OnInit:
// g_publisher.Subscribe(&g_logger);
// g_publisher.Subscribe(&g_stats);
//
// After trade execution:
// g_publisher.Notify(EVENT_POSITION_OPENED, "EURUSD", 1.10500, 0.1);
```
---
## Include File Design (.mqh)
### Header Guard Pattern
Prevents double-inclusion compilation errors. Required for every `.mqh` file.
```mql5
#ifndef MY_CLASS_MQH
#define MY_CLASS_MQH
// Class definition here
#endif // MY_CLASS_MQH
```
Convention: guard name = filename in uppercase with dots replaced by underscores.
- `CTradeManager.mqh` -> `CTRADE_MANAGER_MQH`
- `CSignalBase.mqh` -> `CSIGNAL_BASE_MQH`
### Class Design Conventions
| Convention | Rule | Example |
|------------|------|---------|
| One class per file | Class name matches filename | `CTradeManager` in `CTradeManager.mqh` |
| Class prefix | `C` for classes | `CTradeManager`, `CRiskManager` |
| Interface prefix | `I` for interfaces / abstract bases | `ITradeObserver` |
| Enum prefix | `ENUM_` for enums | `ENUM_SIGNAL`, `ENUM_EA_STATE` |
| Member variables | `m_` prefix | `m_magicNumber`, `m_symbol` |
| Input parameters | Only at EA level | Never inside `.mqh` classes |
| Configuration | Pass via constructor or `Init()` method | `Init(symbol, timeframe)` |
| Destruction | Release handles in destructor | `~CSignalMA() { Release(); }` |
### Input Parameters Rule
Input parameters (`input`) should only appear in the main `.mq5`/`.mq4` EA file. Classes receive their configuration through constructor parameters or `Init()` methods:
```mql5
// CORRECT: input at EA level, passed to class
// In MyEA.mq5:
input int InpFastMA = 10;
int OnInit()
{
CSignalMA *signal = new CSignalMA();
signal.SetParameters(InpFastMA, 20, MODE_EMA); // Config passed in
signal.Init(_Symbol, PERIOD_CURRENT);
}
// WRONG: input inside .mqh class
// class CSignalMA {
// input int m_fastPeriod = 10; // DO NOT do this
// };
```
---
## Complete Templates
### Modular EA Composition Template (MQL5)
Shows the recommended way to compose a modular EA from reusable components.
```mql5
//+------------------------------------------------------------------+
//| ModularTemplate.mq5 |
//| Template showing modular EA composition |
//+------------------------------------------------------------------+
#property copyright "Developer"
#property version "1.00"
//--- Include modules
#include <Core/CSignalBase.mqh>
#include <Core/CSignalMA.mqh>
#include <Core/CTradeManager.mqh>
#include <Core/CRiskManager.mqh>
#include <Filters/CTimeFilter.mqh>
#include <Filters/CSpreadFilter.mqh>
#include <Filters/CVolatilityFilter.mqh>
//--- Input parameters: Strategy
input string InpSection1 = "=== Strategy ==="; // --------
input int InpFastMA = 10; // Fast MA Period
input int InpSlowMA = 20; // Slow MA Period
//--- Input parameters: Risk
input string InpSection2 = "=== Risk ==="; // --------
input double InpRiskPercent = 1.0; // Risk per Trade (%)
input double InpMaxDrawdown = 20.0; // Max Drawdown (%)
input int InpStopLoss = 50; // Stop Loss (pips)
input int InpTakeProfit = 100; // Take Profit (pips)
//--- Input parameters: Filters
input string InpSection3 = "=== Filters ==="; // --------
input int InpStartHour = 8; // Start Hour (server time)
input int InpEndHour = 20; // End Hour (server time)
input double InpMaxSpread = 3.0; // Max Spread (pips)
//--- Input parameters: General
input string InpSection4 = "=== General ==="; // --------
input int InpMagicNumber = 12345; // Magic Number
//--- Module pointers
CSignalMA *g_signal;
CTradeManager *g_trade;
CRiskManager *g_risk;
CTimeFilter *g_timeFilter;
CSpreadFilter *g_spreadFilter;
//+------------------------------------------------------------------+
//| Expert initialization |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Allocate modules
g_signal = new CSignalMA();
g_trade = new CTradeManager();
g_risk = new CRiskManager();
g_timeFilter = new CTimeFilter();
g_spreadFilter = new CSpreadFilter();
//--- Configure and initialize Signal
g_signal.SetParameters(InpFastMA, InpSlowMA, MODE_EMA);
if(!g_signal.Init(_Symbol, PERIOD_CURRENT))
{
Print("INIT FAILED: Signal module");
return(INIT_FAILED);
}
//--- Configure and initialize Trade Manager
if(!g_trade.Init(InpMagicNumber, _Symbol))
{
Print("INIT FAILED: Trade module");
return(INIT_FAILED);
}
//--- Configure and initialize Risk Manager
if(!g_risk.Init(InpRiskPercent, InpMaxDrawdown))
{
Print("INIT FAILED: Risk module");
return(INIT_FAILED);
}
//--- Configure Filters
g_timeFilter.SetHours(InpStartHour, InpEndHour);
g_spreadFilter.SetParameters(_Symbol, InpMaxSpread);
//--- Ready
PrintFormat("EA initialized: %s | Magic=%d | Risk=%.1f%%",
_Symbol, InpMagicNumber, InpRiskPercent);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//--- Release and delete modules (reverse order of creation)
if(g_spreadFilter != NULL) { delete g_spreadFilter; g_spreadFilter = NULL; }
if(g_timeFilter != NULL) { delete g_timeFilter; g_timeFilter = NULL; }
if(g_risk != NULL) { delete g_risk; g_risk = NULL; }
if(g_trade != NULL) { delete g_trade; g_trade = NULL; }
if(g_signal != NULL) { g_signal.Release(); delete g_signal; g_signal = NULL; }
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- Step 1: Only on new bar
if(!IsNewBar())
return;
//--- Step 2: Check filters
if(!g_timeFilter.IsAllowed())
return;
if(!g_spreadFilter.IsAllowed())
return;
//--- Step 3: Check risk
if(!g_risk.CanTrade())
return;
//--- Step 4: Skip if already in position
if(g_trade.HasPosition(_Symbol))
return;
//--- Step 5: Generate signal
ENUM_SIGNAL signal = g_signal.GenerateSignal();
if(signal == SIGNAL_NO)
return;
//--- Step 6: Calculate position size
double lots = g_risk.CalculateLots(_Symbol, InpStopLoss);
//--- Step 7: Execute trade
if(signal == SIGNAL_BUY)
g_trade.OpenBuy(_Symbol, lots, InpStopLoss, InpTakeProfit, "Buy Signal");
else if(signal == SIGNAL_SELL)
g_trade.OpenSell(_Symbol, lots, InpStopLoss, InpTakeProfit, "Sell Signal");
}
//+------------------------------------------------------------------+
//| Detect new bar |
//+------------------------------------------------------------------+
bool IsNewBar()
{
static datetime lastBarTime = 0;
datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
if(currentBarTime != lastBarTime)
{
lastBarTime = currentBarTime;
return true;
}
return false;
}
```
---
## Quick Reference: When to Use Each Pattern
| Scenario | Pattern |
|----------|---------|
| Simple strategy, single indicator, quick prototype | Simple Single-File EA |
| Professional EA, multiple signals, reusable code | Modular EA (Signal + Trade + Risk + Filter) |
| Complex lifecycle, multiple phases, error recovery | State Machine |
| Trend on H4, entry on M15 | Multi-Timeframe Analysis |
| Trade EURUSD + GBPUSD + USDJPY from one EA | Multi-Symbol EA |
| Global managers (trade, logger) | Singleton |
| Swap signal algorithms without changing EA | Strategy |
| Decouple modules, event-driven updates | Observer |
| Dashboard parameters surviving TF change | GlobalVariable persistence (not file I/O) |
---
## Persistent State Across Timeframe Changes (MQL5)
When a user changes the chart's timeframe, MT5 destroys all chart objects and calls `OnDeinit(REASON_PROGRAM)`, then re-runs `OnInit`. For semi-automatic EAs with dashboard UI and runtime parameters (zones, SL/TP, etc.), all runtime state must survive this cycle.
### Problem: OnDeinit May Not Fire on TF Change
`OnDeinit` is **not guaranteed** to be called when the user changes the chart timeframe. This makes file-based persistence (using `FileOpen`/`FileWrite` with `FILE_COMMON`) unreliable — the file may never be written before the EA is destroyed.
### Correct Pattern: GlobalVariable-Based Persistence
**GlobalVariables** persist in the terminal's memory across timeframe changes and terminal restarts. They are the correct mechanism for persisting runtime EA state.
```mql5
//+------------------------------------------------------------------+
//| PERSISTENT STORAGE — GlobalVariable-based |
//+------------------------------------------------------------------+
// GlobalVariables persist across TF changes and terminal restarts.
// Key format: "SmartEntry_<symbol>_<fieldName>" to scope per symbol.
//+------------------------------------------------------------------+
string MakeGVName(string fieldName)
{
return "SmartEntry_" + g_symbol + "_" + fieldName;
}
void SaveGV(string fieldName, double value)
{
GlobalVariableSet(MakeGVName(fieldName), value);
}
double LoadGV(string fieldName, double defaultValue)
{
string name = MakeGVName(fieldName);
if(GlobalVariableCheck(name))
return GlobalVariableGet(name);
return defaultValue;
}
// Save/load boolean (as 0.0/1.0) and datetime using the same pattern.
//+------------------------------------------------------------------+
//| CRITICAL: g_symbol MUST be set BEFORE LoadRuntimeValues() |
//+------------------------------------------------------------------+
int OnInit()
{
g_symbol = _Symbol; // MUST be first — GV names include the symbol
LoadRuntimeValues(); // Now uses correct g_symbol in GV names
InitDashboard();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
SaveRuntimeValues(); // Also save proactively during runtime changes
DestroyDashboard();
}
```
### Key Rules
1. **Always set `g_symbol = _Symbol` BEFORE calling any Load/Save functions** — the symbol is part of the GV key name
2. **Save proactively during runtime** — on every parameter change, not just in `OnDeinit`
3. **Do not rely on `OnDeinit`** for saving critical state — it may not be called on TF change
4. **GV names must be unique per symbol** — append the symbol to the key name to avoid collisions across symbols
### Common Bug: Symbol Set After Load
```mql5
// WRONG — LoadRuntimeValues uses g_symbol but it hasn't been set yet
int OnInit()
{
LoadRuntimeValues(); // g_symbol is "" or stale → wrong GV names!
g_symbol = _Symbol; // Too late
...
}
// CORRECT
int OnInit()
{
g_symbol = _Symbol; // First!
LoadRuntimeValues(); // Uses correct symbol in GV names
...
}
```
### Common Bug: Duplicate Class Instances (Resource Ownership)
If a class manages a resource (e.g., `CTrailStop` manages an ATR handle), ensure only **one** instance owns and manages it. Having both a global instance AND a class member instance leads to silent failures:
```mql5
// WRONG — two CTrailStop instances, only one is properly initialized
CTrailStop g_trailStop; // global: Init() called, ATR handle created
class COrderManager {
CTrailStop m_trailStop; // member: Init() never called, handle = INVALID_HANDLE
};
// Result: ApplyTrailingStop() calls m_trailStop methods → no-op
// CORRECT — use the same global instance everywhere
CTrailStop g_trailStop; // one instance, initialized once in OnInit
class COrderManager {
// Does NOT have its own CTrailStop — calls g_trailStop methods directly
};
```