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