//+------------------------------------------------------------------+ //| EmaPullbackBasic.mq5 — EMA50 pullback EA (phased build) | //| Phase 4: breakout khỏi nến touch (cửa sổ N nến) | //| Phase 5: risk & TP theo R | //| Phase 6: spread/session/DD + chốt 50% @ 2R rồi BE (không cap lệnh/ngày)| //+------------------------------------------------------------------+ #property copyright "EMA Pullback Basic" #property version "0.58" #property description "Phase 6: filters + partial 2R + breakeven" #include enum ENUM_MARKET_STATE { MARKET_NO_TRADE = 0, MARKET_SIDEWAY = 1, MARKET_TREND_UP = 2, MARKET_TREND_DOWN = 3 }; enum ENUM_SETUP_STATE { SETUP_IDLE = 0, SETUP_PULLBACK_ACTIVE = 1, SETUP_TOUCHED = 2 }; //--- inputs input group "=== Indicators ===" input int InpEmaPeriod = 50; input int InpAdxPeriod = 14; input int InpAtrPeriod = 14; input group "=== Regime (Phase 2) ===" input bool InpRegimeEnabled = true; input double InpMinAdx = 22.0; input double InpSidewayAdxMax = 20.0; input int InpEmaSlopeBars = 10; input double InpMinEmaSlope = 0.50; input double InpSidewayMaxEmaSlope = 0.35; input bool InpRequireEmaDirection = true; input group "=== Pullback & touch (Phase 3) ===" input double InpTouchEmaAtr = 0.25; input int InpPullbackLookback = 20; input double InpPullbackMinDistAtr = 1; input double InpPullbackRetraceFrac = 0.40; input int InpMaxBarsPullback = 20; input bool InpRequireCloseVsEmaOnTouch = true; input bool InpDrawSetupMarkers = true; input group "=== Entry breakout (Phase 4) ===" input int InpMaxBarsWaitBreakout = 5; // Sau touch: chờ breakout tối đa N nến input bool InpBreakoutUseTouchHighLow = true; // Buy: Close > High touch; Sell: Close < Low touch input bool InpRequireBreakoutCandleColor = true; // Buy: bullish, Sell: bearish input group "=== Risk (Phase 5) ===" input bool InpTradeEnabled = true; input double InpRiskPercent = 1.0; // R = % balance input double InpTpRR = 3; // TP = InpTpRR × SL distance input double InpSlBufferAtr = 0.10; // SL dưới đáy/ trên đỉnh + buffer × ATR input int InpMaxBarsInTrade = 0; // 0 = không đóng theo thời gian input group "=== Phase 6 — An toàn & quản lý lệnh ===" input bool InpUseSpreadFilter = true; input int InpMaxSpreadPoints = 50; // Không vào lệnh nếu spread > (points) input bool InpUseSessionFilter = true; input int InpSessionStartHour = 8; // Giờ server (bắt đầu) input int InpSessionEndHour = 22; // Giờ server (kết thúc, có thể qua đêm) input bool InpUseDdRiskScale = true; input double InpDdHalveRiskPct = 5.0; // DD từ đỉnh equity → risk × 0.5 input bool InpUsePartialAt2R = true; // Đạt 2R: chốt 50% + SL → BE input double InpPartialCloseRR = 2.0; // Ngưỡng R để chốt một phần input double InpPartialCloseFrac = 0.50; // Phần volume đóng (0.5 = một nửa) input int InpBeOffsetPoints = 2; // BE = entry ± offset (points) // Ghi chú: không giới hạn số lệnh / ngày input group "=== EA ===" input ulong InpMagic = 20260522; input int InpSlippage = 10; input bool InpLogEachBar = true; input bool InpLogStateChange = true; input bool InpShowComment = true; input bool InpShowStatsPanel = true; // Thống kê góc dưới trái //--- globals CTrade g_trade; int g_hEma = INVALID_HANDLE; int g_hAdx = INVALID_HANDLE; int g_hAtr = INVALID_HANDLE; datetime g_lastBarTime = 0; ENUM_MARKET_STATE g_marketState = MARKET_NO_TRADE; string g_regimeDetail = ""; ENUM_SETUP_STATE g_setupState = SETUP_IDLE; int g_setupDir = 0; int g_barsInSetupState = 0; datetime g_touchBarTime = 0; string g_setupDetail = ""; string g_lastStateLog = ""; int g_statTotal = 0; int g_statTP = 0; int g_statSL = 0; double g_equityPeak = 0.0; double g_riskScale = 1.0; ulong g_trackTicket = 0; double g_trackEntry = 0.0; double g_trackRiskDist = 0.0; bool g_trackIsBuy = false; bool g_trackPartialDone = false; const string OBJ_PREFIX = "EPB_"; string StatPfx() { return "EPB_ST_" + IntegerToString((int)InpMagic) + "_"; } //+------------------------------------------------------------------+ string MarketStateToString(const ENUM_MARKET_STATE state) { switch(state) { case MARKET_TREND_UP: return "TREND_UP"; case MARKET_TREND_DOWN: return "TREND_DOWN"; case MARKET_SIDEWAY: return "SIDEWAY"; default: return "NO_TRADE"; } } string SetupStateToString(const ENUM_SETUP_STATE state) { switch(state) { case SETUP_PULLBACK_ACTIVE: return "PULLBACK_ACTIVE"; case SETUP_TOUCHED: return "TOUCHED"; default: return "IDLE"; } } double NormalizePrice(const double price) { const double tick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tick <= 0.0) return NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)); return NormalizeDouble(MathRound(price / tick) * tick, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)); } double NormalizeLots(double v) { const double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); const double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); const double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); if(step <= 0.0) return 0.0; v = MathFloor(v / step) * step; if(v < vmin - 1e-12) return 0.0; if(v > vmax) v = vmax; return NormalizeDouble(v, 8); } bool Copy1(const int handle, const int bufferIndex, const int shift, double &value) { double buf[]; ArraySetAsSeries(buf, true); if(CopyBuffer(handle, bufferIndex, shift, 1, buf) < 1) return false; value = buf[0]; return true; } bool GetEma(const int shift, double &ema) { return Copy1(g_hEma, 0, shift, ema); } bool GetAdx(const int shift, double &adx) { return Copy1(g_hAdx, 0, shift, adx); } bool GetAtr(const int shift, double &atr) { return Copy1(g_hAtr, 0, shift, atr); } bool ReadIndicatorsAtShift(const int shift, double &ema, double &adx, double &atr, string &err) { err = ""; if(!GetEma(shift, ema)) { err = "EMA CopyBuffer failed"; return false; } if(!GetAdx(shift, adx)) { err = "ADX CopyBuffer failed"; return false; } if(!GetAtr(shift, atr)) { err = "ATR CopyBuffer failed"; return false; } return true; } double CalcEmaSlopeAtr(const int shift, const double atr) { if(atr <= 0.0 || InpEmaSlopeBars < 1) return 0.0; double emaNear = 0.0, emaFar = 0.0; if(!GetEma(shift, emaNear) || !GetEma(shift + InpEmaSlopeBars, emaFar)) return 0.0; return MathAbs(emaNear - emaFar) / atr; } bool EmaSlopesUp(const int shift) { double emaNear = 0.0, emaFar = 0.0; if(!GetEma(shift, emaNear) || !GetEma(shift + InpEmaSlopeBars, emaFar)) return false; return emaNear > emaFar + _Point; } bool EmaSlopesDown(const int shift) { double emaNear = 0.0, emaFar = 0.0; if(!GetEma(shift, emaNear) || !GetEma(shift + InpEmaSlopeBars, emaFar)) return false; return emaNear < emaFar - _Point; } ENUM_MARKET_STATE ClassifyMarketState(const int shift, string &detail) { detail = ""; if(!InpRegimeEnabled) { detail = "regime filter OFF"; return MARKET_NO_TRADE; } double ema = 0.0, adx = 0.0, atr = 0.0; string err = ""; if(!ReadIndicatorsAtShift(shift, ema, adx, atr, err)) { detail = err; return MARKET_NO_TRADE; } if(atr <= 0.0) { detail = "ATR=0"; return MARKET_NO_TRADE; } const double emaSlope = CalcEmaSlopeAtr(shift, atr); const double close = iClose(_Symbol, _Period, shift); if(adx < InpSidewayAdxMax) { detail = StringFormat("sideway: ADX %.1f < %.1f", adx, InpSidewayAdxMax); return MARKET_SIDEWAY; } if(emaSlope < InpSidewayMaxEmaSlope) { detail = StringFormat("sideway: slope %.2f < %.2f", emaSlope, InpSidewayMaxEmaSlope); return MARKET_SIDEWAY; } if(adx < InpMinAdx) { detail = StringFormat("ADX %.1f < %.1f", adx, InpMinAdx); return MARKET_NO_TRADE; } if(emaSlope < InpMinEmaSlope) { detail = StringFormat("slope %.2f < %.2f", emaSlope, InpMinEmaSlope); return MARKET_NO_TRADE; } if(close > ema + _Point) { if(InpRequireEmaDirection && !EmaSlopesUp(shift)) { detail = "giá trên EMA, EMA không dốc lên"; return MARKET_NO_TRADE; } detail = StringFormat("OK UP | ADX=%.1f", adx); return MARKET_TREND_UP; } if(close < ema - _Point) { if(InpRequireEmaDirection && !EmaSlopesDown(shift)) { detail = "giá dưới EMA, EMA không dốc xuống"; return MARKET_NO_TRADE; } detail = StringFormat("OK DOWN | ADX=%.1f", adx); return MARKET_TREND_DOWN; } detail = "Close ≈ EMA"; return MARKET_NO_TRADE; } bool CanTradeRegime(const ENUM_MARKET_STATE state) { return state == MARKET_TREND_UP || state == MARKET_TREND_DOWN; } int TradeDirection(const ENUM_MARKET_STATE state) { if(state == MARKET_TREND_UP) return 1; if(state == MARKET_TREND_DOWN) return -1; return 0; } bool PriceTouchedEmaAtShift(const int shift, const double ema, const double atr) { const double tol = InpTouchEmaAtr * atr; const double h = iHigh(_Symbol, _Period, shift); const double l = iLow(_Symbol, _Period, shift); return (l <= ema + tol && h >= ema - tol); } bool PriceTouchedEmaBuy(const int shift, const double ema, const double atr) { if(!PriceTouchedEmaAtShift(shift, ema, atr)) return false; if(!InpRequireCloseVsEmaOnTouch) return true; return iClose(_Symbol, _Period, shift) >= ema - _Point; } bool PriceTouchedEmaSell(const int shift, const double ema, const double atr) { if(!PriceTouchedEmaAtShift(shift, ema, atr)) return false; if(!InpRequireCloseVsEmaOnTouch) return true; return iClose(_Symbol, _Period, shift) <= ema + _Point; } bool CalcPullbackExtensionBuy(const int shift, const double atr, double &maxDistAtr, double &distCloseAtr) { maxDistAtr = 0.0; distCloseAtr = 0.0; if(atr <= 0.0) return false; const int lb = MathMax(3, InpPullbackLookback); for(int s = shift; s < shift + lb; s++) { double emaS = 0.0; if(!GetEma(s, emaS)) return false; const double dist = (iHigh(_Symbol, _Period, s) - emaS) / atr; if(dist > maxDistAtr) maxDistAtr = dist; } double ema0 = 0.0; if(!GetEma(shift, ema0)) return false; distCloseAtr = (iClose(_Symbol, _Period, shift) - ema0) / atr; return true; } bool CalcPullbackExtensionSell(const int shift, const double atr, double &maxDistAtr, double &distCloseAtr) { maxDistAtr = 0.0; distCloseAtr = 0.0; if(atr <= 0.0) return false; const int lb = MathMax(3, InpPullbackLookback); for(int s = shift; s < shift + lb; s++) { double emaS = 0.0; if(!GetEma(s, emaS)) return false; const double dist = (emaS - iLow(_Symbol, _Period, s)) / atr; if(dist > maxDistAtr) maxDistAtr = dist; } double ema0 = 0.0; if(!GetEma(shift, ema0)) return false; distCloseAtr = (ema0 - iClose(_Symbol, _Period, shift)) / atr; return true; } bool PullbackStartBuy(const int shift, const double ema, const double atr, string &why) { why = ""; if(iClose(_Symbol, _Period, shift) <= ema + _Point) { why = "Close không trên EMA"; return false; } double maxDist = 0.0, distClose = 0.0; if(!CalcPullbackExtensionBuy(shift, atr, maxDist, distClose)) { why = "không tính extension"; return false; } if(maxDist < InpPullbackMinDistAtr) { why = StringFormat("chưa extension (%.2f)", maxDist); return false; } if(distClose > maxDist * InpPullbackRetraceFrac) { why = "chưa hồi đủ về EMA"; return false; } why = StringFormat("pullback UP max=%.2f now=%.2f", maxDist, distClose); return true; } bool PullbackStartSell(const int shift, const double ema, const double atr, string &why) { why = ""; if(iClose(_Symbol, _Period, shift) >= ema - _Point) { why = "Close không dưới EMA"; return false; } double maxDist = 0.0, distClose = 0.0; if(!CalcPullbackExtensionSell(shift, atr, maxDist, distClose)) { why = "không tính extension"; return false; } if(maxDist < InpPullbackMinDistAtr) { why = StringFormat("chưa extension (%.2f)", maxDist); return false; } if(distClose > maxDist * InpPullbackRetraceFrac) { why = "chưa hồi đủ về EMA"; return false; } why = StringFormat("pullback DOWN max=%.2f now=%.2f", maxDist, distClose); return true; } bool TrendStructureBrokenBuy(const int shift, const double ema) { return iClose(_Symbol, _Period, shift) < ema - _Point; } bool TrendStructureBrokenSell(const int shift, const double ema) { return iClose(_Symbol, _Period, shift) > ema + _Point; } // Tìm shift của bar touch theo thời gian (shift thay đổi khi có bar mới) int FindTouchBarShift() { if(g_touchBarTime == 0) return -1; const int maxScan = MathMax(InpMaxBarsWaitBreakout + InpMaxBarsPullback + 5, 50); for(int s = 1; s <= maxScan; s++) { if(iTime(_Symbol, _Period, s) == g_touchBarTime) return s; } return -1; } // Số nến đã đóng sau bar touch (signalShift=1 → bar mới nhất) int BarsAfterTouch(const int touchShift, const int signalShift) { if(touchShift < 1 || signalShift >= touchShift) return -1; return touchShift - signalShift; } // Phase 4: breakout khỏi nến touch trong cửa sổ InpMaxBarsWaitBreakout bool BreakoutSignalBuy(const int signalShift, const int touchShift, string &why) { why = ""; if(touchShift < 1) { why = "không có touch bar"; return false; } if(signalShift >= touchShift) { why = "chờ bar sau touch"; return false; } const int barsAfter = BarsAfterTouch(touchShift, signalShift); if(barsAfter < 1) { why = "chưa có bar sau touch"; return false; } if(barsAfter > InpMaxBarsWaitBreakout) { why = StringFormat("quá cửa sổ breakout (%d > %d)", barsAfter, InpMaxBarsWaitBreakout); return false; } const double cSig = iClose(_Symbol, _Period, signalShift); const double oSig = iOpen(_Symbol, _Period, signalShift); const double hTch = iHigh(_Symbol, _Period, touchShift); const double cTch = iClose(_Symbol, _Period, touchShift); if(InpRequireBreakoutCandleColor && cSig <= oSig + _Point) { why = StringFormat("bar+%d chưa bullish", barsAfter); return false; } if(InpBreakoutUseTouchHighLow) { if(cSig <= hTch + _Point) { why = StringFormat("bar+%d chưa phá High touch %.*f (close=%.*f)", barsAfter, _Digits, hTch, _Digits, cSig); return false; } why = StringFormat("breakout BUY bar+%d close>%.*f (High touch)", barsAfter, _Digits, hTch); return true; } if(cSig <= cTch + _Point) { why = StringFormat("bar+%d close <= close touch", barsAfter); return false; } why = StringFormat("breakout BUY bar+%d close>close touch", barsAfter); return true; } bool BreakoutSignalSell(const int signalShift, const int touchShift, string &why) { why = ""; if(touchShift < 1) { why = "không có touch bar"; return false; } if(signalShift >= touchShift) { why = "chờ bar sau touch"; return false; } const int barsAfter = BarsAfterTouch(touchShift, signalShift); if(barsAfter < 1) { why = "chưa có bar sau touch"; return false; } if(barsAfter > InpMaxBarsWaitBreakout) { why = StringFormat("quá cửa sổ breakout (%d > %d)", barsAfter, InpMaxBarsWaitBreakout); return false; } const double cSig = iClose(_Symbol, _Period, signalShift); const double oSig = iOpen(_Symbol, _Period, signalShift); const double lTch = iLow(_Symbol, _Period, touchShift); const double cTch = iClose(_Symbol, _Period, touchShift); if(InpRequireBreakoutCandleColor && cSig >= oSig - _Point) { why = StringFormat("bar+%d chưa bearish", barsAfter); return false; } if(InpBreakoutUseTouchHighLow) { if(cSig >= lTch - _Point) { why = StringFormat("bar+%d chưa phá Low touch %.*f (close=%.*f)", barsAfter, _Digits, lTch, _Digits, cSig); return false; } why = StringFormat("breakout SELL bar+%d close<%.*f (Low touch)", barsAfter, _Digits, lTch); return true; } if(cSig >= cTch - _Point) { why = StringFormat("bar+%d close >= close touch", barsAfter); return false; } why = StringFormat("breakout SELL bar+%d close= 0; i--) { const ulong t = PositionGetTicket(i); if(t == 0 || !PositionSelectByTicket(t)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) continue; return true; } return false; } ulong FindOurPositionTicket() { for(int i = PositionsTotal() - 1; i >= 0; i--) { const ulong t = PositionGetTicket(i); if(t == 0 || !PositionSelectByTicket(t)) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) continue; return t; } return 0; } void ResetPositionTrack() { g_trackTicket = 0; g_trackEntry = 0.0; g_trackRiskDist = 0.0; g_trackIsBuy = false; g_trackPartialDone = false; } void SyncPositionTrack() { const ulong t = FindOurPositionTicket(); if(t == 0) { ResetPositionTrack(); return; } if(t == g_trackTicket && g_trackRiskDist > 0.0) return; if(!PositionSelectByTicket(t)) return; g_trackTicket = t; g_trackEntry = PositionGetDouble(POSITION_PRICE_OPEN); const double sl = PositionGetDouble(POSITION_SL); g_trackRiskDist = MathAbs(g_trackEntry - sl); if(g_trackRiskDist < _Point) g_trackRiskDist = _Point; g_trackIsBuy = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); g_trackPartialDone = false; } void StartPositionTrack(const ulong ticket, const bool isBuy, const double entry, const double sl) { g_trackTicket = ticket; g_trackEntry = entry; g_trackRiskDist = MathAbs(entry - sl); if(g_trackRiskDist < _Point) g_trackRiskDist = _Point; g_trackIsBuy = isBuy; g_trackPartialDone = false; } int CurrentSpreadPoints() { const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(ask <= 0.0 || bid <= 0.0 || _Point <= 0.0) return 0; return (int)MathRound((ask - bid) / _Point); } bool IsSpreadAllowed() { if(!InpUseSpreadFilter) return true; return CurrentSpreadPoints() <= InpMaxSpreadPoints; } bool IsSessionAllowed() { if(!InpUseSessionFilter) return true; MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); const int h = dt.hour; if(InpSessionStartHour == InpSessionEndHour) return true; if(InpSessionStartHour < InpSessionEndHour) return (h >= InpSessionStartHour && h < InpSessionEndHour); return (h >= InpSessionStartHour || h < InpSessionEndHour); } void UpdateRiskScale() { if(!InpUseDdRiskScale) { g_riskScale = 1.0; return; } const double eq = AccountInfoDouble(ACCOUNT_EQUITY); if(g_equityPeak <= 0.0) g_equityPeak = eq; if(eq > g_equityPeak) g_equityPeak = eq; g_riskScale = 1.0; if(g_equityPeak > 0.0) { const double ddPct = (g_equityPeak - eq) / g_equityPeak * 100.0; if(ddPct >= InpDdHalveRiskPct) g_riskScale = 0.5; } } bool IsAllowedNewEntry(string &why) { why = ""; if(!IsSpreadAllowed()) { why = StringFormat("spread %d > %d pts", CurrentSpreadPoints(), InpMaxSpreadPoints); return false; } if(!IsSessionAllowed()) { why = StringFormat("ngoài session %02d–%02d server", InpSessionStartHour, InpSessionEndHour); return false; } return true; } double VolumeForRisk(const bool isBuy, const double entry, const double sl) { if(MathAbs(entry - sl) < _Point) return 0.0; const double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (InpRiskPercent / 100.0) * g_riskScale; double profit = 0.0; if(!OrderCalcProfit(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, _Symbol, 1.0, entry, sl, profit)) return 0.0; const double lossPerLot = MathAbs(profit); if(lossPerLot < DBL_EPSILON) return 0.0; return riskMoney / lossPerLot; } // Phase 5: SL dưới min(low touch, low reject), TP = 1.5R bool CalcSlTpFromSetup(const bool isBuy, const int touchShift, const int rejectShift, const double entry, const double atr, double &sl, double &tp, string &err) { err = ""; if(atr <= 0.0) { err = "ATR=0"; return false; } const double buffer = InpSlBufferAtr * atr; const double minStop = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point; if(isBuy) { const double slBase = MathMin(iLow(_Symbol, _Period, touchShift), iLow(_Symbol, _Period, rejectShift)); sl = NormalizePrice(slBase - buffer); tp = NormalizePrice(entry + InpTpRR * (entry - sl)); if(entry - sl < MathMax(_Point, minStop)) { err = "SL quá gần entry (buy)"; return false; } } else { const double slBase = MathMax(iHigh(_Symbol, _Period, touchShift), iHigh(_Symbol, _Period, rejectShift)); sl = NormalizePrice(slBase + buffer); tp = NormalizePrice(entry - InpTpRR * (sl - entry)); if(sl - entry < MathMax(_Point, minStop)) { err = "SL quá gần entry (sell)"; return false; } } return true; } bool OpenMarketFromReject(const bool isBuy, const int touchShift, const int rejectShift, const double atr) { const double entry = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); double sl = 0.0, tp = 0.0; string err = ""; if(!CalcSlTpFromSetup(isBuy, touchShift, rejectShift, entry, atr, sl, tp, err)) { Print("[EPB] Entry skip — ", err); return false; } const double lot = NormalizeLots(VolumeForRisk(isBuy, entry, sl)); if(lot <= 0.0) { Print("[EPB] Entry skip — lot=0"); return false; } const double riskDist = MathAbs(entry - sl); const double rr = (riskDist > 0.0) ? MathAbs(tp - entry) / riskDist : 0.0; const string cmt = StringFormat("EPB %s R%.1f%% RR1:%.1f", isBuy ? "Buy" : "Sell", InpRiskPercent, InpTpRR); const bool ok = isBuy ? g_trade.Buy(lot, _Symbol, 0.0, sl, tp, cmt) : g_trade.Sell(lot, _Symbol, 0.0, sl, tp, cmt); if(!ok) { PrintFormat("[EPB] Order fail retcode=%d %s", g_trade.ResultRetcode(), g_trade.ResultRetcodeDescription()); return false; } PrintFormat("[EPB] %s | entry=%.*f SL=%.*f TP=%.*f lot=%.2f risk=%.1f%% R:R=1:%.2f", isBuy ? "BUY" : "SELL", _Digits, entry, _Digits, sl, _Digits, tp, lot, InpRiskPercent, rr); const datetime t = iTime(_Symbol, _Period, rejectShift); const double px = isBuy ? iHigh(_Symbol, _Period, rejectShift) : iLow(_Symbol, _Period, rejectShift); DrawSetupMarker("SIG", t, px, isBuy ? clrLime : clrOrangeRed, isBuy ? "Entry Buy" : "Entry Sell"); const ulong posTicket = FindOurPositionTicket(); if(posTicket > 0) StartPositionTrack(posTicket, isBuy, entry, sl); return true; } bool TryPartialCloseAndBreakeven(const ulong ticket) { if(!InpUsePartialAt2R || g_trackPartialDone || g_trackRiskDist <= 0.0) return false; if(ticket == 0 || !PositionSelectByTicket(ticket)) return false; const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); const double px = g_trackIsBuy ? bid : ask; double profitR = 0.0; if(g_trackIsBuy) profitR = (px - g_trackEntry) / g_trackRiskDist; else profitR = (g_trackEntry - px) / g_trackRiskDist; if(profitR < InpPartialCloseRR - 1e-8) return false; const double vol = PositionGetDouble(POSITION_VOLUME); const double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); const double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); if(vol <= vmin + 1e-12) return false; double closeVol = vol * InpPartialCloseFrac; if(step > 0.0) closeVol = MathFloor(closeVol / step) * step; closeVol = NormalizeLots(closeVol); if(closeVol < vmin) { if(vol <= vmin * 2.0 + 1e-12) return false; closeVol = vmin; } if(vol - closeVol < vmin - 1e-12) closeVol = NormalizeLots(vol - vmin); if(closeVol < vmin - 1e-12) return false; if(!g_trade.PositionClosePartial(ticket, closeVol)) { PrintFormat("[EPB] Partial close fail ticket=%I64u vol=%.2f ret=%d", ticket, closeVol, g_trade.ResultRetcode()); return false; } const double beOff = InpBeOffsetPoints * _Point; const double tp = PositionGetDouble(POSITION_TP); double newSl = g_trackIsBuy ? NormalizePrice(g_trackEntry + beOff) : NormalizePrice(g_trackEntry - beOff); const double minDist = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point; if(g_trackIsBuy) { if(bid - newSl < MathMax(_Point, minDist)) newSl = NormalizePrice(bid - MathMax(_Point, minDist)); } else { if(newSl - ask < MathMax(_Point, minDist)) newSl = NormalizePrice(ask + MathMax(_Point, minDist)); } if(!g_trade.PositionModify(ticket, newSl, tp)) { PrintFormat("[EPB] BE modify fail ticket=%I64u SL=%.*f ret=%d", ticket, _Digits, newSl, g_trade.ResultRetcode()); } g_trackPartialDone = true; PrintFormat("[EPB] 2R manage: chốt %.2f lot (%.0f%%) @ %.2fR | SL→BE %.*f", closeVol, InpPartialCloseFrac * 100.0, profitR, _Digits, newSl); return true; } void ManageOpenPosition() { if(!HasOurPosition()) { ResetPositionTrack(); return; } SyncPositionTrack(); const ulong ticket = FindOurPositionTicket(); if(ticket == 0) return; TryPartialCloseAndBreakeven(ticket); if(InpMaxBarsInTrade <= 0) return; if(!PositionSelectByTicket(ticket)) return; const datetime openT = (datetime)PositionGetInteger(POSITION_TIME); const int barsIn = iBarShift(_Symbol, _Period, openT, false); if(barsIn >= 0 && barsIn >= InpMaxBarsInTrade) { g_trade.PositionClose(ticket); Print("[EPB] Đóng lệnh — quá ", InpMaxBarsInTrade, " bar"); ResetPositionTrack(); } } void ResetSetupState(const string reason) { if(g_setupState != SETUP_IDLE && InpLogStateChange) Print("[EPB] Setup → IDLE | ", reason); g_setupState = SETUP_IDLE; g_setupDir = 0; g_barsInSetupState = 0; g_touchBarTime = 0; g_setupDetail = reason; } void DrawSetupMarker(const string tag, const datetime t, const double price, const color clr, const string tip) { if(!InpDrawSetupMarkers) return; const string name = OBJ_PREFIX + tag + "_" + IntegerToString((long)t); if(ObjectFind(0, name) >= 0) return; int arrowCode = 159; if(tag == "TCH") arrowCode = 241; else if(tag == "SIG") arrowCode = (StringFind(tip, "Buy") >= 0) ? 233 : 234; ObjectCreate(0, name, OBJ_ARROW, 0, t, price); ObjectSetInteger(0, name, OBJPROP_ARROWCODE, arrowCode); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_WIDTH, 2); ObjectSetString(0, name, OBJPROP_TOOLTIP, tip); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } void DeleteSetupMarkers() { for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--) { const string n = ObjectName(0, i, 0, -1); if(StringFind(n, OBJ_PREFIX) == 0) ObjectDelete(0, n); } } void TransitionSetupState(const ENUM_SETUP_STATE newState, const int dir, const string detail, const int shift) { const ENUM_SETUP_STATE prev = g_setupState; g_setupState = newState; g_setupDir = dir; g_barsInSetupState = 0; g_setupDetail = detail; const string logLine = StringFormat("Setup %s → %s | dir=%d | %s", SetupStateToString(prev), SetupStateToString(newState), dir, detail); g_lastStateLog = logLine; if(InpLogStateChange) Print("[EPB] ", logLine); if(!InpDrawSetupMarkers || shift < 1) return; const datetime t = iTime(_Symbol, _Period, shift); const double px = (dir > 0) ? iLow(_Symbol, _Period, shift) : iHigh(_Symbol, _Period, shift); if(newState == SETUP_PULLBACK_ACTIVE) DrawSetupMarker("PB", t, px, clrDodgerBlue, "Pullback active"); else if(newState == SETUP_TOUCHED) DrawSetupMarker("TCH", t, px, clrGold, "Touch EMA"); } bool TryEntryOnBreakout(const int signalShift, const double atr) { if(!InpTradeEnabled) { g_setupDetail = "breakout OK — InpTradeEnabled=false"; return false; } if(HasOurPosition()) { g_setupDetail = "đã có position"; return false; } string allowWhy = ""; if(!IsAllowedNewEntry(allowWhy)) { g_setupDetail = allowWhy; return false; } const int touchShift = FindTouchBarShift(); string why = ""; const bool isBuy = (g_setupDir > 0); const bool signal = isBuy ? BreakoutSignalBuy(signalShift, touchShift, why) : BreakoutSignalSell(signalShift, touchShift, why); if(!signal) { g_setupDetail = why; return false; } if(InpLogStateChange) Print("[EPB] ", why); if(OpenMarketFromReject(isBuy, touchShift, signalShift, atr)) { ResetSetupState("entry opened"); return true; } return false; } void UpdateSetupStateMachine(const int shift, const ENUM_MARKET_STATE market, const double ema, const double atr) { const int trendDir = TradeDirection(market); if(!CanTradeRegime(market)) { if(g_setupState != SETUP_IDLE) ResetSetupState("regime: " + MarketStateToString(market)); return; } if(g_setupState != SETUP_IDLE && g_setupDir != 0 && trendDir != g_setupDir) { ResetSetupState("đổi hướng trend"); return; } const bool isBuy = (trendDir > 0); const bool broken = isBuy ? TrendStructureBrokenBuy(shift, ema) : TrendStructureBrokenSell(shift, ema); if(broken) { if(g_setupState != SETUP_IDLE) ResetSetupState(isBuy ? "Close phá dưới EMA" : "Close phá trên EMA"); return; } switch(g_setupState) { case SETUP_IDLE: { string why = ""; const bool start = isBuy ? PullbackStartBuy(shift, ema, atr, why) : PullbackStartSell(shift, ema, atr, why); if(start) TransitionSetupState(SETUP_PULLBACK_ACTIVE, trendDir, why, shift); else g_setupDetail = why; return; } case SETUP_PULLBACK_ACTIVE: { g_barsInSetupState++; const bool touched = isBuy ? PriceTouchedEmaBuy(shift, ema, atr) : PriceTouchedEmaSell(shift, ema, atr); if(touched) { g_touchBarTime = iTime(_Symbol, _Period, shift); TransitionSetupState(SETUP_TOUCHED, g_setupDir, StringFormat("touch EMA @ %s", TimeToString(g_touchBarTime)), shift); return; } if(g_barsInSetupState >= InpMaxBarsPullback) { ResetSetupState(StringFormat("timeout pullback (%d bar)", InpMaxBarsPullback)); return; } g_setupDetail = StringFormat("chờ touch (%d/%d)", g_barsInSetupState, InpMaxBarsPullback); return; } case SETUP_TOUCHED: { g_barsInSetupState++; // Phase 4+5: chờ breakout khỏi nến touch (tối đa InpMaxBarsWaitBreakout nến) if(TryEntryOnBreakout(shift, atr)) return; if(g_barsInSetupState > InpMaxBarsWaitBreakout) { ResetSetupState(StringFormat("timeout chờ breakout (%d nến)", InpMaxBarsWaitBreakout)); return; } const int touchShift = FindTouchBarShift(); const int barsAfter = (touchShift > 0) ? BarsAfterTouch(touchShift, shift) : -1; g_setupDetail = StringFormat("touch@%s — chờ breakout bar+%d (%d/%d) | %s", TimeToString(g_touchBarTime), barsAfter, g_barsInSetupState, InpMaxBarsWaitBreakout, g_setupDetail); return; } } } //--- Thống kê lệnh (góc dưới trái chart) void CreateStatsPanel() { if(!InpShowStatsPanel) return; const string bg = StatPfx() + "BG"; ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(0, bg, OBJPROP_CORNER, CORNER_LEFT_LOWER); ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, 8); ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, 8); ObjectSetInteger(0, bg, OBJPROP_XSIZE, 420); ObjectSetInteger(0, bg, OBJPROP_YSIZE, 52); ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, C'20,20,30'); ObjectSetInteger(0, bg, OBJPROP_BORDER_COLOR, C'80,80,120'); ObjectSetInteger(0, bg, OBJPROP_BACK, false); ObjectSetInteger(0, bg, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, bg, OBJPROP_HIDDEN, true); const string labels[] = {"TITLE", "MAIN"}; const string texts[] = { "▶ EmaPullbackBasic", "Tổng số lệnh: 0 | SL: 0 | TP: 0 | Winrate: —" }; const int yMain = 14; const int yTitle = 34; for(int i = 0; i < 2; i++) { const string n = StatPfx() + labels[i]; ObjectCreate(0, n, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, n, OBJPROP_CORNER, CORNER_LEFT_LOWER); ObjectSetInteger(0, n, OBJPROP_XDISTANCE, 14); ObjectSetInteger(0, n, OBJPROP_YDISTANCE, i == 0 ? yMain : yTitle); ObjectSetString(0, n, OBJPROP_TEXT, texts[i]); ObjectSetInteger(0, n, OBJPROP_FONTSIZE, i == 0 ? 9 : 8); ObjectSetString(0, n, OBJPROP_FONT, "Consolas"); ObjectSetInteger(0, n, OBJPROP_COLOR, i == 0 ? clrWhite : clrSilver); ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, n, OBJPROP_HIDDEN, true); } } void UpdateStatsPanel() { if(!InpShowStatsPanel) return; const double winrate = (g_statTotal > 0) ? (100.0 * g_statTP / g_statTotal) : 0.0; const string mainText = (g_statTotal > 0) ? StringFormat("Tổng số lệnh: %d | SL: %d | TP: %d | Winrate: %.1f%%", g_statTotal, g_statSL, g_statTP, winrate) : "Tổng số lệnh: 0 | SL: 0 | TP: 0 | Winrate: —"; ObjectSetString(0, StatPfx() + "MAIN", OBJPROP_TEXT, mainText); ChartRedraw(0); } void DeleteStatsPanel() { const string p = StatPfx(); for(int i = ObjectsTotal(0, 0, -1) - 1; i >= 0; i--) { if(StringFind(ObjectName(0, i, 0, -1), p) == 0) ObjectDelete(0, ObjectName(0, i, 0, -1)); } } void RecordClosedDeal(const ulong dealTicket) { if(dealTicket == 0 || !HistoryDealSelect(dealTicket)) return; if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) return; if((ulong)HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != InpMagic) return; if(HistoryDealGetInteger(dealTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) return; g_statTotal++; const long reason = HistoryDealGetInteger(dealTicket, DEAL_REASON); if(reason == DEAL_REASON_SL) g_statSL++; else if(reason == DEAL_REASON_TP) g_statTP++; else if(HistoryDealGetDouble(dealTicket, DEAL_PROFIT) >= 0.0) g_statTP++; else g_statSL++; UpdateStatsPanel(); } void UpdateChartComment(const int shift, const double ema, const double adx, const double atr, const ENUM_MARKET_STATE state, const string ®imeDetail) { if(!InpShowComment) return; const datetime barTime = iTime(_Symbol, _Period, shift); const int touchShift = FindTouchBarShift(); Comment( "EmaPullbackBasic — Phase 4/5\n", "Bar: ", TimeToString(barTime), "\n", "Regime: ", MarketStateToString(state), " | Setup: ", SetupStateToString(g_setupState), "\n", "Regime: ", regimeDetail, "\n", "Setup: ", g_setupDetail, "\n", "Touch: ", (g_touchBarTime > 0 ? TimeToString(g_touchBarTime) : "—"), " shift=", touchShift, "\n", "Trade: ", (InpTradeEnabled ? "ON" : "OFF"), " Pos: ", (HasOurPosition() ? "YES" : "NO"), " Risk: ", DoubleToString(InpRiskPercent * g_riskScale, 2), "% (×", DoubleToString(g_riskScale, 1), ") TP=", DoubleToString(InpTpRR, 1), "R\n", "Spread: ", CurrentSpreadPoints(), " pts", InpUsePartialAt2R ? StringFormat(" | 2R: chốt %.0f%%+BE", InpPartialCloseFrac * 100.0) : "", "\n", "Close/EMA/ADX: ", DoubleToString(iClose(_Symbol, _Period, shift), _Digits), " / ", DoubleToString(ema, _Digits), " / ", DoubleToString(adx, 1) ); } void OnNewClosedBar() { const int shift = 1; double ema = 0.0, adx = 0.0, atr = 0.0; string err = ""; if(!ReadIndicatorsAtShift(shift, ema, adx, atr, err)) { Print("[EPB] ", TimeToString(iTime(_Symbol, _Period, 0)), " — ", err); return; } string regimeDetail = ""; g_marketState = ClassifyMarketState(shift, regimeDetail); g_regimeDetail = regimeDetail; if(!HasOurPosition()) UpdateSetupStateMachine(shift, g_marketState, ema, atr); if(InpLogEachBar) { PrintFormat( "[EPB] %s | %s | %s | %s | pos=%s", TimeToString(iTime(_Symbol, _Period, shift)), MarketStateToString(g_marketState), SetupStateToString(g_setupState), g_setupDetail, HasOurPosition() ? "Y" : "N" ); } UpdateChartComment(shift, ema, adx, atr, g_marketState, regimeDetail); UpdateStatsPanel(); } //+------------------------------------------------------------------+ int OnInit() { g_trade.SetExpertMagicNumber(InpMagic); g_trade.SetDeviationInPoints(InpSlippage); g_trade.SetTypeFillingBySymbol(_Symbol); if(InpTpRR <= 0.0) { Print("[EPB] Init failed — InpTpRR phải > 0"); return INIT_PARAMETERS_INCORRECT; } g_hEma = iMA(_Symbol, _Period, InpEmaPeriod, 0, MODE_EMA, PRICE_CLOSE); g_hAdx = iADX(_Symbol, _Period, InpAdxPeriod); g_hAtr = iATR(_Symbol, _Period, InpAtrPeriod); if(g_hEma == INVALID_HANDLE || g_hAdx == INVALID_HANDLE || g_hAtr == INVALID_HANDLE) { Print("[EPB] Init failed — invalid indicator handle"); return INIT_FAILED; } ResetSetupState("init"); ResetPositionTrack(); g_equityPeak = AccountInfoDouble(ACCOUNT_EQUITY); g_riskScale = 1.0; double probe = 0.0; string err = ""; if(!ReadIndicatorsAtShift(1, probe, probe, probe, err)) Print("[EPB] Init warning — ", err); g_lastBarTime = iTime(_Symbol, _Period, 0); CreateStatsPanel(); UpdateStatsPanel(); OnNewClosedBar(); if(InpMaxBarsWaitBreakout < 1) { Print("[EPB] Init failed — InpMaxBarsWaitBreakout phải >= 1"); return INIT_PARAMETERS_INCORRECT; } PrintFormat( "[EPB] Init Phase 6 — trade=%s risk=%.1f%%×DD TP=%.1fR | spread<=%d | session %02d-%02dh | 2R: %.0f%%+BE | no daily cap", InpTradeEnabled ? "ON" : "OFF", InpRiskPercent, InpTpRR, InpMaxSpreadPoints, InpSessionStartHour, InpSessionEndHour, InpPartialCloseFrac * 100.0 ); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { if(g_hEma != INVALID_HANDLE) IndicatorRelease(g_hEma); if(g_hAdx != INVALID_HANDLE) IndicatorRelease(g_hAdx); if(g_hAtr != INVALID_HANDLE) IndicatorRelease(g_hAtr); g_hEma = g_hAdx = g_hAtr = INVALID_HANDLE; DeleteSetupMarkers(); DeleteStatsPanel(); Comment(""); Print("[EPB] Deinit — reason=", reason); } void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return; RecordClosedDeal(trans.deal); } void OnTick() { UpdateRiskScale(); ManageOpenPosition(); const datetime barOpen = iTime(_Symbol, _Period, 0); if(barOpen == g_lastBarTime) return; g_lastBarTime = barOpen; OnNewClosedBar(); } //+------------------------------------------------------------------+