diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..210203d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,50 @@ +\# Codex instructions for MyProEA + + + +This workspace is only for the MyProEA MT5 Expert Advisor project. + + + +Only read, edit, and reason about files inside this folder unless I explicitly provide another file. + + + +Main EA file: + +\- MyProEA.mq5 + + + +Project include files: + +\- Include/ + + + +Strategy files: + +\- Include/Strategies/ + + + +Do not modify files outside this project. + +Do not modify global MQL5 Include files. + +Do not scan unrelated Experts, Indicators, Scripts, Libraries, Logs, or Profiles. + + + +When changing code: + +1\. Keep the existing architecture. + +2\. Make the smallest safe change. + +3\. Do not rewrite unrelated managers or framework files. + +4\. Strategy files should only output buy/sell signals, SL, TP, and metadata expected by the existing project. + +5\. Explain which file you changed and why. + diff --git a/Include/Config.mqh b/Include/Config.mqh index 0b6b649..a83fe05 100644 --- a/Include/Config.mqh +++ b/Include/Config.mqh @@ -13,23 +13,43 @@ enum E_LOT_MODE LOT_MODE_RISK = 1 // Use risk percent method }; -input E_LOT_MODE g_lot_mode = LOT_MODE_FIXED; // Lot Sizing Mode +enum E_TREND_BREAK_MODE +{ + BREAK_MODE_CANDLE_CLOSE = 0, + BREAK_MODE_BID_ASK_TOUCH = 1 +}; + +enum E_STOP_LOSS_MODE +{ + STOP_LOSS_STRUCTURE = 0, + STOP_LOSS_POINTS = 1, + STOP_LOSS_PERCENT = 2 +}; + +enum E_TAKE_PROFIT_MODE +{ + TAKE_PROFIT_POINTS = 1, + TAKE_PROFIT_PERCENT = 2, + TAKE_PROFIT_RISK_REWARD = 3 +}; + +input E_LOT_MODE g_lot_mode = LOT_MODE_RISK; // 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_stop_loss_points = 100; // Deprecated: strategy owns initial SL +input int g_take_profit_points = 200; // Deprecated: strategy owns initial TP input int g_max_spread_points = 100; // Max Spread in points (1.00 USD for XAUUSD) 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) +input int g_trade_start_hour = 8; // Trading Start Hour (0-23), based on broker server time +input int g_trade_end_hour = 20; // Trading End Hour (0-23), based on broker server time +input string g_trading_hours_note = "Broker server time is used; adjust start/end hours for your broker timezone and session preferences"; // informational only // ==================== TRAILING STOP ==================== input bool g_use_trailing_stop = true; // Use Trailing Stop @@ -38,21 +58,67 @@ 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 +// Enhanced break-even / trailing parameters (ATR-aware) +input bool g_use_break_even_atr = true; // Use ATR-based break-even behavior +input double g_break_even_trigger_r = 1.0; // Break-even trigger as multiple of initial risk (ATR or fixed) +input int g_break_even_buffer_points = 20; // Buffer to apply when moving to break-even (points) + +input bool g_use_trailing_stop_atr = true; // Use ATR-based trailing stop +input double g_trail_start_r = 1.5; // Start trailing after this multiple of initial risk +input double g_trail_distance_atr_multiplier = 1.0; // Trailing distance = ATR * this multiplier + +// ==================== STRATEGY EVALUATION ==================== +input bool g_evaluate_on_new_bar = true; // Evaluate strategy on entry timeframe new bar +input bool g_evaluate_every_tick = false; // Evaluate strategy every tick when true + // ==================== STRATEGY PARAMETERS ==================== -// XAUUSD M15 EMA Pullback Continuation Strategy -input ENUM_TIMEFRAMES g_strategy_entry_timeframe = PERIOD_M15; // Entry signal timeframe -input ENUM_TIMEFRAMES g_strategy_trend_timeframe = PERIOD_H1; // Trend filter timeframe -input int g_trend_fast_ema_period = 50; // H1 EMA fast period -input int g_trend_slow_ema_period = 200; // H1 EMA slow period -input int g_entry_fast_ema_period = 20; // M15 EMA for entry confirmation -input int g_entry_pullback_ema_period = 50; // M15 EMA for pullback touch -input int g_rsi_period = 14; // RSI period on M15 -input int g_rsi_buy_threshold = 50; // RSI buy threshold -input int g_rsi_sell_threshold = 50; // RSI sell threshold -input int g_atr_period = 14; // ATR period on M15 -input double g_atr_sl_multiplier = 1.5; // ATR multiplier for SL (future use) -input double g_atr_tp_multiplier = 2.0; // ATR multiplier for TP (future use) -input bool g_use_atr_stops = false; // Use ATR-based SL/TP if supported +input ENUM_TIMEFRAMES g_strategy_entry_timeframe = PERIOD_M15; // Signal/entry timeframe +input ENUM_TIMEFRAMES g_strategy_trend_timeframe = PERIOD_H1; // Filter/trend timeframe + +input int g_signal_swing_depth = 3; +input int g_filter_swing_depth = 5; +input int g_signal_trend_confirm_count = 1; +input int g_filter_trend_confirm_count = 2; + +input bool g_use_atr_filter = true; +input int g_atr_period = 14; +input double g_min_swing_atr_multiplier = 0.5; + +input bool g_use_breakout_entry = true; +input int g_entry_buffer_points = 10; +input int g_signal_expiration_bars = 3; + +input E_TREND_BREAK_MODE g_trend_break_mode = BREAK_MODE_CANDLE_CLOSE; +input int g_break_buffer_points = 0; +input int g_cooldown_bars_after_trend_break = 1; + +input E_STOP_LOSS_MODE g_stop_loss_mode = STOP_LOSS_STRUCTURE; +input int g_sl_points = 50; +input double g_sl_percent = 0.5; +input int g_sl_buffer_points = 50; + +input E_TAKE_PROFIT_MODE g_take_profit_mode = TAKE_PROFIT_RISK_REWARD; +input int g_tp_points = 100; +input double g_tp_percent = 1.0; +input double g_risk_reward_ratio = 2.0; + +input int g_max_trades_per_filter_trend = 1; +input int g_strategy_max_open_positions = 1; + +input int g_strategy_max_spread_points = 30; + +input bool g_show_signal_swings = true; +input bool g_show_filter_swings = true; +input bool g_show_swing_level_lines = true; +input bool g_show_trend_lines = true; +input bool g_show_setup_trigger_lines = true; +input bool g_show_trend_rectangles = false; + +// TP-less trades are intentionally not supported yet. Add an explicit +// allowNoTakeProfit field to TradeSetup before allowing takeProfit = 0. + +input double g_risk_percent = 1.0; // Risk percent per trade +input double g_max_risk_percent = 2.0; // Maximum allowed risk percent // ==================== DEBUG ==================== input bool g_debug_mode = true; // Enable Debug Logging diff --git a/Include/MarketData.mqh b/Include/MarketData.mqh index bf7370e..d6fd1e4 100644 --- a/Include/MarketData.mqh +++ b/Include/MarketData.mqh @@ -78,7 +78,13 @@ public: // Detect new bar on current timeframe bool IsNewBar() { - datetime bar_time = iTime(m_symbol, PERIOD_CURRENT, 0); + return IsNewBar(PERIOD_CURRENT); + } + + // Detect new bar on a supplied timeframe + bool IsNewBar(ENUM_TIMEFRAMES timeframe) + { + datetime bar_time = iTime(m_symbol, timeframe, 0); if(m_last_bar_time == 0) { diff --git a/Include/RiskManager.mqh b/Include/RiskManager.mqh index b217a4c..585d410 100644 --- a/Include/RiskManager.mqh +++ b/Include/RiskManager.mqh @@ -1,6 +1,6 @@ //+------------------------------------------------------------------+ //| RiskManager.mqh - Risk and position sizing management | -//| Calculates lot sizes, validates parameters, checks trading hours | +//| Calculates trade entry levels, stop loss, take profit and lot sizes| //+------------------------------------------------------------------+ #ifndef __RISKMANAGER_MQH__ @@ -10,6 +10,7 @@ #include "Logger.mqh" #include "MarketData.mqh" #include "Utilities.mqh" +#include "Signal.mqh" class CRiskManager { @@ -18,75 +19,186 @@ private: 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 CalculateLotByRisk(double risk_distance) { - double lot = 0.0; + string symbol = mp_market_data.GetSymbol(); + if(!IsSymbolValid(symbol)) + return 0.0; + if(risk_distance <= 0.0) + { + if(mp_logger) + mp_logger.Error("Risk distance must be positive to calculate lot size"); + return 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); - } + double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); + double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); + if(tick_size <= 0.0 || tick_value <= 0.0) + { + if(mp_logger) + mp_logger.Error("Symbol tick size or tick value invalid for lot calculation"); + return 0.0; + } - return ValidateLotSize(lot); + double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); + if(account_equity <= 0.0) + { + if(mp_logger) + mp_logger.Error("Account equity is invalid for lot calculation"); + return 0.0; + } + + double effective_risk_percent = GetEffectiveRiskPercent(); + if(effective_risk_percent <= 0.0) + { + if(mp_logger) + mp_logger.Error("Effective risk percent is invalid"); + return 0.0; + } + + double risk_amount = account_equity * (effective_risk_percent / 100.0); + + // Loss per single lot = (risk_distance / tick_size) * tick_value + double loss_per_lot = (risk_distance / tick_size) * tick_value; + if(loss_per_lot <= 0.0) + { + if(mp_logger) + mp_logger.Error("Calculated monetary loss per lot is non-positive"); + return 0.0; + } + + double raw_lot = risk_amount / loss_per_lot; + double normalized_lot = CUtilities::NormalizeLot(symbol, raw_lot); + + if(normalized_lot <= 0.0) + { + if(mp_logger) + mp_logger.Warning(StringFormat("No valid lot size: raw=%.6f normalized=%.6f (min/max/step constraints)", raw_lot, normalized_lot)); + return 0.0; + } + + if(mp_logger && g_debug_mode) + { + mp_logger.Info(StringFormat("Tick size=%.8f tick value=%.8f risk%%=%.2f rawLot=%.6f finalLot=%.6f", + tick_size, tick_value, effective_risk_percent, raw_lot, normalized_lot)); + } + + return normalized_lot; } - // Calculate lot size based on risk percent - double CalculateLotByRisk(int stop_loss_points) + bool ValidateTradeSetup(const TradeSetup &setup, double &lot) { - 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; + string symbol = mp_market_data.GetSymbol(); + if(!IsSymbolValid(symbol)) + { + if(mp_logger) + mp_logger.Error("Invalid symbol for trade setup validation"); + return false; + } + if(setup.signal != SIGNAL_BUY && setup.signal != SIGNAL_SELL) + { + if(mp_logger && g_debug_mode) + mp_logger.Info("Trade setup rejected: no valid signal"); + return false; + } - // 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; - } + double execution_price = (setup.signal == SIGNAL_BUY) ? mp_market_data.GetAsk() : mp_market_data.GetBid(); + double setup_entry_price = setup.entryPrice; + double setup_diff = 0.0; + if(setup_entry_price > 0.0) + { + setup_diff = MathAbs(setup_entry_price - execution_price); + } - // 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; + if(setup.stopLoss <= 0.0 || setup.takeProfit <= 0.0 || setup.riskDistance <= 0.0) + { + if(mp_logger) + mp_logger.Warning(StringFormat("Trade setup rejected: valid initial SL and TP are required (TP-less trades need a future explicit allowNoTakeProfit field). setupEntry=%.5f execEntry=%.5f sl=%.5f tp=%.5f risk=%.5f", + setup_entry_price, execution_price, setup.stopLoss, setup.takeProfit, setup.riskDistance)); + return false; + } - // Normalize to broker's lot step - lot = CUtilities::NormalizeLot(mp_market_data.GetSymbol(), lot); + bool valid_side = true; + if(setup.signal == SIGNAL_BUY) + { + valid_side = (setup.stopLoss < execution_price && setup.takeProfit > execution_price); + } + else if(setup.signal == SIGNAL_SELL) + { + valid_side = (setup.stopLoss > execution_price && setup.takeProfit < execution_price); + } + + if(!valid_side) + { + if(mp_logger) + mp_logger.Warning(StringFormat("Trade setup rejected: SL/TP invalid for current market entry price (setupEntry=%.5f execEntry=%.5f sl=%.5f tp=%.5f)", + setup_entry_price, execution_price, setup.stopLoss, setup.takeProfit)); + return false; + } + + // Broker minimal stop distance relative to current market execution price + double point = CUtilities::GetPoint(symbol); + long min_stop_points = (long)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); + if(min_stop_points < 0) + min_stop_points = 0; + double min_stop_distance = min_stop_points * point; + double actual_risk_distance = MathAbs(execution_price - setup.stopLoss); + if(actual_risk_distance <= 0.0) + { + if(mp_logger) + mp_logger.Warning(StringFormat("Trade setup rejected: non-positive risk distance after using execution price (execEntry=%.5f sl=%.5f)", + execution_price, setup.stopLoss)); + return false; + } + if(actual_risk_distance < min_stop_distance) + { + if(mp_logger) + mp_logger.Warning(StringFormat("Trade setup rejected: SL too close to current entry price (distance %.5f < broker stop level %.5f)", actual_risk_distance, min_stop_distance)); + return false; + } if(mp_logger && g_debug_mode) - mp_logger.Info(StringFormat("Lot size calculated: %.2f", lot)); + { + mp_logger.Info(StringFormat("Trade setup details: setupEntry=%.5f execEntry=%.5f diff=%.5f SL=%.5f TP=%.5f setupRisk=%.5f execRisk=%.5f reason=%s", + setup_entry_price, execution_price, setup_diff, setup.stopLoss, setup.takeProfit, setup.riskDistance, actual_risk_distance, setup.reason)); + } - return lot; + // Lot sizing: use actual execution entry-to-SL distance in price units + lot = CalculateLotByRisk(actual_risk_distance); + if(lot <= 0.0) + { + if(mp_logger) + mp_logger.Warning("Trade setup rejected: lot sizing failed or below broker minimum after execution price adjustment"); + return false; + } + + if(mp_logger && g_debug_mode) + { + double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); + double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); + mp_logger.Info(StringFormat("Trade setup validated: signal=%s setupEntry=%.5f execEntry=%.5f diff=%.5f SL=%.5f TP=%.5f execRisk=%.5f tickSize=%.8f tickValue=%.8f riskUsed=%.2f finalLot=%.6f reason=%s", + setup.signal == SIGNAL_BUY ? "BUY" : "SELL", + setup_entry_price, execution_price, setup_diff, + setup.stopLoss, setup.takeProfit, actual_risk_distance, + tick_size, tick_value, GetEffectiveRiskPercent(), lot, setup.reason)); + } + + return true; + + return true; } - // Check if spread is acceptable bool IsSpreadAcceptable() { int max_spread = g_max_spread_points; string symbol = mp_market_data.GetSymbol(); - // XAUUSD/gold normally trades with a much wider spread than forex if(StringFind(symbol, "XAU") >= 0 || StringFind(symbol, "GOLD") >= 0) { max_spread = MathMax(max_spread, 200); @@ -95,7 +207,6 @@ public: return mp_market_data.IsSpreadAcceptable(max_spread); } - // Check if trading is allowed by time filter bool IsTradingHourValid() { if(!g_use_trading_hours) @@ -107,23 +218,21 @@ public: 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)); + 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)); + mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)", + current_hour, g_trade_start_hour, g_trade_end_hour)); return false; } } @@ -131,37 +240,103 @@ public: 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; } + +private: + double GetEffectiveRiskPercent() + { + double risk_percent = g_risk_percent; + if(risk_percent > g_max_risk_percent) + { + if(mp_logger) + mp_logger.Warning(StringFormat("RiskPercent capped from %.2f%% to %.2f%% (MaxRiskPercent)", + risk_percent, g_max_risk_percent)); + risk_percent = g_max_risk_percent; + } + return risk_percent; + } + + double GetAtrValue(int shift) + { + string symbol = mp_market_data.GetSymbol(); + int atr_handle = iATR(symbol, g_strategy_entry_timeframe, g_atr_period); + if(atr_handle == INVALID_HANDLE) + return 0.0; + + double atr_value[]; + ArraySetAsSeries(atr_value, true); + ArrayResize(atr_value, 1); + if(CopyBuffer(atr_handle, 0, shift, 1, atr_value) <= 0) + { + IndicatorRelease(atr_handle); + return 0.0; + } + + double atr = atr_value[0]; + IndicatorRelease(atr_handle); + return atr; + } + + double ValidateLotSize(double lot) + { + string symbol = mp_market_data.GetSymbol(); + double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); + double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); + double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); + + if(min_lot <= 0.0 || max_lot <= 0.0 || lot_step <= 0.0 || max_lot < min_lot) + { + if(mp_logger) + mp_logger.Error("Invalid volume step or limits for symbol"); + return 0.0; + } + + if(lot <= 0.0) + return 0.0; + + double normalized = MathFloor(lot / lot_step) * lot_step; + if(normalized < min_lot || normalized > max_lot) + { + if(mp_logger && g_debug_mode) + mp_logger.Warning(StringFormat("Normalized lot %.2f outside allowed range [%.2f, %.2f]", normalized, min_lot, max_lot)); + return 0.0; + } + + normalized = NormalizeDouble(normalized, 2); + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Lot size calculated: %.2f", normalized)); + + return normalized; + } + + bool IsSymbolValid(const string symbol) + { + if(!mp_market_data.IsTradingAllowed()) + { + if(mp_logger) + mp_logger.Error("Symbol trading is not allowed"); + return false; + } + + if(mp_market_data.GetPoint() <= 0.0) + { + if(mp_logger) + mp_logger.Error("Symbol point size is invalid"); + return false; + } + + if(CUtilities::GetContractSize(symbol) <= 0.0) + { + if(mp_logger) + mp_logger.Error("Symbol contract size is invalid"); + return false; + } + + return true; + } }; #endif //__RISKMANAGER_MQH__ diff --git a/Include/Signal.mqh b/Include/Signal.mqh index 3e83044..95aabc2 100644 --- a/Include/Signal.mqh +++ b/Include/Signal.mqh @@ -13,4 +13,15 @@ enum E_SIGNAL SIGNAL_SELL = -1 // Sell signal }; +struct TradeSetup +{ + E_SIGNAL signal; + double entryPrice; + double stopLoss; + double takeProfit; + double riskDistance; + double atrValue; + string reason; +}; + #endif //__SIGNAL_MQH__ diff --git a/Include/Strategy.mqh b/Include/Strategy.mqh index b0882e5..cea0236 100644 --- a/Include/Strategy.mqh +++ b/Include/Strategy.mqh @@ -1,6 +1,8 @@ //+------------------------------------------------------------------+ -//| Strategy.mqh - XAUUSD M15 EMA Pullback Continuation Strategy | -//| Trend direction on H1, pullback on M15, RSI and ATR confirmation | +//| Strategy.mqh - Sweep breakout multi-timeframe swing structure EA | +//| Implements trend filter on one timeframe and breakout setups on | +//| a lower signal timeframe using confirmed swing structure, ATR | +//| filtering, setup arming, and protected breakout entries. | //+------------------------------------------------------------------+ #ifndef __STRATEGY_MQH__ @@ -12,234 +14,967 @@ #include "Config.mqh" #include "Utilities.mqh" +// Local enums for strategy logic +enum E_TREND_STATE +{ + NO_TREND = 0, + UPTREND_ACTIVE = 1, + DOWNTREND_ACTIVE = -1, + TREND_BROKEN = 2, + WAITING_FOR_NEW_TREND = 3 +}; + +struct SSwing +{ + datetime time; + double price; + bool isHigh; + ENUM_TIMEFRAMES timeframe; + ulong id; +}; + +struct SArmedSetup +{ + bool active; + E_SIGNAL direction; + ulong setupID; + ulong trendID; + int barCountAtArm; + double triggerPrice; + bool traded; +}; + class CStrategy { private: - // Indicator handles - int m_ema_h1_fast_handle; - int m_ema_h1_slow_handle; - int m_ema_m15_fast_handle; - int m_rsi_m15_handle; - int m_atr_m15_handle; - - // References CMarketData *mp_market_data; CLogger *mp_logger; - // Timeframes - ENUM_TIMEFRAMES m_entry_timeframe; - ENUM_TIMEFRAMES m_trend_timeframe; + ENUM_TIMEFRAMES m_signal_timeframe; + ENUM_TIMEFRAMES m_filter_timeframe; - // Parameters - int m_trend_fast_ema_period; - int m_trend_slow_ema_period; - int m_entry_fast_ema_period; - int m_entry_pullback_ema_period; - int m_rsi_period; - int m_rsi_buy_threshold; - int m_rsi_sell_threshold; - int m_atr_period; - double m_last_atr_value; - bool m_use_atr_stops; - double m_atr_sl_multiplier; - double m_atr_tp_multiplier; + int m_signal_bar_count; + int m_filter_bar_count; + + int m_signal_atr_handle; + int m_filter_atr_handle; + + E_TREND_STATE m_filter_state; + ulong m_filter_trend_id; + int m_trades_taken_in_current_trend; + int m_cooldown_bars_remaining; + bool m_trend_broken_on_tick; + datetime m_trend_broken_time; + + SSwing m_signal_highs[]; + SSwing m_signal_lows[]; + SSwing m_filter_highs[]; + SSwing m_filter_lows[]; + ulong m_used_setup_ids[]; + + SArmedSetup m_buy_setup; + SArmedSetup m_sell_setup; + + bool m_initialized; public: - // Constructor CStrategy(CMarketData *market_data, CLogger *logger) { mp_market_data = market_data; mp_logger = logger; - m_ema_h1_fast_handle = INVALID_HANDLE; - m_ema_h1_slow_handle = INVALID_HANDLE; - m_ema_m15_fast_handle = INVALID_HANDLE; - m_rsi_m15_handle = INVALID_HANDLE; - m_atr_m15_handle = INVALID_HANDLE; - m_last_atr_value = 0.0; - - m_entry_timeframe = g_strategy_entry_timeframe; - m_trend_timeframe = g_strategy_trend_timeframe; - m_trend_fast_ema_period = g_trend_fast_ema_period; - m_trend_slow_ema_period = g_trend_slow_ema_period; - m_entry_fast_ema_period = g_entry_fast_ema_period; - m_entry_pullback_ema_period = g_entry_pullback_ema_period; - m_rsi_period = g_rsi_period; - m_rsi_buy_threshold = g_rsi_buy_threshold; - m_rsi_sell_threshold = g_rsi_sell_threshold; - m_atr_period = g_atr_period; - m_use_atr_stops = g_use_atr_stops; - m_atr_sl_multiplier = g_atr_sl_multiplier; - m_atr_tp_multiplier = g_atr_tp_multiplier; + m_signal_timeframe = g_strategy_entry_timeframe; + m_filter_timeframe = g_strategy_trend_timeframe; + m_signal_bar_count = 0; + m_filter_bar_count = 0; + m_signal_atr_handle = INVALID_HANDLE; + m_filter_atr_handle = INVALID_HANDLE; + m_calls = 0; + m_filter_state = NO_TREND; + m_filter_trend_id = 0; + m_trades_taken_in_current_trend = 0; + m_cooldown_bars_remaining = 0; + m_trend_broken_on_tick = false; + m_trend_broken_time = 0; + m_buy_setup.active = false; + m_sell_setup.active = false; + m_buy_setup.traded = false; + m_sell_setup.traded = false; + m_initialized = false; } - // Destructor - clean up indicator handles ~CStrategy() { Cleanup(); } - // Initialize strategy and create indicator handles bool Init() { const string symbol = mp_market_data.GetSymbol(); - m_ema_h1_fast_handle = iMA(symbol, m_trend_timeframe, - m_trend_fast_ema_period, 0, MODE_EMA, PRICE_CLOSE); - if(m_ema_h1_fast_handle == INVALID_HANDLE) + if(g_use_atr_filter) + { + m_signal_atr_handle = iATR(symbol, m_signal_timeframe, g_atr_period); + if(m_signal_atr_handle == INVALID_HANDLE) + { + if(mp_logger) + mp_logger.Error("Failed to create ATR indicator for signal timeframe"); + return false; + } + + m_filter_atr_handle = iATR(symbol, m_filter_timeframe, g_atr_period); + if(m_filter_atr_handle == INVALID_HANDLE) + { + if(mp_logger) + mp_logger.Error("Failed to create ATR indicator for filter timeframe"); + IndicatorRelease(m_signal_atr_handle); + m_signal_atr_handle = INVALID_HANDLE; + return false; + } + } + + m_signal_bar_count = iBars(symbol, m_signal_timeframe); + m_filter_bar_count = iBars(symbol, m_filter_timeframe); + if(m_signal_bar_count <= 0 || m_filter_bar_count <= 0) { if(mp_logger) - mp_logger.Error("Failed to create H1 EMA fast indicator"); + mp_logger.Error("Unable to read bars for configured timeframes"); return false; } - m_ema_h1_slow_handle = iMA(symbol, m_trend_timeframe, - m_trend_slow_ema_period, 0, MODE_EMA, PRICE_CLOSE); - if(m_ema_h1_slow_handle == INVALID_HANDLE) - { - if(mp_logger) - mp_logger.Error("Failed to create H1 EMA slow indicator"); - IndicatorRelease(m_ema_h1_fast_handle); - m_ema_h1_fast_handle = INVALID_HANDLE; - return false; - } - - m_ema_m15_fast_handle = iMA(symbol, m_entry_timeframe, - m_entry_fast_ema_period, 0, MODE_EMA, PRICE_CLOSE); - if(m_ema_m15_fast_handle == INVALID_HANDLE) - { - if(mp_logger) - mp_logger.Error("Failed to create M15 EMA fast indicator"); - IndicatorRelease(m_ema_h1_fast_handle); - m_ema_h1_fast_handle = INVALID_HANDLE; - IndicatorRelease(m_ema_h1_slow_handle); - m_ema_h1_slow_handle = INVALID_HANDLE; - return false; - } - - m_rsi_m15_handle = iRSI(symbol, m_entry_timeframe, m_rsi_period, PRICE_CLOSE); - if(m_rsi_m15_handle == INVALID_HANDLE) - { - if(mp_logger) - mp_logger.Error("Failed to create M15 RSI indicator"); - ReleaseAllHandles(); - return false; - } - - m_atr_m15_handle = iATR(symbol, m_entry_timeframe, m_atr_period); - if(m_atr_m15_handle == INVALID_HANDLE) - { - if(mp_logger) - mp_logger.Error("Failed to create M15 ATR indicator"); - ReleaseAllHandles(); - return false; - } - - if(mp_logger) - mp_logger.Info("Strategy (M15 EMA Pullback Continuation) initialized successfully"); + ScanConfirmedSwings(m_filter_timeframe, g_filter_swing_depth, m_filter_highs, m_filter_lows); + ScanConfirmedSwings(m_signal_timeframe, g_signal_swing_depth, m_signal_highs, m_signal_lows); + UpdateFilterTrendState(); + m_initialized = true; return true; } - // Generate trading signal based on H1 trend and M15 pullback continuation E_SIGNAL GetSignal() { + return GetTradeSetup().signal; + } + + TradeSetup GetTradeSetup() + { + TradeSetup setup; + setup.signal = SIGNAL_NONE; + setup.entryPrice = 0.0; + setup.stopLoss = 0.0; + setup.takeProfit = 0.0; + setup.riskDistance = 0.0; + setup.atrValue = 0.0; + setup.reason = "No valid setup"; + const string symbol = mp_market_data.GetSymbol(); + m_calls++; - double ema_h1_fast = iGetIndicatorValue(m_ema_h1_fast_handle, 1); - double ema_h1_slow = iGetIndicatorValue(m_ema_h1_slow_handle, 1); - if(ema_h1_fast == 0.0 || ema_h1_slow == 0.0) - return SIGNAL_NONE; - - double ema_m15_fast_last = iGetIndicatorValue(m_ema_m15_fast_handle, 1); - if(ema_m15_fast_last == 0.0) - return SIGNAL_NONE; - - double close_last = iClose(symbol, m_entry_timeframe, 1); - double open_last = iOpen(symbol, m_entry_timeframe, 1); - - if(close_last <= 0.0 || open_last <= 0.0) - return SIGNAL_NONE; - - if(ema_h1_fast > ema_h1_slow) + if(!m_initialized) { - bool entry_condition = (close_last > ema_m15_fast_last && close_last > open_last); - - if(entry_condition) + if(!Init()) { - if(mp_logger && g_debug_mode) - mp_logger.Info(StringFormat("BUY Signal: H1 EMA50=%.5f > EMA200=%.5f, M15 close=%.5f > EMA20=%.5f", - ema_h1_fast, ema_h1_slow, close_last, ema_m15_fast_last)); - return SIGNAL_BUY; - } - } - else if(ema_h1_fast < ema_h1_slow) - { - bool entry_condition = (close_last < ema_m15_fast_last && close_last < open_last); - - if(entry_condition) - { - if(mp_logger && g_debug_mode) - mp_logger.Info(StringFormat("SELL Signal: H1 EMA50=%.5f < EMA200=%.5f, M15 close=%.5f < EMA20=%.5f", - ema_h1_fast, ema_h1_slow, close_last, ema_m15_fast_last)); - return SIGNAL_SELL; + setup.reason = "Strategy initialization failed"; + return setup; } } - return SIGNAL_NONE; + m_trend_broken_on_tick = false; + + int filter_bar_count = iBars(symbol, m_filter_timeframe); + bool filter_new_bar = (filter_bar_count > m_filter_bar_count); + if(filter_new_bar) + m_filter_bar_count = filter_bar_count; + + int signal_bar_count = iBars(symbol, m_signal_timeframe); + bool signal_new_bar = (signal_bar_count > m_signal_bar_count); + if(signal_new_bar) + m_signal_bar_count = signal_bar_count; + + if(filter_new_bar) + ScanConfirmedSwings(m_filter_timeframe, g_filter_swing_depth, m_filter_highs, m_filter_lows); + if(signal_new_bar) + ScanConfirmedSwings(m_signal_timeframe, g_signal_swing_depth, m_signal_highs, m_signal_lows); + + UpdateFilterTrendState(); + EvaluateTrendBreak(filter_new_bar); + + if(signal_new_bar) + { + ExpireSetups(); + ArmSetups(); + } + + if(m_trend_broken_on_tick) + { + setup.reason = "Filter trend broken on current tick"; + return setup; + } + + if(m_filter_state != UPTREND_ACTIVE && m_filter_state != DOWNTREND_ACTIVE) + { + setup.reason = "No active filter trend"; + return setup; + } + + if(!mp_market_data.IsSpreadAcceptable(g_strategy_max_spread_points)) + { + setup.reason = "Spread too wide for breakout entry"; + return setup; + } + + if(GetOpenPositionCount(symbol) >= g_strategy_max_open_positions) + { + setup.reason = "Maximum open positions achieved"; + return setup; + } + + if(m_buy_setup.active && m_buy_setup.direction == SIGNAL_BUY) + { + if(m_filter_state == UPTREND_ACTIVE && m_buy_setup.trendID == m_filter_trend_id && !m_buy_setup.traded) + { + if(!IsSetupExpired(m_buy_setup)) + { + double ask = mp_market_data.GetAsk(); + double buffer = CUtilities::PointsToPrice(symbol, g_entry_buffer_points); + double threshold = m_buy_setup.triggerPrice + (g_use_breakout_entry ? buffer : 0.0); + if(ask > threshold) + { + double stop_loss = CalculateStopLoss(symbol, SIGNAL_BUY, ask); + double take_profit = CalculateTakeProfit(symbol, SIGNAL_BUY, ask, stop_loss); + if(IsTradeLevelSetValid(SIGNAL_BUY, ask, stop_loss, take_profit)) + { + setup.signal = SIGNAL_BUY; + setup.entryPrice = ask; + setup.stopLoss = CUtilities::NormalizePrice(symbol, stop_loss); + setup.takeProfit = CUtilities::NormalizePrice(symbol, take_profit); + setup.riskDistance = MathAbs(ask - stop_loss); + setup.atrValue = GetLatestATRValue(m_signal_timeframe, 1); + setup.reason = StringFormat("BUY breakout signal: FilterTrendID=%I64u SetupID=%I64u trigger=%.5f", + m_filter_trend_id, m_buy_setup.setupID, m_buy_setup.triggerPrice); + MarkSetupTraded(m_buy_setup); + return setup; + } + setup.reason = "BUY breakout rejected: invalid SL/TP"; + } + } + } + } + + if(m_sell_setup.active && m_sell_setup.direction == SIGNAL_SELL) + { + if(m_filter_state == DOWNTREND_ACTIVE && m_sell_setup.trendID == m_filter_trend_id && !m_sell_setup.traded) + { + if(!IsSetupExpired(m_sell_setup)) + { + double bid = mp_market_data.GetBid(); + double buffer = CUtilities::PointsToPrice(symbol, g_entry_buffer_points); + double threshold = m_sell_setup.triggerPrice - (g_use_breakout_entry ? buffer : 0.0); + if(bid < threshold) + { + double stop_loss = CalculateStopLoss(symbol, SIGNAL_SELL, bid); + double take_profit = CalculateTakeProfit(symbol, SIGNAL_SELL, bid, stop_loss); + if(IsTradeLevelSetValid(SIGNAL_SELL, bid, stop_loss, take_profit)) + { + setup.signal = SIGNAL_SELL; + setup.entryPrice = bid; + setup.stopLoss = CUtilities::NormalizePrice(symbol, stop_loss); + setup.takeProfit = CUtilities::NormalizePrice(symbol, take_profit); + setup.riskDistance = MathAbs(stop_loss - bid); + setup.atrValue = GetLatestATRValue(m_signal_timeframe, 1); + setup.reason = StringFormat("SELL breakout signal: FilterTrendID=%I64u SetupID=%I64u trigger=%.5f", + m_filter_trend_id, m_sell_setup.setupID, m_sell_setup.triggerPrice); + MarkSetupTraded(m_sell_setup); + return setup; + } + setup.reason = "SELL breakout rejected: invalid SL/TP"; + } + } + } + } + + setup.reason = "No valid breakout trigger"; + return setup; } - double GetLastAtrValue() const + void LogDiagnostics() { - return m_last_atr_value; - } - - void Cleanup() - { - ReleaseAllHandles(); + if(mp_logger && g_debug_mode) + { + mp_logger.Info(StringFormat("Strategy diagnostics: calls=%I64d state=%d FilterTrendID=%I64u tradesInTrend=%d buyActive=%s sellActive=%s", + m_calls, m_filter_state, m_filter_trend_id, m_trades_taken_in_current_trend, + m_buy_setup.active ? "true" : "false", + m_sell_setup.active ? "true" : "false")); + } } private: - void ReleaseAllHandles() + long m_calls; + + void Cleanup() { - if(m_ema_h1_fast_handle != INVALID_HANDLE) + if(m_signal_atr_handle != INVALID_HANDLE) { - IndicatorRelease(m_ema_h1_fast_handle); - m_ema_h1_fast_handle = INVALID_HANDLE; + IndicatorRelease(m_signal_atr_handle); + m_signal_atr_handle = INVALID_HANDLE; } - if(m_ema_h1_slow_handle != INVALID_HANDLE) + if(m_filter_atr_handle != INVALID_HANDLE && m_filter_atr_handle != m_signal_atr_handle) { - IndicatorRelease(m_ema_h1_slow_handle); - m_ema_h1_slow_handle = INVALID_HANDLE; - } - if(m_ema_m15_fast_handle != INVALID_HANDLE) - { - IndicatorRelease(m_ema_m15_fast_handle); - m_ema_m15_fast_handle = INVALID_HANDLE; - } - if(m_rsi_m15_handle != INVALID_HANDLE) - { - IndicatorRelease(m_rsi_m15_handle); - m_rsi_m15_handle = INVALID_HANDLE; - } - if(m_atr_m15_handle != INVALID_HANDLE) - { - IndicatorRelease(m_atr_m15_handle); - m_atr_m15_handle = INVALID_HANDLE; + IndicatorRelease(m_filter_atr_handle); + m_filter_atr_handle = INVALID_HANDLE; } } - double iGetIndicatorValue(int handle, int shift) + double GetLatestATRValue(ENUM_TIMEFRAMES timeframe, int shift) { + if(!g_use_atr_filter) + return 0.0; + + int handle = (timeframe == m_signal_timeframe) ? m_signal_atr_handle : m_filter_atr_handle; if(handle == INVALID_HANDLE) return 0.0; - double value[1]; + double value[]; ArraySetAsSeries(value, true); + ArrayResize(value, 1); if(CopyBuffer(handle, 0, shift, 1, value) <= 0) return 0.0; return value[0]; } + + bool ScanConfirmedSwings(ENUM_TIMEFRAMES timeframe, int depth, SSwing &highs[], SSwing &lows[]) + { + const string symbol = mp_market_data.GetSymbol(); + int barCount = iBars(symbol, timeframe); + if(barCount < (depth * 2 + 3)) + return false; + + int request = MathMin(barCount, depth * 20 + 50); + if(request < (depth * 2 + 3)) + request = depth * 2 + 3; + + MqlRates rates[]; + ArraySetAsSeries(rates, true); + ArrayResize(rates, request); + int copied = CopyRates(symbol, timeframe, 0, request, rates); + if(copied <= 0) + return false; + ArrayResize(rates, copied); + + for(int index = depth + 1; index < copied - depth; index++) + { + if(IsSwingHigh(rates, index, depth)) + { + SSwing swing; + swing.time = rates[index].time; + swing.price = rates[index].high; + swing.isHigh = true; + swing.timeframe = timeframe; + swing.id = ((ulong)swing.time << 8) | (ulong)timeframe; + if(!SwingExists(swing, highs)) + { + if(!IsSwingTooSmall(symbol, swing, lows)) + AddSwing(highs, swing); + } + } + else if(IsSwingLow(rates, index, depth)) + { + SSwing swing; + swing.time = rates[index].time; + swing.price = rates[index].low; + swing.isHigh = false; + swing.timeframe = timeframe; + swing.id = ((ulong)swing.time << 8) | (ulong)timeframe; + if(!SwingExists(swing, lows)) + { + if(!IsSwingTooSmall(symbol, swing, highs)) + AddSwing(lows, swing); + } + } + } + + TrimSwings(highs, 64); + TrimSwings(lows, 64); + return true; + } + + bool IsSwingHigh(MqlRates &rates[], int index, int depth) + { + double price = rates[index].high; + for(int offset = 1; offset <= depth; offset++) + { + if(price <= rates[index - offset].high || price <= rates[index + offset].high) + return false; + } + return true; + } + + bool IsSwingLow(MqlRates &rates[], int index, int depth) + { + double price = rates[index].low; + for(int offset = 1; offset <= depth; offset++) + { + if(price >= rates[index - offset].low || price >= rates[index + offset].low) + return false; + } + return true; + } + + bool SwingExists(const SSwing &swing, SSwing &array[]) const + { + for(int i = 0; i < ArraySize(array); i++) + { + if(array[i].id == swing.id) + return true; + } + return false; + } + + void AddSwing(SSwing &array[], const SSwing &swing) + { + ArrayResize(array, ArraySize(array) + 1); + array[ArraySize(array) - 1] = swing; + } + + void TrimSwings(SSwing &array[], int keep) + { + int size = ArraySize(array); + if(size <= keep) + return; + int remove = size - keep; + for(int i = 0; i < keep; i++) + array[i] = array[i + remove]; + ArrayResize(array, keep); + } + + bool IsSwingTooSmall(const string symbol, const SSwing &swing, SSwing &oppositeSwings[]) + { + if(!g_use_atr_filter) + return false; + + double atrValue = GetLatestATRValue(swing.timeframe, 1); + if(atrValue <= 0.0) + return false; + + double minDistance = atrValue * g_min_swing_atr_multiplier; + double lastOpposite = GetLastOppositeSwingPriceBefore(oppositeSwings, swing.time); + if(lastOpposite <= 0.0) + return false; + + if(swing.isHigh) + return (swing.price - lastOpposite) < minDistance; + return (lastOpposite - swing.price) < minDistance; + } + + double GetLastOppositeSwingPriceBefore(SSwing &swings[], datetime beforeTime) const + { + double price = 0.0; + for(int i = ArraySize(swings) - 1; i >= 0; i--) + { + if(swings[i].time < beforeTime) + { + price = swings[i].price; + break; + } + } + return price; + } + + void UpdateFilterTrendState() + { + E_TREND_STATE detected = DetectFilterTrend(); + if(detected == m_filter_state && (m_filter_state == UPTREND_ACTIVE || m_filter_state == DOWNTREND_ACTIVE)) + return; + + if(detected == UPTREND_ACTIVE || detected == DOWNTREND_ACTIVE) + { + if(m_filter_state == TREND_BROKEN) + return; + + if(m_filter_state == WAITING_FOR_NEW_TREND && !IsFreshFilterTrendAfterBreak()) + return; + + if(m_filter_state != detected) + StartNewFilterTrend(detected); + } + else if(m_filter_state == TREND_BROKEN && detected != TREND_BROKEN) + { + // remain in broken/waiting until a new trend is clearly established + if(m_cooldown_bars_remaining <= 0) + m_filter_state = WAITING_FOR_NEW_TREND; + } + else if(m_filter_state == WAITING_FOR_NEW_TREND && detected == NO_TREND) + { + // remain waiting until a fresh trend forms + } + else if(m_filter_state == NO_TREND && detected == NO_TREND) + { + // no state change + } + } + + void StartNewFilterTrend(E_TREND_STATE detected) + { + m_filter_trend_id++; + m_trades_taken_in_current_trend = 0; + m_cooldown_bars_remaining = 0; + m_filter_state = detected; + m_buy_setup.active = false; + m_sell_setup.active = false; + m_buy_setup.traded = false; + m_sell_setup.traded = false; + ArrayResize(m_used_setup_ids, 0); + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("New filter trend detected: %d, FilterTrendID=%I64u", detected, m_filter_trend_id)); + } + + bool IsFreshFilterTrendAfterBreak() + { + if(m_trend_broken_time <= 0) + return true; + datetime lastHighTime = GetLastSwingTime(m_filter_highs); + datetime lastLowTime = GetLastSwingTime(m_filter_lows); + return (lastHighTime > m_trend_broken_time && lastLowTime > m_trend_broken_time); + } + + E_TREND_STATE DetectFilterTrend() const + { + if(ArraySize(m_filter_highs) < (g_filter_trend_confirm_count + 1) || ArraySize(m_filter_lows) < (g_filter_trend_confirm_count + 1)) + return NO_TREND; + + bool higherHighs = true; + for(int i = ArraySize(m_filter_highs) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_highs) - 1; i++) + { + if(m_filter_highs[i + 1].price <= m_filter_highs[i].price) + { + higherHighs = false; + break; + } + } + + bool higherLows = true; + for(int i = ArraySize(m_filter_lows) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_lows) - 1; i++) + { + if(m_filter_lows[i + 1].price <= m_filter_lows[i].price) + { + higherLows = false; + break; + } + } + + if(higherHighs && higherLows) + return UPTREND_ACTIVE; + + bool lowerHighs = true; + for(int i = ArraySize(m_filter_highs) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_highs) - 1; i++) + { + if(m_filter_highs[i + 1].price >= m_filter_highs[i].price) + { + lowerHighs = false; + break; + } + } + + bool lowerLows = true; + for(int i = ArraySize(m_filter_lows) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_lows) - 1; i++) + { + if(m_filter_lows[i + 1].price >= m_filter_lows[i].price) + { + lowerLows = false; + break; + } + } + + if(lowerHighs && lowerLows) + return DOWNTREND_ACTIVE; + + return NO_TREND; + } + + void EvaluateTrendBreak(bool filter_new_bar) + { + if(m_filter_state != UPTREND_ACTIVE && m_filter_state != DOWNTREND_ACTIVE) + { + if(m_filter_state == TREND_BROKEN && filter_new_bar) + { + if(m_cooldown_bars_remaining > 0) + m_cooldown_bars_remaining--; + if(m_cooldown_bars_remaining <= 0) + m_filter_state = WAITING_FOR_NEW_TREND; + } + return; + } + + const string symbol = mp_market_data.GetSymbol(); + double buffer = CUtilities::PointsToPrice(symbol, g_break_buffer_points); + + if(m_filter_state == UPTREND_ACTIVE) + { + double swingLow = GetLastSwingPrice(m_filter_lows); + if(swingLow <= 0.0) + return; + + bool broken = false; + if(g_trend_break_mode == BREAK_MODE_BID_ASK_TOUCH) + broken = (mp_market_data.GetBid() <= swingLow - buffer); + else + { + double last_close = iClose(symbol, m_filter_timeframe, 1); + broken = (last_close > 0.0 && last_close <= swingLow - buffer); + } + + if(broken) + { + m_filter_state = TREND_BROKEN; + m_trend_broken_on_tick = true; + m_trend_broken_time = iTime(symbol, m_filter_timeframe, 1); + if(m_trend_broken_time <= 0) + m_trend_broken_time = TimeCurrent(); + m_cooldown_bars_remaining = g_cooldown_bars_after_trend_break; + CancelSetups("Filter uptrend broken"); + if(mp_logger && g_debug_mode) + mp_logger.Info("Filter uptrend broken"); + } + } + else if(m_filter_state == DOWNTREND_ACTIVE) + { + double swingHigh = GetLastSwingPrice(m_filter_highs); + if(swingHigh <= 0.0) + return; + + bool broken = false; + if(g_trend_break_mode == BREAK_MODE_BID_ASK_TOUCH) + broken = (mp_market_data.GetAsk() >= swingHigh + buffer); + else + { + double last_close = iClose(symbol, m_filter_timeframe, 1); + broken = (last_close > 0.0 && last_close >= swingHigh + buffer); + } + + if(broken) + { + m_filter_state = TREND_BROKEN; + m_trend_broken_on_tick = true; + m_trend_broken_time = iTime(symbol, m_filter_timeframe, 1); + if(m_trend_broken_time <= 0) + m_trend_broken_time = TimeCurrent(); + m_cooldown_bars_remaining = g_cooldown_bars_after_trend_break; + CancelSetups("Filter downtrend broken"); + if(mp_logger && g_debug_mode) + mp_logger.Info("Filter downtrend broken"); + } + } + } + + double GetLastSwingPrice(SSwing &swings[]) const + { + int size = ArraySize(swings); + if(size <= 0) + return 0.0; + return swings[size - 1].price; + } + + void CancelSetups(const string reason) + { + if(m_buy_setup.active) + { + AddUsedSetupID(m_buy_setup.setupID); + m_buy_setup.active = false; + m_buy_setup.traded = true; + } + if(m_sell_setup.active) + { + AddUsedSetupID(m_sell_setup.setupID); + m_sell_setup.active = false; + m_sell_setup.traded = true; + } + if(mp_logger && g_debug_mode) + mp_logger.Info("Canceling setups due to trend break: " + reason); + } + + void ArmSetups() + { + const string symbol = mp_market_data.GetSymbol(); + if(m_trades_taken_in_current_trend >= g_max_trades_per_filter_trend) + return; + + if(m_filter_state == UPTREND_ACTIVE && !m_buy_setup.active) + { + if(HasBullishSignalStructure()) + { + double trigger = GetLastSwingPrice(m_signal_highs); + ulong setupID = ((ulong)GetLastSwingTime(m_signal_highs) << 8) | (ulong)m_signal_timeframe; + if(trigger > 0.0 && setupID > 0 && !IsSetupIDUsed(setupID) && mp_market_data.IsSpreadAcceptable(g_strategy_max_spread_points)) + { + m_buy_setup.active = true; + m_buy_setup.direction = SIGNAL_BUY; + m_buy_setup.setupID = setupID; + m_buy_setup.trendID = m_filter_trend_id; + m_buy_setup.barCountAtArm = m_signal_bar_count; + m_buy_setup.triggerPrice = trigger; + m_buy_setup.traded = false; + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Armed BUY setup: trigger=%.5f trendID=%I64u", trigger, m_filter_trend_id)); + } + } + } + + if(m_filter_state == DOWNTREND_ACTIVE && !m_sell_setup.active) + { + if(HasBearishSignalStructure()) + { + double trigger = GetLastSwingPrice(m_signal_lows); + ulong setupID = ((ulong)GetLastSwingTime(m_signal_lows) << 8) | (ulong)m_signal_timeframe; + if(trigger > 0.0 && setupID > 0 && !IsSetupIDUsed(setupID) && mp_market_data.IsSpreadAcceptable(g_strategy_max_spread_points)) + { + m_sell_setup.active = true; + m_sell_setup.direction = SIGNAL_SELL; + m_sell_setup.setupID = setupID; + m_sell_setup.trendID = m_filter_trend_id; + m_sell_setup.barCountAtArm = m_signal_bar_count; + m_sell_setup.triggerPrice = trigger; + m_sell_setup.traded = false; + if(mp_logger && g_debug_mode) + mp_logger.Info(StringFormat("Armed SELL setup: trigger=%.5f trendID=%I64u", trigger, m_filter_trend_id)); + } + } + } + } + + datetime GetLastSwingTime(SSwing &swings[]) const + { + int size = ArraySize(swings); + if(size <= 0) + return 0; + return swings[size - 1].time; + } + + bool HasBullishSignalStructure() const + { + return DetectSignalTrend(g_signal_trend_confirm_count) == UPTREND_ACTIVE; + } + + bool HasBearishSignalStructure() const + { + return DetectSignalTrend(g_signal_trend_confirm_count) == DOWNTREND_ACTIVE; + } + + E_TREND_STATE DetectSignalTrend(int confirmCount) const + { + if(ArraySize(m_signal_highs) < (confirmCount + 1) || ArraySize(m_signal_lows) < (confirmCount + 1)) + return NO_TREND; + + bool higherHighs = true; + for(int i = ArraySize(m_signal_highs) - confirmCount - 1; i < ArraySize(m_signal_highs) - 1; i++) + { + if(m_signal_highs[i + 1].price <= m_signal_highs[i].price) + { + higherHighs = false; + break; + } + } + + bool higherLows = true; + for(int i = ArraySize(m_signal_lows) - confirmCount - 1; i < ArraySize(m_signal_lows) - 1; i++) + { + if(m_signal_lows[i + 1].price <= m_signal_lows[i].price) + { + higherLows = false; + break; + } + } + + if(higherHighs && higherLows) + return UPTREND_ACTIVE; + + bool lowerHighs = true; + for(int i = ArraySize(m_signal_highs) - confirmCount - 1; i < ArraySize(m_signal_highs) - 1; i++) + { + if(m_signal_highs[i + 1].price >= m_signal_highs[i].price) + { + lowerHighs = false; + break; + } + } + + bool lowerLows = true; + for(int i = ArraySize(m_signal_lows) - confirmCount - 1; i < ArraySize(m_signal_lows) - 1; i++) + { + if(m_signal_lows[i + 1].price >= m_signal_lows[i].price) + { + lowerLows = false; + break; + } + } + + if(lowerHighs && lowerLows) + return DOWNTREND_ACTIVE; + + return NO_TREND; + } + + void ExpireSetups() + { + if(m_buy_setup.active && IsSetupExpired(m_buy_setup)) + { + AddUsedSetupID(m_buy_setup.setupID); + m_buy_setup.active = false; + m_buy_setup.traded = true; + if(mp_logger && g_debug_mode) + mp_logger.Info("Expired BUY setup due to age or trend change"); + } + if(m_sell_setup.active && IsSetupExpired(m_sell_setup)) + { + AddUsedSetupID(m_sell_setup.setupID); + m_sell_setup.active = false; + m_sell_setup.traded = true; + if(mp_logger && g_debug_mode) + mp_logger.Info("Expired SELL setup due to age or trend change"); + } + } + + bool IsSetupExpired(const SArmedSetup &setup) const + { + if(!setup.active) + return true; + if(setup.traded) + return true; + if(setup.trendID != m_filter_trend_id) + return true; + if(setup.direction == SIGNAL_BUY && m_filter_state != UPTREND_ACTIVE) + return true; + if(setup.direction == SIGNAL_SELL && m_filter_state != DOWNTREND_ACTIVE) + return true; + if(g_signal_expiration_bars >= 0 && (m_signal_bar_count - setup.barCountAtArm) >= g_signal_expiration_bars) + return true; + return false; + } + + void MarkSetupTraded(SArmedSetup &setup) + { + AddUsedSetupID(setup.setupID); + setup.traded = true; + setup.active = false; + m_trades_taken_in_current_trend++; + } + + bool IsSetupIDUsed(ulong setupID) const + { + for(int i = 0; i < ArraySize(m_used_setup_ids); i++) + { + if(m_used_setup_ids[i] == setupID) + return true; + } + return false; + } + + void AddUsedSetupID(ulong setupID) + { + if(setupID == 0 || IsSetupIDUsed(setupID)) + return; + int size = ArraySize(m_used_setup_ids); + ArrayResize(m_used_setup_ids, size + 1); + m_used_setup_ids[size] = setupID; + } + + int GetOpenPositionCount(const string symbol) const + { + int count = 0; + for(int i = PositionsTotal() - 1; i >= 0; i--) + { + ulong ticket = PositionGetTicket(i); + if(ticket == 0) + continue; + if(!PositionSelectByTicket(ticket)) + continue; + if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == g_magic_number) + count++; + } + return count; + } + + double CalculateStopLoss(const string symbol, E_SIGNAL direction, double entryPrice) + { + double point = CUtilities::GetPoint(symbol); + double stopLoss = 0.0; + + if(g_stop_loss_mode == STOP_LOSS_STRUCTURE) + { + if(direction == SIGNAL_BUY) + { + double swingLow = GetLastSwingPrice(m_signal_lows); + if(swingLow > 0.0) + stopLoss = swingLow - g_sl_buffer_points * point; + } + else if(direction == SIGNAL_SELL) + { + double swingHigh = GetLastSwingPrice(m_signal_highs); + if(swingHigh > 0.0) + stopLoss = swingHigh + g_sl_buffer_points * point; + } + } + else if(g_stop_loss_mode == STOP_LOSS_POINTS) + { + double distance = CUtilities::PointsToPrice(symbol, g_sl_points); + stopLoss = (direction == SIGNAL_BUY) ? entryPrice - distance : entryPrice + distance; + } + else if(g_stop_loss_mode == STOP_LOSS_PERCENT) + { + double distance = MathAbs(entryPrice) * (g_sl_percent / 100.0); + stopLoss = (direction == SIGNAL_BUY) ? entryPrice - distance : entryPrice + distance; + } + + stopLoss = CUtilities::NormalizePrice(symbol, stopLoss); + if(direction == SIGNAL_BUY && stopLoss >= entryPrice) + return 0.0; + if(direction == SIGNAL_SELL && stopLoss <= entryPrice) + return 0.0; + return stopLoss; + } + + double CalculateTakeProfit(const string symbol, E_SIGNAL direction, double entryPrice, double stopLoss) + { + double tp = 0.0; + double riskDistance = MathAbs(entryPrice - stopLoss); + + if(g_take_profit_mode == TAKE_PROFIT_POINTS) + { + double distance = CUtilities::PointsToPrice(symbol, g_tp_points); + tp = (direction == SIGNAL_BUY) ? entryPrice + distance : entryPrice - distance; + } + else if(g_take_profit_mode == TAKE_PROFIT_PERCENT) + { + double distance = MathAbs(entryPrice) * (g_tp_percent / 100.0); + tp = (direction == SIGNAL_BUY) ? entryPrice + distance : entryPrice - distance; + } + else if(g_take_profit_mode == TAKE_PROFIT_RISK_REWARD) + { + tp = (direction == SIGNAL_BUY) ? entryPrice + riskDistance * g_risk_reward_ratio : entryPrice - riskDistance * g_risk_reward_ratio; + } + + tp = CUtilities::NormalizePrice(symbol, tp); + if(direction == SIGNAL_BUY && tp <= entryPrice) + return 0.0; + if(direction == SIGNAL_SELL && tp >= entryPrice) + return 0.0; + return tp; + } + + bool IsTradeLevelSetValid(E_SIGNAL direction, double entryPrice, double stopLoss, double takeProfit) const + { + if(stopLoss <= 0.0) + return false; + if(direction == SIGNAL_BUY && stopLoss >= entryPrice) + return false; + if(direction == SIGNAL_SELL && stopLoss <= entryPrice) + return false; + if(takeProfit <= 0.0) + return false; + if(direction == SIGNAL_BUY && takeProfit <= entryPrice) + return false; + if(direction == SIGNAL_SELL && takeProfit >= entryPrice) + return false; + return true; + } }; #endif //__STRATEGY_MQH__ diff --git a/Include/TradeManager.mqh b/Include/TradeManager.mqh index c88b8c3..1f7b720 100644 --- a/Include/TradeManager.mqh +++ b/Include/TradeManager.mqh @@ -1,6 +1,6 @@ //+------------------------------------------------------------------+ //| TradeManager.mqh - Trade execution using CTrade class | -//| Handles buy/sell orders with SL/TP and magic number | +//| Handles buy/sell orders with explicit SL/TP and risk controls | //+------------------------------------------------------------------+ #ifndef __TRADEMANAGER_MQH__ @@ -19,100 +19,105 @@ private: 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) + bool OpenBuyTrade(const string symbol, double lot, double sl, double tp, double setupEntryPrice = 0.0) { - if(lot <= 0) + if(lot <= 0.0) { if(mp_logger) mp_logger.Error(StringFormat("Invalid lot size: %.2f", lot)); return false; } + // TP-less trades are intentionally unsupported until TradeSetup has an + // explicit allowNoTakeProfit field; do not infer that from tp = 0. + if(sl <= 0.0 || tp <= 0.0) + { + if(mp_logger) + mp_logger.Error("Invalid SL or TP provided for BUY order"); + 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)); + { + double sl_distance = MathAbs(ask - sl); + double tp_distance = MathAbs(tp - ask); + double setup_diff = (setupEntryPrice > 0.0) ? MathAbs(setupEntryPrice - ask) : 0.0; + string setup_info = (setupEntryPrice > 0.0) ? StringFormat("setupEntry=%.5f, execEntry=%.5f, diff=%.5f, ", setupEntryPrice, ask, setup_diff) : "execEntry=" + DoubleToString(ask, 5) + ", "; + mp_logger.Info(StringFormat("Opening BUY: %s lot=%.2f, SL=%.5f, TP=%.5f, SL dist=%.5f, TP dist=%.5f", + setup_info, lot, sl, tp, sl_distance, tp_distance)); + } 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)); - } + 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) + bool OpenSellTrade(const string symbol, double lot, double sl, double tp, double setupEntryPrice = 0.0) { - if(lot <= 0) + if(lot <= 0.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); + // TP-less trades are intentionally unsupported until TradeSetup has an + // explicit allowNoTakeProfit field; do not infer that from tp = 0. + if(sl <= 0.0 || tp <= 0.0) + { + if(mp_logger) + mp_logger.Error("Invalid SL or TP provided for SELL order"); + return false; + } + double bid = mp_risk_manager.GetMarketData().GetBid(); if(mp_logger && g_debug_mode) - mp_logger.Info(StringFormat("Opening SELL: lot=%.2f, SL=%.5f, TP=%.5f", lot, sl, tp)); + { + double sl_distance = MathAbs(sl - bid); + double tp_distance = MathAbs(bid - tp); + double setup_diff = (setupEntryPrice > 0.0) ? MathAbs(setupEntryPrice - bid) : 0.0; + string setup_info = (setupEntryPrice > 0.0) ? StringFormat("setupEntry=%.5f, execEntry=%.5f, diff=%.5f, ", setupEntryPrice, bid, setup_diff) : "execEntry=" + DoubleToString(bid, 5) + ", "; + mp_logger.Info(StringFormat("Opening SELL: %s lot=%.2f, SL=%.5f, TP=%.5f, SL dist=%.5f, TP dist=%.5f", + setup_info, lot, sl, tp, sl_distance, tp_distance)); + } 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)); - } + 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) @@ -122,18 +127,13 @@ public: 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())); - } + mp_logger.Error(StringFormat("Close position failed. Ticket: %I64d, Result: %d", ticket, m_trade.ResultRetcode())); return false; } @@ -143,7 +143,6 @@ public: return true; } - // Modify position SL and/or TP bool ModifyPosition(ulong ticket, double sl, double tp) { if(ticket == 0) @@ -158,10 +157,7 @@ public: 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())); - } + mp_logger.Error(StringFormat("Position modify failed. Ticket: %I64d, Result: %d", ticket, m_trade.ResultRetcode())); return false; } @@ -171,13 +167,11 @@ public: return true; } - // Get last result code uint GetResultRetcode() const { return m_trade.ResultRetcode(); } - // Get CTrade instance CTrade* GetTradeObject() { return &m_trade; diff --git a/Include/TrailingStop.mqh b/Include/TrailingStop.mqh index af527dc..59f82b6 100644 --- a/Include/TrailingStop.mqh +++ b/Include/TrailingStop.mqh @@ -31,7 +31,7 @@ public: // Update trailing stop for all positions void UpdateAllPositions() { - if(!g_use_trailing_stop && !g_use_break_even) + if(!g_use_trailing_stop && !g_use_break_even && !g_use_trailing_stop_atr && !g_use_break_even_atr) return; for(int i = PositionsTotal() - 1; i >= 0; i--) @@ -96,35 +96,44 @@ private: 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_buffer = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), - g_break_even_sl); + string symbol = mp_market_data.GetSymbol(); double current_price = (pos_type == POSITION_TYPE_BUY) ? mp_market_data.GetBid() : mp_market_data.GetAsk(); double price_move = 0.0; if(pos_type == POSITION_TYPE_BUY) - { price_move = current_price - open_price; - } else if(pos_type == POSITION_TYPE_SELL) - { price_move = open_price - current_price; - } else - { - return false; - } - - // Break-even only if price has moved enough from entry - if(price_move < break_even_trigger) return false; - // For BUY: move SL to entry price plus optional buffer + if(current_sl <= 0.0) + return false; + + double initial_risk = (pos_type == POSITION_TYPE_BUY) + ? (open_price - current_sl) + : (current_sl - open_price); + if(initial_risk <= 0.0) + return false; + + double trigger_distance = g_break_even_trigger_r * initial_risk; + if(price_move < trigger_distance) + return false; + + double buffer = CUtilities::PointsToPrice(symbol, g_break_even_buffer_points); + double point = CUtilities::GetPoint(symbol); + long min_stop_points = (long)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); + double min_stop_distance = min_stop_points * point; + if(pos_type == POSITION_TYPE_BUY) { - double be_sl = open_price + break_even_buffer; - be_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), be_sl); + double be_sl = CUtilities::NormalizePrice(symbol, open_price + buffer); + if((mp_market_data.GetBid() - be_sl) < min_stop_distance) + { + if(mp_logger && g_debug_mode) + mp_logger.Info("Break-even skipped: would violate broker minimal stop distance"); + return false; + } if(be_sl > current_sl) { @@ -134,11 +143,15 @@ private: return true; } } - // For SELL: move SL to entry price minus optional buffer else if(pos_type == POSITION_TYPE_SELL) { - double be_sl = open_price - break_even_buffer; - be_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), be_sl); + double be_sl = CUtilities::NormalizePrice(symbol, open_price - buffer); + if((be_sl - mp_market_data.GetAsk()) < min_stop_distance) + { + if(mp_logger && g_debug_mode) + mp_logger.Info("Break-even skipped: would violate broker minimal stop distance"); + return false; + } if(be_sl < current_sl) { @@ -155,18 +168,58 @@ private: // Apply trailing stop logic bool ApplyTrailingStop(ENUM_POSITION_TYPE pos_type, double current_sl, double &new_sl) { + string symbol = mp_market_data.GetSymbol(); 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(current_sl <= 0.0) + return false; + + double open_price = PositionGetDouble(POSITION_PRICE_OPEN); + double initial_risk = (pos_type == POSITION_TYPE_BUY) + ? (open_price - current_sl) + : (current_sl - open_price); + if(initial_risk <= 0.0) + return false; + + double price = (pos_type == POSITION_TYPE_BUY) ? bid : ask; + double price_move = (pos_type == POSITION_TYPE_BUY) ? (price - open_price) : (open_price - price); + double trail_start_distance = g_trail_start_r * initial_risk; + if(price_move < trail_start_distance) + return false; + + int atr_handle = iATR(symbol, g_strategy_entry_timeframe, g_atr_period); + if(atr_handle == INVALID_HANDLE) + return false; + + double atr_value[]; + ArraySetAsSeries(atr_value, true); + ArrayResize(atr_value, 1); + if(CopyBuffer(atr_handle, 0, 1, 1, atr_value) <= 0) + { + IndicatorRelease(atr_handle); + return false; + } + IndicatorRelease(atr_handle); + + double trail_distance = atr_value[0] * g_trail_distance_atr_multiplier; + if(trail_distance <= 0.0) + return false; + + double point = CUtilities::GetPoint(symbol); + long min_stop_points = (long)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); + double min_stop_distance = min_stop_points * point; + if(pos_type == POSITION_TYPE_BUY) { - double candidate_sl = bid - trail_distance; - candidate_sl = CUtilities::NormalizePrice(mp_market_data.GetSymbol(), candidate_sl); + double candidate_sl = CUtilities::NormalizePrice(symbol, bid - trail_distance); + if((bid - candidate_sl) < min_stop_distance) + { + if(mp_logger && g_debug_mode) + mp_logger.Info("Trailing stop skipped: candidate SL too close to price (broker stop level)"); + return false; + } - // Only move SL up (never down) if(candidate_sl > current_sl) { new_sl = candidate_sl; @@ -175,13 +228,16 @@ private: 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); + double candidate_sl = CUtilities::NormalizePrice(symbol, ask + trail_distance); + if((candidate_sl - ask) < min_stop_distance) + { + if(mp_logger && g_debug_mode) + mp_logger.Info("Trailing stop skipped: candidate SL too close to price (broker stop level)"); + return false; + } - // Only move SL down (never up) if(candidate_sl < current_sl) { new_sl = candidate_sl; diff --git a/Include/Utilities.mqh b/Include/Utilities.mqh index 4e8794c..1022353 100644 --- a/Include/Utilities.mqh +++ b/Include/Utilities.mqh @@ -27,18 +27,35 @@ public: // Guard against invalid broker volume data if(min_lot <= 0.0 || max_lot <= 0.0 || lot_step <= 0.0 || max_lot < min_lot) return 0.0; - - // 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; - if(lot < min_lot) - lot = min_lot; - - return NormalizeDouble(lot, 2); + // Compute precision from volume step + int precision = 0; + double s = lot_step; + const double eps = 1e-9; + while(MathAbs(MathRound(s) - s) > eps && precision < 8) + { + s *= 10.0; + precision++; + } + + // Round down to nearest step + double normalized = MathFloor(lot / lot_step) * lot_step; + normalized = NormalizeDouble(normalized, precision); + + // If normalized is below minimum, indicate invalid by returning 0.0 + if(normalized < min_lot) + return 0.0; + + // Clamp to maximum allowed (rounded down to step) + if(normalized > max_lot) + { + normalized = MathFloor(max_lot / lot_step) * lot_step; + normalized = NormalizeDouble(normalized, precision); + if(normalized < min_lot) + return 0.0; + } + + return normalized; } // Convert points to price distance diff --git a/MyProEA.ex5 b/MyProEA.ex5 index 25453c9..252d1c9 100644 Binary files a/MyProEA.ex5 and b/MyProEA.ex5 differ diff --git a/MyProEA.mq5 b/MyProEA.mq5 index 500a326..459f36e 100644 --- a/MyProEA.mq5 +++ b/MyProEA.mq5 @@ -34,6 +34,12 @@ CPositionManager *g_position_manager = NULL; CTradeManager *g_trade_manager = NULL; CTrailingStop *g_trailing_stop = NULL; +// Diagnostics counters for blocked entries +long g_blocked_existing_position = 0; +long g_blocked_spread = 0; +long g_blocked_hours = 0; +long g_blocked_max_positions = 0; + //+------------------------------------------------------------------+ // EA Initialization //+------------------------------------------------------------------+ @@ -84,11 +90,13 @@ int OnInit() // 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("Chart Timeframe: %s", EnumToString(PERIOD_CURRENT))); + g_logger.Info(StringFormat("Strategy Evaluation: %s on %s", + g_evaluate_every_tick ? "Every tick" : (g_evaluate_on_new_bar ? "New bar" : "Every tick"), + EnumToString(g_strategy_entry_timeframe))); 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("Initial SL/TP source: Strategy TradeSetup"); g_logger.Info(StringFormat("Max Positions: %d, Max Spread: %d points", g_max_positions, g_max_spread_points)); @@ -177,65 +185,84 @@ void OnTick() // Step 2: Check spread - if spread is too wide, don't trade if(!g_risk_manager.IsSpreadAcceptable()) { + g_blocked_spread++; + if(g_debug_mode) + g_logger.Info(StringFormat("Blocked entry due to spread (count=%d)", g_blocked_spread)); return; } // Step 3: Check trading hours if(!g_risk_manager.IsTradingHourValid()) { + g_blocked_hours++; + if(g_debug_mode) + g_logger.Info(StringFormat("Blocked entry due to trading hours (count=%d)", g_blocked_hours)); 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()) + // Step 5: Evaluate strategy on configured entry timeframe unless every-tick mode is enabled + bool should_evaluate_strategy = true; + if(!g_evaluate_every_tick && g_evaluate_on_new_bar) + should_evaluate_strategy = g_market_data.IsNewBar(g_strategy_entry_timeframe); + + if(!should_evaluate_strategy) { return; } - // Step 6: Get trading signal from strategy - E_SIGNAL signal = g_strategy.GetSignal(); + // Log diagnostics on each new entry bar + if(g_strategy != NULL) + g_strategy.LogDiagnostics(); + if(g_logger != NULL && g_debug_mode) + g_logger.Info(StringFormat("Blocked totals: existing=%d spread=%d hours=%d maxpos=%d", g_blocked_existing_position, g_blocked_spread, g_blocked_hours, g_blocked_max_positions)); - if(signal == SIGNAL_NONE) + // Step 6: Get the trade setup from strategy + TradeSetup setup = g_strategy.GetTradeSetup(); + + if(setup.signal == SIGNAL_NONE) { + if(g_debug_mode && g_logger != NULL) + g_logger.Info(StringFormat("No trade setup: %s", setup.reason)); return; } // Step 7: Check if we already have a position if(g_position_manager.HasOpenPosition(_Symbol)) { + g_blocked_existing_position++; if(g_debug_mode) - g_logger.Info("Already have open position, skipping entry"); + g_logger.Info(StringFormat("Already have open position, skipping entry (count=%d)", g_blocked_existing_position)); 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)"); + g_blocked_max_positions++; + g_logger.Warning(StringFormat("New position not allowed (max positions reached) (count=%d)", g_blocked_max_positions)); return; } - // Step 9: Calculate lot size - double lot = g_risk_manager.CalculateLotSize(g_stop_loss_points); - if(lot <= 0) + double lot = 0.0; + if(!g_risk_manager.ValidateTradeSetup(setup, lot)) { - g_logger.Error("Invalid lot size calculated"); + if(g_logger) + g_logger.Warning(StringFormat("Trade setup rejected: %s", setup.reason)); return; } - // Step 10: Execute trade based on signal bool trade_success = false; - if(signal == SIGNAL_BUY) + if(setup.signal == SIGNAL_BUY) { - trade_success = g_trade_manager.OpenBuyTrade(_Symbol, lot); + trade_success = g_trade_manager.OpenBuyTrade(_Symbol, lot, setup.stopLoss, setup.takeProfit, setup.entryPrice); } - else if(signal == SIGNAL_SELL) + else if(setup.signal == SIGNAL_SELL) { - trade_success = g_trade_manager.OpenSellTrade(_Symbol, lot); + trade_success = g_trade_manager.OpenSellTrade(_Symbol, lot, setup.stopLoss, setup.takeProfit, setup.entryPrice); } if(trade_success)