//+------------------------------------------------------------------+ //| MssEntry.mqh — Limit @ M5 FVG edge | SL H0/L0 M5 | TP min RR | //+------------------------------------------------------------------+ #ifndef ICT2026_MSSENTRY_MQH #define ICT2026_MSSENTRY_MQH #include #include #include #include CTrade g_ictMssTrade; double IctMssEntry_NormalizePrice(const string sym, const double price) { const double tick = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); if(tick <= 0.0) return NormalizeDouble(price, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS)); return NormalizeDouble(MathRound(price / tick) * tick, (int)SymbolInfoInteger(sym, SYMBOL_DIGITS)); } double IctMssEntry_NormalizeVolume(const string sym, double vol) { const double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP); const double vmin = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN); const double vmax = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX); if(step <= 0.0) return 0.0; vol = MathFloor(vol / step) * step; if(vol < vmin - 1e-12) return 0.0; if(vol > vmax) vol = vmax; return NormalizeDouble(vol, 2); } double IctMssEntry_VolumeForRisk(const string sym, const bool isBuy, const double entry, const double sl) { const double pt = SymbolInfoDouble(sym, SYMBOL_POINT); if(MathAbs(entry - sl) < pt) return 0.0; const double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (InpMssRiskPct / 100.0); double profit = 0.0; if(!OrderCalcProfit(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, sym, 1.0, entry, sl, profit)) return 0.0; const double lossPerLot = MathAbs(profit); if(lossPerLot < DBL_EPSILON) return 0.0; return riskMoney / lossPerLot; } double IctMssEntry_Atr(const string sym, const ENUM_TIMEFRAMES tf, const int period) { const int h = iATR(sym, tf, period); if(h == INVALID_HANDLE) return 0.0; double buf[]; ArraySetAsSeries(buf, true); if(CopyBuffer(h, 0, 1, 1, buf) != 1) { IndicatorRelease(h); return 0.0; } IndicatorRelease(h); return buf[0]; } double IctMssEntry_LimitPrice(const IctFvgZone &zone) { if(zone.side == ICT_FVG_BULL) return zone.upper; if(zone.side == ICT_FVG_BEAR) return zone.lower; return 0.0; } bool IctMssEntry_ComputeLevels(const string sym, const IctFvgZone &m5Zone, double &entryOut, double &slOut, double &tpOut) { entryOut = slOut = tpOut = 0.0; entryOut = IctMssEntry_LimitPrice(m5Zone); if(entryOut <= 0.0) return false; const bool isBuy = (m5Zone.side == ICT_FVG_BULL); const ENUM_ICT_BIAS bias = isBuy ? ICT_BIAS_BULL : ICT_BIAS_BEAR; double swingSl = 0.0; if(g_ictLowTf.mss.chochLocked && g_ictLowTf.mss.slSwingPrice > 0.0) swingSl = g_ictLowTf.mss.slSwingPrice; else if(!IctMss_GetConfirmMssSwing(sym, bias, swingSl)) { if(g_ictLowTf.mss.slSwingPrice > 0.0) swingSl = g_ictLowTf.mss.slSwingPrice; else return false; } const double atrM5 = IctMssEntry_Atr(sym, InpConfirmTf, InpFvgAtrPeriod); const double bufSl = (atrM5 > 0.0) ? atrM5 * InpMssSlAtrMult : 10.0 * _Point; const double minRR = MathMax(1.0, InpMssMinRR); if(isBuy) { slOut = swingSl - bufSl; const double risk = entryOut - slOut; if(risk <= _Point) return false; tpOut = entryOut + risk * minRR; } else { slOut = swingSl + bufSl; const double risk = slOut - entryOut; if(risk <= _Point) return false; tpOut = entryOut - risk * minRR; } entryOut = IctMssEntry_NormalizePrice(sym, entryOut); slOut = IctMssEntry_NormalizePrice(sym, slOut); tpOut = IctMssEntry_NormalizePrice(sym, tpOut); if(isBuy && (slOut >= entryOut - _Point || tpOut <= entryOut + _Point)) return false; if(!isBuy && (slOut <= entryOut + _Point || tpOut >= entryOut - _Point)) return false; return true; } bool IctMssEntry_HasOpenExposure(const string sym) { for(int i = PositionsTotal() - 1; i >= 0; i--) { const ulong ticket = PositionGetTicket(i); if(ticket == 0 || !PositionSelectByTicket(ticket)) continue; if(PositionGetString(POSITION_SYMBOL) != sym) continue; if((ulong)PositionGetInteger(POSITION_MAGIC) == InpMssMagic) return true; } for(int i = OrdersTotal() - 1; i >= 0; i--) { const ulong ticket = OrderGetTicket(i); if(ticket == 0 || !OrderSelect(ticket)) continue; if(OrderGetString(ORDER_SYMBOL) != sym) continue; if((ulong)OrderGetInteger(ORDER_MAGIC) != InpMssMagic) continue; const ENUM_ORDER_TYPE t = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); if(t == ORDER_TYPE_BUY_LIMIT || t == ORDER_TYPE_SELL_LIMIT) return true; } return false; } bool IctMssEntry_CancelTicket(const ulong ticket) { if(ticket == 0) return true; if(!OrderSelect(ticket)) return true; return g_ictMssTrade.OrderDelete(ticket); } bool IctMssEntry_PlaceLimit(const string sym, const bool isBuy, const double entry, const double sl, const double tp, const double volume, ulong &ticketOut) { ticketOut = 0; g_ictMssTrade.SetExpertMagicNumber(InpMssMagic); g_ictMssTrade.SetDeviationInPoints(20); const string cmt = StringFormat("ICT26_MSS %s", isBuy ? "BUY" : "SELL"); bool ok = false; if(InpMssPendingExpireHours > 0) { const datetime exp = TimeCurrent() + (datetime)(InpMssPendingExpireHours * 3600); if(isBuy) ok = g_ictMssTrade.BuyLimit(volume, entry, sym, sl, tp, ORDER_TIME_SPECIFIED, exp, cmt); else ok = g_ictMssTrade.SellLimit(volume, entry, sym, sl, tp, ORDER_TIME_SPECIFIED, exp, cmt); } else { if(isBuy) ok = g_ictMssTrade.BuyLimit(volume, entry, sym, sl, tp, ORDER_TIME_GTC, 0, cmt); else ok = g_ictMssTrade.SellLimit(volume, entry, sym, sl, tp, ORDER_TIME_GTC, 0, cmt); } if(ok) ticketOut = g_ictMssTrade.ResultOrder(); else if(InpDebug) PrintFormat("[ICT2026/Entry] Order fail %d — %s", g_ictMssTrade.ResultRetcode(), g_ictMssTrade.ResultRetcodeDescription()); return ok; } void IctMssEntry_Init() { g_ictMssTrade.SetExpertMagicNumber(InpMssMagic); } void IctMssEntry_Update(const string sym) { if(g_ictLowTf.mss.phase == ICT_MSS_IDLE && g_ictLowTf.mss.pendingTicket > 0) { IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); g_ictLowTf.mss.pendingTicket = 0; } if(!InpMssTradeEnabled) { if(g_ictLowTf.mss.phase >= ICT_MSS_M5_FVG) IctMss_JournalEntryBlock("InpMssTradeEnabled=false"); return; } if(!g_ictIntraday.isAllowTrade || g_ictLowTf.mss.phase < ICT_MSS_M5_FVG || g_ictLowTf.mss.m5FvgId == 0) { if(g_ictLowTf.mss.pendingTicket > 0) { IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); g_ictLowTf.mss.pendingTicket = 0; } if(g_ictLowTf.mss.phase >= ICT_MSS_CHOCH) { if(!g_ictIntraday.isAllowTrade) IctMss_JournalEntryBlock("AllowTrade=false"); else if(g_ictLowTf.mss.phase < ICT_MSS_M5_FVG) IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); else if(g_ictLowTf.mss.m5FvgId == 0) IctMss_JournalEntryBlock("Chưa có M5 FVG cho entry"); } return; } if(InpMssOnePosition && IctMssEntry_HasOpenExposure(sym)) { if(g_ictLowTf.mss.pendingTicket > 0 && !OrderSelect(g_ictLowTf.mss.pendingTicket)) g_ictLowTf.mss.pendingTicket = 0; g_ictLowTf.mss.displayReason = StringFormat("%s | position/pending active", IctMssPhaseText(g_ictLowTf.mss.phase)); IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); return; } const int mIdx = IctConfirmFvg_FindById(g_ictLowTf.mss.m5FvgId); if(mIdx < 0) return; const IctFvgZone m5 = g_ictConfirmFvgZones[mIdx]; double entry = 0.0, sl = 0.0, tp = 0.0; if(!IctMssEntry_ComputeLevels(sym, m5, entry, sl, tp)) { g_ictLowTf.mss.displayReason = StringFormat("%s | chờ H0/L0 M5 cho SL", IctMssPhaseText(g_ictLowTf.mss.phase)); IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); return; } const bool isBuy = (m5.side == ICT_FVG_BULL); const double vol = IctMssEntry_NormalizeVolume(sym, IctMssEntry_VolumeForRisk(sym, isBuy, entry, sl)); if(vol <= 0.0) { g_ictLowTf.mss.displayReason = "Lot=0 (SL quá gần hoặc risk)"; IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); return; } const double risk = isBuy ? (entry - sl) : (sl - entry); const double rr = (risk > _Point) ? (isBuy ? (tp - entry) : (entry - tp)) / risk : 0.0; const double pt = SymbolInfoDouble(sym, SYMBOL_POINT) * 2.0; if(g_ictLowTf.mss.pendingTicket > 0 && OrderSelect(g_ictLowTf.mss.pendingTicket)) { if(MathAbs(g_ictLowTf.mss.pendingEntry - entry) < pt && MathAbs(g_ictLowTf.mss.pendingSl - sl) < pt && MathAbs(g_ictLowTf.mss.pendingTp - tp) < pt) { g_ictLowTf.mss.displayReason = StringFormat("Limit %.2f SL %.2f TP %.2f | RR %.1f", entry, sl, tp, rr); return; } IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket); g_ictLowTf.mss.pendingTicket = 0; } ulong ticket = 0; if(!IctMssEntry_PlaceLimit(sym, isBuy, entry, sl, tp, vol, ticket)) { g_ictLowTf.mss.displayReason = StringFormat("Đặt limit thất bại (%d %s)", g_ictMssTrade.ResultRetcode(), g_ictMssTrade.ResultRetcodeDescription()); IctMss_JournalEntryBlock(g_ictLowTf.mss.displayReason); return; } g_ictLowTf.mss.pendingTicket = ticket; g_ictLowTf.mss.pendingEntry = entry; g_ictLowTf.mss.pendingSl = sl; g_ictLowTf.mss.pendingTp = tp; g_ictLowTf.mss.displayReason = StringFormat("Sell/Buy limit %.2f | SL %.2f (H0/L0) | TP %.2f RR%.1f", entry, sl, tp, rr); } #endif