Martingale_Pulse/Martingale Pulse.mq5

1209 lines
49 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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 <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
//--- 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;
}