526 lines
16 KiB
Plaintext
526 lines
16 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| XRR_Pullback.mq5 |
|
|
//| Author: Hadi (for XAUUSD,M15) |
|
|
//+------------------------------------------------------------------+
|
|
#property strict
|
|
#property version "0.1"
|
|
#property description "M15 pullback EA: Major zone (HH/LL), minor same-direction zone, pullback entry, SL under minor, TP at 2R, 1% risk."
|
|
|
|
//------------------------------- Inputs -----------------------------
|
|
|
|
input string Inp_Symbol = "XAUUSD";
|
|
input ENUM_TIMEFRAMES Inp_WorkTF = PERIOD_M15;
|
|
|
|
// Zone detection params
|
|
input int Inp_MajorLookbackBars = 400; // bars to scan for HH/LL major zone
|
|
input int Inp_MinorWindowBars = 80; // window to find first same-direction minor zone post-major
|
|
input int Inp_ZonePadPoints = 50; // padding around zones (points)
|
|
|
|
// Confirmation / filters
|
|
input bool Inp_UseHTFConfluence = true;
|
|
input ENUM_TIMEFRAMES Inp_HTF = PERIOD_H1; // confluence timeframe
|
|
input int Inp_HTFShiftBars = 3; // HTF confirmation window
|
|
input bool Inp_UseEngulfingConfirm = true;
|
|
input bool Inp_UseRSIDivergence = false;
|
|
input int Inp_RSI_Period = 14;
|
|
input bool Inp_UseVolumeSpike = false;
|
|
input double Inp_VolSpikeFactor = 1.5; // vs 20-bar average
|
|
|
|
// Risk management
|
|
input double Inp_RiskPercent = 1.0; // 1% per trade
|
|
input bool Inp_UseATRforSL = false;
|
|
input int Inp_ATR_Period = 14;
|
|
input double Inp_ATR_Mult = 1.5;
|
|
input double Inp_MinSLPoints = 200; // floor SL (points)
|
|
input double Inp_R_MultipleTP = 2.0; // fixed R:R=1:2
|
|
|
|
// Risk-free (optional)
|
|
input bool Inp_MoveToBEatTP1 = true;
|
|
input double Inp_TP1_R = 1.0; // move to BE at 1R
|
|
input double Inp_BE_OffsetPoints = 10; // lock-in small +offset
|
|
|
|
// Sessions
|
|
input bool Inp_UseSessionFilter = true;
|
|
input int Inp_LondonOpenHour = 8; // broker time
|
|
input int Inp_LondonCloseHour = 17;
|
|
input int Inp_NYOpenHour = 13;
|
|
input int Inp_NYCloseHour = 21;
|
|
input bool Inp_AllowNYOverlapOnly = false;
|
|
|
|
// Entry placement
|
|
input bool Inp_UseLimitAtZone = true; // limit order at minor zone
|
|
input int Inp_MaxSlippagePoints = 100;
|
|
input int Inp_MinRetestWickPoints = 30;
|
|
|
|
// Safety / engine
|
|
input int Inp_MinBarsLoaded = 600;
|
|
input int Inp_SignalCoolOffBars = 20; // avoid rapid re-entries
|
|
input bool Inp_OnePositionOnly = true;
|
|
|
|
//------------------------------- Structures -------------------------
|
|
struct Zone
|
|
{
|
|
bool isValid;
|
|
double priceLow;
|
|
double priceHigh;
|
|
int barIndex; // starting bar index of zone
|
|
bool isSupply; // true: resistance (HH), false: support (LL)
|
|
};
|
|
|
|
struct MinorZone
|
|
{
|
|
bool isValid;
|
|
double priceLow;
|
|
double priceHigh;
|
|
int barIndex;
|
|
bool isSupply; // same direction as major
|
|
};
|
|
|
|
datetime lastSignalTimeBuy = 0;
|
|
datetime lastSignalTimeSell = 0;
|
|
|
|
//
|
|
int atrHandler = INVALID_HANDLE;
|
|
|
|
//------------------------------- Helpers ----------------------------
|
|
int DigitsAdjust()
|
|
{
|
|
return (int)SymbolInfoInteger(Inp_Symbol, SYMBOL_DIGITS);
|
|
}
|
|
double PointAdjust()
|
|
{
|
|
return SymbolInfoDouble(Inp_Symbol, SYMBOL_POINT);
|
|
}
|
|
double TickValue()
|
|
{
|
|
double v;
|
|
SymbolInfoDouble(Inp_Symbol, SYMBOL_TRADE_TICK_VALUE, v);
|
|
return v;
|
|
}
|
|
double TickSize()
|
|
{
|
|
double s;
|
|
SymbolInfoDouble(Inp_Symbol, SYMBOL_TRADE_TICK_SIZE, s);
|
|
return s;
|
|
}
|
|
|
|
bool IsSessionAllowed()
|
|
{
|
|
if (!Inp_UseSessionFilter)
|
|
return true;
|
|
MqlDateTime t;
|
|
TimeToStruct(TimeCurrent(), t);
|
|
int hour = t.hour;
|
|
bool london = (hour >= Inp_LondonOpenHour && hour < Inp_LondonCloseHour);
|
|
bool ny = (hour >= Inp_NYOpenHour && hour < Inp_NYCloseHour);
|
|
if (Inp_AllowNYOverlapOnly)
|
|
return (london && ny);
|
|
return (london || ny);
|
|
}
|
|
|
|
bool EngulfingBearish(int shift)
|
|
{
|
|
double o0 = iOpen(Inp_Symbol, Inp_WorkTF, shift), c0 = iClose(Inp_Symbol, Inp_WorkTF, shift);
|
|
double o1 = iOpen(Inp_Symbol, Inp_WorkTF, shift + 1), c1 = iClose(Inp_Symbol, Inp_WorkTF, shift + 1);
|
|
return (c0 < o0 && c1 > o1 && o0 < c1 && c0 < o1 && (o0 - o1) > 0 && (c1 - c0) > 0 && (o1 < c0)); // rough engulfing
|
|
}
|
|
bool EngulfingBullish(int shift)
|
|
{
|
|
double o0 = iOpen(Inp_Symbol, Inp_WorkTF, shift), c0 = iClose(Inp_Symbol, Inp_WorkTF, shift);
|
|
double o1 = iOpen(Inp_Symbol, Inp_WorkTF, shift + 1), c1 = iClose(Inp_Symbol, Inp_WorkTF, shift + 1);
|
|
return (c0 > o0 && c1 < o1 && o0 > c1 && c0 > o1 && (o0 - o1) > 0 && (c0 - c1) > 0 && (o1 > c0)); // rough engulfing inverse
|
|
}
|
|
|
|
bool VolumeSpike(int shift)
|
|
{
|
|
if (!Inp_UseVolumeSpike)
|
|
return true;
|
|
double avg = 0;
|
|
int n = 20;
|
|
for (int i = shift + 1; i < shift + 1 + n; i++)
|
|
avg += (double)iVolume(Inp_Symbol, Inp_WorkTF, i);
|
|
avg /= n;
|
|
double v0 = (double)iVolume(Inp_Symbol, Inp_WorkTF, shift);
|
|
return (v0 >= avg * Inp_VolSpikeFactor);
|
|
}
|
|
|
|
bool HTFConfluence(bool wantSupply)
|
|
{
|
|
if (!Inp_UseHTFConfluence)
|
|
return true;
|
|
// Simple HTF direction: last HTF candle close vs open
|
|
double o = iOpen(Inp_Symbol, Inp_HTF, Inp_HTFShiftBars), c = iClose(Inp_Symbol, Inp_HTF, Inp_HTFShiftBars);
|
|
bool htfDown = (c < o), htfUp = (c > o);
|
|
return wantSupply ? htfDown : htfUp;
|
|
}
|
|
|
|
//------------------------------- Zone detection ---------------------
|
|
Zone FindMajorZone()
|
|
{
|
|
Zone z;
|
|
z.isValid = false;
|
|
z.isSupply = false;
|
|
z.priceLow = 0;
|
|
z.priceHigh = 0;
|
|
z.barIndex = -1;
|
|
int bars = iBars(Inp_Symbol, Inp_WorkTF);
|
|
if (bars < Inp_MinBarsLoaded)
|
|
return z;
|
|
|
|
// Highest high / lowest low over lookback
|
|
double hh = -DBL_MAX, ll = DBL_MAX;
|
|
int hhBar = -1, llBar = -1;
|
|
for (int i = 1; i <= Inp_MajorLookbackBars; i++)
|
|
{
|
|
double h = iHigh(Inp_Symbol, Inp_WorkTF, i), l = iLow(Inp_Symbol, Inp_WorkTF, i);
|
|
if (h > hh)
|
|
{
|
|
hh = h;
|
|
hhBar = i;
|
|
}
|
|
if (l < ll)
|
|
{
|
|
ll = l;
|
|
llBar = i;
|
|
}
|
|
}
|
|
|
|
// Decide which one is closest to current action: use most recent extreme
|
|
if (hhBar < llBar)
|
|
{
|
|
z.isSupply = true;
|
|
z.isValid = true;
|
|
z.barIndex = hhBar;
|
|
z.priceLow = hh - Inp_ZonePadPoints * PointAdjust();
|
|
z.priceHigh = hh + Inp_ZonePadPoints * PointAdjust();
|
|
}
|
|
else
|
|
{
|
|
z.isSupply = false;
|
|
z.isValid = true;
|
|
z.barIndex = llBar;
|
|
z.priceLow = ll - Inp_ZonePadPoints * PointAdjust();
|
|
z.priceHigh = ll + Inp_ZonePadPoints * PointAdjust();
|
|
}
|
|
return z;
|
|
}
|
|
|
|
MinorZone FindFirstMinorSameDirection(const Zone &major)
|
|
{
|
|
MinorZone mz;
|
|
mz.isValid = false;
|
|
mz.isSupply = major.isSupply;
|
|
mz.priceLow = 0;
|
|
mz.priceHigh = 0;
|
|
mz.barIndex = -1;
|
|
if (!major.isValid)
|
|
return mz;
|
|
|
|
// Scan forward from major's bar toward current for first reaction zone aligned with major
|
|
int start = major.barIndex - 1;
|
|
int end = MathMax(1, start - Inp_MinorWindowBars);
|
|
|
|
// Heuristic: supply minor = bearish impulse followed by base; demand minor = bullish impulse followed by base
|
|
for (int i = start; i >= end; i--)
|
|
{
|
|
double o = iOpen(Inp_Symbol, Inp_WorkTF, i), c = iClose(Inp_Symbol, Inp_WorkTF, i);
|
|
double h = iHigh(Inp_Symbol, Inp_WorkTF, i), l = iLow(Inp_Symbol, Inp_WorkTF, i);
|
|
|
|
bool bearImpulse = (c < o && (o - c) > (h - l) * 0.5);
|
|
bool bullImpulse = (c > o && (c - o) > (h - l) * 0.5);
|
|
|
|
if (major.isSupply && bearImpulse)
|
|
{
|
|
// Base zone from next candle tight range
|
|
double h1 = iHigh(Inp_Symbol, Inp_WorkTF, i - 1), l1 = iLow(Inp_Symbol, Inp_WorkTF, i - 1);
|
|
mz.isValid = true;
|
|
mz.barIndex = i - 1;
|
|
mz.priceLow = l1 - Inp_ZonePadPoints * PointAdjust();
|
|
mz.priceHigh = h1 + Inp_ZonePadPoints * PointAdjust();
|
|
break;
|
|
}
|
|
if (!major.isSupply && bullImpulse)
|
|
{
|
|
double h1 = iHigh(Inp_Symbol, Inp_WorkTF, i - 1), l1 = iLow(Inp_Symbol, Inp_WorkTF, i - 1);
|
|
mz.isValid = true;
|
|
mz.barIndex = i - 1;
|
|
mz.priceLow = l1 - Inp_ZonePadPoints * PointAdjust();
|
|
mz.priceHigh = h1 + Inp_ZonePadPoints * PointAdjust();
|
|
break;
|
|
}
|
|
}
|
|
return mz;
|
|
}
|
|
|
|
//------------------------------- Signal logic -----------------------
|
|
bool PriceRetestedZone(const MinorZone &mz, bool supply, int shift = 0)
|
|
{
|
|
if (!mz.isValid)
|
|
return false;
|
|
double high = iHigh(Inp_Symbol, Inp_WorkTF, shift), low = iLow(Inp_Symbol, Inp_WorkTF, shift);
|
|
// For supply: retest when high touches zone; for demand: retest when low touches zone
|
|
if (supply)
|
|
return (high >= mz.priceLow && high <= mz.priceHigh && (high - iClose(Inp_Symbol, Inp_WorkTF, shift)) * 1.0 / PointAdjust() >= Inp_MinRetestWickPoints);
|
|
else
|
|
return (low <= mz.priceHigh && low >= mz.priceLow && (iClose(Inp_Symbol, Inp_WorkTF, shift) - low) * 1.0 / PointAdjust() >= Inp_MinRetestWickPoints);
|
|
}
|
|
|
|
bool ConfirmAtZone(bool supply)
|
|
{
|
|
int shift = 0;
|
|
if (Inp_UseEngulfingConfirm)
|
|
{
|
|
if (supply)
|
|
{
|
|
if (!EngulfingBearish(shift))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!EngulfingBullish(shift))
|
|
return false;
|
|
}
|
|
}
|
|
if (!VolumeSpike(shift))
|
|
return false;
|
|
if (!HTFConfluence(supply))
|
|
return false;
|
|
// Optional: RSI divergence could be added here if enabled.
|
|
return true;
|
|
}
|
|
|
|
//------------------------------- Risk & orders -----------------------
|
|
double CalcPositionSize(double sl_points)
|
|
{
|
|
double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (Inp_RiskPercent / 100.0);
|
|
double tickValue = TickValue();
|
|
double tickSize = TickSize();
|
|
// Convert SL points to quoted ticks
|
|
double ticks = sl_points * PointAdjust() / tickSize;
|
|
if (ticks <= 0.0)
|
|
return 0.0;
|
|
double lots = riskMoney / (ticks * tickValue);
|
|
// Normalize to symbol step
|
|
double minLot = SymbolInfoDouble(Inp_Symbol, SYMBOL_VOLUME_MIN);
|
|
double lotStep = SymbolInfoDouble(Inp_Symbol, SYMBOL_VOLUME_STEP);
|
|
double maxLot = SymbolInfoDouble(Inp_Symbol, SYMBOL_VOLUME_MAX);
|
|
lots = MathFloor(lots / lotStep) * lotStep;
|
|
lots = MathMax(minLot, MathMin(lots, maxLot));
|
|
return lots;
|
|
}
|
|
|
|
bool PlaceEntry(const MinorZone &mz, bool supply)
|
|
{
|
|
double bid = SymbolInfoDouble(Inp_Symbol, SYMBOL_BID);
|
|
double ask = SymbolInfoDouble(Inp_Symbol, SYMBOL_ASK);
|
|
double point = PointAdjust();
|
|
|
|
// SL beneath/above minor zone (or ATR-based)
|
|
double sl, entry, tp;
|
|
double sl_points;
|
|
|
|
if (supply)
|
|
{
|
|
entry = Inp_UseLimitAtZone ? mz.priceLow : bid; // sell near zone top/low
|
|
double baseSL = mz.priceHigh + Inp_ZonePadPoints * point;
|
|
if (Inp_UseATRforSL)
|
|
{
|
|
//
|
|
double atrs[];
|
|
CopyBuffer(atrHandler, MAIN_LINE, 0, 1, atrs);
|
|
double atr = atrs[0];
|
|
baseSL = MathMax(baseSL, entry + (atr * Inp_ATR_Mult));
|
|
}
|
|
sl = baseSL;
|
|
sl_points = (sl - entry) / point;
|
|
sl_points = MathMax(sl_points, Inp_MinSLPoints);
|
|
tp = entry - (sl_points * point * Inp_R_MultipleTP);
|
|
// Position sizing
|
|
double lots = CalcPositionSize(sl_points);
|
|
if (lots <= 0)
|
|
return false;
|
|
|
|
MqlTradeRequest req;
|
|
MqlTradeResult res;
|
|
ZeroMemory(req);
|
|
ZeroMemory(res);
|
|
req.action = TRADE_ACTION_DEAL;
|
|
req.symbol = Inp_Symbol;
|
|
req.type = ORDER_TYPE_SELL;
|
|
req.volume = lots;
|
|
req.price = Inp_UseLimitAtZone ? entry : bid;
|
|
req.sl = sl;
|
|
req.tp = tp;
|
|
req.deviation = Inp_MaxSlippagePoints;
|
|
bool ok = OrderSend(req, res);
|
|
if (ok)
|
|
{
|
|
lastSignalTimeSell = TimeCurrent();
|
|
}
|
|
return ok;
|
|
}
|
|
else
|
|
{
|
|
entry = Inp_UseLimitAtZone ? mz.priceHigh : ask; // buy near zone top/high
|
|
double baseSL = mz.priceLow - Inp_ZonePadPoints * point;
|
|
if (Inp_UseATRforSL)
|
|
{
|
|
//
|
|
double atrs[];
|
|
CopyBuffer(atrHandler, MAIN_LINE, 0, 1, atrs);
|
|
double atr = atrs[0];
|
|
baseSL = MathMin(baseSL, entry - (atr * Inp_ATR_Mult));
|
|
}
|
|
sl = baseSL;
|
|
sl_points = (entry - sl) / point;
|
|
sl_points = MathMax(sl_points, Inp_MinSLPoints);
|
|
tp = entry + (sl_points * point * Inp_R_MultipleTP);
|
|
double lots = CalcPositionSize(sl_points);
|
|
if (lots <= 0)
|
|
return false;
|
|
|
|
MqlTradeRequest req;
|
|
MqlTradeResult res;
|
|
ZeroMemory(req);
|
|
ZeroMemory(res);
|
|
req.action = TRADE_ACTION_DEAL;
|
|
req.symbol = Inp_Symbol;
|
|
req.type = ORDER_TYPE_BUY;
|
|
req.volume = lots;
|
|
req.price = Inp_UseLimitAtZone ? entry : ask;
|
|
req.sl = sl;
|
|
req.tp = tp;
|
|
req.deviation = Inp_MaxSlippagePoints;
|
|
bool ok = OrderSend(req, res);
|
|
if (ok)
|
|
{
|
|
lastSignalTimeBuy = TimeCurrent();
|
|
}
|
|
return ok;
|
|
}
|
|
}
|
|
|
|
//------------------------------- Position management ----------------
|
|
void ManageOpenPositions()
|
|
{
|
|
int total = PositionsTotal();
|
|
double point = PointAdjust();
|
|
|
|
for (int i = 0; i < total; i++)
|
|
{
|
|
string sym = PositionGetSymbol(i);
|
|
if (sym != Inp_Symbol)
|
|
continue;
|
|
if (!PositionSelect(sym))
|
|
continue;
|
|
|
|
long type = (long)PositionGetInteger(POSITION_TYPE);
|
|
double price = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
double sl = PositionGetDouble(POSITION_SL);
|
|
double tp = PositionGetDouble(POSITION_TP);
|
|
double current = (type == POSITION_TYPE_BUY) ? SymbolInfoDouble(sym, SYMBOL_BID) : SymbolInfoDouble(sym, SYMBOL_ASK);
|
|
|
|
// Risk-free move to BE at TP1 (= 1R)
|
|
if (Inp_MoveToBEatTP1 && tp > 0 && sl > 0)
|
|
{
|
|
double rDist = MathAbs(tp - price) / Inp_R_MultipleTP;
|
|
bool hitTP1 = (type == POSITION_TYPE_BUY ? (current >= price + rDist) : (current <= price - rDist));
|
|
|
|
if (hitTP1)
|
|
{
|
|
double newSL = (type == POSITION_TYPE_BUY ? price + Inp_BE_OffsetPoints * point : price - Inp_BE_OffsetPoints * point);
|
|
if ((type == POSITION_TYPE_BUY && newSL > sl) || (type == POSITION_TYPE_SELL && newSL < sl))
|
|
{
|
|
MqlTradeRequest req;
|
|
MqlTradeResult res;
|
|
ZeroMemory(req);
|
|
ZeroMemory(res);
|
|
req.action = TRADE_ACTION_SLTP;
|
|
req.symbol = sym;
|
|
req.position = (ulong)PositionGetInteger(POSITION_TICKET);
|
|
req.sl = newSL;
|
|
req.tp = tp;
|
|
OrderSend(req, res);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------- Engine ------------------------------
|
|
bool CoolOffOk(bool supply)
|
|
{
|
|
datetime last = supply ? lastSignalTimeSell : lastSignalTimeBuy;
|
|
if (last == 0)
|
|
return true;
|
|
// Convert bars to seconds approximate using last M15 duration
|
|
int seconds = Inp_SignalCoolOffBars * 15 * 60;
|
|
return (TimeCurrent() - last) >= seconds;
|
|
}
|
|
|
|
void TrySignal()
|
|
{
|
|
if (!IsSessionAllowed())
|
|
return;
|
|
|
|
Zone major = FindMajorZone();
|
|
if (!major.isValid)
|
|
return;
|
|
|
|
MinorZone minor = FindFirstMinorSameDirection(major);
|
|
if (!minor.isValid)
|
|
return;
|
|
|
|
// Only act on retest + confirmation
|
|
bool retest = PriceRetestedZone(minor, major.isSupply, 0);
|
|
if (!retest)
|
|
return;
|
|
if (!ConfirmAtZone(major.isSupply))
|
|
return;
|
|
if (Inp_OnePositionOnly && PositionsTotal() > 0)
|
|
return;
|
|
|
|
if (!CoolOffOk(major.isSupply))
|
|
return;
|
|
|
|
PlaceEntry(minor, major.isSupply);
|
|
}
|
|
|
|
//------------------------------- Standard events --------------------
|
|
int OnInit()
|
|
{
|
|
//
|
|
if (Symbol() != Inp_Symbol)
|
|
{
|
|
SymbolSelect(Inp_Symbol, true);
|
|
}
|
|
|
|
//
|
|
atrHandler = iATR(Inp_Symbol, Inp_WorkTF, Inp_ATR_Period);
|
|
return (INIT_SUCCEEDED);
|
|
}
|
|
|
|
void OnDeinit(const int reason)
|
|
{
|
|
}
|
|
|
|
void OnTick()
|
|
{
|
|
static datetime lastBarTime = 0;
|
|
MqlRates rates[];
|
|
int copied = CopyRates(Inp_Symbol, Inp_WorkTF, 0, Inp_MinBarsLoaded, rates);
|
|
if (copied < Inp_MinBarsLoaded)
|
|
return;
|
|
|
|
datetime curBarTime = rates[0].time;
|
|
if (curBarTime != lastBarTime)
|
|
{
|
|
// New bar on M15: evaluate signal
|
|
TrySignal();
|
|
lastBarTime = curBarTime;
|
|
}
|
|
|
|
// Manage open positions continuously
|
|
ManageOpenPositions();
|
|
}
|