| //+------------------------------------------------------------------+
|
| //| XAU/XAG Risk-Based ATR EA (MT5) |
|
| //| Trades XAUUSD & XAGUSD with EMA+RSI(+ADX) filters |
|
| //| SL/TP and lot size based on account value and risk % |
|
| //+------------------------------------------------------------------+
|
| #property strict
|
|
|
| #include <Trade/Trade.mqh>
|
| CTrade trade;
|
|
|
| //------------------------- Inputs ----------------------------------
|
| // Symbols (broker naming may differ, e.g. "XAUUSD.", "GOLD", etc.)
|
| input string InpSymbol1 = "XAUUSD";
|
| input string InpSymbol2 = "XAGUSD";
|
|
|
| // Timeframe and indicators
|
| input ENUM_TIMEFRAMES InpTF = PERIOD_M15;
|
|
|
| input int InpFastEMA = 50;
|
| input int InpSlowEMA = 200;
|
| input int InpRSIPeriod = 14;
|
| input double InpRSIBuyAbove = 52.0;
|
| input double InpRSISellBelow = 48.0;
|
|
|
| input bool InpUseADX = true;
|
| input int InpADXPeriod = 14;
|
| input double InpADXMin = 18.0;
|
|
|
| input int InpATRPeriod = 14;
|
| input double InpSL_ATR_Mult = 2.0; // Stop distance = ATR * mult
|
| input double InpRiskReward = 2.0; // TP distance = SL distance * RR
|
|
|
| // Risk / execution
|
| input double InpRiskPercent = 1.0; // % of balance to risk per trade
|
| input int InpMaxPositionsTotal = 2; // total open positions across symbols
|
| input double InpMaxSpreadPoints = 50; // skip if spread > this (points)
|
| input int InpSlippagePoints = 20; // max deviation in points
|
| input bool InpAllowNewBarOnly = true; // only evaluate on new bar
|
|
|
| // Optional trade session control (server time)
|
| input bool InpUseSessionFilter = false;
|
| input int InpSessionStartHour = 6;
|
| input int InpSessionEndHour = 20;
|
|
|
| //------------------------- Internals -------------------------------
|
| datetime g_lastBarTime = 0;
|
|
|
| // Helper: check if symbol is one of ours
|
| bool IsTargetSymbol(const string sym)
|
| {
|
| return (sym == InpSymbol1 || sym == InpSymbol2);
|
| }
|
|
|
| // Helper: count open positions total (or for a symbol)
|
| int CountPositions(const string sym = "")
|
| {
|
| int count = 0;
|
| for(int i=0; i<PositionsTotal(); i++)
|
| {
|
| if(PositionSelectByIndex(i))
|
| {
|
| string ps = PositionGetString(POSITION_SYMBOL);
|
| if(sym == "" || ps == sym) count++;
|
| }
|
| }
|
| return count;
|
| }
|
|
|
| // Helper: spread filter
|
| bool SpreadOK(const string sym)
|
| {
|
| double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
| double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
| if(ask <= 0 || bid <= 0) return false;
|
|
|
| double pt = SymbolInfoDouble(sym, SYMBOL_POINT);
|
| double spreadPoints = (ask - bid) / pt;
|
| return (spreadPoints <= InpMaxSpreadPoints);
|
| }
|
|
|
| // Helper: session filter (server time)
|
| bool SessionOK()
|
| {
|
| if(!InpUseSessionFilter) return true;
|
| MqlDateTime t; TimeToStruct(TimeCurrent(), t);
|
| // Handle overnight sessions if needed
|
| if(InpSessionStartHour <= InpSessionEndHour)
|
| return (t.hour >= InpSessionStartHour && t.hour < InpSessionEndHour);
|
| else
|
| return (t.hour >= InpSessionStartHour || t.hour < InpSessionEndHour);
|
| }
|
|
|
| // Normalize lots to symbol constraints
|
| double NormalizeLots(const string sym, double lots)
|
| {
|
| double minLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
|
| double maxLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
|
| double stepLot = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
|
|
|
| if(lots < minLot) lots = minLot;
|
| if(lots > maxLot) lots = maxLot;
|
|
|
| // Round down to step
|
| double steps = MathFloor(lots / stepLot);
|
| double norm = steps * stepLot;
|
|
|
| // Ensure not below min due to rounding
|
| if(norm < minLot) norm = minLot;
|
|
|
| return norm;
|
| }
|
|
|
| // Compute lot size from risk % and stop distance (price units)
|
| double LotsFromRisk(const string sym, double stopDistancePrice)
|
| {
|
| // Risk amount in account currency
|
| double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
| double riskMoney = balance * (InpRiskPercent / 100.0);
|
| if(riskMoney <= 0) return 0.0;
|
|
|
| // Convert stop distance (price) to money per 1 lot using tick value/size
|
| double tickValue = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE);
|
| double tickSize = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
|
|
|
| if(tickValue <= 0 || tickSize <= 0) return 0.0;
|
|
|
| // How many ticks in stop distance?
|
| double ticks = stopDistancePrice / tickSize;
|
| if(ticks <= 0) return 0.0;
|
|
|
| // Loss per 1 lot if SL hit:
|
| double lossPerLot = ticks * tickValue;
|
| if(lossPerLot <= 0) return 0.0;
|
|
|
| double lots = riskMoney / lossPerLot;
|
| return NormalizeLots(sym, lots);
|
| }
|
|
|
| // Get indicator value helpers (current closed bar = shift 1)
|
| double iEMA(const string sym, ENUM_TIMEFRAMES tf, int period, int shift)
|
| {
|
| int h = iMA(sym, tf, period, 0, MODE_EMA, PRICE_CLOSE);
|
| if(h == INVALID_HANDLE) return EMPTY_VALUE;
|
|
|
| double buf[];
|
| if(CopyBuffer(h, 0, shift, 1, buf) != 1) return EMPTY_VALUE;
|
| IndicatorRelease(h);
|
| return buf[0];
|
| }
|
|
|
| double iRSIValue(const string sym, ENUM_TIMEFRAMES tf, int period, int shift)
|
| {
|
| int h = iRSI(sym, tf, period, PRICE_CLOSE);
|
| if(h == INVALID_HANDLE) return EMPTY_VALUE;
|
|
|
| double buf[];
|
| if(CopyBuffer(h, 0, shift, 1, buf) != 1) return EMPTY_VALUE;
|
| IndicatorRelease(h);
|
| return buf[0];
|
| }
|
|
|
| double iADXValue(const string sym, ENUM_TIMEFRAMES tf, int period, int shift)
|
| {
|
| int h = iADX(sym, tf, period);
|
| if(h == INVALID_HANDLE) return EMPTY_VALUE;
|
|
|
| double buf[];
|
| if(CopyBuffer(h, 0, shift, 1, buf) != 1) return EMPTY_VALUE;
|
| IndicatorRelease(h);
|
| return buf[0];
|
| }
|
|
|
| double iATRValue(const string sym, ENUM_TIMEFRAMES tf, int period, int shift)
|
| {
|
| int h = iATR(sym, tf, period);
|
| if(h == INVALID_HANDLE) return EMPTY_VALUE;
|
|
|
| double buf[];
|
| if(CopyBuffer(h, 0, shift, 1, buf) != 1) return EMPTY_VALUE;
|
| IndicatorRelease(h);
|
| return buf[0];
|
| }
|
|
|
| // Decide trade direction: +1 buy, -1 sell, 0 none
|
| int GetSignal(const string sym)
|
| {
|
| // Use closed bar values to reduce repainting
|
| int shift = 1;
|
|
|
| double fast = iEMA(sym, InpTF, InpFastEMA, shift);
|
| double slow = iEMA(sym, InpTF, InpSlowEMA, shift);
|
| double rsi = iRSIValue(sym, InpTF, InpRSIPeriod, shift);
|
|
|
| if(fast == EMPTY_VALUE || slow == EMPTY_VALUE || rsi == EMPTY_VALUE) return 0;
|
|
|
| if(InpUseADX)
|
| {
|
| double adx = iADXValue(sym, InpTF, InpADXPeriod, shift);
|
| if(adx == EMPTY_VALUE) return 0;
|
| if(adx < InpADXMin) return 0; // avoid weak trends
|
| }
|
|
|
| // Trend + momentum confirmation
|
| if(fast > slow && rsi >= InpRSIBuyAbove) return +1;
|
| if(fast < slow && rsi <= InpRSISellBelow) return -1;
|
|
|
| return 0;
|
| }
|
|
|
| // Place trade with ATR-based SL/TP and risk-based lots
|
| bool ExecuteTrade(const string sym, int direction)
|
| {
|
| if(direction == 0) return false;
|
|
|
| // Basic constraints
|
| if(!SymbolInfoInteger(sym, SYMBOL_SELECT))
|
| SymbolSelect(sym, true);
|
|
|
| if(!SpreadOK(sym)) return false;
|
| if(!SessionOK()) return false;
|
|
|
| if(CountPositions("") >= InpMaxPositionsTotal) return false;
|
| if(CountPositions(sym) > 0) return false; // one trade per symbol
|
|
|
| double atr = iATRValue(sym, InpTF, InpATRPeriod, 1);
|
| if(atr == EMPTY_VALUE || atr <= 0) return false;
|
|
|
| double slDist = atr * InpSL_ATR_Mult;
|
| double tpDist = slDist * InpRiskReward;
|
|
|
| double ask = SymbolInfoDouble(sym, SYMBOL_ASK);
|
| double bid = SymbolInfoDouble(sym, SYMBOL_BID);
|
| double pt = SymbolInfoDouble(sym, SYMBOL_POINT);
|
|
|
| if(ask <= 0 || bid <= 0 || pt <= 0) return false;
|
|
|
| // Lot size from risk
|
| double lots = LotsFromRisk(sym, slDist);
|
| if(lots <= 0) return false;
|
|
|
| trade.SetDeviationInPoints(InpSlippagePoints);
|
|
|
| double price, sl, tp;
|
|
|
| if(direction > 0)
|
| {
|
| price = ask;
|
| sl = price - slDist;
|
| tp = price + tpDist;
|
| sl = NormalizeDouble(sl, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS));
|
| tp = NormalizeDouble(tp, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS));
|
|
|
| return trade.Buy(lots, sym, price, sl, tp, "EMA+RSI ATR risk buy");
|
| }
|
| else
|
| {
|
| price = bid;
|
| sl = price + slDist;
|
| tp = price - tpDist;
|
| sl = NormalizeDouble(sl, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS));
|
| tp = NormalizeDouble(tp, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS));
|
|
|
| return trade.Sell(lots, sym, price, sl, tp, "EMA+RSI ATR risk sell");
|
| }
|
| }
|
|
|
| // New bar detection (for the configured timeframe)
|
| bool IsNewBar()
|
| {
|
| if(!InpAllowNewBarOnly) return true;
|
|
|
| datetime t = iTime(_Symbol, InpTF, 0);
|
| if(t == 0) return false;
|
| if(t != g_lastBarTime)
|
| {
|
| g_lastBarTime = t;
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
| //------------------------- MT5 Events ------------------------------
|
| int OnInit()
|
| {
|
| // Ensure symbols are in Market Watch
|
| SymbolSelect(InpSymbol1, true);
|
| SymbolSelect(InpSymbol2, true);
|
| g_lastBarTime = 0;
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnTick()
|
| {
|
| if(!IsNewBar()) return;
|
|
|
| // Evaluate both symbols on each new bar
|
| string syms[2] = {InpSymbol1, InpSymbol2};
|
|
|
| for(int i=0; i<2; i++)
|
| {
|
| string sym = syms[i];
|
| if(!IsTargetSymbol(sym)) continue;
|
|
|
| int sig = GetSignal(sym);
|
| if(sig != 0)
|
| ExecuteTrade(sym, sig);
|
| }
|
| }
|
| //+------------------------------------------------------------------+
|