309 lines
9 KiB
Text
309 lines
9 KiB
Text
//+------------------------------------------------------------------+
| |||
//| 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);
| |||
}
| |||
}
| |||
//+------------------------------------------------------------------+
|