Files
2026-05-25 02:01:26 +07:00

1283 lines
42 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//+------------------------------------------------------------------+
//| 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 <Trade\Trade.mqh>
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<close touch", barsAfter);
return true;
}
bool HasOurPosition() {
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 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 &regimeDetail) {
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();
}
//+------------------------------------------------------------------+