commit 2682cfdca100eb2dc704017b7c60d96b78a847d1 Author: peter Date: Wed May 27 12:32:26 2026 -0400 Initial MQL5 EA Project diff --git a/Include/Config.mqh b/Include/Config.mqh new file mode 100644 index 0000000..e978712 --- /dev/null +++ b/Include/Config.mqh @@ -0,0 +1,51 @@ +//+------------------------------------------------------------------+ +//| Config.mqh - Global configuration and input parameters | +//| Central place for all EA settings | +//+------------------------------------------------------------------+ + +#ifndef __CONFIG_MQH__ +#define __CONFIG_MQH__ + +// ==================== LOT SIZING ==================== +enum E_LOT_MODE +{ + LOT_MODE_FIXED = 0, // Use fixed lot size + LOT_MODE_RISK = 1 // Use risk percent method +}; + +input E_LOT_MODE g_lot_mode = LOT_MODE_FIXED; // Lot Sizing Mode +input double g_fixed_lot = 0.1; // Fixed Lot Size (if LOT_MODE_FIXED) +input double g_risk_percent = 2.0; // Risk Percent (if LOT_MODE_RISK) +input double g_max_lot = 10.0; // Maximum Lot Size +input double g_min_lot = 0.01; // Minimum Lot Size + +// ==================== TRADE MANAGEMENT ==================== +input int g_magic_number = 12345; // Magic Number for trades +input int g_stop_loss_points = 100; // Stop Loss in points +input int g_take_profit_points = 200; // Take Profit in points +input int g_max_spread_points = 10; // Max Spread in points +input int g_max_positions = 1; // Max positions at once + +// ==================== TRADING HOURS ==================== +input bool g_use_trading_hours = false; // Enable trading hour filter +input int g_trade_start_hour = 8; // Trading Start Hour (0-23) +input int g_trade_end_hour = 20; // Trading End Hour (0-23) + +// ==================== TRAILING STOP ==================== +input bool g_use_trailing_stop = true; // Use Trailing Stop +input int g_trailing_stop_points= 50; // Trailing Stop Distance +input bool g_use_break_even = true; // Use Break Even +input int g_break_even_profit = 10; // Break Even Trigger profit +input int g_break_even_sl = 2; // Break Even SL distance + +// ==================== STRATEGY PARAMETERS ==================== +input int g_ma_fast_period = 10; // Fast MA Period +input int g_ma_slow_period = 20; // Slow MA Period +input int g_ma_shift = 0; // MA Shift +input ENUM_MA_METHOD g_ma_method = MODE_SMA; // MA Method +input ENUM_APPLIED_PRICE g_ma_price = PRICE_CLOSE; // MA Applied Price + +// ==================== DEBUG ==================== +input bool g_debug_mode = true; // Enable Debug Logging + +#endif //__CONFIG_MQH__ diff --git a/Include/Logger.mqh b/Include/Logger.mqh new file mode 100644 index 0000000..6f55687 --- /dev/null +++ b/Include/Logger.mqh @@ -0,0 +1,74 @@ +//+------------------------------------------------------------------+ +//| Logger.mqh - Simple logging system | +//| Provides info, warning, and error logging with formatting | +//+------------------------------------------------------------------+ + +#ifndef __LOGGER_MQH__ +#define __LOGGER_MQH__ + +#include "Config.mqh" + +class CLogger +{ +private: + bool m_debug_mode; + +public: + // Constructor + CLogger(bool debug_mode = true) + { + m_debug_mode = debug_mode; + } + + // Log info level message + void Info(const string message) + { + if(m_debug_mode) + PrintFormat("[INFO] %s", message); + } + + // Log warning level message + void Warning(const string message) + { + PrintFormat("[WARNING] %s", message); + } + + // Log error level message + void Error(const string message) + { + PrintFormat("[ERROR] %s", message); + } + + // Log formatted message (info level) + // Note: MQL5 does not support user-defined variadic functions, so + // pass an already-formatted string to this method. + void InfoFormat(const string formatted) + { + if(m_debug_mode) + { + PrintFormat("[INFO] %s", formatted); + } + } + + // Log formatted message (error level) + // Note: MQL5 does not support user-defined variadic functions, so + // pass an already-formatted string to this method. + void ErrorFormat(const string formatted) + { + PrintFormat("[ERROR] %s", formatted); + } + + // Set debug mode + void SetDebugMode(bool debug_mode) + { + m_debug_mode = debug_mode; + } + + // Get debug mode + bool GetDebugMode() const + { + return m_debug_mode; + } +}; + +#endif //__LOGGER_MQH__ diff --git a/Include/MarketData.mqh b/Include/MarketData.mqh new file mode 100644 index 0000000..bf7370e --- /dev/null +++ b/Include/MarketData.mqh @@ -0,0 +1,186 @@ +//+------------------------------------------------------------------+ +//| MarketData.mqh - Market data wrapper | +//| Handles bid/ask, spread, symbol info, new bar detection | +//+------------------------------------------------------------------+ + +#ifndef __MARKETDATA_MQH__ +#define __MARKETDATA_MQH__ + +#include "Config.mqh" +#include "Logger.mqh" +#include "Utilities.mqh" + +class CMarketData +{ +private: + string m_symbol; + CLogger *mp_logger; + datetime m_last_bar_time; + int m_digits; + double m_point; + +public: + // Constructor + CMarketData(const string symbol, CLogger *logger) + { + m_symbol = symbol; + mp_logger = logger; + m_last_bar_time = 0; + m_digits = CUtilities::GetDigits(symbol); + m_point = CUtilities::GetPoint(symbol); + } + + // Get current bid price + double GetBid() const + { + return SymbolInfoDouble(m_symbol, SYMBOL_BID); + } + + // Get current ask price + double GetAsk() const + { + return SymbolInfoDouble(m_symbol, SYMBOL_ASK); + } + + // Get current spread in points + int GetSpreadPoints() const + { + double spread_price = GetAsk() - GetBid(); + return CUtilities::PriceToPoints(m_symbol, spread_price); + } + + // Get current spread in price + double GetSpreadPrice() const + { + return GetAsk() - GetBid(); + } + + // Check if spread is acceptable + bool IsSpreadAcceptable(int max_spread_points) const + { + int current_spread = GetSpreadPoints(); + if(current_spread > max_spread_points) + { + if(mp_logger) + { + // Build message without using variadic StringFormat to avoid parser issues + string msg = "Spread too wide: "; + msg += IntegerToString(current_spread); + msg += " > "; + msg += IntegerToString(max_spread_points); + mp_logger.Warning(msg); + } + return false; + } + return true; + } + + // Detect new bar on current timeframe + bool IsNewBar() + { + datetime bar_time = iTime(m_symbol, PERIOD_CURRENT, 0); + + if(m_last_bar_time == 0) + { + m_last_bar_time = bar_time; + return true; + } + + if(bar_time != m_last_bar_time) + { + m_last_bar_time = bar_time; + return true; + } + + return false; + } + + // Get current close price + double GetClose() const + { + return iClose(m_symbol, PERIOD_CURRENT, 0); + } + + // Get current open price + double GetOpen() const + { + return iOpen(m_symbol, PERIOD_CURRENT, 0); + } + + // Get current high price + double GetHigh() const + { + return iHigh(m_symbol, PERIOD_CURRENT, 0); + } + + // Get current low price + double GetLow() const + { + return iLow(m_symbol, PERIOD_CURRENT, 0); + } + + // Get close price of bar N bars ago + double GetCloseAt(int shift) const + { + return iClose(m_symbol, PERIOD_CURRENT, shift); + } + + // Get open price of bar N bars ago + double GetOpenAt(int shift) const + { + return iOpen(m_symbol, PERIOD_CURRENT, shift); + } + + // Get symbol digits + int GetDigits() const + { + return m_digits; + } + + // Get symbol point + double GetPoint() const + { + return m_point; + } + + // Check if trading is allowed for this symbol + bool IsTradingAllowed() const + { + // Ensure symbol allows trading (full trade mode) + long trade_mode = (long)SymbolInfoInteger(m_symbol, SYMBOL_TRADE_MODE); + if(trade_mode != SYMBOL_TRADE_MODE_FULL) + return false; + + // Basic check: ensure symbol is tradeable (has non-zero point and digits) + if(SymbolInfoDouble(m_symbol, SYMBOL_POINT) <= 0.0) + return false; + + return true; + } + + // Get symbol name + string GetSymbol() const + { + return m_symbol; + } + + // Get minimum volume + double GetMinVolume() const + { + return SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_MIN); + } + + // Get maximum volume + double GetMaxVolume() const + { + return SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_MAX); + } + + // Get volume step + double GetVolumeStep() const + { + return SymbolInfoDouble(m_symbol, SYMBOL_VOLUME_STEP); + } +}; + +#endif //__MARKETDATA_MQH__ diff --git a/Include/PositionManager.mqh b/Include/PositionManager.mqh new file mode 100644 index 0000000..5a6b233 --- /dev/null +++ b/Include/PositionManager.mqh @@ -0,0 +1,146 @@ +//+------------------------------------------------------------------+ +//| PositionManager.mqh - Position tracking and validation | +//| Checks for existing positions, prevents duplicates | +//+------------------------------------------------------------------+ + +#ifndef __POSITIONMANAGER_MQH__ +#define __POSITIONMANAGER_MQH__ + +#include "Config.mqh" +#include "Logger.mqh" + +class CPositionManager +{ +private: + CLogger *mp_logger; + +public: + // Constructor + CPositionManager(CLogger *logger) + { + mp_logger = logger; + } + + // Check if there is already an open position for this symbol and magic + bool HasOpenPosition(const string symbol) + { + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(PositionSelectByTicket(PositionGetTicket(i))) + { + if(PositionGetString(POSITION_SYMBOL) == symbol && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + return true; + } + } + } + return false; + } + + // Get current position count for this symbol and magic + int GetPositionCount(const string symbol) + { + int count = 0; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(PositionSelectByTicket(PositionGetTicket(i))) + { + if(PositionGetString(POSITION_SYMBOL) == symbol && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + count++; + } + } + } + return count; + } + + // Get current position type (OP_BUY, OP_SELL, or -1 if none) + int GetPositionType(const string symbol) + { + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(PositionSelectByTicket(PositionGetTicket(i))) + { + if(PositionGetString(POSITION_SYMBOL) == symbol && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + return (int)PositionGetInteger(POSITION_TYPE); + } + } + } + return -1; + } + + // Get current position ticket (returns 0 if none) + ulong GetPositionTicket(const string symbol) + { + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + ulong ticket = PositionGetTicket(i); + if(PositionSelectByTicket(ticket)) + { + if(PositionGetString(POSITION_SYMBOL) == symbol && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + return ticket; + } + } + } + return 0; + } + + // Get current position profit + double GetPositionProfit(const string symbol) + { + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(PositionSelectByTicket(PositionGetTicket(i))) + { + if(PositionGetString(POSITION_SYMBOL) == symbol && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + return PositionGetDouble(POSITION_PROFIT); + } + } + } + return 0.0; + } + + // Check if new position is allowed (not exceeding max positions) + bool IsNewPositionAllowed(const string symbol) + { + int current_count = GetPositionCount(symbol); + + if(current_count >= g_max_positions) + { + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Max positions reached: %d >= %d", + current_count, g_max_positions)); + return false; + } + + return true; + } + + // Get total open lots for symbol and magic + double GetTotalOpenLots(const string symbol) + { + double total_lots = 0.0; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(PositionSelectByTicket(PositionGetTicket(i))) + { + if(PositionGetString(POSITION_SYMBOL) == symbol && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + total_lots += PositionGetDouble(POSITION_VOLUME); + } + } + } + return total_lots; + } +}; + +#endif //__POSITIONMANAGER_MQH__ diff --git a/Include/RiskManager.mqh b/Include/RiskManager.mqh new file mode 100644 index 0000000..d64f5a3 --- /dev/null +++ b/Include/RiskManager.mqh @@ -0,0 +1,158 @@ +//+------------------------------------------------------------------+ +//| RiskManager.mqh - Risk and position sizing management | +//| Calculates lot sizes, validates parameters, checks trading hours | +//+------------------------------------------------------------------+ + +#ifndef __RISKMANAGER_MQH__ +#define __RISKMANAGER_MQH__ + +#include "Config.mqh" +#include "Logger.mqh" +#include "MarketData.mqh" +#include "Utilities.mqh" + +class CRiskManager +{ +private: + CMarketData *mp_market_data; + CLogger *mp_logger; + +public: + // Constructor + CRiskManager(CMarketData *market_data, CLogger *logger) + { + mp_market_data = market_data; + mp_logger = logger; + } + + // Calculate lot size based on configuration + double CalculateLotSize(int stop_loss_points) + { + double lot = 0.0; + + if(g_lot_mode == LOT_MODE_FIXED) + { + lot = g_fixed_lot; + } + else if(g_lot_mode == LOT_MODE_RISK) + { + lot = CalculateLotByRisk(stop_loss_points); + } + + return ValidateLotSize(lot); + } + + // Calculate lot size based on risk percent + double CalculateLotByRisk(int stop_loss_points) + { + double account_balance = AccountInfoDouble(ACCOUNT_BALANCE); + double stop_loss_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), stop_loss_points); + double contract_size = CUtilities::GetContractSize(mp_market_data.GetSymbol()); + + if(stop_loss_distance == 0 || contract_size == 0) + return g_min_lot; + + // Risk = Account Balance * Risk Percent / 100 + double risk_amount = account_balance * (g_risk_percent / 100.0); + + // Lot = Risk Amount / (SL Distance * Contract Size * Point) + double point = mp_market_data.GetPoint(); + double lot = risk_amount / (stop_loss_distance * contract_size); + + return lot; + } + + // Validate and normalize lot size + double ValidateLotSize(double lot) + { + // Apply global limits first + if(lot < g_min_lot) + lot = g_min_lot; + if(lot > g_max_lot) + lot = g_max_lot; + + // Normalize to broker's lot step + lot = CUtilities::NormalizeLot(mp_market_data.GetSymbol(), lot); + + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Lot size calculated: %.2f", lot)); + + return lot; + } + + // Check if spread is acceptable + bool IsSpreadAcceptable() + { + return mp_market_data.IsSpreadAcceptable(g_max_spread_points); + } + + // Check if trading is allowed by time filter + bool IsTradingHourValid() + { + if(!g_use_trading_hours) + return true; + + MqlDateTime time_struct; + TimeToStruct(TimeCurrent(), time_struct); + int current_hour = time_struct.hour; + + if(g_trade_start_hour <= g_trade_end_hour) + { + // Normal case: e.g., 8:00 to 20:00 + if(current_hour < g_trade_start_hour || current_hour >= g_trade_end_hour) + { + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)", + current_hour, g_trade_start_hour, g_trade_end_hour)); + return false; + } + } + else + { + // Overnight case: e.g., 20:00 to 8:00 + if(current_hour < g_trade_start_hour && current_hour >= g_trade_end_hour) + { + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)", + current_hour, g_trade_start_hour, g_trade_end_hour)); + return false; + } + } + + return true; + } + + // Calculate stop loss price in absolute terms + double CalculateStopLossPrice(bool buy) + { + double bid = mp_market_data.GetBid(); + double ask = mp_market_data.GetAsk(); + double entry_price = buy ? ask : bid; + double sl_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), g_stop_loss_points); + + double sl_price = buy ? (entry_price - sl_distance) : (entry_price + sl_distance); + + return CUtilities::NormalizePrice(mp_market_data.GetSymbol(), sl_price); + } + + // Calculate take profit price in absolute terms + double CalculateTakeProfitPrice(bool buy) + { + double bid = mp_market_data.GetBid(); + double ask = mp_market_data.GetAsk(); + double entry_price = buy ? ask : bid; + double tp_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), g_take_profit_points); + + double tp_price = buy ? (entry_price + tp_distance) : (entry_price - tp_distance); + + return CUtilities::NormalizePrice(mp_market_data.GetSymbol(), tp_price); + } + + // Get market data reference + CMarketData* GetMarketData() + { + return mp_market_data; + } +}; + +#endif //__RISKMANAGER_MQH__ diff --git a/Include/Signal.mqh b/Include/Signal.mqh new file mode 100644 index 0000000..3e83044 --- /dev/null +++ b/Include/Signal.mqh @@ -0,0 +1,16 @@ +//+------------------------------------------------------------------+ +//| Signal.mqh - Trade signal enumeration | +//| Defines possible signals from strategy | +//+------------------------------------------------------------------+ + +#ifndef __SIGNAL_MQH__ +#define __SIGNAL_MQH__ + +enum E_SIGNAL +{ + SIGNAL_NONE = 0, // No signal + SIGNAL_BUY = 1, // Buy signal + SIGNAL_SELL = -1 // Sell signal +}; + +#endif //__SIGNAL_MQH__ diff --git a/Include/Strategy.mqh b/Include/Strategy.mqh new file mode 100644 index 0000000..c58934e --- /dev/null +++ b/Include/Strategy.mqh @@ -0,0 +1,151 @@ +//+------------------------------------------------------------------+ +//| Strategy.mqh - Trading strategy implementation | +//| Simple MA crossover: Buy when Fast MA > Slow MA, Sell opposite | +//+------------------------------------------------------------------+ + +#ifndef __STRATEGY_MQH__ +#define __STRATEGY_MQH__ + +#include "Signal.mqh" +#include "MarketData.mqh" +#include "Logger.mqh" +#include "Config.mqh" + +class CStrategy +{ +private: + int m_ma_fast_handle; + int m_ma_slow_handle; + CMarketData *mp_market_data; + CLogger *mp_logger; + + int m_fast_period; + int m_slow_period; + int m_ma_shift; + ENUM_MA_METHOD m_ma_method; + ENUM_APPLIED_PRICE m_ma_price; + +public: + // Constructor + CStrategy(CMarketData *market_data, CLogger *logger) + { + mp_market_data = market_data; + mp_logger = logger; + + m_fast_period = g_ma_fast_period; + m_slow_period = g_ma_slow_period; + m_ma_shift = g_ma_shift; + m_ma_method = g_ma_method; + m_ma_price = g_ma_price; + + m_ma_fast_handle = INVALID_HANDLE; + m_ma_slow_handle = INVALID_HANDLE; + } + + // Destructor - clean up indicator handles + ~CStrategy() + { + Cleanup(); + } + + // Initialize strategy and create indicator handles + bool Init() + { + // Create fast MA handle + m_ma_fast_handle = iMA(mp_market_data.GetSymbol(), PERIOD_CURRENT, + m_fast_period, m_ma_shift, m_ma_method, m_ma_price); + + if(m_ma_fast_handle == INVALID_HANDLE) + { + if(mp_logger) + mp_logger.Error("Failed to create Fast MA indicator"); + return false; + } + + // Create slow MA handle + m_ma_slow_handle = iMA(mp_market_data.GetSymbol(), PERIOD_CURRENT, + m_slow_period, m_ma_shift, m_ma_method, m_ma_price); + + if(m_ma_slow_handle == INVALID_HANDLE) + { + if(mp_logger) + mp_logger.Error("Failed to create Slow MA indicator"); + return false; + } + + if(mp_logger) + mp_logger.Info("Strategy initialized successfully"); + + return true; + } + + // Generate trading signal + E_SIGNAL GetSignal() + { + if(m_ma_fast_handle == INVALID_HANDLE || m_ma_slow_handle == INVALID_HANDLE) + return SIGNAL_NONE; + + double ma_fast = iGetMainValue(m_ma_fast_handle, 0); + double ma_slow = iGetMainValue(m_ma_slow_handle, 0); + + if(ma_fast == 0 || ma_slow == 0) + return SIGNAL_NONE; + + // Get previous values for confirmation + double ma_fast_prev = iGetMainValue(m_ma_fast_handle, 1); + double ma_slow_prev = iGetMainValue(m_ma_slow_handle, 1); + + if(ma_fast_prev == 0 || ma_slow_prev == 0) + return SIGNAL_NONE; + + // Simple MA crossover logic + // BUY: Fast MA crosses above Slow MA + if(ma_fast_prev <= ma_slow_prev && ma_fast > ma_slow) + { + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("BUY Signal: MA Fast=%.5f > MA Slow=%.5f", ma_fast, ma_slow)); + return SIGNAL_BUY; + } + + // SELL: Fast MA crosses below Slow MA + if(ma_fast_prev >= ma_slow_prev && ma_fast < ma_slow) + { + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("SELL Signal: MA Fast=%.5f < MA Slow=%.5f", ma_fast, ma_slow)); + return SIGNAL_SELL; + } + + return SIGNAL_NONE; + } + + // Clean up indicator handles + void Cleanup() + { + if(m_ma_fast_handle != INVALID_HANDLE) + { + IndicatorRelease(m_ma_fast_handle); + m_ma_fast_handle = INVALID_HANDLE; + } + + if(m_ma_slow_handle != INVALID_HANDLE) + { + IndicatorRelease(m_ma_slow_handle); + m_ma_slow_handle = INVALID_HANDLE; + } + } + +private: + // Safe way to get indicator value + double iGetMainValue(int handle, int shift) + { + double value[]; + ArraySetAsSeries(value, true); + + if(CopyBuffer(handle, 0, shift, 1, value) <= 0) + return 0.0; + + return value[0]; + } +}; + +#endif //__STRATEGY_MQH__ diff --git a/Include/TradeManager.mqh b/Include/TradeManager.mqh new file mode 100644 index 0000000..c88b8c3 --- /dev/null +++ b/Include/TradeManager.mqh @@ -0,0 +1,187 @@ +//+------------------------------------------------------------------+ +//| TradeManager.mqh - Trade execution using CTrade class | +//| Handles buy/sell orders with SL/TP and magic number | +//+------------------------------------------------------------------+ + +#ifndef __TRADEMANAGER_MQH__ +#define __TRADEMANAGER_MQH__ + +#include +#include "Config.mqh" +#include "Logger.mqh" +#include "RiskManager.mqh" + +class CTradeManager +{ +private: + CTrade m_trade; + CLogger *mp_logger; + CRiskManager *mp_risk_manager; + +public: + // Constructor + CTradeManager(CLogger *logger, CRiskManager *risk_manager) + { + mp_logger = logger; + mp_risk_manager = risk_manager; + + // Set magic number + m_trade.SetExpertMagicNumber((ulong)g_magic_number); + + // Set async/sync mode + m_trade.SetAsyncMode(false); + + // Set slippage + m_trade.SetDeviationInPoints(10); + } + + // Destructor + ~CTradeManager() + { + } + + // Open BUY trade + bool OpenBuyTrade(const string symbol, double lot) + { + if(lot <= 0) + { + if(mp_logger) + mp_logger.Error(StringFormat("Invalid lot size: %.2f", lot)); + return false; + } + + double ask = mp_risk_manager.GetMarketData().GetAsk(); + double sl = mp_risk_manager.CalculateStopLossPrice(true); + double tp = mp_risk_manager.CalculateTakeProfitPrice(true); + + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Opening BUY: lot=%.2f, SL=%.5f, TP=%.5f", lot, sl, tp)); + + if(!m_trade.Buy(lot, symbol, ask, sl, tp)) + { + if(mp_logger) + { + mp_logger.Error(StringFormat("Buy trade failed. Result code: %d, Error: %s", + m_trade.ResultRetcode(), m_trade.ResultRetcodeDescription())); + } + return false; + } + + if(mp_logger) + { + mp_logger.Info(StringFormat("Buy trade opened. Ticket: %I64d, Volume: %.2f", + m_trade.ResultOrder(), lot)); + } + + return true; + } + + // Open SELL trade + bool OpenSellTrade(const string symbol, double lot) + { + if(lot <= 0) + { + if(mp_logger) + mp_logger.Error(StringFormat("Invalid lot size: %.2f", lot)); + return false; + } + + double bid = mp_risk_manager.GetMarketData().GetBid(); + double sl = mp_risk_manager.CalculateStopLossPrice(false); + double tp = mp_risk_manager.CalculateTakeProfitPrice(false); + + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Opening SELL: lot=%.2f, SL=%.5f, TP=%.5f", lot, sl, tp)); + + if(!m_trade.Sell(lot, symbol, bid, sl, tp)) + { + if(mp_logger) + { + mp_logger.Error(StringFormat("Sell trade failed. Result code: %d, Error: %s", + m_trade.ResultRetcode(), m_trade.ResultRetcodeDescription())); + } + return false; + } + + if(mp_logger) + { + mp_logger.Info(StringFormat("Sell trade opened. Ticket: %I64d, Volume: %.2f", + m_trade.ResultOrder(), lot)); + } + + return true; + } + + // Close position by ticket + bool ClosePosition(ulong ticket) + { + if(ticket == 0) + return false; + + if(!PositionSelectByTicket(ticket)) + return false; + + double volume = PositionGetDouble(POSITION_VOLUME); + string symbol = PositionGetString(POSITION_SYMBOL); + + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Closing position ticket %I64d, volume %.2f", ticket, volume)); + + if(!m_trade.PositionClose(ticket)) + { + if(mp_logger) + { + mp_logger.Error(StringFormat("Close position failed. Ticket: %I64d, Result: %d", + ticket, m_trade.ResultRetcode())); + } + return false; + } + + if(mp_logger) + mp_logger.Info(StringFormat("Position closed. Ticket: %I64d", ticket)); + + return true; + } + + // Modify position SL and/or TP + bool ModifyPosition(ulong ticket, double sl, double tp) + { + if(ticket == 0) + return false; + + if(!PositionSelectByTicket(ticket)) + return false; + + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Modifying position %I64d: SL=%.5f, TP=%.5f", ticket, sl, tp)); + + if(!m_trade.PositionModify(ticket, sl, tp)) + { + if(mp_logger) + { + mp_logger.Error(StringFormat("Position modify failed. Ticket: %I64d, Result: %d", + ticket, m_trade.ResultRetcode())); + } + return false; + } + + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Position modified. Ticket: %I64d", ticket)); + + return true; + } + + // Get last result code + uint GetResultRetcode() const + { + return m_trade.ResultRetcode(); + } + + // Get CTrade instance + CTrade* GetTradeObject() + { + return &m_trade; + } +}; + +#endif //__TRADEMANAGER_MQH__ diff --git a/Include/TrailingStop.mqh b/Include/TrailingStop.mqh new file mode 100644 index 0000000..4c7ba31 --- /dev/null +++ b/Include/TrailingStop.mqh @@ -0,0 +1,183 @@ +//+------------------------------------------------------------------+ +//| TrailingStop.mqh - Trailing stop and break-even management | +//| Modifies SL based on profit targets | +//+------------------------------------------------------------------+ + +#ifndef __TRAILINGSTOP_MQH__ +#define __TRAILINGSTOP_MQH__ + +#include "Config.mqh" +#include "Logger.mqh" +#include "MarketData.mqh" +#include "Utilities.mqh" +#include "TradeManager.mqh" + +class CTrailingStop +{ +private: + CMarketData *mp_market_data; + CLogger *mp_logger; + CTradeManager *mp_trade_manager; + +public: + // Constructor + CTrailingStop(CMarketData *market_data, CLogger *logger, CTradeManager *trade_manager) + { + mp_market_data = market_data; + mp_logger = logger; + mp_trade_manager = trade_manager; + } + + // Update trailing stop for all positions + void UpdateAllPositions() + { + if(!g_use_trailing_stop && !g_use_break_even) + return; + + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + if(PositionSelectByTicket(PositionGetTicket(i))) + { + if(PositionGetString(POSITION_SYMBOL) == mp_market_data.GetSymbol() && + PositionGetInteger(POSITION_MAGIC) == g_magic_number) + { + UpdatePosition(PositionGetTicket(i)); + } + } + } + } + + // Update trailing stop for a single position + void UpdatePosition(ulong ticket) + { + if(ticket == 0) + return; + + if(!PositionSelectByTicket(ticket)) + return; + + ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + double current_sl = PositionGetDouble(POSITION_SL); + double current_tp = PositionGetDouble(POSITION_TP); + double open_price = PositionGetDouble(POSITION_PRICE_OPEN); + double profit = PositionGetDouble(POSITION_PROFIT); + + double new_sl = current_sl; + double new_tp = current_tp; + bool need_modify = false; + + // Apply break-even logic first + if(g_use_break_even) + { + if(ApplyBreakEven(pos_type, open_price, current_sl, profit, new_sl)) + { + need_modify = true; + } + } + + // Apply trailing stop logic + if(g_use_trailing_stop) + { + if(ApplyTrailingStop(pos_type, current_sl, new_sl)) + { + need_modify = true; + } + } + + // Only modify if SL changed + if(need_modify && new_sl != current_sl) + { + mp_trade_manager.ModifyPosition(ticket, new_sl, new_tp); + } + } + +private: + // Apply break-even logic + bool ApplyBreakEven(ENUM_POSITION_TYPE pos_type, double open_price, + double current_sl, double profit, double &new_sl) + { + double break_even_trigger = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), + g_break_even_profit); + double break_even_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), + g_break_even_sl); + + // Break-even only if profit threshold is reached + if(profit < break_even_trigger) + return false; + + // For BUY: move SL to BE (open price + distance) + if(pos_type == POSITION_TYPE_BUY) + { + double be_sl = open_price + break_even_distance; + be_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), be_sl); + + if(be_sl > current_sl) + { + new_sl = be_sl; + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Break-even applied: BUY SL moved to %.5f", be_sl)); + return true; + } + } + // For SELL: move SL to BE (open price - distance) + else if(pos_type == POSITION_TYPE_SELL) + { + double be_sl = open_price - break_even_distance; + be_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), be_sl); + + if(be_sl < current_sl) + { + new_sl = be_sl; + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Break-even applied: SELL SL moved to %.5f", be_sl)); + return true; + } + } + + return false; + } + + // Apply trailing stop logic + bool ApplyTrailingStop(ENUM_POSITION_TYPE pos_type, double current_sl, double &new_sl) + { + double bid = mp_market_data.GetBid(); + double ask = mp_market_data.GetAsk(); + double trail_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), + g_trailing_stop_points); + + // For BUY: trailing stop follows price from below + if(pos_type == POSITION_TYPE_BUY) + { + double candidate_sl = bid - trail_distance; + candidate_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), candidate_sl); + + // Only move SL up (never down) + if(candidate_sl > current_sl) + { + new_sl = candidate_sl; + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Trailing stop applied: BUY SL moved to %.5f", candidate_sl)); + return true; + } + } + // For SELL: trailing stop follows price from above + else if(pos_type == POSITION_TYPE_SELL) + { + double candidate_sl = ask + trail_distance; + candidate_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), candidate_sl); + + // Only move SL down (never up) + if(candidate_sl < current_sl) + { + new_sl = candidate_sl; + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Trailing stop applied: SELL SL moved to %.5f", candidate_sl)); + return true; + } + } + + return false; + } +}; + +#endif //__TRAILINGSTOP_MQH__ diff --git a/Include/Utilities.mqh b/Include/Utilities.mqh new file mode 100644 index 0000000..7ce6f7a --- /dev/null +++ b/Include/Utilities.mqh @@ -0,0 +1,81 @@ +//+------------------------------------------------------------------+ +//| Utilities.mqh - Helper utility functions | +//| Price normalization, lot normalization, etc. | +//+------------------------------------------------------------------+ + +#ifndef __UTILITIES_MQH__ +#define __UTILITIES_MQH__ + +class CUtilities +{ +public: + // Normalize price to bid/ask + static double NormalizePrice(const string symbol, double price) + { + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + int digits = GetDigits(symbol); + return NormalizeDouble(price, digits); + } + + // Normalize lot size to contract size step + static double NormalizeLot(const string symbol, double lot) + { + double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); + double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); + double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); + + // Apply limits + if(lot < min_lot) lot = min_lot; + if(lot > max_lot) lot = max_lot; + + // Round to step + lot = MathFloor(lot / lot_step) * lot_step; + + return NormalizeDouble(lot, 2); + } + + // Convert points to price distance + static double PointsToPrice(const string symbol, int points) + { + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + return (double)points * point; + } + + // Convert price distance to points + static int PriceToPoints(const string symbol, double price_distance) + { + double point = SymbolInfoDouble(symbol, SYMBOL_POINT); + return (int)(price_distance / point); + } + + // Get point value + static double GetPoint(const string symbol) + { + return SymbolInfoDouble(symbol, SYMBOL_POINT); + } + + // Get digits + static int GetDigits(const string symbol) + { + long digits = SymbolInfoInteger(symbol, SYMBOL_DIGITS); + if(digits < 0) + digits = 0; + if(digits > 255) + digits = 255; + return (int)digits; + } + + // Safe double comparison with precision + static bool DoubleEquals(double a, double b, double tolerance = 0.00001) + { + return MathAbs(a - b) < tolerance; + } + + // Get contract size (lot multiplier) + static double GetContractSize(const string symbol) + { + return SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE); + } +}; + +#endif //__UTILITIES_MQH__ diff --git a/MyProEA.ex5 b/MyProEA.ex5 new file mode 100644 index 0000000..4a7bc7c Binary files /dev/null and b/MyProEA.ex5 differ diff --git a/MyProEA.mq5 b/MyProEA.mq5 new file mode 100644 index 0000000..557ea7c --- /dev/null +++ b/MyProEA.mq5 @@ -0,0 +1,242 @@ +//+------------------------------------------------------------------+ +//| MyProEA.mq5 - Professional Expert Advisor | +//| Main EA file with clean lifecycle and separation of concerns | +//+------------------------------------------------------------------+ + +#property copyright "Educational EA - BackTesting Only" +#property link "https://www.mql5.com" +#property version "1.00" +#property strict +#property description "Professional modular EA with MA crossover strategy" +#property description "For educational and backtesting purposes only" + +// Include all class definitions +#include "Include/Config.mqh" +#include "Include/Logger.mqh" +#include "Include/Utilities.mqh" +#include "Include/MarketData.mqh" +#include "Include/Signal.mqh" +#include "Include/Strategy.mqh" +#include "Include/RiskManager.mqh" +#include "Include/PositionManager.mqh" +#include "Include/TradeManager.mqh" +#include "Include/TrailingStop.mqh" + +//+------------------------------------------------------------------+ +// Global class instances +//+------------------------------------------------------------------+ + +CLogger *g_logger = NULL; +CMarketData *g_market_data = NULL; +CStrategy *g_strategy = NULL; +CRiskManager *g_risk_manager = NULL; +CPositionManager *g_position_manager = NULL; +CTradeManager *g_trade_manager = NULL; +CTrailingStop *g_trailing_stop = NULL; + +//+------------------------------------------------------------------+ +// EA Initialization +//+------------------------------------------------------------------+ +int OnInit() +{ + // Create logger instance + g_logger = new CLogger(g_debug_mode); + g_logger.Info("===== EA INITIALIZATION START ====="); + + // Create market data handler + g_market_data = new CMarketData(_Symbol, g_logger); + if(!g_market_data.IsTradingAllowed()) + { + g_logger.Error("Trading not allowed for this symbol"); + return INIT_FAILED; + } + + // Create strategy + g_strategy = new CStrategy(g_market_data, g_logger); + if(!g_strategy.Init()) + { + g_logger.Error("Failed to initialize strategy"); + return INIT_FAILED; + } + + // Create risk manager + g_risk_manager = new CRiskManager(g_market_data, g_logger); + + // Create position manager + g_position_manager = new CPositionManager(g_logger); + + // Create trade manager + g_trade_manager = new CTradeManager(g_logger, g_risk_manager); + + // Create trailing stop manager + g_trailing_stop = new CTrailingStop(g_market_data, g_logger, g_trade_manager); + + // Log configuration + g_logger.Info(StringFormat("Magic Number: %d", g_magic_number)); + g_logger.Info(StringFormat("Symbol: %s", _Symbol)); + g_logger.Info(StringFormat("Timeframe: %s", EnumToString(PERIOD_CURRENT))); + g_logger.Info(StringFormat("Lot Mode: %s", + g_lot_mode == LOT_MODE_FIXED ? "FIXED" : "RISK")); + g_logger.Info(StringFormat("Stop Loss: %d points, Take Profit: %d points", + g_stop_loss_points, g_take_profit_points)); + g_logger.Info(StringFormat("Max Positions: %d, Max Spread: %d points", + g_max_positions, g_max_spread_points)); + + g_logger.Info("===== EA INITIALIZATION COMPLETE ====="); + return INIT_SUCCEEDED; +} + +//+------------------------------------------------------------------+ +// EA Deinitialization +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + g_logger.Info("===== EA DEINITIALIZATION START ====="); + + string reason_text = ""; + switch(reason) + { + case REASON_ACCOUNT: reason_text = "Account changed"; break; + case REASON_CHARTCHANGE: reason_text = "Chart changed"; break; + case REASON_CHARTCLOSE: reason_text = "Chart closed"; break; + case REASON_PARAMETERS: reason_text = "Parameters changed"; break; + case REASON_RECOMPILE: reason_text = "EA recompiled"; break; + case REASON_REMOVE: reason_text = "EA removed"; break; + default: reason_text = "Unknown reason"; break; + } + + g_logger.Info(StringFormat("Deinit reason: %s (%d)", reason_text, reason)); + + // Clean up strategy (release indicator handles) + if(g_strategy != NULL) + { + delete g_strategy; + g_strategy = NULL; + } + + // Clean up all other objects + if(g_trailing_stop != NULL) + { + delete g_trailing_stop; + g_trailing_stop = NULL; + } + + if(g_trade_manager != NULL) + { + delete g_trade_manager; + g_trade_manager = NULL; + } + + if(g_position_manager != NULL) + { + delete g_position_manager; + g_position_manager = NULL; + } + + if(g_risk_manager != NULL) + { + delete g_risk_manager; + g_risk_manager = NULL; + } + + if(g_market_data != NULL) + { + delete g_market_data; + g_market_data = NULL; + } + + if(g_logger != NULL) + { + g_logger.Info("===== EA DEINITIALIZATION COMPLETE ====="); + delete g_logger; + g_logger = NULL; + } +} + +//+------------------------------------------------------------------+ +// Main EA Logic - OnTick() +//+------------------------------------------------------------------+ +void OnTick() +{ + // Step 1: Update market data (always) + if(!g_market_data.IsTradingAllowed()) + { + return; + } + + // Step 2: Check spread - if spread is too wide, don't trade + if(!g_risk_manager.IsSpreadAcceptable()) + { + return; + } + + // Step 3: Check trading hours + if(!g_risk_manager.IsTradingHourValid()) + { + return; + } + + // Step 4: Manage existing positions (trailing stop, break-even) + g_trailing_stop.UpdateAllPositions(); + + // Step 5: Only process signals on new bar + if(!g_market_data.IsNewBar()) + { + return; + } + + // Step 6: Get trading signal from strategy + E_SIGNAL signal = g_strategy.GetSignal(); + + if(signal == SIGNAL_NONE) + { + return; + } + + // Step 7: Check if we already have a position + if(g_position_manager.HasOpenPosition(_Symbol)) + { + if(g_debug_mode) + g_logger.Info("Already have open position, skipping entry"); + return; + } + + // Step 8: Check if new position is allowed + if(!g_position_manager.IsNewPositionAllowed(_Symbol)) + { + g_logger.Warning("New position not allowed (max positions reached)"); + return; + } + + // Step 9: Calculate lot size + double lot = g_risk_manager.CalculateLotSize(g_stop_loss_points); + if(lot <= 0) + { + g_logger.Error("Invalid lot size calculated"); + return; + } + + // Step 10: Execute trade based on signal + bool trade_success = false; + + if(signal == SIGNAL_BUY) + { + trade_success = g_trade_manager.OpenBuyTrade(_Symbol, lot); + } + else if(signal == SIGNAL_SELL) + { + trade_success = g_trade_manager.OpenSellTrade(_Symbol, lot); + } + + if(trade_success) + { + if(g_logger) + g_logger.Info("Trade executed successfully"); + } + else + { + if(g_logger) + g_logger.Error("Trade execution failed"); + } +} +