//+------------------------------------------------------------------+ //| MP by SPLpulse.mq5 | //| Copyright 2025, SPLpulse | //| https://www.splpulse.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, SPLpulse" #property link "https://www.splpulse.com" #property version "1.0" #property description "A versatile Expert Advisor combining multiple trading strategies with advanced risk management." #property description "Features include Martingale, dynamic lot sizing, trailing profit, trading sessions, and daily profit/drawdown limits." #include #include //--- MP by SPLpulse //--- CTrade and CPositionInfo instances CTrade trade; CPositionInfo position; //--- Enumeration for Entry Strategy enum ENUM_ENTRY_STRATEGY { MARKET_ORDER, // Entry via Market Order STOP_ORDER, // Entry via Stop Order LIMIT_ORDER // Entry via Limit Order }; //--- Enumeration for Price Action Strategy enum ENUM_PRICE_ACTION_TYPE { HFT_TICK_MOMENTUM, // HFT Momentum CANDLESTICK_ENGULFING, // Strategy based on engulfing candlestick pattern RSI_REVERSAL, // RSI crosses back from overbought/oversold EMA_CROSSOVER // Candle closes across the EMA }; //--- Enumeration for Martingale Strategy enum ENUM_MARTINGALE_TYPE { CLASSIC_MULTIPLIER, // lot = previous_lot * multiplier MULTIPLIER_WITH_SUM, // lot = (previous_lot * multiplier) + initial_lot SUM_WITH_INITIAL // lot = previous_lot + initial_lot }; //--- Enumeration for Drawdown Type enum ENUM_DRAWDOWN_TYPE { FIXED_AMOUNT, // Drawdown as a fixed of daily starting balance PERCENTAGE // Drawdown as a percentage of daily starting balance }; //--- Enumeration for Lot Sizing Mode enum ENUM_LOT_SIZE_MODE { FIXED_LOT, // Use a fixed initial lot size PERCENT_OF_BALANCE // Calculate initial lot based on risk percentage of balance }; //--- Enumeration for Drawdown Time Calculation enum ENUM_DRAWDOWN_TIME_MODE { SERVER_TIME, // Based on Broker's Server Time (resets at 00:00) CUSTOM_TIME // Based on a custom UTC time range }; //--- EA Inputs sinput group "1. Core Trading Strategy" sinput ENUM_PRICE_ACTION_TYPE InpPriceActionType = EMA_CROSSOVER; // Price Action Strategy sinput ENUM_ENTRY_STRATEGY InpEntryStrategy = MARKET_ORDER; // Entry Strategy sinput int InpPendingOrderDistancePoints = 500; // Distance for pending orders from current price sinput int InpPendingUpdateInterval = 100; // Seconds to wait before updating pending orders sinput group "2. Indicator Settings" sinput ENUM_TIMEFRAMES InpChartTimeframe = PERIOD_CURRENT; // Timeframe for Indicators sinput int InpTickMomentumCount = 6; // Ticks for HFT Momentum sinput int InpEmaPeriod = 15; // EMA Period for Crossover sinput int InpRsiPeriod = 14; // RSI Period sinput int InpRsiOverbought = 70; // RSI Overbought Level sinput int InpRsiOversold = 30; // RSI Oversold Level sinput group "3. Lot Sizing & Martingale" sinput ENUM_LOT_SIZE_MODE InpLotSizeMode = PERCENT_OF_BALANCE; // Lot Sizing Mode sinput double InpFixedInitialLot = 0.01; // Initial Lot Size (for Fixed Lot mode) sinput double InpRiskPercentage = 1.0; // Risk Percentage of Balance (for Percent mode) sinput ENUM_MARTINGALE_TYPE InpMartingaleType = CLASSIC_MULTIPLIER; // Martingale Type sinput double InpMartingaleMultiplier = 2.0; // Martingale Multiplier on Loss sinput int InpMaxOrdersPerRound = 5; // Maximum orders per Martingale round sinput double InpMaxLotSize = 5.0; // Maximum Lot Size sinput group "4. Trade Management" sinput int InpStopLossPoints = 1000; // Stop Loss in Points sinput double InpRiskRewardRatio = 2; // Risk:Reward Ratio (e.g., 1.5 means TP is 1.5 * SL) sinput int InpTradeCooldownSeconds = 30; // Cooldown in seconds between trades (0 = disabled) sinput bool InpEnableTrailingProfit = false; // Enable Trailing Profit sinput double InpTrailingStartMoney = 50; // Start trailing when profit is >= this amount sinput double InpTrailingStopMoney = 10; // Trail distance from current price sinput group "5. Time & Session Management" sinput bool InpEnableTradingSessions = true; // Enable Trading Sessions (false = trade 24/7) sinput bool InpCloseAtSessionEnd = true; // Close all trades at the end of a session sinput string InpMondayTimes = "01:00-23:00"; // Monday Trading Times sinput string InpTuesdayTimes = "01:00-23:00"; // Tuesday Trading Times sinput string InpWednesdayTimes = "01:00-23:00"; // Wednesday Trading Times sinput string InpThursdayTimes = "01:00-23:00"; // Thursday Trading Times sinput string InpFridayTimes = "01:00-23:00"; // Friday Trading Times sinput string InpSaturdayTimes = "00:00-00:00"; // Saturday Trading Times sinput string InpSundayTimes = "00:00-00:00"; // Sunday Trading Times (HH:MM-HH:MM;...) sinput group "6. Daily Risk Management" sinput bool InpEnableDailyProfitLimit = true; // Enable Daily Profit Limit sinput double InpDailyProfitLimitAmount = 1000.0; // Daily Profit Limit Amount (in deposit currency) sinput bool InpEnableDrawdown = true; // Enable Drawdown Protection sinput ENUM_DRAWDOWN_TYPE InpDrawdownType = PERCENTAGE; // Drawdown Calculation Type sinput double InpDrawdownFixedAmount = 300.0; // Max Drawdown (Fixed Amount) sinput double InpDrawdownPercentage = 5.0; // Max Drawdown (Percentage) sinput ENUM_DRAWDOWN_TIME_MODE InpDrawdownTimeMode = SERVER_TIME; // Drawdown Calculation Time sinput string InpDrawdownStartTimeUTC = "00:01"; // Custom Start Time (UTC) sinput string InpDrawdownEndTimeUTC = "23:59"; // Custom End Time (UTC) sinput group "7. EA Identification" sinput ulong InpMagicNumber = 105091; // Magic Number for this EA //--- Global variables double g_current_lot_size; // To hold the current lot size, considering Martingale int g_order_count_in_round = 0; // Counter for trades in the current round datetime g_last_pending_update_time = 0; // Timer for updating pending orders bool g_is_currently_in_session = false; // To track session state bool g_is_closing_for_reversal = false; // State flag for handling reversals datetime g_last_trade_close_time = 0; // Timestamp of the last trade closure for cooldown //--- Global variables for Risk Management double g_initial_balance_for_day = 0; int g_last_reset_day_of_year = 0; // For SERVER_TIME mode int g_last_reset_day_of_year_utc = 0; // For CUSTOM_TIME mode bool g_risk_limit_reached_today = false; bool g_stop_message_printed = false; // To prevent log flooding when trading is suspended //--- Indicator handles int rsi_handle = INVALID_HANDLE; int ema_handle = INVALID_HANDLE; //+------------------------------------------------------------------+ //| Forward declarations | //+------------------------------------------------------------------+ bool IsWithinTradingSession(); int CloseAllOpenPositions(); int CancelPendingOrders(); void CheckRiskManagement(); double NormalizeLotSize(double lot); double CalculateLotSizeBasedOnRisk(); int TimeToMinutes(string time_str); void HandleMartingale(double profit, double closed_volume); void SetInitialSLTP(ulong position_ticket); bool HasSufficientMargin(double lots); int GetTradeSignal(); void ExecuteTrade(int signal); double CalculateMaxAffordableLot(); //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize trade object trade.SetExpertMagicNumber(InpMagicNumber); trade.SetDeviationInPoints(10); // Set allowable slippage in points trade.SetTypeFilling(ORDER_FILLING_FOK); // Set order filling type trade.SetAsyncMode(false); // Force synchronous execution to prevent race conditions //--- Initialize lot size if(InpLotSizeMode == FIXED_LOT) { g_current_lot_size = InpFixedInitialLot; } else // PERCENT_OF_BALANCE { g_current_lot_size = CalculateLotSizeBasedOnRisk(); } g_current_lot_size = NormalizeLotSize(g_current_lot_size); //--- Check initial lot size if(g_current_lot_size < SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) { g_current_lot_size = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); Print("Initial lot size was too small. Adjusted to minimum allowed: ", g_current_lot_size); } //--- Use the selected timeframe, or the current chart's if PERIOD_CURRENT is selected ENUM_TIMEFRAMES timeframe = (InpChartTimeframe == PERIOD_CURRENT) ? (ENUM_TIMEFRAMES)_Period : InpChartTimeframe; //--- Initialize Indicator Handles rsi_handle = iRSI(_Symbol, timeframe, InpRsiPeriod, PRICE_CLOSE); if(rsi_handle == INVALID_HANDLE) { Print("Error creating RSI indicator handle - ", GetLastError()); return(INIT_FAILED); } ema_handle = iMA(_Symbol, timeframe, InpEmaPeriod, 0, MODE_EMA, PRICE_CLOSE); if(ema_handle == INVALID_HANDLE) { Print("Error creating EMA indicator handle - ", GetLastError()); return(INIT_FAILED); } //--- Set initial session state g_is_currently_in_session = IsWithinTradingSession(); //--- Initialize Risk Management variables MqlDateTime dt_server; TimeCurrent(dt_server); g_last_reset_day_of_year = dt_server.day_of_year; MqlDateTime dt_utc; datetime now_utc = TimeGMT(); TimeToStruct(now_utc, dt_utc); g_last_reset_day_of_year_utc = dt_utc.day_of_year; g_initial_balance_for_day = AccountInfoDouble(ACCOUNT_BALANCE); g_risk_limit_reached_today = false; Print("EA Initialized. Lot Mode: ", EnumToString(InpLotSizeMode), ", Initial Lot: ", g_current_lot_size); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release indicator handles if(rsi_handle != INVALID_HANDLE) IndicatorRelease(rsi_handle); if(ema_handle != INVALID_HANDLE) IndicatorRelease(ema_handle); Print("EA Deinitialized. Reason code: ", reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check risk management limits first CheckRiskManagement(); //--- If a risk limit has been reached, halt all further operations for the day if(g_risk_limit_reached_today) { if(!g_stop_message_printed) { Print("Trading suspended for the day because a daily profit or drawdown limit was reached."); g_stop_message_printed = true; } return; } //--- If we are waiting for a reversal trade to close, do nothing else. if(g_is_closing_for_reversal) { return; } //--- Check for trade cooldown period --- if(InpTradeCooldownSeconds > 0 && TimeCurrent() - g_last_trade_close_time < InpTradeCooldownSeconds) { return; // Still in cooldown, do not proceed to check for new trades. } //--- Session Management Logic if(InpEnableTradingSessions) { g_is_currently_in_session = IsWithinTradingSession(); if (!g_is_currently_in_session) // If we are OUTSIDE of the allowed session { if (InpCloseAtSessionEnd) { int closed_positions = CloseAllOpenPositions(); int cancelled_orders = CancelPendingOrders(); if(closed_positions > 0 || cancelled_orders > 0) { Print("Closed ", closed_positions, " positions and cancelled ", cancelled_orders, " orders at session end."); } } return; // Halt further trading logic for this tick } } //--- Handle trailing profit for open positions if(InpEnableTrailingProfit) { HandleTrailingProfit(); } //--- Update pending orders if the strategy is not Market Order and interval has passed if(InpEntryStrategy != MARKET_ORDER && TimeCurrent() - g_last_pending_update_time >= InpPendingUpdateInterval) { UpdatePendingOrders(); g_last_pending_update_time = TimeCurrent(); } //--- Main trading logic: get signal and act on it int signal = GetTradeSignal(); if(signal != 0) // If there's a buy or sell signal { // Check if a position already exists if(position.SelectByMagic(_Symbol, InpMagicNumber)) { // Reversal logic: If signal is opposite to current position, close it. if((signal == 1 && position.PositionType() == POSITION_TYPE_SELL) || (signal == -1 && position.PositionType() == POSITION_TYPE_BUY)) { trade.PositionClose(position.Ticket()); g_is_closing_for_reversal = true; // Set flag and wait for confirmation via OnTradeTransaction return; // Exit. New trade will be opened on a subsequent tick. } } else // No position exists, so we can consider opening a new one { if(TotalPositionsAndOrders() == 0) // Ensure no pending orders either { ExecuteTrade(signal); } } } } //+------------------------------------------------------------------+ //| Check daily profit and drawdown limits | //+------------------------------------------------------------------+ void CheckRiskManagement() { bool should_reset = false; // --- 1. Check if it's time to reset the daily stats --- if(InpDrawdownTimeMode == SERVER_TIME) { MqlDateTime dt; TimeCurrent(dt); if(dt.day_of_year != g_last_reset_day_of_year) { should_reset = true; g_last_reset_day_of_year = dt.day_of_year; } } else // CUSTOM_TIME { MqlDateTime dt_utc; datetime now_utc = TimeGMT(); TimeToStruct(now_utc, dt_utc); int start_minutes = TimeToMinutes(InpDrawdownStartTimeUTC); int current_utc_minutes = dt_utc.hour * 60 + dt_utc.min; // Reset if it's a new UTC day and we have passed the designated start time if(dt_utc.day_of_year != g_last_reset_day_of_year_utc && current_utc_minutes >= start_minutes) { should_reset = true; g_last_reset_day_of_year_utc = dt_utc.day_of_year; } } if(should_reset) { g_initial_balance_for_day = AccountInfoDouble(ACCOUNT_BALANCE); g_risk_limit_reached_today = false; g_stop_message_printed = false; Print("New trading day detected. Daily profit and drawdown stats reset."); } // --- 2. If a limit was already hit in the current period, do nothing else --- if(g_risk_limit_reached_today) { return; } // --- 3. Determine if we are within the active period for checking limits --- bool is_active_period = true; // Default for SERVER_TIME mode if(InpDrawdownTimeMode == CUSTOM_TIME) { MqlDateTime dt_utc; datetime now_utc = TimeGMT(); TimeToStruct(now_utc, dt_utc); int start_minutes = TimeToMinutes(InpDrawdownStartTimeUTC); int end_minutes = TimeToMinutes(InpDrawdownEndTimeUTC); int current_utc_minutes = dt_utc.hour * 60 + dt_utc.min; if(start_minutes != -1 && end_minutes != -1) { is_active_period = (current_utc_minutes >= start_minutes && current_utc_minutes <= end_minutes); } else { is_active_period = false; // Invalid time format if(!g_stop_message_printed) Print("Invalid Custom Drawdown Time format. Risk management is disabled."); } } if(!is_active_period) { return; // Not in the time window to check for DD/profit limits } // --- 4. Perform the actual Profit and Drawdown checks --- // Daily Profit Limit Check if(InpEnableDailyProfitLimit) { double current_daily_profit = AccountInfoDouble(ACCOUNT_EQUITY) - g_initial_balance_for_day; if(current_daily_profit >= InpDailyProfitLimitAmount) { Print("Daily profit limit of ", InpDailyProfitLimitAmount, " reached. Closing all positions and stopping for the day."); CloseAllOpenPositions(); CancelPendingOrders(); g_risk_limit_reached_today = true; return; } } // Drawdown Protection Check (based on daily starting balance) if(InpEnableDrawdown) { double current_daily_loss = g_initial_balance_for_day - AccountInfoDouble(ACCOUNT_EQUITY); if(current_daily_loss < 0) current_daily_loss = 0; double drawdown_limit = 0; if(InpDrawdownType == FIXED_AMOUNT) { drawdown_limit = InpDrawdownFixedAmount; } else // PERCENTAGE { drawdown_limit = g_initial_balance_for_day * (InpDrawdownPercentage / 100.0); } if(current_daily_loss >= drawdown_limit && drawdown_limit > 0) { Print("Maximum drawdown limit of ", drawdown_limit, " reached. Closing all positions and stopping for the day."); CloseAllOpenPositions(); CancelPendingOrders(); g_risk_limit_reached_today = true; return; } } } //+------------------------------------------------------------------+ //| Generates a trade signal. Returns 1 for buy, -1 for sell, 0 for none.| //+------------------------------------------------------------------+ int GetTradeSignal() { bool buy_signal = false; bool sell_signal = false; ENUM_TIMEFRAMES timeframe = (InpChartTimeframe == PERIOD_CURRENT) ? (ENUM_TIMEFRAMES)_Period : InpChartTimeframe; switch(InpPriceActionType) { case HFT_TICK_MOMENTUM: { if(InpTickMomentumCount < 2) break; MqlTick ticks_array[]; ArrayResize(ticks_array, InpTickMomentumCount); if(CopyTicks(_Symbol, ticks_array, COPY_TICKS_ALL, 0, InpTickMomentumCount) < InpTickMomentumCount) break; bool is_upward = true, is_downward = true; for(int i = 1; i < InpTickMomentumCount; i++) { if(ticks_array[i].bid <= ticks_array[i-1].bid) is_upward = false; if(ticks_array[i].ask >= ticks_array[i-1].ask) is_downward = false; } if(is_upward) buy_signal = true; else if(is_downward) sell_signal = true; break; } case CANDLESTICK_ENGULFING: { static datetime last_bar_time = 0; datetime current_bar_time = iTime(_Symbol, timeframe, 0); if(last_bar_time == current_bar_time) break; last_bar_time = current_bar_time; MqlRates rates[2]; if(CopyRates(_Symbol, timeframe, 1, 2, rates) < 2) break; if(rates[0].close < rates[0].open && rates[1].close > rates[1].open && rates[1].close > rates[0].open && rates[1].open < rates[0].close) buy_signal = true; else if(rates[0].close > rates[0].open && rates[1].close < rates[1].open && rates[1].close < rates[0].open && rates[1].open > rates[0].close) sell_signal = true; break; } case RSI_REVERSAL: { static datetime last_bar_time = 0; datetime current_bar_time = iTime(_Symbol, timeframe, 0); if(last_bar_time == current_bar_time) break; last_bar_time = current_bar_time; double rsi_buffer[2]; if(CopyBuffer(rsi_handle, 0, 1, 2, rsi_buffer) < 2) break; if(rsi_buffer[0] < InpRsiOversold && rsi_buffer[1] >= InpRsiOversold) buy_signal = true; else if(rsi_buffer[0] > InpRsiOverbought && rsi_buffer[1] <= InpRsiOverbought) sell_signal = true; break; } case EMA_CROSSOVER: { static datetime last_bar_time = 0; datetime current_bar_time = iTime(_Symbol, timeframe, 0); if(last_bar_time == current_bar_time) break; last_bar_time = current_bar_time; MqlRates rates[2]; double ema_buffer[2]; if(CopyRates(_Symbol, timeframe, 1, 2, rates) < 2) break; if(CopyBuffer(ema_handle, 0, 1, 2, ema_buffer) < 2) break; if(rates[0].close < ema_buffer[0] && rates[1].close > ema_buffer[1]) buy_signal = true; else if(rates[0].close > ema_buffer[0] && rates[1].close < ema_buffer[1]) sell_signal = true; break; } } if(buy_signal) return 1; if(sell_signal) return -1; return 0; } //+------------------------------------------------------------------+ //| Executes a trade based on the provided signal. | //+------------------------------------------------------------------+ void ExecuteTrade(int signal) { if(signal == 0) return; // --- Margin-Based Lot Capping at the last moment --- double max_affordable_lot = CalculateMaxAffordableLot(); if(g_current_lot_size > max_affordable_lot) { Print("Martingale lot ", DoubleToString(g_current_lot_size,2), " exceeds available margin. Capping at ", DoubleToString(max_affordable_lot,2)); g_current_lot_size = max_affordable_lot; g_current_lot_size = NormalizeLotSize(g_current_lot_size); // Re-normalize after capping } if(!HasSufficientMargin(g_current_lot_size)) { Print("Insufficient margin for lot size ", g_current_lot_size, ". Skipping trade."); return; } MqlTick current_tick; if(!SymbolInfoTick(_Symbol, current_tick)) return; double ask = current_tick.ask; double bid = current_tick.bid; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double stop_loss_offset = InpStopLossPoints * point; double take_profit_offset = InpStopLossPoints * InpRiskRewardRatio * point; double pending_distance = InpPendingOrderDistancePoints * point; if(signal == 1) // Buy Signal { switch(InpEntryStrategy) { case MARKET_ORDER: trade.Buy(g_current_lot_size, _Symbol, ask, 0, 0, "MP by SPLpulse"); break; case STOP_ORDER: trade.BuyStop(g_current_lot_size, ask + pending_distance, _Symbol, (ask + pending_distance) - stop_loss_offset, (ask + pending_distance) + take_profit_offset, 0, 0, "MP by SPLpulse"); break; case LIMIT_ORDER: trade.BuyLimit(g_current_lot_size, ask - pending_distance, _Symbol, (ask - pending_distance) - stop_loss_offset, (ask - pending_distance) + take_profit_offset, 0, 0, "MP by SPLpulse"); break; } } else if(signal == -1) // Sell Signal { switch(InpEntryStrategy) { case MARKET_ORDER: trade.Sell(g_current_lot_size, _Symbol, bid, 0, 0, "MP by SPLpulse"); break; case STOP_ORDER: trade.SellStop(g_current_lot_size, bid - pending_distance, _Symbol, (bid - pending_distance) + stop_loss_offset, (bid - pending_distance) - take_profit_offset, 0, 0, "MP by SPLpulse"); break; case LIMIT_ORDER: trade.SellLimit(g_current_lot_size, bid + pending_distance, _Symbol, (bid + pending_distance) + stop_loss_offset, (bid + pending_distance) - take_profit_offset, 0, 0, "MP by SPLpulse"); break; } } } //+------------------------------------------------------------------+ //| Trade Transaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { //--- Check if the transaction is related to our EA ulong magic_number = 0; //--- Get magic number based on transaction type if(trans.type == TRADE_TRANSACTION_DEAL_ADD && trans.deal != 0) { if(HistoryDealSelect(trans.deal)) magic_number = HistoryDealGetInteger(trans.deal, DEAL_MAGIC); } else if(trans.type == TRADE_TRANSACTION_ORDER_ADD && trans.order != 0) { if(OrderSelect(trans.order)) magic_number = OrderGetInteger(ORDER_MAGIC); } //--- Only proceed if this transaction belongs to our EA if(magic_number == InpMagicNumber) { //--- Check if a deal was added (a trade was executed) if(trans.type == TRADE_TRANSACTION_DEAL_ADD && HistoryDealSelect(trans.deal)) { long deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); //--- A new position has been opened if(deal_entry == DEAL_ENTRY_IN && InpEntryStrategy == MARKET_ORDER) { ulong position_ticket = HistoryDealGetInteger(trans.deal, DEAL_POSITION_ID); SetInitialSLTP(position_ticket); } //--- A position was closed else if(deal_entry == DEAL_ENTRY_OUT) { g_is_closing_for_reversal = false; // Reset the flag now that close is confirmed. g_last_trade_close_time = TimeCurrent(); // Set cooldown timer double profit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT); double closed_volume = HistoryDealGetDouble(trans.deal, DEAL_VOLUME); HandleMartingale(profit, closed_volume); } } //--- Logic to close opposite pending order if one has been triggered if(InpEntryStrategy != MARKET_ORDER && trans.type == TRADE_TRANSACTION_DEAL_ADD) { if(PositionsTotal() > 0 && position.SelectByMagic(_Symbol, InpMagicNumber)) { CancelPendingOrders(); } } } } //+------------------------------------------------------------------+ //| Sets the initial Stop Loss and Take Profit on a new position | //+------------------------------------------------------------------+ void SetInitialSLTP(ulong position_ticket) { if(!position.SelectByTicket(position_ticket)) return; // Ensure it's our position, just in case if(position.Magic() != InpMagicNumber) return; // Get fresh market and symbol info right before modification MqlTick current_tick; if(!SymbolInfoTick(_Symbol, current_tick)) { Print("Could not get current tick to set SL/TP for position #", position_ticket); return; } double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); long stops_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); double open_price = position.PriceOpen(); double sl_offset = InpStopLossPoints * point; double tp_offset = InpStopLossPoints * InpRiskRewardRatio * point; double new_sl = 0; double new_tp = 0; if(position.PositionType() == POSITION_TYPE_BUY) { new_sl = open_price - sl_offset; new_tp = open_price + tp_offset; // --- Safety Check: Ensure SL is not too close to the current Bid price --- double min_valid_sl = current_tick.bid - (stops_level * point); if (new_sl > min_valid_sl) { new_sl = min_valid_sl; Print("Adjusting SL for position #", position_ticket, " to meet broker's minimum distance."); } } else // POSITION_TYPE_SELL { new_sl = open_price + sl_offset; new_tp = open_price - tp_offset; // --- Safety Check: Ensure SL is not too close to the current Ask price --- double min_valid_sl = current_tick.ask + (stops_level * point); if (new_sl < min_valid_sl) { new_sl = min_valid_sl; Print("Adjusting SL for position #", position_ticket, " to meet broker's minimum distance."); } } if(trade.PositionModify(position_ticket, NormalizeDouble(new_sl, _Digits), NormalizeDouble(new_tp, _Digits))) { Print("Successfully set initial SL/TP for position #", position_ticket); } else { Print("Failed to set initial SL/TP for position #", position_ticket, ". Error: ", GetLastError()); } } //+------------------------------------------------------------------+ //| Handle Martingale Logic | //+------------------------------------------------------------------+ void HandleMartingale(double profit, double closed_volume) { // First, determine the correct initial lot for this cycle, in case we need it for resets or SUM strategies. double initial_lot; if (InpLotSizeMode == FIXED_LOT) { initial_lot = InpFixedInitialLot; } else { initial_lot = CalculateLotSizeBasedOnRisk(); } if (profit < 0) // --- LOSS --- { g_order_count_in_round++; Print("Trade #", g_order_count_in_round, " in this round closed with a loss."); if (g_order_count_in_round >= InpMaxOrdersPerRound) { Print("Maximum orders per round (", InpMaxOrdersPerRound, ") reached. Resetting martingale cycle."); g_order_count_in_round = 0; g_current_lot_size = initial_lot; // Reset to the fresh initial lot } else { // Apply Martingale logic to get the next lot size switch (InpMartingaleType) { case CLASSIC_MULTIPLIER: g_current_lot_size = closed_volume * InpMartingaleMultiplier; break; case MULTIPLIER_WITH_SUM: g_current_lot_size = (closed_volume * InpMartingaleMultiplier) + initial_lot; break; case SUM_WITH_INITIAL: g_current_lot_size = closed_volume + initial_lot; break; } } } else // --- PROFIT --- { Print("Trade closed with a profit. Resetting martingale cycle."); g_order_count_in_round = 0; g_current_lot_size = initial_lot; // Reset to the fresh initial lot } //--- Normalize the final lot size for the next trade g_current_lot_size = NormalizeLotSize(g_current_lot_size); Print("New lot size for next trade (before final margin check): ", g_current_lot_size); } //+------------------------------------------------------------------+ //| Normalize Lot Size | //+------------------------------------------------------------------+ double NormalizeLotSize(double lot) { double min_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double broker_max_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double volume_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double max_volume = MathMin(InpMaxLotSize, broker_max_volume); if(lot < min_volume) lot = min_volume; if(lot > max_volume) lot = max_volume; if(volume_step > 0) { lot = MathFloor(lot / volume_step) * volume_step; } return NormalizeDouble(lot, 2); } //+------------------------------------------------------------------+ //| Calculate Lot Size Based on Risk | //+------------------------------------------------------------------+ double CalculateLotSizeBasedOnRisk() { if(InpStopLossPoints <= 0) { Print("Cannot calculate risk-based lot size with Stop Loss set to 0. Using minimum lot size."); return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } double account_balance = AccountInfoDouble(ACCOUNT_BALANCE); double risk_amount = account_balance * (InpRiskPercentage / 100.0); // Calculate the value of one point for one lot double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_size <= 0) { Print("Invalid tick size for symbol. Using minimum lot."); return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } double point_value_per_lot = tick_value * (_Point / tick_size); if(point_value_per_lot <= 0) { Print("Invalid point value per lot for symbol. Using minimum lot."); return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } // Calculate total loss for one lot if SL is hit double loss_per_lot = InpStopLossPoints * point_value_per_lot; if(loss_per_lot <= 0) { Print("Could not determine loss per lot for risk calculation. Using minimum lot size."); return SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } // Calculate the required lot size and return it without normalization here return risk_amount / loss_per_lot; } //+------------------------------------------------------------------+ //| Count total open positions and pending orders by this EA | //+------------------------------------------------------------------+ int TotalPositionsAndOrders() { int count = 0; //--- Count open positions for(int i = PositionsTotal() - 1; i >= 0; i--) { if(position.SelectByIndex(i)) { if(position.Magic() == InpMagicNumber && position.Symbol() == _Symbol) { count++; } } } //--- Count pending orders for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong order_ticket = OrderGetTicket(i); if(OrderSelect(order_ticket)) { if(OrderGetInteger(ORDER_MAGIC) == InpMagicNumber && OrderGetString(ORDER_SYMBOL) == _Symbol) { count++; } } } return count; } //+------------------------------------------------------------------+ //| Cancel all pending orders for this EA | //+------------------------------------------------------------------+ int CancelPendingOrders() { int cancelled_count = 0; for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(OrderSelect(ticket)) { if(OrderGetInteger(ORDER_MAGIC) == InpMagicNumber && OrderGetString(ORDER_SYMBOL) == _Symbol) { if(trade.OrderDelete(ticket)) { cancelled_count++; } } } } return cancelled_count; } //+------------------------------------------------------------------+ //| Update all pending orders to trail the current price | //+------------------------------------------------------------------+ void UpdatePendingOrders() { if(OrdersTotal() == 0) return; // No pending orders to update Print("Checking to update pending orders..."); //--- Get current market prices MqlTick current_tick; if(!SymbolInfoTick(_Symbol, current_tick)) { Print("Error getting current tick data for pending order update"); return; } double ask = current_tick.ask; double bid = current_tick.bid; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); //--- Calculate new SL, TP, and distance values double stop_loss_price_offset = InpStopLossPoints * point; double take_profit_price_offset = InpStopLossPoints * InpRiskRewardRatio * point; double pending_distance = InpPendingOrderDistancePoints * point; //--- Loop through all pending orders for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(OrderSelect(ticket)) { //--- Check if the order belongs to this EA if(OrderGetInteger(ORDER_MAGIC) == InpMagicNumber && OrderGetString(ORDER_SYMBOL) == _Symbol) { ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); ENUM_ORDER_TYPE_TIME type_time = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME); datetime expiration = (datetime)OrderGetInteger(ORDER_TIME_EXPIRATION); double stoplimit_price = OrderGetDouble(ORDER_PRICE_STOPLIMIT); double new_price = 0; double new_sl = 0; double new_tp = 0; //--- Determine new prices based on order type switch(order_type) { case ORDER_TYPE_BUY_STOP: new_price = ask + pending_distance; new_sl = new_price - stop_loss_price_offset; new_tp = new_price + take_profit_price_offset; break; case ORDER_TYPE_SELL_STOP: new_price = bid - pending_distance; new_sl = new_price + stop_loss_price_offset; new_tp = new_price - take_profit_price_offset; break; case ORDER_TYPE_BUY_LIMIT: new_price = ask - pending_distance; new_sl = new_price - stop_loss_price_offset; new_tp = new_price + take_profit_price_offset; break; case ORDER_TYPE_SELL_LIMIT: new_price = bid + pending_distance; new_sl = new_price + stop_loss_price_offset; new_tp = new_price - take_profit_price_offset; break; default: continue; // Not a pending order we manage this way } //--- Normalize prices new_price = NormalizeDouble(new_price, _Digits); new_sl = NormalizeDouble(new_sl, _Digits); new_tp = NormalizeDouble(new_tp, _Digits); //--- Modify the order if(trade.OrderModify(ticket, new_price, new_sl, new_tp, type_time, expiration, stoplimit_price)) { Print("Successfully updated pending order #", ticket, " to new price ", new_price); } else { Print("Error updating pending order #", ticket, ". Error code: ", GetLastError()); } } } } } //+------------------------------------------------------------------+ //| Handle Trailing Profit in Money | //+------------------------------------------------------------------+ void HandleTrailingProfit() { //--- Check if there are any positions to trail if(PositionsTotal() <= 0) return; //--- Get tick and point info once MqlTick current_tick; if(!SymbolInfoTick(_Symbol, current_tick)) return; double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tick_size == 0) return; // Avoid division by zero //--- This is the value of a 1-point move for a 1-lot contract, in the deposit currency double value_per_point_1_lot = tick_value * (_Point / tick_size); if(value_per_point_1_lot == 0) return; // Avoid division by zero long stops_level = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); //--- Loop through all open positions for(int i = PositionsTotal() - 1; i >= 0; i--) { if(position.SelectByIndex(i)) { //--- Check if the position belongs to this EA and symbol if(position.Magic() == InpMagicNumber && position.Symbol() == _Symbol) { double current_profit = position.Profit(); //--- Check if profit is enough to start trailing if(current_profit < InpTrailingStartMoney) { continue; // Skip to the next position } //--- Calculate the number of points that the trailing stop money represents double value_per_point_position = value_per_point_1_lot * position.Volume(); if(value_per_point_position == 0) continue; double points_offset = InpTrailingStopMoney / value_per_point_position; double price_offset = points_offset * _Point; double new_sl_price = 0; //--- Determine new SL based on position type if(position.PositionType() == POSITION_TYPE_BUY) { new_sl_price = current_tick.bid - price_offset; //--- We only modify if the new SL is better (higher) than the current one //--- AND if the new SL respects the broker's minimum distance from the current BID price. if(new_sl_price > (position.StopLoss() + _Point) && (current_tick.bid - new_sl_price) >= (stops_level * _Point)) { if(!trade.PositionModify(position.Ticket(), NormalizeDouble(new_sl_price, _Digits), position.TakeProfit())) { Print("Error modifying BUY position #", position.Ticket(), " for trailing stop. Error: ", GetLastError()); } else { Print("Trailing stop for BUY #", position.Ticket(), " updated to ", NormalizeDouble(new_sl_price, _Digits)); } } } else if(position.PositionType() == POSITION_TYPE_SELL) { new_sl_price = current_tick.ask + price_offset; //--- We only modify if the new SL is better (lower) and not zero //--- AND if the new SL respects the broker's minimum distance from the current ASK price. if((new_sl_price < (position.StopLoss() - _Point) || position.StopLoss() == 0) && (new_sl_price - current_tick.ask) >= (stops_level * _Point)) { if(!trade.PositionModify(position.Ticket(), NormalizeDouble(new_sl_price, _Digits), position.TakeProfit())) { Print("Error modifying SELL position #", position.Ticket(), " for trailing stop. Error: ", GetLastError()); } else { Print("Trailing stop for SELL #", position.Ticket(), " updated to ", NormalizeDouble(new_sl_price, _Digits)); } } } } } } } //+------------------------------------------------------------------+ //| Helper function to convert HH:MM string to minutes of the day | //+------------------------------------------------------------------+ int TimeToMinutes(string time_str) { string parts[]; if(StringSplit(time_str, ':', parts) != 2) return -1; // Invalid format int hour = (int)StringToInteger(parts[0]); int minute = (int)StringToInteger(parts[1]); if(hour < 0 || hour > 23 || minute < 0 || minute > 59) return -1; // Invalid time return (hour * 60 + minute); } //+------------------------------------------------------------------+ //| Check if the current time is within a defined trading session | //+------------------------------------------------------------------+ bool IsWithinTradingSession() { MqlDateTime current_time; TimeCurrent(current_time); // Gets server time int current_day_of_week = current_time.day_of_week; // 0=Sunday, 1=Monday, ... int current_minutes_of_day = current_time.hour * 60 + current_time.min; string session_string = ""; //--- Get the correct session string for the current day switch(current_day_of_week) { case 0: session_string = InpSundayTimes; break; case 1: session_string = InpMondayTimes; break; case 2: session_string = InpTuesdayTimes; break; case 3: session_string = InpWednesdayTimes; break; case 4: session_string = InpThursdayTimes; break; case 5: session_string = InpFridayTimes; break; case 6: session_string = InpSaturdayTimes; break; } //--- If the string is empty or indicates closed, return false if(session_string == "" || session_string == "00:00-00:00") return false; //--- Split the string by ';' to handle multiple sessions in one day string session_parts[]; int num_sessions = StringSplit(session_string, ';', session_parts); for(int i = 0; i < num_sessions; i++) { //--- Split each session part by '-' to get start and end times string time_range[]; if(StringSplit(session_parts[i], '-', time_range) != 2) continue; // Skip invalid range format int start_minutes = TimeToMinutes(time_range[0]); int end_minutes = TimeToMinutes(time_range[1]); if (start_minutes == -1 || end_minutes == -1) continue; // Invalid time format //--- Check if current time falls within the session range if(current_minutes_of_day >= start_minutes && current_minutes_of_day < end_minutes) { return true; // We are in a valid session } } //--- If no matching session was found after checking all ranges return false; } //+------------------------------------------------------------------+ //| Close all open positions managed by this EA | //+------------------------------------------------------------------+ int CloseAllOpenPositions() { int closed_count = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(position.SelectByIndex(i)) { if(position.Magic() == InpMagicNumber && position.Symbol() == _Symbol) { if(trade.PositionClose(position.Ticket())) { closed_count++; } } } } return closed_count; } //+------------------------------------------------------------------+ //| Checks if there is enough free margin to open a trade | //+------------------------------------------------------------------+ bool HasSufficientMargin(double lots) { double margin_required; // For a buy position (margin is usually symmetrical for buy/sell) if(!OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, lots, SymbolInfoDouble(_Symbol, SYMBOL_ASK), margin_required)) { Print("Could not calculate margin. Assuming insufficient funds."); return false; } if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) < margin_required) { return false; } return true; } //+------------------------------------------------------------------+ //| Calculates the maximum lot size affordable with current free margin.| //+------------------------------------------------------------------+ double CalculateMaxAffordableLot() { double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); double one_lot_margin; // Calculate margin required for 1.0 lot if(!OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, 1.0, SymbolInfoDouble(_Symbol, SYMBOL_ASK), one_lot_margin)) { Print("Could not calculate margin for 1.0 lot. Cannot determine max affordable lot."); return 0.0; // Return 0 to prevent trading } if (one_lot_margin <= 0.0001) // Check for zero or very small margin to avoid division by zero { Print("Margin for 1 lot is zero. Cannot determine max affordable lot."); return 0.0; } // Max lot is free margin divided by margin for one lot. Subtract a small amount for buffer. double max_lot = (free_margin / one_lot_margin) * 0.98; // Use 98% of free margin for buffer return max_lot; }